├── README.md ├── custom_components └── harmony_ac │ ├── __init__.py │ ├── climate.py │ └── manifest.json ├── hacs.json └── img └── thermostat_card.png /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Harmony Hub Climate Controller 4 | 5 | [![homeassistant_community](https://img.shields.io/badge/HA%20community-forum-brightgreen)](https://community.home-assistant.io/t/harmony-hub-climate-component-for-a-c-integration/76793) [![Github Stars](https://img.shields.io/github/stars/so3n/HA_harmony_climate_component)](https://github.com/so3n/HA_harmony_climate_component) 6 | [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/so3n) 7 | 8 | Harmony Hub Climate Controller allows you to control IR climate devices (eg. split system air conditioners) through a Harmony Hub. 9 | 10 | This component appears to home assistant as a climate device and as such can be intuitively used to control an air conditioner or other climate device. 11 | 12 | ![Thermostat Lovelace Card](https://raw.githubusercontent.com/so3n/HA_harmony_climate_component/master/img/thermostat_card.png) 13 | 14 | I forked from this [project](https://github.com/vpnmaster/homeassistant-custom-components), which was created for Broadlink RM Devices, so thanks goes to [vpnmaster](https://github.com/vpnmaster) for doing the hard work in creating that component. 15 | 16 | ## Installing 17 | 18 | Recommended install via [HACS](https://hacs.xyz/), otherwise follow the manual steps below: 19 | 1. Download or clone this project, and place the `custom_components` folder and its contents into your Home Assistant config folder. 20 | 2. Ensure `climate.py` is located in a folder named `harmony_ac` within the `custom_components` folder. 21 | 22 | 23 | ## Configuration 24 | Once this custom component is installed, add the following to your `configuration.yaml` to use it in your HA installation 25 | 26 | ```yaml 27 | climate: 28 | - platform: harmony_ac 29 | remote_entity: remote.living_room 30 | device_id: 12345678 31 | ``` 32 | *** _refer below how to obtain `device_id`_ and for all the configuration options 33 | 34 | ### Main Configuration Options 35 | 36 | | Variable | Type | Required | Default | Description | 37 | | ---------------- | ------- | -------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------- | 38 | | name | string | FALSE | Harmony Climate Controller | Name you would like to give this climate component | 39 | | remote_entity | string | TRUE | | `entity_id` of your existing harmony device in HA that will send the IR commands | 40 | | device_id | integer | TRUE | | The ID which Harmony has assigned to the climate device you wish to control
(refer to FAQ's below on how to obtain) | 41 | | min_temp | float | FALSE | 16 | Set minimum temperature range | 42 | | max_temp | float | FALSE | 30 | Set maximum temperature range | 43 | | target_temp | float | FALSE | 20 | Set initial target temperature | 44 | | target_temp_step | float | FALSE | 1 | Set target temperature step | 45 | | temp_sensor | string | FALSE | | `entity_id` for a temperature sensor, target_sensor.state must be temperature | 46 | | customize | list | FALSE | | List of options to customize. Refer to table below | 47 | | debug_mode | boolean | FALSE | `false` | When set to `true` commands are sent to Home Assistant Log only (no commands are sent to Harmony Device). | 48 | 49 | ### `Customize`Configuration Options 50 | 51 | | Variable | Type | Required | Default | Description | 52 | | ------------------ | ---- | -------- | ------------------------------------- | ----------------------------------------------------------------------------------------------- | 53 | | operations | list | FALSE | - heat
- cool
- auto | List of operation modes (nest under `customize`)
_do not include the OFF mode in this list_ | 54 | | fan_modes | list | FALSE | - auto
- low
- mid
- high | List of fan modes (nest under `customize`) | 55 | | no_temp_operations | list | FALSE | | List of operation modes that will not send a target temperature (nest under `customize`) | 56 | 57 | ### Example Usage 58 | ```yaml 59 | climate: 60 | - platform: harmony_ac 61 | name: Living Room 62 | remote_entity: remote.living_room 63 | device_id: 12345678 64 | min_temp: 18 65 | max_temp: 30 66 | target_temp: 20 67 | target_temp_step: 1 68 | temp_sensor: sensor.living_room_temp 69 | customize: 70 | operations: 71 | - cool 72 | - heat 73 | - dry 74 | - fan_only 75 | - auto 76 | no_temp_operations: 77 | - dry 78 | - fan_only 79 | fan_modes: 80 | - auto 81 | - low 82 | - mid 83 | - high 84 | ``` 85 | 86 | ## FAQ's 87 | 88 | ### How to obtain your Air Conditioner's Device ID 89 | This assumes you have already setup the official home assitant [harmony component](https://www.home-assistant.io/components/remote.harmony/) and have added your air conditioner as a device in the [MyHarmony](https://www.myharmony.com) software. 90 | * in your home assistant config folder, delete `harmony_*.conf` if it exists. eg. mine is called `harmony_living_room.conf` 91 | * restart home assistant 92 | * when it boots up it should create a new `harmony_*.conf` file. Open this and find your air conditioner device, it should have an ID number next to it. 93 | 94 | ### How to learn and name all the IR commands for your air conditioner 95 | This part is unfortunately going to be manual. Every combination of **operations** (heat, cool, dry, fan_only, heat_cool, etc), **fan modes** (low, mid, high, auto, etc) and **temperatures**, will need to be learned manually within the MyHarmony software. The naming convention of each command is important for this component to work. 96 | 97 | * in MyHarmony, go to devices > your air conditioner and click on **Add or Fix a Command** 98 | * Click on **add a missing command** and enter a name in the following format: *OperationFanmodeTemperature* eg. *CoolHigh18* and then follow the prompts within MyHarmony 99 | 100 | some important notes about the naming convention for commands: 101 | * **Operation** must be one of the operations listed in your configuration.yaml file. If the operation has two words, remove the underscore and capitalize each word: `FanOnly` 102 | * **Fanmode** must be one of the fan_modes listed in your configuration.yaml file 103 | * **Temperature** must be an integer in the range specified by your min/max temp in configuration.yaml file. Operations configured in `no_temp_operations` should not include a temperature in the Harmony command 104 | * The only exception to these rules is the 'off' command. Just name this as **Off** in MyHarmony 105 | 106 | Some valid examples of command names based on the above configuration.yaml example 107 | ``` 108 | Off 109 | CoolAuto18 110 | CoolAuto19 111 | CoolAuto20 112 | ... 113 | CoolAuto30 114 | CoolHigh18 115 | CoolHigh19 116 | CoolHigh20 117 | ... 118 | CoolHigh30 119 | HeatLow18 120 | HeatLow19 121 | etc 122 | etc 123 | ``` 124 | 125 | Any modes configured in the `no_temp_operations` Customization entry should not include a temperature: 126 | ``` 127 | FanOnlyHigh 128 | FanOnlyLow 129 | DryHigh 130 | DryLow 131 | etc 132 | etc 133 | ``` 134 | 135 |
136 | 137 | [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/so3n) 138 | -------------------------------------------------------------------------------- /custom_components/harmony_ac/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickneos/HA_harmony_climate_component/a2a1a325152023ca9bd6128a5933e3f33b69423b/custom_components/harmony_ac/__init__.py -------------------------------------------------------------------------------- /custom_components/harmony_ac/climate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for Harmony Hub devices as a Climate Component. 3 | 4 | https://github.com/so3n/HA_harmony_climate_component 5 | """ 6 | import asyncio 7 | import logging 8 | import voluptuous as vol 9 | import homeassistant.helpers.config_validation as cv 10 | 11 | 12 | from homeassistant.components.climate import ClimateEntity, PLATFORM_SCHEMA 13 | from homeassistant.components.climate.const import ( 14 | ClimateEntityFeature, 15 | HVACMode, 16 | HVAC_MODES, ATTR_HVAC_MODE) 17 | from homeassistant.const import ( 18 | CONF_NAME, CONF_CUSTOMIZE, STATE_ON, STATE_UNKNOWN, STATE_UNAVAILABLE, 19 | ATTR_TEMPERATURE, PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE) 20 | from homeassistant.helpers.event import async_track_state_change_event 21 | from homeassistant.core import callback 22 | from homeassistant.helpers.restore_state import RestoreEntity 23 | 24 | _LOGGER = logging.getLogger(__name__) 25 | 26 | SUPPORT_FLAGS = ( 27 | ClimateEntityFeature.TARGET_TEMPERATURE | 28 | ClimateEntityFeature.FAN_MODE | 29 | ClimateEntityFeature.TURN_OFF | 30 | ClimateEntityFeature.TURN_ON 31 | ) 32 | 33 | CONF_REMOTE_ENTITY = 'remote_entity' 34 | CONF_MIN_TEMP = 'min_temp' 35 | CONF_MAX_TEMP = 'max_temp' 36 | CONF_TARGET_TEMP = 'target_temp' 37 | CONF_TARGET_TEMP_STEP = 'target_temp_step' 38 | CONF_TEMP_SENSOR = 'temp_sensor' 39 | CONF_OPERATIONS = 'operations' 40 | CONF_FAN_MODES = 'fan_modes' 41 | CONF_NO_TEMP_OPERATIONS = 'no_temp_operations' 42 | CONF_DEVICE_ID = 'device_id' 43 | CONF_DEBUG_MODE = 'debug_mode' 44 | 45 | DEFAULT_NAME = 'Harmony Climate Controller' 46 | DEFAULT_MIN_TEMP = 16 47 | DEFAULT_MAX_TEMP = 30 48 | DEFAULT_TARGET_TEMP = 20 49 | DEFAULT_TARGET_TEMP_STEP = 1 50 | DEFAULT_OPERATION_LIST = [HVACMode.HEAT, HVACMode.COOL, HVACMode.AUTO] 51 | DEFAULT_NO_TEMP_OPERATION_LIST = [] 52 | DEFAULT_FAN_MODE_LIST = ['auto', 'low', 'mid', 'high'] 53 | DEFAULT_DEBUG_MODE = False 54 | 55 | CUSTOMIZE_SCHEMA = vol.Schema({ 56 | vol.Optional(CONF_OPERATIONS): vol.All(cv.ensure_list, [cv.string]), 57 | vol.Optional(CONF_FAN_MODES): vol.All(cv.ensure_list, [cv.string]), 58 | vol.Optional(CONF_NO_TEMP_OPERATIONS): vol.All(cv.ensure_list, [cv.string]) 59 | }) 60 | 61 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 62 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): 63 | cv.string, 64 | vol.Required(CONF_REMOTE_ENTITY): 65 | cv.entity_id, 66 | vol.Required(CONF_DEVICE_ID): 67 | cv.string, 68 | vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): 69 | cv.positive_int, 70 | vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): 71 | cv.positive_int, 72 | vol.Optional(CONF_TARGET_TEMP, default=DEFAULT_TARGET_TEMP): 73 | cv.positive_int, 74 | vol.Optional(CONF_TARGET_TEMP_STEP, default=DEFAULT_TARGET_TEMP_STEP): 75 | cv.positive_int, 76 | vol.Optional(CONF_TEMP_SENSOR): 77 | cv.entity_id, 78 | vol.Optional(CONF_DEBUG_MODE, default=DEFAULT_DEBUG_MODE): 79 | cv.boolean, 80 | vol.Optional(CONF_CUSTOMIZE, default={}): 81 | CUSTOMIZE_SCHEMA 82 | }) 83 | 84 | async def async_setup_platform(hass, config, async_add_entities, 85 | discovery_info=None): 86 | """Set up the Harmony Hub Climate platform.""" 87 | name = config.get(CONF_NAME) 88 | remote_entity = config.get(CONF_REMOTE_ENTITY) 89 | device_id = config.get(CONF_DEVICE_ID) 90 | 91 | min_temp = config.get(CONF_MIN_TEMP) 92 | max_temp = config.get(CONF_MAX_TEMP) 93 | target_temp = config.get(CONF_TARGET_TEMP) 94 | target_temp_step = config.get(CONF_TARGET_TEMP_STEP) 95 | temperature_sensor = config.get(CONF_TEMP_SENSOR) 96 | debug_mode = config.get(CONF_DEBUG_MODE) 97 | operation_list = ( 98 | config.get(CONF_CUSTOMIZE).get(CONF_OPERATIONS, []) or 99 | DEFAULT_OPERATION_LIST) 100 | fan_list = ( 101 | config.get(CONF_CUSTOMIZE).get(CONF_FAN_MODES, []) or 102 | DEFAULT_FAN_MODE_LIST) 103 | no_temp_operations_list = ( 104 | config.get(CONF_CUSTOMIZE).get(CONF_NO_TEMP_OPERATIONS, []) or 105 | DEFAULT_NO_TEMP_OPERATION_LIST) 106 | 107 | async_add_entities([ 108 | HarmonyIRClimate(hass, name, remote_entity, device_id, min_temp, 109 | max_temp, target_temp, target_temp_step, 110 | temperature_sensor, operation_list, fan_list, 111 | debug_mode, no_temp_operations_list) 112 | ]) 113 | 114 | class HarmonyIRClimate(ClimateEntity, RestoreEntity): 115 | 116 | def __init__(self, hass, name, remote_entity, device_id, min_temp, 117 | max_temp, target_temp, target_temp_step, 118 | temperature_sensor, operation_list, fan_list, 119 | debug_mode, no_temp_operations_list): 120 | """Initialize Harmony IR Climate device.""" 121 | self.hass = hass 122 | self._name = name 123 | self._remote_entity = remote_entity 124 | self._device_id = device_id 125 | self._min_temp = min_temp 126 | self._max_temp = max_temp 127 | self._target_temperature = target_temp 128 | self._target_temperature_step = target_temp_step 129 | self._temperature_sensor = temperature_sensor 130 | self._debug_mode = debug_mode 131 | 132 | valid_hvac_modes = [x for x in operation_list if x in HVAC_MODES] 133 | valid_no_temp_operation_modes = [x for x in no_temp_operations_list if x in HVAC_MODES] 134 | 135 | self._operation_modes = [HVACMode.OFF] + valid_hvac_modes 136 | self._no_temp_operation_modes = valid_no_temp_operation_modes 137 | self._fan_modes = fan_list 138 | 139 | self._hvac_mode = HVACMode.OFF 140 | self._current_fan_mode = self._fan_modes[0] 141 | self._last_on_operation = None 142 | 143 | self._current_temperature = None 144 | self._unit_of_measurement = hass.config.units.temperature_unit 145 | self._support_flags = SUPPORT_FLAGS 146 | self._enable_turn_on_off_backwards_compatibility = False 147 | 148 | 149 | async def async_added_to_hass(self): 150 | """Run when entity about to be added.""" 151 | await super().async_added_to_hass() 152 | 153 | last_state = await self.async_get_last_state() 154 | 155 | if last_state is not None: 156 | self._hvac_mode = last_state.state 157 | self._current_fan_mode = last_state.attributes['fan_mode'] 158 | self._target_temperature = last_state.attributes['temperature'] 159 | 160 | if 'last_on_operation' in last_state.attributes: 161 | self._last_on_operation = last_state.attributes['last_on_operation'] 162 | 163 | if self._temperature_sensor: 164 | async_track_state_change_event(self.hass, [self._temperature_sensor], 165 | self._async_temp_sensor_changed) 166 | 167 | temp_sensor_state = self.hass.states.get(self._temperature_sensor) 168 | if temp_sensor_state and temp_sensor_state.state != STATE_UNKNOWN: 169 | self._async_update_temp(temp_sensor_state) 170 | 171 | @property 172 | def name(self): 173 | """Return the name of the climate device.""" 174 | return self._name 175 | 176 | @property 177 | def state(self): 178 | """Return the current state.""" 179 | if self.hvac_mode != HVACMode.OFF: 180 | return self.hvac_mode 181 | return HVACMode.OFF 182 | 183 | @property 184 | def temperature_unit(self): 185 | """Return the unit of measurement.""" 186 | return self._unit_of_measurement 187 | 188 | @property 189 | def min_temp(self): 190 | """Return the polling state.""" 191 | return self._min_temp 192 | 193 | @property 194 | def max_temp(self): 195 | """Return the polling state.""" 196 | return self._max_temp 197 | 198 | @property 199 | def target_temperature(self): 200 | """Return the temperature we try to reach.""" 201 | return self._target_temperature 202 | 203 | @property 204 | def target_temperature_step(self): 205 | """Return the supported step of target temperature.""" 206 | return self._target_temperature_step 207 | 208 | @property 209 | def hvac_modes(self): 210 | """Return the list of available operation modes.""" 211 | return self._operation_modes 212 | 213 | @property 214 | def hvac_mode(self): 215 | """Return hvac mode ie. heat, cool.""" 216 | return self._hvac_mode 217 | 218 | @property 219 | def last_on_operation(self): 220 | """Return the last non-idle operation ie. heat, cool.""" 221 | return self._last_on_operation 222 | 223 | @property 224 | def fan_modes(self): 225 | """Return the list of available fan modes.""" 226 | return self._fan_modes 227 | 228 | @property 229 | def fan_mode(self): 230 | """Return the fan setting.""" 231 | return self._current_fan_mode 232 | 233 | @property 234 | def current_temperature(self): 235 | """Return the current temperature.""" 236 | return self._current_temperature 237 | 238 | @property 239 | def supported_features(self): 240 | """Return the list of supported features.""" 241 | return self._support_flags 242 | 243 | @property 244 | def should_poll(self): 245 | """Return the polling state.""" 246 | return False 247 | 248 | async def async_set_temperature(self, **kwargs): 249 | """Set new target temperatures.""" 250 | hvac_mode = kwargs.get(ATTR_HVAC_MODE) 251 | temperature = kwargs.get(ATTR_TEMPERATURE) 252 | 253 | if temperature is None: 254 | return 255 | 256 | if temperature < self._min_temp or temperature > self._max_temp: 257 | _LOGGER.warning('The temperature value is out of min/max range') 258 | return 259 | 260 | if self._target_temperature_step == PRECISION_WHOLE: 261 | self._target_temperature = round(temperature) 262 | else: 263 | self._target_temperature = round(temperature, 1) 264 | 265 | if hvac_mode: 266 | await self.async_set_hvac_mode(hvac_mode) 267 | return 268 | 269 | if not self._hvac_mode.lower() == HVACMode.OFF: 270 | await self.async_send_command() 271 | 272 | self.async_write_ha_state() 273 | 274 | async def async_set_hvac_mode(self, hvac_mode): 275 | """Set operation mode.""" 276 | self._hvac_mode = hvac_mode 277 | 278 | if not hvac_mode == HVACMode.OFF: 279 | self._last_on_operation = hvac_mode 280 | 281 | await self.async_send_command() 282 | self.async_write_ha_state() 283 | 284 | async def async_set_fan_mode(self, fan_mode): 285 | """Set fan mode.""" 286 | self._current_fan_mode = fan_mode 287 | 288 | if not self._hvac_mode.lower() == HVACMode.OFF: 289 | await self.async_send_command() 290 | self.async_write_ha_state() 291 | 292 | async def async_turn_off(self): 293 | """Turn off.""" 294 | await self.async_set_hvac_mode(HVACMode.OFF) 295 | 296 | async def async_turn_on(self): 297 | """Turn on.""" 298 | if self._last_on_operation is not None: 299 | await self.async_set_hvac_mode(self._last_on_operation) 300 | else: 301 | await self.async_set_hvac_mode(self._operation_modes[1]) 302 | 303 | async def async_send_command(self): 304 | """Send command to harmony device""" 305 | 306 | operation_mode = self._hvac_mode 307 | operation_mode_command_string = "".join(x.capitalize() or '_' for x in self._hvac_mode .split('_')) # Remove underscores and capitalize each word 308 | fan_mode = self._current_fan_mode 309 | target_temperature = '{0:g}'.format(self._target_temperature) 310 | 311 | if operation_mode.lower() == HVACMode.OFF: 312 | command = 'Off' 313 | elif operation_mode.lower() in self._no_temp_operation_modes: 314 | command = operation_mode_command_string + fan_mode.capitalize() 315 | else: 316 | command = operation_mode_command_string + fan_mode.capitalize() + target_temperature.capitalize() 317 | 318 | service_data = { 319 | 'entity_id': self._remote_entity, 320 | 'device': self._device_id, 321 | 'command': command 322 | } 323 | 324 | _LOGGER.debug( 325 | "remote.send_command %s", service_data 326 | ) 327 | 328 | if self._debug_mode: 329 | return 330 | 331 | await self.hass.services.async_call( 332 | 'remote', 'send_command', service_data) 333 | 334 | @callback 335 | def _async_temp_sensor_changed(self, event): 336 | """Handle temperature changes.""" 337 | new_state = event.data.get("new_state") 338 | if new_state is None: 339 | return 340 | 341 | self._async_update_temp(new_state) 342 | self.async_write_ha_state() 343 | 344 | 345 | def _async_update_temp(self, state): 346 | """Update thermostat with latest state from temperature sensor.""" 347 | try: 348 | if state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): 349 | self._current_temperature = float(state.state) 350 | except ValueError as ex: 351 | _LOGGER.error("Unable to update from temperature sensor: %s", ex) 352 | -------------------------------------------------------------------------------- /custom_components/harmony_ac/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "harmony_ac", 3 | "name": "Harmony AC", 4 | "documentation": "https://github.com/nickneos/HA_harmony_climate_component", 5 | "version": "0.3.0", 6 | "dependencies": [], 7 | "codeowners": [ 8 | "@nickneos" 9 | ], 10 | "requirements": [], 11 | "issue_tracker": "https://github.com/nickneos/HA_harmony_climate_component/issues" 12 | } 13 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Harmony Hub Climate Controller", 3 | "domains": ["climate"], 4 | "homeassistant": "0.96.0", 5 | "render_readme": true 6 | } 7 | -------------------------------------------------------------------------------- /img/thermostat_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickneos/HA_harmony_climate_component/a2a1a325152023ca9bd6128a5933e3f33b69423b/img/thermostat_card.png --------------------------------------------------------------------------------