From e75f98ca405613d07d8478f850e22281e2ff8afe Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Sun, 19 Jan 2025 05:56:08 +0100 Subject: [PATCH 1/7] refatored light class code added device data files checks for light class --- codes/light/1070.json | 1 - custom_components/smartir/device_data.py | 151 +++++++++ custom_components/smartir/light.py | 381 ++++++++++++----------- 3 files changed, 347 insertions(+), 186 deletions(-) diff --git a/codes/light/1070.json b/codes/light/1070.json index 0416d133f..c52c787ec 100644 --- a/codes/light/1070.json +++ b/codes/light/1070.json @@ -6,7 +6,6 @@ "brightness": [ 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 255 ], - "colorTemperature": [], "commands": { "on": "BZgjrRE/AuAXAQGDBuAFA0AB4AcTQA/gFwFAI+ALAwcQoZgj0Qg/Ag==", "off": "B34jkRFpAhQC4BUDAWcG4AUDQC/gAxMAFCABQA8BFAJAA0ABQAfgAQMFZwYUAhQCQAfgDQMHOKF+I6MIaQI=", diff --git a/custom_components/smartir/device_data.py b/custom_components/smartir/device_data.py index c19508e9f..44f09b239 100644 --- a/custom_components/smartir/device_data.py +++ b/custom_components/smartir/device_data.py @@ -462,4 +462,155 @@ def check_file_media_player(file_name, device_data, device_class, check_data): @staticmethod def check_file_light(file_name, device_data, device_class, check_data): + color_modes = {} + + if not ( + "commands" in device_data + and isinstance(device_data["commands"], dict) + and len(device_data["commands"]) + ): + _LOGGER.error( + "Invalid %s device JSON file '%s': missing or invalid attribute 'commands'.", + device_class, + file_name, + ) + return False + commands = device_data["commands"] + + if "brightness" in device_data: + if not ( + isinstance(device_data["brightness"], list) + and len(device_data["brightness"]) + and sorted(device_data["brightness"]) == device_data["brightness"] + ): + _LOGGER.error( + "Invalid %s device JSON file '%s': invalid parameter 'brightness'", + device_class, + file_name, + ) + return False + color_modes["brightness"] = True + else: + for cmd in ["brightness", "dim", "brighten"]: + if cmd in commands: + _LOGGER.error( + "Invalid %s device JSON file '%s': invalid command '%s'", + device_class, + file_name, + cmd, + ) + return False + + if "colorTemperature" in device_data: + if not ( + isinstance(device_data["colorTemperature"], list) + and len(device_data["colorTemperature"]) + and sorted(device_data["colorTemperature"]) + == device_data["colorTemperature"] + ): + _LOGGER.error( + "Invalid %s device JSON file '%s': invalid parameter 'colorTemperature'", + device_class, + file_name, + ) + return False + color_modes["color_temp"] = True + else: + for cmd in ["colorTemperature", "colder", "warmer"]: + if cmd in commands: + _LOGGER.error( + "Invalid %s device JSON file '%s': invalid command '%s'", + device_class, + file_name, + cmd, + ) + return False + + if "brightness" in color_modes: + if "brightness" in commands: + if not ( + isinstance(commands["brightness"], list) + and len(commands["brightness"]) + ): + _LOGGER.error( + "Invalid %s device JSON file '%s': invalid commands 'brightness'", + device_class, + file_name, + ) + return False + + # check if same IR command were find in the device data + if not ( + len(device_data["brightness"]) == len(commands["brightness"]) + and all( + key in commands["brightness"] + for key in device_data["brightness"] + ) + ): + _LOGGER.info( + "Invalid %s device JSON file '%s': parameter 'brightness' and commands 'brightness' contain different keys.", + device_class, + file_name, + ) + return False + elif not ( + "dim" in commands + and isinstance(commands["dim"], str) + and "brighten" in commands + and isinstance(commands["brighten"], str) + ): + _LOGGER.info( + "Invalid %s device JSON file '%s': parameter 'brightness' require either command 'brightness' or 'dim' and 'brighten' commands to be defined.", + device_class, + file_name, + ) + return False + + elif len(color_modes) > 1: + _LOGGER.error( + "Invalid %s device JSON file '%s': Any color mode require parameter 'brightness' to be defined as well", + device_class, + file_name, + ) + return False + + if "color_temp" in color_modes: + if "colorTemperature" in commands: + if not ( + isinstance(commands["colorTemperature"], list) + and len(commands["colorTemperature"]) + ): + _LOGGER.error( + "Invalid %s device JSON file '%s': invalid commands 'colorTemperature'", + device_class, + file_name, + ) + return False + + # check if same IR command were find in the device data + if len(device_data["colorTemperature"]) == len( + commands["colorTemperature"] + ) and all( + key in commands["colorTemperature"] + for key in device_data["colorTemperature"] + ): + _LOGGER.info( + "Invalid %s device JSON file '%s': parameter 'colorTemperature' and commands 'colorTemperature' contain different keys.", + device_class, + file_name, + ) + return False + elif not ( + "colder" in commands + and isinstance(commands["colder"], str) + and "warmer" in commands + and isinstance(commands["warmer"], str) + ): + _LOGGER.info( + "Invalid %s device JSON file '%s': parameter 'colorTemperature' require either command 'colorTemperature' or 'warmer' and 'colder' to be defined.", + device_class, + file_name, + ) + return False + return True diff --git a/custom_components/smartir/light.py b/custom_components/smartir/light.py index ec7244bdd..d1e8eda32 100644 --- a/custom_components/smartir/light.py +++ b/custom_components/smartir/light.py @@ -14,23 +14,13 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from .smartir_helpers import closest_match_index +from .smartir_helpers import closest_match_index, closest_match_value from .smartir_entity import load_device_data_file, SmartIR, PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "SmartIR Light" -CMD_BRIGHTNESS_INCREASE = "brighten" -CMD_BRIGHTNESS_DECREASE = "dim" -CMD_COLOR_MODE_COLDER = "colder" -CMD_COLOR_MODE_WARMER = "warmer" -CMD_POWER_ON = "on" -CMD_POWER_OFF = "off" -CMD_NIGHTLIGHT = "night" -CMD_COLOR_TEMPERATURE = "colorTemperature" -CMD_BRIGHTNESS = "brightness" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} ) @@ -62,35 +52,22 @@ def __init__(self, hass: HomeAssistant, config: ConfigType, device_data): SmartIR.__init__(self, hass, config, device_data) self._brightness = None - self._colortemp = None - - self._brightnesses = device_data["brightness"] - self._colortemps = device_data["colorTemperature"] - - if CMD_COLOR_TEMPERATURE in self._commands or ( - CMD_COLOR_MODE_COLDER in self._commands - and CMD_COLOR_MODE_WARMER in self._commands - ): - self._colortemp = self.max_color_temp_kelvin - - if ( - CMD_NIGHTLIGHT in self._commands - or CMD_BRIGHTNESS in self._commands - or ( - CMD_BRIGHTNESS_INCREASE in self._commands - and CMD_BRIGHTNESS_DECREASE in self._commands - ) - ): - self._brightness = 100 - self._support_brightness = True - else: - self._support_brightness = False + self._color_temp = None + + self._brightness_list = device_data.get("brightness") + self._color_temp_list = device_data.get("colorTemperatures") - if self._colortemp: + if self._color_temp_list is not None: + # The light can be dimmed and its color temperature is present in the state. self._attr_supported_color_modes = [ColorMode.COLOR_TEMP] - elif self._support_brightness: + self._brightness = self._brightness_list[-1] + self._color_temp = self._color_temp_list[-1] + elif self._brightness_list is not None: + # The light can be dimmed. This mode must be the only supported mode if supported by the light. self._attr_supported_color_modes = [ColorMode.BRIGHTNESS] - elif CMD_POWER_OFF in self._commands and CMD_POWER_ON in self._commands: + self._brightness = self._brightness_list[-1] + else: + # The light can be turned on or off. This mode must be the only supported mode if supported by the light. self._attr_supported_color_modes = [ColorMode.ONOFF] async def async_added_to_hass(self): @@ -102,7 +79,7 @@ async def async_added_to_hass(self): if ATTR_BRIGHTNESS in last_state.attributes: self._brightness = last_state.attributes[ATTR_BRIGHTNESS] if ATTR_COLOR_TEMP_KELVIN in last_state.attributes: - self._colortemp = last_state.attributes[ATTR_COLOR_TEMP_KELVIN] + self._color_temp = last_state.attributes[ATTR_COLOR_TEMP_KELVIN] @property def color_mode(self): @@ -111,19 +88,19 @@ def color_mode(self): @property def color_temp_kelvin(self): - return self._colortemp + return self._color_temp @property def min_color_temp_kelvin(self): - if self._colortemps: - return self._colortemps[0] + if self._color_temp_list: + return self._color_temp_list[0] else: return None @property def max_color_temp_kelvin(self): - if self._colortemps: - return self._colortemps[-1] + if self._color_temp_list: + return self._color_temp_list[-1] else: return None @@ -150,155 +127,189 @@ def extra_state_attributes(self): "commands_encoding": self._commands_encoding, } - async def async_turn_on(self, **params): - did_something = False - # Turn the light on if off - if self._state != STATE_ON and not self._on_by_remote: - self._state = STATE_ON - if CMD_POWER_ON in self._commands: - did_something = True - await self.send_command(CMD_POWER_ON) - else: - if ATTR_COLOR_TEMP_KELVIN not in params: - _LOGGER.debug( - f"No power on command found, setting last color {self._colortemp}K" - ) - params[ATTR_COLOR_TEMP_KELVIN] = self._colortemp - if ATTR_BRIGHTNESS not in params: - _LOGGER.debug( - f"No power on command found, setting last brightness {self._brightness}" - ) - params[ATTR_BRIGHTNESS] = self._brightness - - if ( - ATTR_COLOR_TEMP_KELVIN in params - and ColorMode.COLOR_TEMP in self.supported_color_modes - ): - did_something = True - target = params.get(ATTR_COLOR_TEMP_KELVIN) - old_color_temp = closest_match_index(self._colortemp, self._colortemps) - new_color_temp = closest_match_index(target, self._colortemps) - final_color_temp = f"{self._colortemps[new_color_temp]}" - if ( - CMD_COLOR_TEMPERATURE in self._commands - and isinstance(self._commands[CMD_COLOR_TEMPERATURE], dict) - and final_color_temp in self._commands[CMD_COLOR_TEMPERATURE] - ): - _LOGGER.debug( - f"Changing color temp from {self._colortemp}K to {target}K using found remote command for {final_color_temp}K" - ) - found_command = self._commands[CMD_COLOR_TEMPERATURE][final_color_temp] - self._colortemp = self._colortemps[new_color_temp] - await self.send_remote_command(found_command) - else: - _LOGGER.debug( - f"Changing color temp from {self._colortemp}K step {old_color_temp} to {target}K step {new_color_temp}" - ) - steps = new_color_temp - old_color_temp - if steps < 0: - cmd = CMD_COLOR_MODE_WARMER - steps = abs(steps) - else: - cmd = CMD_COLOR_MODE_COLDER - - if steps > 0 and cmd: - # If we are heading for the highest or lowest value, - # take the opportunity to resync by issuing enough - # commands to go the full range. - if ( - new_color_temp == len(self._colortemps) - 1 - or new_color_temp == 0 - ): - steps = len(self._colortemps) - self._colortemp = self._colortemps[new_color_temp] - await self.send_command(cmd, steps) - - if ATTR_BRIGHTNESS in params and self._support_brightness: - # before checking the supported brightnesses, make a special case - # when a nightlight is fitted for brightness of 1 - if params.get(ATTR_BRIGHTNESS) == 1 and CMD_NIGHTLIGHT in self._commands: - self._brightness = 1 - self._state = STATE_ON - did_something = True - await self.send_command(CMD_NIGHTLIGHT) - - elif self._brightnesses: - did_something = True - target = params.get(ATTR_BRIGHTNESS) - old_brightness = closest_match_index( - self._brightness, self._brightnesses - ) - new_brightness = closest_match_index(target, self._brightnesses) - final_brightness = f"{self._brightnesses[new_brightness]}" - if ( - CMD_BRIGHTNESS in self._commands - and isinstance(self._commands[CMD_BRIGHTNESS], dict) - and final_brightness in self._commands[CMD_BRIGHTNESS] - ): - _LOGGER.debug( - f"Changing brightness from {self._brightness} to {target} using found remote command for {final_brightness}" - ) - found_command = self._commands[CMD_BRIGHTNESS][final_brightness] - self._brightness = self._brightnesses[new_brightness] - await self.send_remote_command(found_command) - else: - _LOGGER.debug( - f"Changing brightness from {self._brightness} step {old_brightness} to {target} step {new_brightness}" - ) - steps = new_brightness - old_brightness - if steps < 0: - cmd = CMD_BRIGHTNESS_DECREASE - steps = abs(steps) - else: - cmd = CMD_BRIGHTNESS_INCREASE + async def async_turn_on(self, **kwargs): + brightness = kwargs.get(ATTR_BRIGHTNESS) + color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN) - if steps > 0 and cmd: - # If we are heading for the highest or lowest value, - # take the opportunity to resync by issuing enough - # commands to go the full range. - if ( - new_brightness == len(self._brightnesses) - 1 - or new_brightness == 0 - ): - steps = len(self._brightnesses) - self._brightness = self._brightnesses[new_brightness] - await self.send_command(cmd, steps) - - # If we did nothing above, and the light is not detected as on - # already issue the on command, even though we think the light - # is on. This is because we may be out of sync due to use of the - # remote when we don't have anything to detect it. - # If we do have such monitoring, avoid issuing the command in case - # on and off are the same remote code. - if not did_something and not self._on_by_remote: - self._state = STATE_ON - await self.send_command(CMD_POWER_ON) - - self.async_write_ha_state() + if self._brightness_list is not None and brightness is None: + _LOGGER.debug( + "No power on brightness argument found, setting last brightness '%s'", + self._brightness, + ) + brightness = self._brightness + + if self._color_temp_list is not None and color_temp is None: + _LOGGER.debug( + "No power on color temperature argument found, setting last color temperature '%s'", + self._color_temp, + ) + color_temp = self._color_temp + + await self._send_command(STATE_ON, brightness, color_temp) async def async_turn_off(self): - if self._state != STATE_OFF: - self._state = STATE_OFF - await self.send_command(CMD_POWER_OFF) - self.async_write_ha_state() + await self._send_command(STATE_OFF) async def async_toggle(self): await (self.async_turn_on() if not self.is_on else self.async_turn_off()) - async def send_command(self, cmd, count=1): - if cmd not in self._commands: - _LOGGER.error(f"Unknown command '{cmd}'") - return - _LOGGER.debug(f"Sending {cmd} remote command {count} times.") - remote_cmd = self._commands.get(cmd) - await self.send_remote_command(remote_cmd, count) - - async def send_remote_command(self, remote_cmd, count=1): + async def send_command(self, state, brightness, color_temp): async with self._temp_lock: - self._on_by_remote = False + if self._power_sensor and self._state != state: + self._async_power_sensor_check_schedule(state) + try: - for _ in range(count): - await self._controller.send(remote_cmd) - await asyncio.sleep(self._delay) + if state == STATE_OFF: + if "off" in self._commands.keys() and isinstance( + self._commands["off"], str + ): + if ( + "on" in self._commands.keys() + and isinstance(self._commands["on"], str) + and self._commands["on"] == self._commands["off"] + and self._state == STATE_OFF + ): + # prevent to resend 'off' command if same as 'on' and device is already off + _LOGGER.debug( + "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", + self._state, + "off", + ) + else: + _LOGGER.debug("Found 'off' operation mode command.") + await self._controller.send(self._commands["off"]) + await asyncio.sleep(self._delay) + else: + _LOGGER.error("Missing device IR code for 'off' mode.") + return + else: + if "on" in self._commands.keys() and isinstance( + self._commands["on"], str + ): + if ( + "off" in self._commands.keys() + and isinstance(self._commands["off"], str) + and self._commands["off"] == self._commands["on"] + and self._state == STATE_ON + ): + # prevent to resend 'on' command if same as 'off' and device is already on + _LOGGER.debug( + "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", + self._state, + "on", + ) + else: + # if on code is not present, the on bit can be still set later in the all operation/fan codes""" + _LOGGER.debug("Found 'on' operation mode command.") + await self._controller.send(self._commands["on"]) + await asyncio.sleep(self._delay) + + if color_temp is not None: + if "colorTemperature" in self._commands and isinstance( + self._commands["colorTemperature"], dict + ): + color_temp = closest_match_value( + color_temp, self._color_temp_list + ) + _LOGGER.debug( + "Changing color temp from '%s'K to '%s'K using found remote command for {final_color_temp}K", + color_temp, + ) + await self._controller.send( + self._commands["colorTemperature"][str(color_temp)] + ) + await asyncio.sleep(self._delay) + else: + old_color_temp_index = closest_match_index( + self._color_temp, self._color_temp_list + ) + new_color_temp_index = closest_match_index( + color_temp, self._color_temp_list + ) + color_temp = self._color_temp_list[new_color_temp_index] + steps = new_color_temp_index - old_color_temp_index + if steps < 0: + cmd = "warmer" + steps = abs(steps) + else: + cmd = "colder" + + if ( + new_color_temp_index == len(self._color_temp_list) - 1 + or new_color_temp_index == 0 + ): + # If we are heading for the highest or lowest value, + # take the opportunity to resync by issuing enough + # commands to go the full range. + steps = len(self._color_temp_list) + + _LOGGER.debug( + "Changing color temp from '%s'K index {old_color_temp} to {target}K index {new_color_temp}" + ) + while steps > 0: + steps -= 1 + await self._controller.send(self._commands[cmd]) + await asyncio.sleep(self._delay) + + if brightness is not None: + # before checking the supported brightnesses, make a special case + # when a nightlight is fitted for brightness of 1 + if brightness == 1 and "night" in self._commands: + await self._controller.send(self._commands["night"]) + await asyncio.sleep(self._delay) + elif "brightness" in self._commands and isinstance( + self._commands["brightness"], dict + ): + brightness = closest_match_value( + brightness, self._brightness_list + ) + _LOGGER.debug( + "Changing color temp from '%s'K to '%s'K using found remote command for {final_color_temp}K", + color_temp, + ) + await self._controller.send( + self._commands["brightness"][str(brightness)] + ) + await asyncio.sleep(self._delay) + else: + old_brightness_index = closest_match_index( + self._brightness, self._brightness_list + ) + new_brightness_index = closest_match_index( + brightness, self._brightness_list + ) + brightness = self._brightness_list[new_brightness_index] + steps = new_brightness_index - old_brightness_index + if steps < 0: + cmd = "dim" + steps = abs(steps) + else: + cmd = "brighten" + + if ( + new_brightness_index == len(self._brightness_list) - 1 + or new_brightness_index == 0 + ): + # If we are heading for the highest or lowest value, + # take the opportunity to resync by issuing enough + # commands to go the full range. + steps = len(self._brightness_list) + + _LOGGER.debug( + "Changing color temp from '%s'K index {old_color_temp} to {target}K index {new_color_temp}" + ) + while steps > 0: + steps -= 1 + await self._controller.send(self._commands[cmd]) + await asyncio.sleep(self._delay) + + self._on_by_remote = False + self._state = state + self._brightness = brightness + self._color_temp = color_temp + self.async_write_ha_state() + except Exception as e: - _LOGGER.exception(e) + _LOGGER.exception( + "Exception raised in the in the _send_command '%s'", e + ) From 37b6336570a08a44531807c151a84c535a2e254a Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Sun, 19 Jan 2025 06:02:20 +0100 Subject: [PATCH 2/7] remove unused imports --- custom_components/smartir/fan.py | 4 ++-- custom_components/smartir/light.py | 2 +- custom_components/smartir/media_player.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/smartir/fan.py b/custom_components/smartir/fan.py index c5855f14d..b71a24c3f 100644 --- a/custom_components/smartir/fan.py +++ b/custom_components/smartir/fan.py @@ -9,8 +9,8 @@ DIRECTION_REVERSE, DIRECTION_FORWARD, ) -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN -from homeassistant.core import HomeAssistant, Event, EventStateChangedData +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType diff --git a/custom_components/smartir/light.py b/custom_components/smartir/light.py index d1e8eda32..d9bde81c5 100644 --- a/custom_components/smartir/light.py +++ b/custom_components/smartir/light.py @@ -10,7 +10,7 @@ LightEntity, ) from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, Event, EventStateChangedData +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType diff --git a/custom_components/smartir/media_player.py b/custom_components/smartir/media_player.py index 453611751..19e771cab 100644 --- a/custom_components/smartir/media_player.py +++ b/custom_components/smartir/media_player.py @@ -8,8 +8,8 @@ MediaPlayerEntityFeature, MediaType, ) -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN -from homeassistant.core import HomeAssistant, Event, EventStateChangedData +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType From 640917bc15d8e1669c24e219e9aeca5a40ddf567 Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Sun, 19 Jan 2025 16:00:02 +0100 Subject: [PATCH 3/7] unify send_command function name across classes multiple fixes for light implementation --- custom_components/smartir/climate.py | 4 ++-- custom_components/smartir/fan.py | 14 +++++------ custom_components/smartir/light.py | 29 ++++++++++++++--------- custom_components/smartir/media_player.py | 26 ++++++++++---------- 4 files changed, 38 insertions(+), 35 deletions(-) diff --git a/custom_components/smartir/climate.py b/custom_components/smartir/climate.py index 43165d639..4efa37a43 100644 --- a/custom_components/smartir/climate.py +++ b/custom_components/smartir/climate.py @@ -1,7 +1,7 @@ import asyncio import logging -import voluptuous as vol +import voluptuous as vol # type: ignore from numbers import Number from homeassistant.components.climate import ClimateEntity @@ -704,7 +704,7 @@ async def _send_command( except Exception as e: _LOGGER.exception( - "Exception raised in the in the _send_command '%s'", e + "Exception raised in the in the send_command '%s'", e ) async def _async_temp_sensor_changed( diff --git a/custom_components/smartir/fan.py b/custom_components/smartir/fan.py index b71a24c3f..de761c4bd 100644 --- a/custom_components/smartir/fan.py +++ b/custom_components/smartir/fan.py @@ -1,7 +1,7 @@ import asyncio import logging -import voluptuous as vol +import voluptuous as vol # type: ignore from homeassistant.components.fan import ( FanEntity, @@ -161,7 +161,7 @@ async def async_set_percentage(self, percentage: int) -> None: state = STATE_ON speed = percentage_to_ordered_list_item(self._speed_list, percentage) - await self._send_command( + await self.send_command( state, speed, self._current_direction, self._oscillating ) @@ -170,7 +170,7 @@ async def async_oscillate(self, oscillating: bool) -> None: if not self._support_flags & FanEntityFeature.OSCILLATE: return - await self._send_command( + await self.send_command( self._state, self._speed, self._current_direction, oscillating ) @@ -179,7 +179,7 @@ async def async_set_direction(self, direction: str): if not self._support_flags & FanEntityFeature.DIRECTION: return - await self._send_command(self._state, self._speed, direction, self._oscillating) + await self.send_command(self._state, self._speed, direction, self._oscillating) async def async_turn_on( self, percentage: int = None, preset_mode: str = None, **kwargs @@ -194,7 +194,7 @@ async def async_turn_off(self): """Turn off the fan.""" await self.async_set_percentage(0) - async def _send_command(self, state, speed, direction, oscillate): + async def send_command(self, state, speed, direction, oscillate): async with self._temp_lock: if self._power_sensor and self._state != state: @@ -281,6 +281,4 @@ async def _send_command(self, state, speed, direction, oscillate): self.async_write_ha_state() except Exception as e: - _LOGGER.exception( - "Exception raised in the in the _send_command '%s'", e - ) + _LOGGER.exception("Exception raised in the in the send_command '%s'", e) diff --git a/custom_components/smartir/light.py b/custom_components/smartir/light.py index d9bde81c5..86eedc103 100644 --- a/custom_components/smartir/light.py +++ b/custom_components/smartir/light.py @@ -1,7 +1,7 @@ import asyncio import logging -import voluptuous as vol +import voluptuous as vol # type: ignore from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -45,7 +45,7 @@ async def async_setup_platform( async_add_entities([SmartIRLight(hass, config, device_data)]) -class SmartIRLight(LightEntity, RestoreEntity): +class SmartIRLight(SmartIR, LightEntity, RestoreEntity): def __init__(self, hass: HomeAssistant, config: ConfigType, device_data): # Initialize SmartIR device @@ -76,9 +76,18 @@ async def async_added_to_hass(self): last_state = await self.async_get_last_state() if last_state is not None: - if ATTR_BRIGHTNESS in last_state.attributes: + if ( + ATTR_BRIGHTNESS in last_state.attributes + and self._brightness_list is not None + and last_state.attributes[ATTR_BRIGHTNESS] in self._brightness_list + ): self._brightness = last_state.attributes[ATTR_BRIGHTNESS] - if ATTR_COLOR_TEMP_KELVIN in last_state.attributes: + if ( + ATTR_COLOR_TEMP_KELVIN in last_state.attributes + and self._color_temp_list is not None + and last_state.attributes[ATTR_COLOR_TEMP_KELVIN] + in self._color_temp_list + ): self._color_temp = last_state.attributes[ATTR_COLOR_TEMP_KELVIN] @property @@ -128,8 +137,8 @@ def extra_state_attributes(self): } async def async_turn_on(self, **kwargs): - brightness = kwargs.get(ATTR_BRIGHTNESS) - color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN) + brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness) + color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN, self._color_temp) if self._brightness_list is not None and brightness is None: _LOGGER.debug( @@ -145,10 +154,10 @@ async def async_turn_on(self, **kwargs): ) color_temp = self._color_temp - await self._send_command(STATE_ON, brightness, color_temp) + await self.send_command(STATE_ON, brightness, color_temp) async def async_turn_off(self): - await self._send_command(STATE_OFF) + await self.send_command(STATE_OFF, self._brightness, self._color_temp) async def async_toggle(self): await (self.async_turn_on() if not self.is_on else self.async_turn_off()) @@ -310,6 +319,4 @@ async def send_command(self, state, brightness, color_temp): self.async_write_ha_state() except Exception as e: - _LOGGER.exception( - "Exception raised in the in the _send_command '%s'", e - ) + _LOGGER.exception("Exception raised in the in the send_command '%s'", e) diff --git a/custom_components/smartir/media_player.py b/custom_components/smartir/media_player.py index 19e771cab..445e86e31 100644 --- a/custom_components/smartir/media_player.py +++ b/custom_components/smartir/media_player.py @@ -1,7 +1,7 @@ import asyncio import logging -import voluptuous as vol +import voluptuous as vol # type: ignore from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( @@ -169,36 +169,36 @@ def extra_state_attributes(self): async def async_turn_off(self): """Turn the media player off.""" - await self._send_command(STATE_OFF, []) + await self.send_command(STATE_OFF, []) async def async_turn_on(self): """Turn the media player off.""" - await self._send_command(STATE_ON, []) + await self.send_command(STATE_ON, []) async def async_media_previous_track(self): """Send previous track command.""" - await self._send_command(self._state, [["previousChannel"]]) + await self.send_command(self._state, [["previousChannel"]]) async def async_media_next_track(self): """Send next track command.""" - await self._send_command(self._state, [["nextChannel"]]) + await self.send_command(self._state, [["nextChannel"]]) async def async_volume_down(self): """Turn volume down for media player.""" - await self._send_command(self._state, [["volumeDown"]]) + await self.send_command(self._state, [["volumeDown"]]) async def async_volume_up(self): """Turn volume up for media player.""" - await self._send_command(self._state, [["volumeUp"]]) + await self.send_command(self._state, [["volumeUp"]]) async def async_mute_volume(self, mute): """Mute the volume.""" - await self._send_command(self._state, [["mute"]]) + await self.send_command(self._state, [["mute"]]) async def async_select_source(self, source): """Select channel from source.""" self._source = source - await self._send_command(self._state, [["sources", source]]) + await self.send_command(self._state, [["sources", source]]) async def async_play_media(self, media_type, media_id, **kwargs): """Support channel change through play_media service.""" @@ -213,9 +213,9 @@ async def async_play_media(self, media_type, media_id, **kwargs): commands = [] for digit in media_id: commands.append(["sources", "Channel {}".format(digit)]) - await self._send_command(STATE_ON, commands) + await self.send_command(STATE_ON, commands) - async def _send_command(self, state, commands): + async def send_command(self, state, commands): async with self._temp_lock: if self._power_sensor and self._state != state: @@ -300,6 +300,4 @@ async def _send_command(self, state, commands): self.async_write_ha_state() except Exception as e: - _LOGGER.exception( - "Exception raised in the in the _send_command '%s'", e - ) + _LOGGER.exception("Exception raised in the in the send_command '%s'", e) From 5570cf0a70bc8153a89cc8bbb85eace6cd6718fe Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Sun, 19 Jan 2025 16:08:17 +0100 Subject: [PATCH 4/7] fixed potential problematic comment formating fixed colorTeperature typo in the light --- custom_components/smartir/climate.py | 2 +- custom_components/smartir/fan.py | 2 +- custom_components/smartir/light.py | 4 ++-- custom_components/smartir/media_player.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/smartir/climate.py b/custom_components/smartir/climate.py index 4efa37a43..e75615082 100644 --- a/custom_components/smartir/climate.py +++ b/custom_components/smartir/climate.py @@ -535,7 +535,7 @@ async def _send_command( "on", ) else: - # if on code is not present, the on bit can be still set later in the all operation/fan codes""" + # if on code is not present, the on bit can be still set later in the all operation codes _LOGGER.debug("Found 'on' operation mode command.") await self._controller.send(self._commands["on"]) await asyncio.sleep(self._delay) diff --git a/custom_components/smartir/fan.py b/custom_components/smartir/fan.py index de761c4bd..a6493e760 100644 --- a/custom_components/smartir/fan.py +++ b/custom_components/smartir/fan.py @@ -241,7 +241,7 @@ async def send_command(self, state, speed, direction, oscillate): "on", ) else: - # if on code is not present, the on bit can be still set later in the all operation/fan codes""" + # if on code is not present, the on bit can be still set later in the all operation codes _LOGGER.debug("Found 'on' operation mode command.") await self._controller.send(self._commands["on"]) await asyncio.sleep(self._delay) diff --git a/custom_components/smartir/light.py b/custom_components/smartir/light.py index 86eedc103..2d911e464 100644 --- a/custom_components/smartir/light.py +++ b/custom_components/smartir/light.py @@ -55,7 +55,7 @@ def __init__(self, hass: HomeAssistant, config: ConfigType, device_data): self._color_temp = None self._brightness_list = device_data.get("brightness") - self._color_temp_list = device_data.get("colorTemperatures") + self._color_temp_list = device_data.get("colorTemperature") if self._color_temp_list is not None: # The light can be dimmed and its color temperature is present in the state. @@ -208,7 +208,7 @@ async def send_command(self, state, brightness, color_temp): "on", ) else: - # if on code is not present, the on bit can be still set later in the all operation/fan codes""" + # if on code is not present, the on bit can be still set later in the all operation codes _LOGGER.debug("Found 'on' operation mode command.") await self._controller.send(self._commands["on"]) await asyncio.sleep(self._delay) diff --git a/custom_components/smartir/media_player.py b/custom_components/smartir/media_player.py index 445e86e31..6eb22b717 100644 --- a/custom_components/smartir/media_player.py +++ b/custom_components/smartir/media_player.py @@ -262,7 +262,7 @@ async def send_command(self, state, commands): "on", ) else: - # if on code is not present, the on bit can be still set later in the all operation/fan codes""" + # if on code is not present, the on bit can be still set later in the all operation codes _LOGGER.debug("Found 'on' operation mode command.") await self._controller.send(self._commands["on"]) await asyncio.sleep(self._delay) From 64bd8e8e1027008bc4e20ad4e85efae13fecbe86 Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Sun, 19 Jan 2025 16:19:12 +0100 Subject: [PATCH 5/7] fix missing SmartIR class in fan and media_player classes --- custom_components/smartir/fan.py | 2 +- custom_components/smartir/media_player.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/smartir/fan.py b/custom_components/smartir/fan.py index a6493e760..f58191266 100644 --- a/custom_components/smartir/fan.py +++ b/custom_components/smartir/fan.py @@ -52,7 +52,7 @@ async def async_setup_platform( async_add_entities([SmartIRFan(hass, config, device_data)]) -class SmartIRFan(FanEntity, RestoreEntity): +class SmartIRFan(SmartIR, FanEntity, RestoreEntity): _enable_turn_on_off_backwards_compatibility = False def __init__(self, hass: HomeAssistant, config: ConfigType, device_data): diff --git a/custom_components/smartir/media_player.py b/custom_components/smartir/media_player.py index 6eb22b717..10a2a8884 100644 --- a/custom_components/smartir/media_player.py +++ b/custom_components/smartir/media_player.py @@ -51,7 +51,7 @@ async def async_setup_platform( async_add_entities([SmartIRMediaPlayer(hass, config, device_data)]) -class SmartIRMediaPlayer(MediaPlayerEntity, RestoreEntity): +class SmartIRMediaPlayer(SmartIR, MediaPlayerEntity, RestoreEntity): def __init__(self, hass: HomeAssistant, config: ConfigType, device_data): # Initialize SmartIR device From 44f8e3b5a9f1a7599a6b0459fbe38d61902479b5 Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Mon, 20 Jan 2025 07:13:27 +0100 Subject: [PATCH 6/7] created generic power_on/power_off functions added optimistic_state configuration parameter added generic update_entity callback fixed light class logging messages --- custom_components/smartir/climate.py | 52 ++------------- custom_components/smartir/fan.py | 44 +------------ custom_components/smartir/light.py | 66 ++++++------------- custom_components/smartir/media_player.py | 44 +------------ custom_components/smartir/smartir_entity.py | 72 ++++++++++++++++++--- 5 files changed, 92 insertions(+), 186 deletions(-) diff --git a/custom_components/smartir/climate.py b/custom_components/smartir/climate.py index e75615082..01d6ae509 100644 --- a/custom_components/smartir/climate.py +++ b/custom_components/smartir/climate.py @@ -177,6 +177,8 @@ def __init__(self, hass: HomeAssistant, config: ConfigType, device_data): self._support_flags = self._support_flags | ClimateEntityFeature.SWING_MODE self._swing_mode = self._swing_modes[0] + self.__update_entity = self._async_update_hvac_action + async def async_added_to_hass(self): """Run when entity about to be added.""" await super().async_added_to_hass() @@ -493,52 +495,10 @@ async def _send_command( _LOGGER.debug("Found '%s' operation mode command.", off_mode) await self._controller.send(self._commands[off_mode]) await asyncio.sleep(self._delay) - elif "off" in self._commands.keys() and isinstance( - self._commands["off"], str - ): - if ( - "on" in self._commands.keys() - and isinstance(self._commands["on"], str) - and self._commands["on"] == self._commands["off"] - and self._state == STATE_OFF - ): - # prevent to resend 'off' command if same as 'on' and device is already off - _LOGGER.debug( - "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", - self._state, - "off", - ) - else: - _LOGGER.debug("Found 'off' operation mode command.") - await self._controller.send(self._commands["off"]) - await asyncio.sleep(self._delay) else: - _LOGGER.error( - "Missing device IR code for 'off' or '%s' operation mode.", - off_mode, - ) - return + await self._async_power_off() else: - if "on" in self._commands.keys() and isinstance( - self._commands["on"], str - ): - if ( - "off" in self._commands.keys() - and isinstance(self._commands["off"], str) - and self._commands["off"] == self._commands["on"] - and self._state == STATE_ON - ): - # prevent to resend 'on' command if same as 'off' and device is already on - _LOGGER.debug( - "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", - self._state, - "on", - ) - else: - # if on code is not present, the on bit can be still set later in the all operation codes - _LOGGER.debug("Found 'on' operation mode command.") - await self._controller.send(self._commands["on"]) - await asyncio.sleep(self._delay) + await self._async_power_on() commands = self._commands if hvac_mode in commands.keys(): @@ -703,9 +663,7 @@ async def _send_command( self.async_write_ha_state() except Exception as e: - _LOGGER.exception( - "Exception raised in the in the send_command '%s'", e - ) + _LOGGER.exception("Exception raised in the in the send_command '%s'", e) async def _async_temp_sensor_changed( self, event: Event[EventStateChangedData] diff --git a/custom_components/smartir/fan.py b/custom_components/smartir/fan.py index f58191266..a2f0a9633 100644 --- a/custom_components/smartir/fan.py +++ b/custom_components/smartir/fan.py @@ -202,49 +202,9 @@ async def send_command(self, state, speed, direction, oscillate): try: if state == STATE_OFF: - if "off" in self._commands.keys() and isinstance( - self._commands["off"], str - ): - if ( - "on" in self._commands.keys() - and isinstance(self._commands["on"], str) - and self._commands["on"] == self._commands["off"] - and self._state == STATE_OFF - ): - # prevent to resend 'off' command if same as 'on' and device is already off - _LOGGER.debug( - "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", - self._state, - "off", - ) - else: - _LOGGER.debug("Found 'off' operation mode command.") - await self._controller.send(self._commands["off"]) - await asyncio.sleep(self._delay) - else: - _LOGGER.error("Missing device IR code for 'off' mode.") - return + await self._async_power_off() else: - if "on" in self._commands.keys() and isinstance( - self._commands["on"], str - ): - if ( - "off" in self._commands.keys() - and isinstance(self._commands["off"], str) - and self._commands["off"] == self._commands["on"] - and self._state == STATE_ON - ): - # prevent to resend 'on' command if same as 'off' and device is already on - _LOGGER.debug( - "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", - self._state, - "on", - ) - else: - # if on code is not present, the on bit can be still set later in the all operation codes - _LOGGER.debug("Found 'on' operation mode command.") - await self._controller.send(self._commands["on"]) - await asyncio.sleep(self._delay) + await self._async_power_on() if oscillate: if "oscillate" in self._commands: diff --git a/custom_components/smartir/light.py b/custom_components/smartir/light.py index 2d911e464..bb22103ab 100644 --- a/custom_components/smartir/light.py +++ b/custom_components/smartir/light.py @@ -169,49 +169,9 @@ async def send_command(self, state, brightness, color_temp): try: if state == STATE_OFF: - if "off" in self._commands.keys() and isinstance( - self._commands["off"], str - ): - if ( - "on" in self._commands.keys() - and isinstance(self._commands["on"], str) - and self._commands["on"] == self._commands["off"] - and self._state == STATE_OFF - ): - # prevent to resend 'off' command if same as 'on' and device is already off - _LOGGER.debug( - "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", - self._state, - "off", - ) - else: - _LOGGER.debug("Found 'off' operation mode command.") - await self._controller.send(self._commands["off"]) - await asyncio.sleep(self._delay) - else: - _LOGGER.error("Missing device IR code for 'off' mode.") - return + await self._async_power_off() else: - if "on" in self._commands.keys() and isinstance( - self._commands["on"], str - ): - if ( - "off" in self._commands.keys() - and isinstance(self._commands["off"], str) - and self._commands["off"] == self._commands["on"] - and self._state == STATE_ON - ): - # prevent to resend 'on' command if same as 'off' and device is already on - _LOGGER.debug( - "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", - self._state, - "on", - ) - else: - # if on code is not present, the on bit can be still set later in the all operation codes - _LOGGER.debug("Found 'on' operation mode command.") - await self._controller.send(self._commands["on"]) - await asyncio.sleep(self._delay) + await self._async_power_on() if color_temp is not None: if "colorTemperature" in self._commands and isinstance( @@ -221,7 +181,8 @@ async def send_command(self, state, brightness, color_temp): color_temp, self._color_temp_list ) _LOGGER.debug( - "Changing color temp from '%s'K to '%s'K using found remote command for {final_color_temp}K", + "Changing color temp from '%s'K to '%s'K using command found in 'colorTemperature' commands", + self._color_temp, color_temp, ) await self._controller.send( @@ -253,7 +214,12 @@ async def send_command(self, state, brightness, color_temp): steps = len(self._color_temp_list) _LOGGER.debug( - "Changing color temp from '%s'K index {old_color_temp} to {target}K index {new_color_temp}" + "Changing color temp from '%s'K index '%s' to '%s'K index '%s' using command '%s'", + self._color_temp, + old_color_temp_index, + color_temp, + new_color_temp_index, + cmd, ) while steps > 0: steps -= 1 @@ -273,8 +239,9 @@ async def send_command(self, state, brightness, color_temp): brightness, self._brightness_list ) _LOGGER.debug( - "Changing color temp from '%s'K to '%s'K using found remote command for {final_color_temp}K", - color_temp, + "Changing brightness from '%s' to '%s' using command found in 'brightness' commands", + self._brightness, + brightness, ) await self._controller.send( self._commands["brightness"][str(brightness)] @@ -305,7 +272,12 @@ async def send_command(self, state, brightness, color_temp): steps = len(self._brightness_list) _LOGGER.debug( - "Changing color temp from '%s'K index {old_color_temp} to {target}K index {new_color_temp}" + "Changing brightness from '%s'K index '%s' to '%s'K index '%s' using command '%s'", + self._brightness, + old_brightness_index, + brightness, + new_brightness_index, + cmd, ) while steps > 0: steps -= 1 diff --git a/custom_components/smartir/media_player.py b/custom_components/smartir/media_player.py index 10a2a8884..bb9559ff6 100644 --- a/custom_components/smartir/media_player.py +++ b/custom_components/smartir/media_player.py @@ -223,49 +223,9 @@ async def send_command(self, state, commands): try: if state == STATE_OFF: - if "off" in self._commands.keys() and isinstance( - self._commands["off"], str - ): - if ( - "on" in self._commands.keys() - and isinstance(self._commands["on"], str) - and self._commands["on"] == self._commands["off"] - and self._state == STATE_OFF - ): - # prevent to resend 'off' command if same as 'on' and device is already off - _LOGGER.debug( - "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", - self._state, - "off", - ) - else: - _LOGGER.debug("Found 'off' operation mode command.") - await self._controller.send(self._commands["off"]) - await asyncio.sleep(self._delay) - else: - _LOGGER.error("Missing device IR code for 'off' mode.") - return + await self._async_power_off() else: - if "on" in self._commands.keys() and isinstance( - self._commands["on"], str - ): - if ( - "off" in self._commands.keys() - and isinstance(self._commands["off"], str) - and self._commands["off"] == self._commands["on"] - and self._state == STATE_ON - ): - # prevent to resend 'on' command if same as 'off' and device is already on - _LOGGER.debug( - "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", - self._state, - "on", - ) - else: - # if on code is not present, the on bit can be still set later in the all operation codes - _LOGGER.debug("Found 'on' operation mode command.") - await self._controller.send(self._commands["on"]) - await asyncio.sleep(self._delay) + await self._async_power_on() for keys in commands: data = self._commands diff --git a/custom_components/smartir/smartir_entity.py b/custom_components/smartir/smartir_entity.py index 085457537..efa743edf 100644 --- a/custom_components/smartir/smartir_entity.py +++ b/custom_components/smartir/smartir_entity.py @@ -2,14 +2,10 @@ import logging import os.path -import voluptuous as vol +import voluptuous as vol # type: ignore from homeassistant.core import HomeAssistant, Event, EventStateChangedData, callback from homeassistant.components.climate import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, - STATE_ON, - STATE_OFF -) +from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF from homeassistant.helpers.event import async_track_state_change_event, async_call_later import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -20,12 +16,14 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_DELAY = 0.5 +DEFAULT_OPTIMISTIC_STATE = True DEFAULT_POWER_SENSOR_DELAY = 10 CONF_UNIQUE_ID = "unique_id" CONF_DEVICE_CODE = "device_code" CONF_CONTROLLER_DATA = "controller_data" CONF_DELAY = "delay" +CONF_OPTIMISTIC_STATE = "optimistic" CONF_POWER_SENSOR = "power_sensor" CONF_POWER_SENSOR_DELAY = "power_sensor_delay" CONF_POWER_SENSOR_RESTORE_STATE = "power_sensor_restore_state" @@ -36,6 +34,9 @@ vol.Required(CONF_DEVICE_CODE): cv.positive_int, vol.Required(CONF_CONTROLLER_DATA): get_controller_schema(vol, cv), vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_float, + vol.Optional( + CONF_OPTIMISTIC_STATE, default=DEFAULT_OPTIMISTIC_STATE + ): cv.boolean, vol.Optional(CONF_POWER_SENSOR): cv.entity_id, vol.Optional( CONF_POWER_SENSOR_DELAY, default=DEFAULT_POWER_SENSOR_DELAY @@ -132,6 +133,7 @@ def __init__(self, hass: HomeAssistant, config: ConfigType, device_data): self._device_code = config.get(CONF_DEVICE_CODE) self._controller_data = config.get(CONF_CONTROLLER_DATA) self._delay = config.get(CONF_DELAY) + self._optimistic_state = config.get(CONF_OPTIMISTIC_STATE) self._power_sensor = config.get(CONF_POWER_SENSOR) self._power_sensor_delay = config.get(CONF_POWER_SENSOR_DELAY) self._power_sensor_restore_state = config.get(CONF_POWER_SENSOR_RESTORE_STATE) @@ -140,6 +142,7 @@ def __init__(self, hass: HomeAssistant, config: ConfigType, device_data): self._on_by_remote = False self._power_sensor_check_expect = None self._power_sensor_check_cancel = None + self._update_entity = None self._manufacturer = device_data["manufacturer"] self._supported_models = device_data["supportedModels"] @@ -190,12 +193,14 @@ async def _async_power_sensor_changed( if new_state.state == STATE_ON and self._state != STATE_ON: self._state = STATE_ON self._on_by_remote = True - await self._async_update_hvac_action() + if self._update_entity is not None: + await self._update_entity() elif new_state.state == STATE_OFF: self._on_by_remote = False if self._state != STATE_OFF: self._state = STATE_OFF - await self._async_update_hvac_action() + if self._update_entity is not None: + await self._update_entity() self.async_write_ha_state() @callback @@ -237,6 +242,57 @@ def _async_power_sensor_check(*_): ) _LOGGER.debug("Scheduled power sensor check for '%s' state", state) + async def _async_power_on(self): + if "on" in self._commands.keys() and isinstance(self._commands["on"], str): + if ( + self._power_sensor is not None or self._optimistic_state + ) and self._state == STATE_ON: + # prevent to send 'on' command if assumed state is on and we use power sensors or assumed state is optimistic + _LOGGER.debug( + "As state is based on power sensors or optimistic state is set to 'True' and current assumed state is '%s', skipping sending '%s' command", + self._state, + "on", + ) + elif ( + "off" in self._commands.keys() + and isinstance(self._commands["off"], str) + and self._commands["off"] == self._commands["on"] + and self._state == STATE_ON + ): + # prevent to resend 'on' command if same as 'off' and device is already on + _LOGGER.debug( + "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", + self._state, + "on", + ) + else: + # if on code is not present, the on bit can be still set later in the all operation codes + _LOGGER.debug("Found 'on' operation mode command.") + await self._controller.send(self._commands["on"]) + await asyncio.sleep(self._delay) + + async def _async_power_off(self): + if "off" in self._commands.keys() and isinstance(self._commands["off"], str): + if ( + "on" in self._commands.keys() + and isinstance(self._commands["on"], str) + and self._commands["on"] == self._commands["off"] + and self._state == STATE_OFF + ): + # prevent to resend 'off' command if same as 'on' and device is already off + _LOGGER.debug( + "As 'on' and 'off' commands are identical and device is already in requested '%s' state, skipping sending '%s' command", + self._state, + "off", + ) + else: + _LOGGER.debug("Found 'off' operation mode command.") + await self._controller.send(self._commands["off"]) + await asyncio.sleep(self._delay) + else: + _LOGGER.error("Missing device IR code for 'off' mode.") + return + @property def unique_id(self): """Return a unique ID.""" From 4be9603c69614b77da01b1d6f6b30add258ef55b Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Mon, 20 Jan 2025 10:31:51 +0100 Subject: [PATCH 7/7] use optimistic_state also for case when on and off commands are same logging improvements --- custom_components/smartir/smartir_entity.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/custom_components/smartir/smartir_entity.py b/custom_components/smartir/smartir_entity.py index efa743edf..480bde1e3 100644 --- a/custom_components/smartir/smartir_entity.py +++ b/custom_components/smartir/smartir_entity.py @@ -258,6 +258,7 @@ async def _async_power_on(self): and isinstance(self._commands["off"], str) and self._commands["off"] == self._commands["on"] and self._state == STATE_ON + and self._optimistic_state ): # prevent to resend 'on' command if same as 'off' and device is already on _LOGGER.debug( @@ -266,10 +267,14 @@ async def _async_power_on(self): "on", ) else: - # if on code is not present, the on bit can be still set later in the all operation codes - _LOGGER.debug("Found 'on' operation mode command.") + _LOGGER.debug("Sending 'on' command.") await self._controller.send(self._commands["on"]) await asyncio.sleep(self._delay) + else: + # if on code is not present, the on bit can be still set later in the all functional codes + _LOGGER.debug( + "Missing device IR code for 'on' command, this may not be and issue as an 'on' bit could be set in functional codes." + ) async def _async_power_off(self): if "off" in self._commands.keys() and isinstance(self._commands["off"], str): @@ -286,12 +291,11 @@ async def _async_power_off(self): "off", ) else: - _LOGGER.debug("Found 'off' operation mode command.") + _LOGGER.debug("Sending 'off' command.") await self._controller.send(self._commands["off"]) await asyncio.sleep(self._delay) else: - _LOGGER.error("Missing device IR code for 'off' mode.") - return + _LOGGER.error("Missing device IR code for 'off' command.") @property def unique_id(self):