From 60cc1832b49a0f4a0a149e55eb31821cccf2afd7 Mon Sep 17 00:00:00 2001 From: litinoveweedle <15144712+litinoveweedle@users.noreply.github.com> Date: Sat, 21 Sep 2024 22:12:43 +0200 Subject: [PATCH] initial light support refactoring for current SmartIR implementation --- custom_components/smartir/__init__.py | 27 ++++ custom_components/smartir/light.py | 193 +++++++++++++------------- docs/LIGHT_CODES.md | 6 + test_device_data.py | 3 +- 4 files changed, 132 insertions(+), 97 deletions(-) create mode 100644 docs/LIGHT_CODES.md diff --git a/custom_components/smartir/__init__.py b/custom_components/smartir/__init__.py index 4626a93e..038c5475 100644 --- a/custom_components/smartir/__init__.py +++ b/custom_components/smartir/__init__.py @@ -171,6 +171,11 @@ async def check_file(file_name, device_data, device_class, check_data): file_name, device_data, device_class, check_data ): return True + elif device_class == "light": + if DeviceData.check_file_light( + file_name, device_data, device_class, check_data + ): + return True return False @staticmethod @@ -532,6 +537,11 @@ def check_file_fan(file_name, device_data, device_class, check_data): def check_file_media_player(file_name, device_data, device_class, check_data): return True + @staticmethod + def check_file_light(file_name, device_data, device_class, check_data): + return True + + # round to given precision @staticmethod def precision_round(number, precision): if precision == 0.1: @@ -544,3 +554,20 @@ def precision_round(number, precision): return round(float(number) / int(precision)) * int(precision) else: return None + + # find the closest match in a sorted list + @staticmethod + def closest_match(value, list): + prev_val = None + for index, entry in enumerate(list): + if entry > (value or 0): + if prev_val is None: + return index + diff_lo = value - prev_val + diff_hi = entry - value + if diff_lo < diff_hi: + return index - 1 + return index + prev_val = entry + + return len(list) - 1 diff --git a/custom_components/smartir/light.py b/custom_components/smartir/light.py index dd8e8566..3aaa2484 100644 --- a/custom_components/smartir/light.py +++ b/custom_components/smartir/light.py @@ -1,7 +1,5 @@ import asyncio -import json import logging -import os.path import voluptuous as vol @@ -12,28 +10,28 @@ LightEntity, PLATFORM_SCHEMA, ) -from homeassistant.const import ( - CONF_NAME, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import callback -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant, Event, EventStateChangedData, callback +from homeassistant.helpers.event import async_track_state_change_event, async_call_later import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity -from . import COMPONENT_ABS_DIR, Helper -from .controller import get_controller +from homeassistant.helpers.typing import ConfigType +from . import DeviceData +from .controller import get_controller, get_controller_schema _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "SmartIR Light" DEFAULT_DELAY = 0.5 +DEFAULT_POWER_SENSOR_DELAY = 10 CONF_UNIQUE_ID = "unique_id" CONF_DEVICE_CODE = "device_code" CONF_CONTROLLER_DATA = "controller_data" CONF_DELAY = "delay" CONF_POWER_SENSOR = "power_sensor" +CONF_POWER_SENSOR_DELAY = "power_sensor_delay" +CONF_POWER_SENSOR_RESTORE_STATE = "power_sensor_restore_state" CMD_BRIGHTNESS_INCREASE = "brighten" CMD_BRIGHTNESS_DECREASE = "dim" @@ -48,84 +46,39 @@ vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_DEVICE_CODE): cv.positive_int, - vol.Required(CONF_CONTROLLER_DATA): cv.string, + vol.Required(CONF_CONTROLLER_DATA): get_controller_schema(vol, cv), vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.string, vol.Optional(CONF_POWER_SENSOR): cv.entity_id, + vol.Optional( + CONF_POWER_SENSOR_DELAY, default=DEFAULT_POWER_SENSOR_DELAY + ): cv.positive_int, + vol.Optional(CONF_POWER_SENSOR_RESTORE_STATE, default=True): cv.boolean, } ) async def async_setup_platform( - hass, - config, - async_add_entities, - discovery_info=None, + hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None ): """Set up the IR Light platform.""" - device_code = config.get(CONF_DEVICE_CODE) - device_files_subdir = os.path.join("codes", "light") - device_files_absdir = os.path.join(COMPONENT_ABS_DIR, device_files_subdir) - - if not os.path.isdir(device_files_absdir): - os.makedirs(device_files_absdir) - - device_json_filename = str(device_code) + ".json" - device_json_path = os.path.join(device_files_absdir, device_json_filename) - - if not os.path.exists(device_json_path): - _LOGGER.warning( - "Couldn't find the device Json file. The component " - "will try to download it from the Github repo." + _LOGGER.debug("Setting up the SmartIR light platform") + if not ( + device_data := await DeviceData.load_file( + config.get(CONF_DEVICE_CODE), + "light", + {}, + hass, ) - - try: - codes_source = ( - "https://raw.githubusercontent.com/" - "smartHomeHub/SmartIR/master/" - "codes/light/{}.json" - ) - - await Helper.downloader( - codes_source.format(device_code), - device_json_path, - ) - except Exception: - _LOGGER.error( - "There was an error while downloading the device Json file. " - "Please check your internet connection or if the device code " - "exists on GitHub. If the problem still exists please " - "place the file manually in the proper directory." - ) - return - - with open(device_json_path) as j: - try: - device_data = json.load(j) - except Exception: - _LOGGER.error("The device JSON file is invalid") - return + ): + _LOGGER.error("SmartIR light device data init failed!") + return async_add_entities([SmartIRLight(hass, config, device_data)]) -# find the closest match in a sorted list -def closest_match(value, list): - prev_val = None - for index, entry in enumerate(list): - if entry > (value or 0): - if prev_val is None: - return index - diff_lo = value - prev_val - diff_hi = entry - value - if diff_lo < diff_hi: - return index - 1 - return index - prev_val = entry - - return len(list) - 1 - - class SmartIRLight(LightEntity, RestoreEntity): + _attr_should_poll = False + def __init__(self, hass, config, device_data): self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) @@ -134,6 +87,16 @@ def __init__(self, hass, config, device_data): self._controller_data = config.get(CONF_CONTROLLER_DATA) self._delay = config.get(CONF_DELAY) 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) + + self._power = STATE_ON + self._brightness = None + self._colortemp = None + self._on_by_remote = False + self._support_color_mode = ColorMode.UNKNOWN + self._power_sensor_check_expect = None + self._power_sensor_check_cancel = None self._manufacturer = device_data["manufacturer"] self._supported_models = device_data["supportedModels"] @@ -143,14 +106,6 @@ def __init__(self, hass, config, device_data): self._colortemps = device_data["colorTemperature"] self._commands = device_data["commands"] - self._power = STATE_ON - self._brightness = None - self._colortemp = None - - self._temp_lock = asyncio.Lock() - self._on_by_remote = False - self._support_color_mode = ColorMode.UNKNOWN - if ( CMD_COLORMODE_COLDER in self._commands and CMD_COLORMODE_WARMER in self._commands @@ -176,6 +131,9 @@ def __init__(self, hass, config, device_data): ): self._support_color_mode = ColorMode.ONOFF + # Init exclusive lock for sending IR commands + self._temp_lock = asyncio.Lock() + # Init the IR/RF controller self._controller = get_controller( self.hass, @@ -268,8 +226,8 @@ async def async_turn_on(self, **params): and ColorMode.COLOR_TEMP == self._support_color_mode ): target = params.get(ATTR_COLOR_TEMP_KELVIN) - old_color_temp = closest_match(self._colortemp, self._colortemps) - new_color_temp = closest_match(target, self._colortemps) + old_color_temp = DeviceData.closest_match(self._colortemp, self._colortemps) + new_color_temp = DeviceData.closest_match(target, self._colortemps) _LOGGER.debug( f"Changing color temp from {self._colortemp}K step {old_color_temp} to {target}K step {new_color_temp}" ) @@ -302,8 +260,10 @@ async def async_turn_on(self, **params): elif self._brightnesses: target = params.get(ATTR_BRIGHTNESS) - old_brightness = closest_match(self._brightness, self._brightnesses) - new_brightness = closest_match(target, self._brightnesses) + old_brightness = DeviceData.closest_match( + self._brightness, self._brightnesses + ) + new_brightness = DeviceData.closest_match(target, self._brightnesses) did_something = True _LOGGER.debug( f"Changing brightness from {self._brightness} step {old_brightness} to {target} step {new_brightness}" @@ -361,21 +321,62 @@ async def send_command(self, cmd, count=1): except Exception as e: _LOGGER.exception(e) - @callback - async def _async_power_sensor_changed(self, event): + async def _async_power_sensor_changed( + self, event: Event[EventStateChangedData] + ) -> None: """Handle power sensor changes.""" + old_state = event.data["old_state"] new_state = event.data["new_state"] if new_state is None: return - old_state = event.data["old_state"] - if new_state.state == old_state.state: + + if old_state is not None and new_state.state == old_state.state: return - if new_state.state == STATE_ON: + if new_state.state == STATE_ON and self._state == STATE_OFF: + self._state = STATE_ON self._on_by_remote = True - await self.async_write_ha_state() - - if new_state.state == STATE_OFF: + elif new_state.state == STATE_OFF: self._on_by_remote = False - self._power = STATE_OFF - await self.async_write_ha_state() \ No newline at end of file + if self._state == STATE_ON: + self._state = STATE_OFF + self.async_write_ha_state() + + @callback + def _async_power_sensor_check_schedule(self, state): + if self._power_sensor_check_cancel: + self._power_sensor_check_cancel() + self._power_sensor_check_cancel = None + self._power_sensor_check_expect = None + + @callback + def _async_power_sensor_check(*_): + self._power_sensor_check_cancel = None + expected_state = self._power_sensor_check_expect + self._power_sensor_check_expect = None + current_state = getattr( + self.hass.states.get(self._power_sensor), "state", None + ) + _LOGGER.debug( + "Executing power sensor check for expected state '%s', current state '%s'.", + expected_state, + current_state, + ) + + if ( + expected_state in [STATE_ON, STATE_OFF] + and current_state in [STATE_ON, STATE_OFF] + and expected_state != current_state + ): + self._state = current_state + _LOGGER.debug( + "Power sensor check failed, reverted device state to '%s'.", + self._state, + ) + self.async_write_ha_state() + + self._power_sensor_check_expect = state + self._power_sensor_check_cancel = async_call_later( + self.hass, self._power_sensor_delay, _async_power_sensor_check + ) + _LOGGER.debug("Scheduled power sensor check for '%s' state.", state) diff --git a/docs/LIGHT_CODES.md b/docs/LIGHT_CODES.md new file mode 100644 index 00000000..365c0298 --- /dev/null +++ b/docs/LIGHT_CODES.md @@ -0,0 +1,6 @@ +## Available codes for Light devices + +The following are the code files created by the amazing people in the community. Before you start creating your own code file, try if one of them works for your device. **Please clone this repo and open Pull Request to include your own working and not included codes in the supported models.** Contributing to your own code files is most welcome. + + + \ No newline at end of file diff --git a/test_device_data.py b/test_device_data.py index f63408e7..b2314b1d 100644 --- a/test_device_data.py +++ b/test_device_data.py @@ -11,6 +11,7 @@ }, "fan": {}, "media_player": {}, + "light": {}, } @@ -43,7 +44,7 @@ async def test_json(file_path, docs): async def main(): exit = 0 generate_docs = False - docs = {"climate": [], "fan": [], "media_player": []} + docs = {"climate": [], "fan": [], "media_player": [], "light": []} files = sys.argv files.pop(0)