├── 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 | [](https://community.home-assistant.io/t/harmony-hub-climate-component-for-a-c-integration/76793) [](https://github.com/so3n/HA_harmony_climate_component)
6 | [](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 | 
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 | [](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
--------------------------------------------------------------------------------