From bab68a1d3845a3f77e4e3c55a473d50e1b0c76ba Mon Sep 17 00:00:00 2001 From: rikman122 Date: Fri, 11 Jun 2021 16:10:47 +0200 Subject: [PATCH 001/119] first approach --- custom_components/localtuya/const.py | 14 +- .../localtuya/translations/en.json | 20 ++ custom_components/localtuya/vacuum.py | 218 ++++++++++++++++++ 3 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 custom_components/localtuya/vacuum.py diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index bd8a5d3..ff21ff9 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -41,11 +41,23 @@ CONF_FAN_SPEED_HIGH = "fan_speed_high" # sensor CONF_SCALING = "scaling" +# vacuum +CONF_IDLE_STATUS_VALUE = "idle_status_value" +CONF_RETURNING_STATUS_VALUE = "returning_status_value" +CONF_DOCKED_STATUS_VALUE = "docked_status_value" +CONF_BATTERY_DP = "battery_dp" +CONF_MODE_DP = "mode_dp" +CONF_MODES = "modes" +CONF_FAN_SPEED_DP = "fan_speed_dp" +CONF_FAN_SPEEDS = "fan_speeds" +CONF_CLEAN_TIME_DP = "clean_time_dp" +CONF_CLEAN_AREA_DP = "clean_area_dp" + DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch", "vacuum"] TUYA_DEVICE = "tuya_device" diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 6ce233a..027d8be 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -60,6 +60,16 @@ "scaling": "Scaling Factor", "state_on": "On Value", "state_off": "Off Value", + "idle_status_value": "Idle Status (comma-separated)", + "returning_status_value": "Returning Status", + "docked_status_value": "Docked Status (comma-separated)", + "battery_dp": "Battery status DP (Usually 14)", + "mode_dp": "Mode DP (Usually 27)", + "modes": "Modes list [start,pause/stop,return home,others...]", + "fan_speed_dp": "Fan speeds DP (Usually 30)", + "fan_speeds": "Fan speeds list (comma-separated)", + "clean_time_dp": "Clean Time DP (Usually 33)", + "clean_area_dp": "Clean Area DP (Usually 32)", "brightness": "Brightness (only for white color)", "brightness_lower": "Brightness Lower Value", "brightness_upper": "Brightness Upper Value", @@ -112,6 +122,16 @@ "scaling": "Scaling Factor", "state_on": "On Value", "state_off": "Off Value", + "idle_status_value": "Idle Status (comma-separated)", + "returning_status_value": "Returning Status", + "docked_status_value": "Docked Status (comma-separated)", + "battery_dp": "Battery status DP (Usually 14)", + "mode_dp": "Mode DP (Usually 27)", + "modes": "Modes list [start,pause/stop,return home,others...]", + "fan_speed_dp": "Fan speeds DP (Usually 30)", + "fan_speeds": "Fan speeds list (comma-separated)", + "clean_time_dp": "Clean Time DP (Usually 33)", + "clean_area_dp": "Clean Area DP (Usually 32)", "brightness": "Brightness (only for white color)", "brightness_lower": "Brightness Lower Value", "brightness_upper": "Brightness Upper Value", diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py new file mode 100644 index 0000000..4a5c9b5 --- /dev/null +++ b/custom_components/localtuya/vacuum.py @@ -0,0 +1,218 @@ +"""Platform to locally control Tuya-based vacuum devices.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.vacuum import ( + DOMAIN, + STATE_CLEANING, + STATE_DOCKED, + STATE_IDLE, + STATE_RETURNING, + SUPPORT_BATTERY, + SUPPORT_FAN_SPEED, + SUPPORT_PAUSE, + SUPPORT_RETURN_HOME, + SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STATUS, + SUPPORT_STOP, + StateVacuumEntity, +) + +from .common import LocalTuyaEntity, async_setup_entry + +from .const import ( + CONF_IDLE_STATUS_VALUE, + CONF_RETURNING_STATUS_VALUE, + CONF_DOCKED_STATUS_VALUE, + CONF_BATTERY_DP, + CONF_MODE_DP, + CONF_MODES, + CONF_FAN_SPEED_DP, + CONF_FAN_SPEEDS, + CONF_CLEAN_TIME_DP, + CONF_CLEAN_AREA_DP +) + +_LOGGER = logging.getLogger(__name__) + +CLEAN_TIME = "clean_time" +CLEAN_AREA = "clean_area" +MODES_LIST = "cleaning_mode_list" +MODE = "cleaning_mode" + +DEFAULT_IDLE_STATUS = "standby,sleep" +DEFAULT_RETURNING_STATUS = "docking" +DEFAULT_DOCKED_STATUS = "charging,chargecompleted" +DEFAULT_MODES = "smart,standby,chargego,wall_follow,spiral,single" +DEFAULT_FAN_SPEEDS = "low,normal,high" + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Required(CONF_IDLE_STATUS_VALUE, default=DEFAULT_IDLE_STATUS): str, + vol.Required(CONF_DOCKED_STATUS_VALUE, default=DEFAULT_DOCKED_STATUS): str, + vol.Optional(CONF_RETURNING_STATUS_VALUE, default=DEFAULT_RETURNING_STATUS): str, + vol.Optional(CONF_BATTERY_DP): vol.In(dps), + vol.Optional(CONF_MODE_DP): vol.In(dps), + vol.Optional(CONF_MODES, default=DEFAULT_MODES): str, + vol.Optional(CONF_FAN_SPEED_DP): vol.In(dps), + vol.Optional(CONF_FAN_SPEEDS, default=DEFAULT_FAN_SPEEDS): str, + vol.Optional(CONF_CLEAN_TIME_DP): vol.In(dps), + vol.Optional(CONF_CLEAN_AREA_DP): vol.In(dps), + } + + +class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): + """Tuya vacuum device.""" + + def __init__(self, device, config_entry, switchid, **kwargs): + """Initialize a new LocaltuyaVacuum.""" + super().__init__(device, config_entry, switchid ,_LOGGER, **kwargs) + self._state = None + self._battery_level = None + self._attrs = {} + + self._idle_status_list = [] + if self.has_config(CONF_IDLE_STATUS_VALUE): + self._idle_status_list = self._config[CONF_IDLE_STATUS_VALUE].split(",") + + self._modes_list = [] + if self.has_config(CONF_MODES): + self._modes_list = self._config[CONF_MODES].split(",") + self._attrs[MODES_LIST] = self._modes_list + if len(self._modes_list) >= 3: + self._start_mode = self._modes_list[0] + self._pause_mode = self._modes_list[1] + self._return_mode = self._modes_list[2] + + self._docked_status_list = [] + if self.has_config(CONF_DOCKED_STATUS_VALUE): + self._docked_status_list = self._config[CONF_DOCKED_STATUS_VALUE].split(",") + + self._fan_speed_list = [] + if self.has_config(CONF_FAN_SPEEDS): + self._fan_speed_list = self._config[CONF_FAN_SPEEDS].split(",") + + self._fan_speed = "" + self._cleaning_mode = "" + + print("Initialized vacuum [{}]".format(self.name)) + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = SUPPORT_START | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_STATUS | SUPPORT_STATE + + if self.has_config(CONF_RETURNING_STATUS_VALUE): + supported_features = supported_features | SUPPORT_RETURN_HOME + if self.has_config(CONF_FAN_SPEED_DP): + supported_features = supported_features | SUPPORT_FAN_SPEED + if self.has_config(CONF_BATTERY_DP): + supported_features = supported_features | SUPPORT_BATTERY + + return supported_features + + @property + def state(self): + """Return the vacuum state.""" + return self._state + + @property + def battery_level(self): + """Return the current battery level.""" + return self._battery_level + + @property + def device_state_attributes(self): + """Return the specific state attributes of this vacuum cleaner.""" + return self._attrs + + @property + def fan_speed(self): + """Return the current fan speed.""" + return self._fan_speed + + @property + def fan_speed_list(self) -> list: + """Return the list of available fan speeds.""" + return self._fan_speed_list + + @property + def error(self): + """Return error message.""" + return "" + + async def async_start(self, **kwargs): + """Turn the vacuum on and start cleaning.""" + await self._device.set_dp(self._start_mode, self._config[CONF_MODE_DP]) + + async def async_pause(self, **kwargs): + """Stop the vacuum cleaner, do not return to base.""" + if self._pause_mode: + await self._device.set_dp(self._pause_mode, self._config[CONF_MODE_DP]) + else: + _LOGGER.error("Missing command for pause in commands set.") + + async def async_return_to_base(self, **kwargs): + """Set the vacuum cleaner to return to the dock.""" + if self._return_mode: + await self._device.set_dp(self._return_mode, self._config[CONF_MODE_DP]) + else: + _LOGGER.error("Missing command for pause in commands set.") + + async def async_stop(self, **kwargs): + """Turn the vacuum off stopping the cleaning and returning home.""" + self.async_pause() + + async def async_clean_spot(self, **kwargs): + """Perform a spot clean-up.""" + return None + + async def async_locate(self, **kwargs): + """Locate the vacuum cleaner.""" + return None + + async def async_set_fan_speed(self, **kwargs): + """Set the fan speed.""" + fan_speed = kwargs["fan_speed"] + await self._device.set_dp(fan_speed, self._config[CONF_FAN_SPEED_DP]) + + async def async_send_command(self, command, params=None, **kwargs): + """Send a command to a vacuum cleaner.""" + if command == "set_mode" and 'mode' in params: + mode = params['mode'] + await self._device.set_dp(mode, self._config[CONF_MODE_DP]) + + def status_updated(self): + """Device status was updated.""" + state_value = str(self.dps(self._dp_id)) + if state_value in self._idle_status_list: + self._state = STATE_IDLE + elif state_value in self._docked_status_list: + self._state = STATE_DOCKED + elif state_value == self._config[CONF_RETURNING_STATUS_VALUE]: + self._state = STATE_RETURNING + else: + self._state = STATE_CLEANING + + if self.has_config(CONF_BATTERY_DP): + self._battery_level = self.dps_conf(CONF_BATTERY_DP) + + self._cleaning_mode = "" + if self.has_config(CONF_MODES): + self._cleaning_mode = self.dps_conf(CONF_MODE_DP) + self._attrs[MODE] = self._cleaning_mode + + self._fan_speed = "" + if self.has_config(CONF_FAN_SPEEDS): + self._fan_speed = self.dps_conf(CONF_FAN_SPEED_DP) + + if self.has_config(CONF_CLEAN_TIME_DP): + self._attrs[CLEAN_TIME] = self.dps_conf(CONF_CLEAN_TIME_DP) + + if self.has_config(CONF_CLEAN_AREA_DP): + self._attrs[CLEAN_AREA] = self.dps_conf(CONF_CLEAN_AREA_DP) + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaVacuum, flow_schema) \ No newline at end of file From 1f98b6533b264bfcb0628209b3da6ab0f4545e3b Mon Sep 17 00:00:00 2001 From: rikman122 Date: Fri, 11 Jun 2021 16:20:03 +0200 Subject: [PATCH 002/119] added sample YAML --- custom_components/localtuya/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index 0b762f2..c1d77ed 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -53,6 +53,20 @@ localtuya: current: 18 # Optional current_consumption: 19 # Optional voltage: 20 # Optional + + - platform: vacuum + friendly_name: Vacuum + id: 28 + idle_status_value: "standby,sleep" + returning_status_value: "docking" + docked_status_value: "charging,chargecompleted" + battery_dp: 14 + mode_dp: 27 + modes: "smart,standby,chargego,wall_follow,spiral,single" + fan_speed_dp: 30 + fan_speeds: "low,normal,high" + clean_time_dp: 33 + clean_area_dp: 32 """ import asyncio import logging From 3e78b0b9ba10a3c478d9b1f6f8e70b0faef919fb Mon Sep 17 00:00:00 2001 From: rikman122 Date: Sat, 12 Jun 2021 12:55:24 +0200 Subject: [PATCH 003/119] added Power control DP --- custom_components/localtuya/const.py | 1 + .../localtuya/translations/en.json | 6 +++-- custom_components/localtuya/vacuum.py | 25 +++++++++---------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index ff21ff9..f844609 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -42,6 +42,7 @@ CONF_FAN_SPEED_HIGH = "fan_speed_high" CONF_SCALING = "scaling" # vacuum +CONF_POWERGO_DP = "powergo_dp" CONF_IDLE_STATUS_VALUE = "idle_status_value" CONF_RETURNING_STATUS_VALUE = "returning_status_value" CONF_DOCKED_STATUS_VALUE = "docked_status_value" diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 027d8be..d1f9b24 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -60,12 +60,13 @@ "scaling": "Scaling Factor", "state_on": "On Value", "state_off": "Off Value", + "powergo_dp": "Power DP (Usually 25 or 2)", "idle_status_value": "Idle Status (comma-separated)", "returning_status_value": "Returning Status", "docked_status_value": "Docked Status (comma-separated)", "battery_dp": "Battery status DP (Usually 14)", "mode_dp": "Mode DP (Usually 27)", - "modes": "Modes list [start,pause/stop,return home,others...]", + "modes": "Modes list in this order: return home,others...", "fan_speed_dp": "Fan speeds DP (Usually 30)", "fan_speeds": "Fan speeds list (comma-separated)", "clean_time_dp": "Clean Time DP (Usually 33)", @@ -122,12 +123,13 @@ "scaling": "Scaling Factor", "state_on": "On Value", "state_off": "Off Value", + "powergo_dp": "Power DP (Usually 25 or 2)", "idle_status_value": "Idle Status (comma-separated)", "returning_status_value": "Returning Status", "docked_status_value": "Docked Status (comma-separated)", "battery_dp": "Battery status DP (Usually 14)", "mode_dp": "Mode DP (Usually 27)", - "modes": "Modes list [start,pause/stop,return home,others...]", + "modes": "Modes list in this order: return home,others...", "fan_speed_dp": "Fan speeds DP (Usually 30)", "fan_speeds": "Fan speeds list (comma-separated)", "clean_time_dp": "Clean Time DP (Usually 33)", diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py index 4a5c9b5..94021c0 100644 --- a/custom_components/localtuya/vacuum.py +++ b/custom_components/localtuya/vacuum.py @@ -23,6 +23,7 @@ from homeassistant.components.vacuum import ( from .common import LocalTuyaEntity, async_setup_entry from .const import ( + CONF_POWERGO_DP, CONF_IDLE_STATUS_VALUE, CONF_RETURNING_STATUS_VALUE, CONF_DOCKED_STATUS_VALUE, @@ -42,16 +43,17 @@ CLEAN_AREA = "clean_area" MODES_LIST = "cleaning_mode_list" MODE = "cleaning_mode" -DEFAULT_IDLE_STATUS = "standby,sleep" +DEFAULT_IDLE_STATUS = "standby,sleep,pause" DEFAULT_RETURNING_STATUS = "docking" DEFAULT_DOCKED_STATUS = "charging,chargecompleted" -DEFAULT_MODES = "smart,standby,chargego,wall_follow,spiral,single" +DEFAULT_MODES = "chargego,smart,standby,wall_follow,spiral,single" DEFAULT_FAN_SPEEDS = "low,normal,high" def flow_schema(dps): """Return schema used in config flow.""" return { vol.Required(CONF_IDLE_STATUS_VALUE, default=DEFAULT_IDLE_STATUS): str, + vol.Required(CONF_POWERGO_DP): vol.In(dps), vol.Required(CONF_DOCKED_STATUS_VALUE, default=DEFAULT_DOCKED_STATUS): str, vol.Optional(CONF_RETURNING_STATUS_VALUE, default=DEFAULT_RETURNING_STATUS): str, vol.Optional(CONF_BATTERY_DP): vol.In(dps), @@ -82,10 +84,7 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): if self.has_config(CONF_MODES): self._modes_list = self._config[CONF_MODES].split(",") self._attrs[MODES_LIST] = self._modes_list - if len(self._modes_list) >= 3: - self._start_mode = self._modes_list[0] - self._pause_mode = self._modes_list[1] - self._return_mode = self._modes_list[2] + self._return_mode = self._modes_list[0] self._docked_status_list = [] if self.has_config(CONF_DOCKED_STATUS_VALUE): @@ -146,25 +145,25 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): async def async_start(self, **kwargs): """Turn the vacuum on and start cleaning.""" - await self._device.set_dp(self._start_mode, self._config[CONF_MODE_DP]) + await self._device.set_dp(True, self._config[CONF_POWERGO_DP]) async def async_pause(self, **kwargs): """Stop the vacuum cleaner, do not return to base.""" - if self._pause_mode: - await self._device.set_dp(self._pause_mode, self._config[CONF_MODE_DP]) - else: - _LOGGER.error("Missing command for pause in commands set.") + await self._device.set_dp(False, self._config[CONF_POWERGO_DP]) async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" if self._return_mode: await self._device.set_dp(self._return_mode, self._config[CONF_MODE_DP]) else: - _LOGGER.error("Missing command for pause in commands set.") + _LOGGER.error("Missing command for return home in commands set.") async def async_stop(self, **kwargs): """Turn the vacuum off stopping the cleaning and returning home.""" - self.async_pause() + if self._return_mode: + await self._device.set_dp(self._return_mode, self._config[CONF_MODE_DP]) + else: + _LOGGER.error("Missing command for return home in commands set.") async def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" From f374dd4f4aedca94a0243d625c4a0055d6dda6b7 Mon Sep 17 00:00:00 2001 From: Taras Nychko Date: Mon, 14 Jun 2021 23:25:21 +0100 Subject: [PATCH 004/119] Add support for paused state --- custom_components/localtuya/const.py | 1 + custom_components/localtuya/translations/en.json | 2 ++ custom_components/localtuya/vacuum.py | 9 ++++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index f844609..76739dc 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -53,6 +53,7 @@ CONF_FAN_SPEED_DP = "fan_speed_dp" CONF_FAN_SPEEDS = "fan_speeds" CONF_CLEAN_TIME_DP = "clean_time_dp" CONF_CLEAN_AREA_DP = "clean_area_dp" +CONF_PAUSED_STATE = "paused_state" DATA_DISCOVERY = "discovery" diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index d1f9b24..724c2c4 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -71,6 +71,7 @@ "fan_speeds": "Fan speeds list (comma-separated)", "clean_time_dp": "Clean Time DP (Usually 33)", "clean_area_dp": "Clean Area DP (Usually 32)", + "paused_state": "Pause state (pause, paused, etc)", "brightness": "Brightness (only for white color)", "brightness_lower": "Brightness Lower Value", "brightness_upper": "Brightness Upper Value", @@ -134,6 +135,7 @@ "fan_speeds": "Fan speeds list (comma-separated)", "clean_time_dp": "Clean Time DP (Usually 33)", "clean_area_dp": "Clean Area DP (Usually 32)", + "paused_state": "Pause state (pause, paused, etc)", "brightness": "Brightness (only for white color)", "brightness_lower": "Brightness Lower Value", "brightness_upper": "Brightness Upper Value", diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py index 94021c0..de91d14 100644 --- a/custom_components/localtuya/vacuum.py +++ b/custom_components/localtuya/vacuum.py @@ -9,6 +9,7 @@ from homeassistant.components.vacuum import ( STATE_DOCKED, STATE_IDLE, STATE_RETURNING, + STATE_PAUSED, SUPPORT_BATTERY, SUPPORT_FAN_SPEED, SUPPORT_PAUSE, @@ -17,6 +18,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STATE, SUPPORT_STATUS, SUPPORT_STOP, + SUPPORT_PAUSE, StateVacuumEntity, ) @@ -33,7 +35,8 @@ from .const import ( CONF_FAN_SPEED_DP, CONF_FAN_SPEEDS, CONF_CLEAN_TIME_DP, - CONF_CLEAN_AREA_DP + CONF_CLEAN_AREA_DP, + CONF_PAUSED_STATE, ) _LOGGER = logging.getLogger(__name__) @@ -48,6 +51,7 @@ DEFAULT_RETURNING_STATUS = "docking" DEFAULT_DOCKED_STATUS = "charging,chargecompleted" DEFAULT_MODES = "chargego,smart,standby,wall_follow,spiral,single" DEFAULT_FAN_SPEEDS = "low,normal,high" +DEFAULT_PAUSED_STATE = "paused" def flow_schema(dps): """Return schema used in config flow.""" @@ -63,6 +67,7 @@ def flow_schema(dps): vol.Optional(CONF_FAN_SPEEDS, default=DEFAULT_FAN_SPEEDS): str, vol.Optional(CONF_CLEAN_TIME_DP): vol.In(dps), vol.Optional(CONF_CLEAN_AREA_DP): vol.In(dps), + vol.Optional(CONF_PAUSED_STATE, default=DEFAULT_PAUSED_STATE): str, } @@ -193,6 +198,8 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): self._state = STATE_DOCKED elif state_value == self._config[CONF_RETURNING_STATUS_VALUE]: self._state = STATE_RETURNING + elif state_value == self._config[CONF_PAUSED_STATE]: + self._state = STATE_PAUSED else: self._state = STATE_CLEANING From 77380992cbc09a3ca15b1b732f299e4e4165d631 Mon Sep 17 00:00:00 2001 From: rikman122 Date: Thu, 17 Jun 2021 09:45:29 +0200 Subject: [PATCH 005/119] separate real modes from status modes --- custom_components/localtuya/const.py | 1 + .../localtuya/translations/en.json | 6 ++++-- custom_components/localtuya/vacuum.py | 18 ++++++++++-------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 76739dc..c803baa 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -54,6 +54,7 @@ CONF_FAN_SPEEDS = "fan_speeds" CONF_CLEAN_TIME_DP = "clean_time_dp" CONF_CLEAN_AREA_DP = "clean_area_dp" CONF_PAUSED_STATE = "paused_state" +CONF_RETURN_MODE = "return_mode" DATA_DISCOVERY = "discovery" diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 724c2c4..4392803 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -66,7 +66,8 @@ "docked_status_value": "Docked Status (comma-separated)", "battery_dp": "Battery status DP (Usually 14)", "mode_dp": "Mode DP (Usually 27)", - "modes": "Modes list in this order: return home,others...", + "modes": "Modes list", + "return_mode": "Return home mode", "fan_speed_dp": "Fan speeds DP (Usually 30)", "fan_speeds": "Fan speeds list (comma-separated)", "clean_time_dp": "Clean Time DP (Usually 33)", @@ -130,7 +131,8 @@ "docked_status_value": "Docked Status (comma-separated)", "battery_dp": "Battery status DP (Usually 14)", "mode_dp": "Mode DP (Usually 27)", - "modes": "Modes list in this order: return home,others...", + "modes": "Modes list", + "return_mode": "Return home mode", "fan_speed_dp": "Fan speeds DP (Usually 30)", "fan_speeds": "Fan speeds list (comma-separated)", "clean_time_dp": "Clean Time DP (Usually 33)", diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py index de91d14..094e0f5 100644 --- a/custom_components/localtuya/vacuum.py +++ b/custom_components/localtuya/vacuum.py @@ -37,6 +37,7 @@ from .const import ( CONF_CLEAN_TIME_DP, CONF_CLEAN_AREA_DP, CONF_PAUSED_STATE, + CONF_RETURN_MODE, ) _LOGGER = logging.getLogger(__name__) @@ -46,12 +47,13 @@ CLEAN_AREA = "clean_area" MODES_LIST = "cleaning_mode_list" MODE = "cleaning_mode" -DEFAULT_IDLE_STATUS = "standby,sleep,pause" +DEFAULT_IDLE_STATUS = "standby,sleep" DEFAULT_RETURNING_STATUS = "docking" DEFAULT_DOCKED_STATUS = "charging,chargecompleted" -DEFAULT_MODES = "chargego,smart,standby,wall_follow,spiral,single" +DEFAULT_MODES = "smart,wall_follow,spiral,single" DEFAULT_FAN_SPEEDS = "low,normal,high" DEFAULT_PAUSED_STATE = "paused" +DEFAULT_RETURN_MODE = "chargego" def flow_schema(dps): """Return schema used in config flow.""" @@ -63,6 +65,7 @@ def flow_schema(dps): vol.Optional(CONF_BATTERY_DP): vol.In(dps), vol.Optional(CONF_MODE_DP): vol.In(dps), vol.Optional(CONF_MODES, default=DEFAULT_MODES): str, + vol.Optional(CONF_RETURN_MODE, default=DEFAULT_RETURN_MODE): str, vol.Optional(CONF_FAN_SPEED_DP): vol.In(dps), vol.Optional(CONF_FAN_SPEEDS, default=DEFAULT_FAN_SPEEDS): str, vol.Optional(CONF_CLEAN_TIME_DP): vol.In(dps), @@ -89,7 +92,6 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): if self.has_config(CONF_MODES): self._modes_list = self._config[CONF_MODES].split(",") self._attrs[MODES_LIST] = self._modes_list - self._return_mode = self._modes_list[0] self._docked_status_list = [] if self.has_config(CONF_DOCKED_STATUS_VALUE): @@ -109,7 +111,7 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): """Flag supported features.""" supported_features = SUPPORT_START | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_STATUS | SUPPORT_STATE - if self.has_config(CONF_RETURNING_STATUS_VALUE): + if self.has_config(CONF_RETURN_MODE): supported_features = supported_features | SUPPORT_RETURN_HOME if self.has_config(CONF_FAN_SPEED_DP): supported_features = supported_features | SUPPORT_FAN_SPEED @@ -158,15 +160,15 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - if self._return_mode: - await self._device.set_dp(self._return_mode, self._config[CONF_MODE_DP]) + if self.has_config(CONF_RETURN_MODE): + await self._device.set_dp(self._config[CONF_RETURN_MODE], self._config[CONF_MODE_DP]) else: _LOGGER.error("Missing command for return home in commands set.") async def async_stop(self, **kwargs): """Turn the vacuum off stopping the cleaning and returning home.""" - if self._return_mode: - await self._device.set_dp(self._return_mode, self._config[CONF_MODE_DP]) + if self.has_config(CONF_RETURN_MODE): + await self._device.set_dp(self._config[CONF_RETURN_MODE], self._config[CONF_MODE_DP]) else: _LOGGER.error("Missing command for return home in commands set.") From 0f3ad66849b58f30e29aaa4e378c6732d16d36eb Mon Sep 17 00:00:00 2001 From: rikman122 Date: Thu, 17 Jun 2021 11:46:07 +0200 Subject: [PATCH 006/119] add stop and locate commands --- custom_components/localtuya/const.py | 2 ++ .../localtuya/translations/en.json | 4 ++++ custom_components/localtuya/vacuum.py | 19 ++++++++++++++----- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index c803baa..80dc4d6 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -53,8 +53,10 @@ CONF_FAN_SPEED_DP = "fan_speed_dp" CONF_FAN_SPEEDS = "fan_speeds" CONF_CLEAN_TIME_DP = "clean_time_dp" CONF_CLEAN_AREA_DP = "clean_area_dp" +CONF_LOCATE_DP = "locate_dp" CONF_PAUSED_STATE = "paused_state" CONF_RETURN_MODE = "return_mode" +CONF_STOP_STATUS = "stop_status" DATA_DISCOVERY = "discovery" diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 4392803..e268d4d 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -72,7 +72,9 @@ "fan_speeds": "Fan speeds list (comma-separated)", "clean_time_dp": "Clean Time DP (Usually 33)", "clean_area_dp": "Clean Area DP (Usually 32)", + "locate_dp": "Locate DP (Usually 31)", "paused_state": "Pause state (pause, paused, etc)", + "stop_status": "Stop status", "brightness": "Brightness (only for white color)", "brightness_lower": "Brightness Lower Value", "brightness_upper": "Brightness Upper Value", @@ -137,7 +139,9 @@ "fan_speeds": "Fan speeds list (comma-separated)", "clean_time_dp": "Clean Time DP (Usually 33)", "clean_area_dp": "Clean Area DP (Usually 32)", + "locate_dp": "Locate DP (Usually 31)", "paused_state": "Pause state (pause, paused, etc)", + "stop_status": "Stop status", "brightness": "Brightness (only for white color)", "brightness_lower": "Brightness Lower Value", "brightness_upper": "Brightness Upper Value", diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py index 094e0f5..7e11f8d 100644 --- a/custom_components/localtuya/vacuum.py +++ b/custom_components/localtuya/vacuum.py @@ -19,6 +19,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_PAUSE, + SUPPORT_LOCATE, StateVacuumEntity, ) @@ -36,8 +37,10 @@ from .const import ( CONF_FAN_SPEEDS, CONF_CLEAN_TIME_DP, CONF_CLEAN_AREA_DP, + CONF_LOCATE_DP, CONF_PAUSED_STATE, CONF_RETURN_MODE, + CONF_STOP_STATUS, ) _LOGGER = logging.getLogger(__name__) @@ -54,6 +57,7 @@ DEFAULT_MODES = "smart,wall_follow,spiral,single" DEFAULT_FAN_SPEEDS = "low,normal,high" DEFAULT_PAUSED_STATE = "paused" DEFAULT_RETURN_MODE = "chargego" +DEFAULT_STOP_STATUS = "standby" def flow_schema(dps): """Return schema used in config flow.""" @@ -70,7 +74,9 @@ def flow_schema(dps): vol.Optional(CONF_FAN_SPEEDS, default=DEFAULT_FAN_SPEEDS): str, vol.Optional(CONF_CLEAN_TIME_DP): vol.In(dps), vol.Optional(CONF_CLEAN_AREA_DP): vol.In(dps), + vol.Optional(CONF_LOCATE_DP): vol.In(dps), vol.Optional(CONF_PAUSED_STATE, default=DEFAULT_PAUSED_STATE): str, + vol.Optional(CONF_STOP_STATUS, default=DEFAULT_STOP_STATUS): str, } @@ -117,6 +123,8 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): supported_features = supported_features | SUPPORT_FAN_SPEED if self.has_config(CONF_BATTERY_DP): supported_features = supported_features | SUPPORT_BATTERY + if self.has_config(CONF_LOCATE_DP): + supported_features = supported_features | SUPPORT_LOCATE return supported_features @@ -166,11 +174,11 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): _LOGGER.error("Missing command for return home in commands set.") async def async_stop(self, **kwargs): - """Turn the vacuum off stopping the cleaning and returning home.""" - if self.has_config(CONF_RETURN_MODE): - await self._device.set_dp(self._config[CONF_RETURN_MODE], self._config[CONF_MODE_DP]) + """Turn the vacuum off stopping the cleaning""" + if self.has_config(CONF_STOP_STATUS): + await self._device.set_dp(self._config[CONF_STOP_STATUS], self._config[CONF_MODE_DP]) else: - _LOGGER.error("Missing command for return home in commands set.") + _LOGGER.error("Missing command for stop in commands set.") async def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" @@ -178,7 +186,8 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): async def async_locate(self, **kwargs): """Locate the vacuum cleaner.""" - return None + if self.has_config(CONF_LOCATE_DP): + await self._device.set_dp('', self._config[CONF_LOCATE_DP]) async def async_set_fan_speed(self, **kwargs): """Set the fan speed.""" From ec793a1137b2d103d142310094e4bcd47cdb7140 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 18 Aug 2021 11:05:36 +0930 Subject: [PATCH 007/119] Update en.json --- custom_components/localtuya/translations/en.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 6ce233a..2b12013 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -72,9 +72,8 @@ "scene": "Scene", "fan_speed_control": "Fan Speed Control", "fan_oscillating_control": "Fan Oscillating Control", - "fan_speed_low": "Fan Low Speed Setting", - "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_direction": "Fan Direction Control", + "fan_speed_count": "Fan Speed Count" } } } From 5217014f224002ee169657746d3d3ad3b4eb981f Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 18 Aug 2021 11:06:41 +0930 Subject: [PATCH 008/119] Update const.py --- custom_components/localtuya/const.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index bd8a5d3..b110ed4 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -33,10 +33,10 @@ CONF_SPAN_TIME = "span_time" # fan CONF_FAN_SPEED_CONTROL = "fan_speed_control" -CONF_FAN_OSCILLATING_CONTROL = "fan_oscillating_control" -CONF_FAN_SPEED_LOW = "fan_speed_low" -CONF_FAN_SPEED_MEDIUM = "fan_speed_medium" -CONF_FAN_SPEED_HIGH = "fan_speed_high" +# CONF_FAN_OSCILLATING_CONTROL = "fan_oscillating_control" +CONF_FAN_DIRECTION = "fan_direction" +CONF_FAN_SPEED_COUNT = "fan_speed_count" + # sensor CONF_SCALING = "scaling" From 761a5f133aa00710302d610a40a0c21623d55573 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 18 Aug 2021 11:07:17 +0930 Subject: [PATCH 009/119] Update fan.py --- custom_components/localtuya/fan.py | 170 ++++++++++++++++++++--------- 1 file changed, 120 insertions(+), 50 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index b45567e..f4bf87a 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -1,3 +1,9 @@ +# DPS [1] VALUE [True] --> fan on,off > True, False +# DPS [3] VALUE [6] --> fan speed > 1-6 +# DPS [4] VALUE [forward] --> fan direction > forward, reverse +# DPS [102] VALUE [normal] --> preset mode > normal, sleep, nature +# DPS [103] VALUE [off] --> timer > off, 1hour, 2hour, 4hour, 8hour + """Platform to locally control Tuya-based fan devices.""" import logging from functools import partial @@ -5,10 +11,9 @@ from functools import partial import voluptuous as vol from homeassistant.components.fan import ( DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, + DIRECTION_FORWARD, + DIRECTION_REVERSE, + SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity, @@ -18,9 +23,16 @@ from .common import LocalTuyaEntity, async_setup_entry from .const import ( CONF_FAN_OSCILLATING_CONTROL, CONF_FAN_SPEED_CONTROL, - CONF_FAN_SPEED_HIGH, - CONF_FAN_SPEED_LOW, - CONF_FAN_SPEED_MEDIUM, + CONF_FAN_DIRECTION, + CONF_FAN_SPEED_COUNT +) + +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, + percentage_to_ranged_value, + ranged_value_to_percentage, + int_states_in_range ) _LOGGER = logging.getLogger(__name__) @@ -31,17 +43,16 @@ def flow_schema(dps): return { vol.Optional(CONF_FAN_SPEED_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_OSCILLATING_CONTROL): vol.In(dps), - vol.Optional(CONF_FAN_SPEED_LOW, default=SPEED_LOW): vol.In( - [SPEED_LOW, "1", "2", "small"] - ), - vol.Optional(CONF_FAN_SPEED_MEDIUM, default=SPEED_MEDIUM): vol.In( - [SPEED_MEDIUM, "mid", "2", "3"] - ), - vol.Optional(CONF_FAN_SPEED_HIGH, default=SPEED_HIGH): vol.In( - [SPEED_HIGH, "auto", "3", "4", "large"] - ), + vol.Optional(CONF_FAN_DIRECTION): vol.In(dps), + vol.Optional(CONF_FAN_SPEED_COUNT, default=3): cv.positive_int } +# make this configurable later +DIRECTION_TO_TUYA = { + DIRECTION_REVERSE: "reverse", + DIRECTION_FORWARD: "forward", +} +TUYA_DIRECTION_TO_HA = {v: k for (k, v) in DIRECTION_TO_TUYA.items()} class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Representation of a Tuya fan.""" @@ -56,28 +67,40 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Initialize the entity.""" super().__init__(device, config_entry, fanid, _LOGGER, **kwargs) self._is_on = False - self._speed = None self._oscillating = None + self._direction = None + self._percentage = None @property def oscillating(self): """Return current oscillating status.""" return self._oscillating + @property + def current_direction(self): + """Return the current direction of the fan.""" + direction = self.service.value(CharacteristicsTypes.ROTATION_DIRECTION) + return TUYA_DIRECTION_TO_HA[direction] + @property def is_on(self): """Check if Tuya fan is on.""" return self._is_on @property - def speed(self) -> str: - """Return the current speed.""" - return self._speed + def percentage(self): + """Return the current percentage.""" + return self._percentage - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + # @property + # def speed(self) -> str: + # """Return the current speed.""" + # return self._speed + + # @property + # def speed_list(self) -> list: + # """Get the list of available speeds.""" + # return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the entity.""" @@ -92,22 +115,38 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): await self._device.set_dp(False, self._dp_id) self.schedule_update_ha_state() - async def async_set_speed(self, speed: str) -> None: + async def async_set_percentage(self, percentage): """Set the speed of the fan.""" - mapping = { - SPEED_LOW: self._config.get(CONF_FAN_SPEED_LOW), - SPEED_MEDIUM: self._config.get(CONF_FAN_SPEED_MEDIUM), - SPEED_HIGH: self._config.get(CONF_FAN_SPEED_HIGH), - } + if percentage == 0: + return await self.async_turn_off() - if speed == SPEED_OFF: - await self._device.set_dp(False, self._dp_id) - else: + if not self.is_on: + await self.async_turn_on() + + if percentage is not None await self._device.set_dp( - mapping.get(speed), self._config.get(CONF_FAN_SPEED_CONTROL) - ) + math.ceil( + percentage_to_ranged_value(self._speed_range, percentage) + ), + self._config.get(CONF_FAN_SPEED_CONTROL) + ) - self.schedule_update_ha_state() + # async def async_set_speed(self, speed: str) -> None: + # """Set the speed of the fan.""" + # mapping = { + # SPEED_LOW: self._config.get(CONF_FAN_SPEED_LOW), + # SPEED_MEDIUM: self._config.get(CONF_FAN_SPEED_MEDIUM), + # SPEED_HIGH: self._config.get(CONF_FAN_SPEED_HIGH), + # } + + # if speed == SPEED_OFF: + # await self._device.set_dp(False, self._dp_id) + # else: + # await self._device.set_dp( + # mapping.get(speed), self._config.get(CONF_FAN_SPEED_CONTROL) + # ) + + # self.schedule_update_ha_state() async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation.""" @@ -116,38 +155,69 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): ) self.schedule_update_ha_state() + async def async_set_direction(self, direction): + """Set the direction of the fan.""" + await self.async_put_characteristics( + {CharacteristicsTypes.ROTATION_DIRECTION: DIRECTION_TO_TUYA[direction]} + ) + @property def supported_features(self) -> int: """Flag supported features.""" - supports = 0 + features = 0 if self.has_config(CONF_FAN_OSCILLATING_CONTROL): - supports |= SUPPORT_OSCILLATE - if self.has_config(CONF_FAN_SPEED_CONTROL): - supports |= SUPPORT_SET_SPEED + features |= SUPPORT_OSCILLATE - return supports + if self.has_config(CONF_FAN_SPEED_CONTROL): + features |= SUPPORT_SET_SPEED + + if self.has_config(CONF_FAN_DIRECTION): + features |= SUPPORT_DIRECTION + + return features + + @property + def speed_count(self) -> int: + """Speed count for the fan.""" + return self.has_config(CONF_FAN_SPEED_COUNT) + ) def status_updated(self): """Get state of Tuya fan.""" - mappings = { - self._config.get(CONF_FAN_SPEED_LOW): SPEED_LOW, - self._config.get(CONF_FAN_SPEED_MEDIUM): SPEED_MEDIUM, - self._config.get(CONF_FAN_SPEED_HIGH): SPEED_HIGH, - } self._is_on = self.dps(self._dp_id) - if self.has_config(CONF_FAN_SPEED_CONTROL): - self._speed = mappings.get(self.dps_conf(CONF_FAN_SPEED_CONTROL)) - if self.speed is None: + if self.has_config(CONF_FAN_SPEED_COUNT): + self._percentage = ranged_value_to_percentage( + self._speed_range, self.dps_conf(CONF_FAN_SPEED_CONTROL) + ) + if self._percentage is None: self.warning( "%s/%s: Ignoring unknown fan controller state: %s", self.name, self.entity_id, self.dps_conf(CONF_FAN_SPEED_CONTROL), ) - self._speed = None + self._percentage = None + + + # mappings = { + # self._config.get(CONF_FAN_SPEED_LOW): SPEED_LOW, + # self._config.get(CONF_FAN_SPEED_MEDIUM): SPEED_MEDIUM, + # self._config.get(CONF_FAN_SPEED_HIGH): SPEED_HIGH, + # } + + # if self.has_config(CONF_FAN_SPEED_CONTROL): + # self._speed = mappings.get(self.dps_conf(CONF_FAN_SPEED_CONTROL)) + # if self.speed is None: + # self.warning( + # "%s/%s: Ignoring unknown fan controller state: %s", + # self.name, + # self.entity_id, + # self.dps_conf(CONF_FAN_SPEED_CONTROL), + # ) + # self._speed = None if self.has_config(CONF_FAN_OSCILLATING_CONTROL): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) From 4eb1806eb4a5e58849ae2a60115b191b1e3bcd50 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 18 Aug 2021 11:24:52 +0930 Subject: [PATCH 010/119] Update fan.py --- custom_components/localtuya/fan.py | 52 +++++++----------------------- 1 file changed, 12 insertions(+), 40 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index f4bf87a..859b204 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -79,8 +79,8 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): @property def current_direction(self): """Return the current direction of the fan.""" - direction = self.service.value(CharacteristicsTypes.ROTATION_DIRECTION) - return TUYA_DIRECTION_TO_HA[direction] + return self._direction + @property def is_on(self): @@ -123,7 +123,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if not self.is_on: await self.async_turn_on() - if percentage is not None + if percentage is not None: await self._device.set_dp( math.ceil( percentage_to_ranged_value(self._speed_range, percentage) @@ -131,22 +131,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._config.get(CONF_FAN_SPEED_CONTROL) ) - # async def async_set_speed(self, speed: str) -> None: - # """Set the speed of the fan.""" - # mapping = { - # SPEED_LOW: self._config.get(CONF_FAN_SPEED_LOW), - # SPEED_MEDIUM: self._config.get(CONF_FAN_SPEED_MEDIUM), - # SPEED_HIGH: self._config.get(CONF_FAN_SPEED_HIGH), - # } - - # if speed == SPEED_OFF: - # await self._device.set_dp(False, self._dp_id) - # else: - # await self._device.set_dp( - # mapping.get(speed), self._config.get(CONF_FAN_SPEED_CONTROL) - # ) - - # self.schedule_update_ha_state() async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation.""" @@ -157,9 +141,10 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): async def async_set_direction(self, direction): """Set the direction of the fan.""" - await self.async_put_characteristics( - {CharacteristicsTypes.ROTATION_DIRECTION: DIRECTION_TO_TUYA[direction]} + await self._device.set_dp( + DIRECTION_TO_TUYA[direction], self._config.get(CONF_FAN_DIRECTION) ) + self.schedule_update_ha_state() @property def supported_features(self) -> int: @@ -181,7 +166,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): def speed_count(self) -> int: """Speed count for the fan.""" return self.has_config(CONF_FAN_SPEED_COUNT) - ) + def status_updated(self): """Get state of Tuya fan.""" @@ -201,26 +186,13 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): ) self._percentage = None - - # mappings = { - # self._config.get(CONF_FAN_SPEED_LOW): SPEED_LOW, - # self._config.get(CONF_FAN_SPEED_MEDIUM): SPEED_MEDIUM, - # self._config.get(CONF_FAN_SPEED_HIGH): SPEED_HIGH, - # } - - # if self.has_config(CONF_FAN_SPEED_CONTROL): - # self._speed = mappings.get(self.dps_conf(CONF_FAN_SPEED_CONTROL)) - # if self.speed is None: - # self.warning( - # "%s/%s: Ignoring unknown fan controller state: %s", - # self.name, - # self.entity_id, - # self.dps_conf(CONF_FAN_SPEED_CONTROL), - # ) - # self._speed = None - if self.has_config(CONF_FAN_OSCILLATING_CONTROL): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) + if self.has_config(CONF_FAN_DIRECTION): + self._oscillating = TUYA_DIRECTION_TO_HA[ + self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) + ] + async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) From b8acadc1956871d2e368fda4f135b287008d884e Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 18 Aug 2021 13:15:04 +0930 Subject: [PATCH 011/119] working add more config options and presets in future --- custom_components/localtuya/fan.py | 34 ++++++++++++++---------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 859b204..b0e1008 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -1,13 +1,9 @@ -# DPS [1] VALUE [True] --> fan on,off > True, False -# DPS [3] VALUE [6] --> fan speed > 1-6 -# DPS [4] VALUE [forward] --> fan direction > forward, reverse -# DPS [102] VALUE [normal] --> preset mode > normal, sleep, nature -# DPS [103] VALUE [off] --> timer > off, 1hour, 2hour, 4hour, 8hour - """Platform to locally control Tuya-based fan devices.""" import logging from functools import partial +import math +import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.components.fan import ( DOMAIN, @@ -70,6 +66,9 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._oscillating = None self._direction = None self._percentage = None + self._speed_range = ( + 1, self._config.get(CONF_FAN_SPEED_COUNT), + ) @property def oscillating(self): @@ -125,9 +124,9 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if percentage is not None: await self._device.set_dp( - math.ceil( + str(math.ceil( percentage_to_ranged_value(self._speed_range, percentage) - ), + )), self._config.get(CONF_FAN_SPEED_CONTROL) ) @@ -165,7 +164,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): @property def speed_count(self) -> int: """Speed count for the fan.""" - return self.has_config(CONF_FAN_SPEED_COUNT) + return int_states_in_range(self._speed_range) def status_updated(self): @@ -174,25 +173,24 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._is_on = self.dps(self._dp_id) if self.has_config(CONF_FAN_SPEED_COUNT): - self._percentage = ranged_value_to_percentage( - self._speed_range, self.dps_conf(CONF_FAN_SPEED_CONTROL) - ) - if self._percentage is None: + value = int(self.dps_conf(CONF_FAN_SPEED_CONTROL)) + if value is not None: + self._percentage = ranged_value_to_percentage(self._speed_range, value) + else: self.warning( "%s/%s: Ignoring unknown fan controller state: %s", self.name, self.entity_id, - self.dps_conf(CONF_FAN_SPEED_CONTROL), + value, ) - self._percentage = None if self.has_config(CONF_FAN_OSCILLATING_CONTROL): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) if self.has_config(CONF_FAN_DIRECTION): - self._oscillating = TUYA_DIRECTION_TO_HA[ - self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) - ] + value = self.dps_conf(CONF_FAN_DIRECTION) + if value is not None: + self._direction = TUYA_DIRECTION_TO_HA[value] async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) From e61307597123ee8da9a0c858e2e191ed96767373 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 19 Aug 2021 19:53:32 +0930 Subject: [PATCH 012/119] update to new fan entity percentage configuration --- .../localtuya/translations/en.json | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 2b12013..de40762 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -70,10 +70,14 @@ "color_temp_max_kelvin": "Maximum Color Temperature in K", "music_mode": "Music mode available", "scene": "Scene", - "fan_speed_control": "Fan Speed Control", - "fan_oscillating_control": "Fan Oscillating Control", - "fan_direction": "Fan Direction Control", - "fan_speed_count": "Fan Speed Count" + "fan_speed_control": "Fan Speed Control dps", + "fan_oscillating_control": "Fan Oscillating Control dps", + "fan_speed_min": "minimum fan speed integer", + "fan_speed_max": "maximum fan speed integer", + "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", + "fan_direction":"fan direction dps", + "fan_direction_forward": "forward dps string", + "fan_direction_reverse": "reverser dps string" } } } @@ -121,11 +125,14 @@ "color_temp_max_kelvin": "Maximum Color Temperature in K", "music_mode": "Music mode available", "scene": "Scene", - "fan_speed_control": "Fan Speed Control", - "fan_oscillating_control": "Fan Oscillating Control", - "fan_speed_low": "Fan Low Speed Setting", - "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_speed_control": "Fan Speed Control dps", + "fan_oscillating_control": "Fan Oscillating Control dps", + "fan_speed_min": "minimum fan speed integer", + "fan_speed_max": "maximum fan speed integer", + "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", + "fan_direction":"fan direction dps", + "fan_direction_forward": "forward dps string", + "fan_direction_reverse": "reverser dps string" } }, "yaml_import": { From 94c3af1c7d49533f8a66b9e5490aae575bf458dc Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 19 Aug 2021 19:56:54 +0930 Subject: [PATCH 013/119] update to new fan entity percentage --- custom_components/localtuya/const.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index b110ed4..9662b3b 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -33,10 +33,13 @@ CONF_SPAN_TIME = "span_time" # fan CONF_FAN_SPEED_CONTROL = "fan_speed_control" -# CONF_FAN_OSCILLATING_CONTROL = "fan_oscillating_control" +CONF_FAN_OSCILLATING_CONTROL = "fan_oscillating_control" +CONF_FAN_SPEED_MIN = "fan_speed_min" +CONF_FAN_SPEED_MAX = "fan_speed_max" +CONF_FAN_ORDERED_LIST = "fan_speed_ordered_list" CONF_FAN_DIRECTION = "fan_direction" -CONF_FAN_SPEED_COUNT = "fan_speed_count" - +CONF_FAN_DIRECTION_FWD = "fan_direction_forward" +CONF_FAN_DIRECTION_REV = "fan_direction_reverse" # sensor CONF_SCALING = "scaling" From 4cc7ffc07273838236480031045051f232f6b2d4 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 19 Aug 2021 19:57:51 +0930 Subject: [PATCH 014/119] update to new fan entity configuration --- custom_components/localtuya/fan.py | 129 ++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 41 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index b0e1008..4e2af6b 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -1,3 +1,9 @@ +# DPS [1] VALUE [True] --> fan on,off > True, False +# DPS [3] VALUE [6] --> fan speed > 1-6 +# DPS [4] VALUE [forward] --> fan direction > forward, reverse +# DPS [102] VALUE [normal] --> preset mode > normal, sleep, nature +# DPS [103] VALUE [off] --> timer > off, 1hour, 2hour, 4hour, 8hour + """Platform to locally control Tuya-based fan devices.""" import logging from functools import partial @@ -20,7 +26,11 @@ from .const import ( CONF_FAN_OSCILLATING_CONTROL, CONF_FAN_SPEED_CONTROL, CONF_FAN_DIRECTION, - CONF_FAN_SPEED_COUNT + CONF_FAN_DIRECTION_FWD, + CONF_FAN_DIRECTION_REV, + CONF_FAN_SPEED_MIN, + CONF_FAN_SPEED_MAX, + CONF_FAN_ORDERED_LIST ) from homeassistant.util.percentage import ( @@ -40,15 +50,13 @@ def flow_schema(dps): vol.Optional(CONF_FAN_SPEED_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_OSCILLATING_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_DIRECTION): vol.In(dps), - vol.Optional(CONF_FAN_SPEED_COUNT, default=3): cv.positive_int + vol.Optional(CONF_FAN_DIRECTION_FWD, default="forward"): cv.string, + vol.Optional(CONF_FAN_DIRECTION_REV, default="reverse"): cv.string, + vol.Optional(CONF_FAN_SPEED_MIN, default=1): cv.positive_int, + vol.Optional(CONF_FAN_SPEED_MAX, default=9): cv.positive_int, + vol.Optional(CONF_FAN_ORDERED_LIST, default="disabled"): cv.string, } -# make this configurable later -DIRECTION_TO_TUYA = { - DIRECTION_REVERSE: "reverse", - DIRECTION_FORWARD: "forward", -} -TUYA_DIRECTION_TO_HA = {v: k for (k, v) in DIRECTION_TO_TUYA.items()} class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Representation of a Tuya fan.""" @@ -67,8 +75,20 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._direction = None self._percentage = None self._speed_range = ( - 1, self._config.get(CONF_FAN_SPEED_COUNT), + self._config.get(CONF_FAN_SPEED_MIN), + self._config.get(CONF_FAN_SPEED_MAX), ) + self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).split(",") + self._ordered_list_mode = None + + if (type(self._ordered_list) is list and len(self._ordered_list) > 1): + self._use_ordered_list = True + _LOGGER.debug("Fan _use_ordered_list: %s > %s", self._use_ordered_list, self._ordered_list) + + else: + self._use_ordered_list = False + _LOGGER.debug("Fan _use_ordered_list: %s", self._use_ordered_list) + @property def oscillating(self): @@ -80,7 +100,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Return the current direction of the fan.""" return self._direction - @property def is_on(self): """Check if Tuya fan is on.""" @@ -91,18 +110,9 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Return the current percentage.""" return self._percentage - # @property - # def speed(self) -> str: - # """Return the current speed.""" - # return self._speed - - # @property - # def speed_list(self) -> list: - # """Get the list of available speeds.""" - # return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the entity.""" + _LOGGER.debug("Fan async_turn_on") await self._device.set_dp(True, self._dp_id) if speed is not None: await self.async_set_speed(speed) @@ -111,11 +121,17 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): async def async_turn_off(self, **kwargs) -> None: """Turn off the entity.""" + + _LOGGER.debug("Fan async_turn_on") + await self._device.set_dp(False, self._dp_id) self.schedule_update_ha_state() async def async_set_percentage(self, percentage): """Set the speed of the fan.""" + + _LOGGER.debug("Fan async_set_percentage: %s", percentage) + if percentage == 0: return await self.async_turn_off() @@ -123,16 +139,28 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): await self.async_turn_on() if percentage is not None: - await self._device.set_dp( - str(math.ceil( - percentage_to_ranged_value(self._speed_range, percentage) - )), - self._config.get(CONF_FAN_SPEED_CONTROL) - ) + if self._use_ordered_list: + await self._device.set_dp( + str( + percentage_to_ordered_list_item(self._ordered_list, percentage) + ), + self._config.get(CONF_FAN_SPEED_CONTROL) + ) + _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) + + else: + await self._device.set_dp( + str(math.ceil( + percentage_to_ranged_value(self._speed_range, percentage) + )), + self._config.get(CONF_FAN_SPEED_CONTROL) + ) + _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._ordered_list, percentage)) async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation.""" + _LOGGER.debug("Fan async_oscillate: %s", oscillating) await self._device.set_dp( oscillating, self._config.get(CONF_FAN_OSCILLATING_CONTROL) ) @@ -140,8 +168,16 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): async def async_set_direction(self, direction): """Set the direction of the fan.""" + _LOGGER.debug("Fan async_set_direction: %s", direction) + + if direction == DIRECTION_FORWARD: + value = self._config.get(CONF_FAN_DIRECTION_FWD) + + if direction == DIRECTION_REVERSE: + value = self._config.get(CONF_FAN_DIRECTION_REV) + await self._device.set_dp( - DIRECTION_TO_TUYA[direction], self._config.get(CONF_FAN_DIRECTION) + value, self._config.get(CONF_FAN_DIRECTION) ) self.schedule_update_ha_state() @@ -164,7 +200,9 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): @property def speed_count(self) -> int: """Speed count for the fan.""" - return int_states_in_range(self._speed_range) + speed_count = int_states_in_range(self._speed_range) + _LOGGER.debug("Fan speed_count: %s", speed_count) + return speed_count def status_updated(self): @@ -172,25 +210,34 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._is_on = self.dps(self._dp_id) - if self.has_config(CONF_FAN_SPEED_COUNT): - value = int(self.dps_conf(CONF_FAN_SPEED_CONTROL)) - if value is not None: - self._percentage = ranged_value_to_percentage(self._speed_range, value) - else: - self.warning( - "%s/%s: Ignoring unknown fan controller state: %s", - self.name, - self.entity_id, - value, - ) + current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) + if self._use_ordered_list: + _LOGGER.debug("Fan current_speed ordered_list_item_to_percentage: %s from %s", current_speed, self._ordered_list) + if current_speed is not None: + self._percentage = ordered_list_item_to_percentage(self._ordered_list, current_speed) + + else: + _LOGGER.debug("Fan current_speed ranged_value_to_percentage: %s from %s", current_speed, self._speed_range) + if current_speed is not None: + self._percentage = ranged_value_to_percentage(self._speed_range, int(current_speed)) + + _LOGGER.debug("Fan current_percentage: %s", self._percentage) if self.has_config(CONF_FAN_OSCILLATING_CONTROL): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) + _LOGGER.debug("Fan current_oscillating : %s", self._oscillating) + if self.has_config(CONF_FAN_DIRECTION): value = self.dps_conf(CONF_FAN_DIRECTION) if value is not None: - self._direction = TUYA_DIRECTION_TO_HA[value] - + if value == self._config.get(CONF_FAN_DIRECTION_FWD): + self._direction = DIRECTION_FORWARD + + if value == self._config.get(CONF_FAN_DIRECTION_REV): + self._direction = DIRECTION_REVERSE + + _LOGGER.debug("Fan current_direction : %s > %s > %s", value, self._direction, self._config.get(CONF_FAN_DIRECTION_FWD)) + async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) From 5cbf556b9136fdacbc435bc8b023b5fd3086518a Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 19 Aug 2021 19:59:20 +0930 Subject: [PATCH 015/119] remove comments --- custom_components/localtuya/fan.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 4e2af6b..b4dad12 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -1,9 +1,3 @@ -# DPS [1] VALUE [True] --> fan on,off > True, False -# DPS [3] VALUE [6] --> fan speed > 1-6 -# DPS [4] VALUE [forward] --> fan direction > forward, reverse -# DPS [102] VALUE [normal] --> preset mode > normal, sleep, nature -# DPS [103] VALUE [off] --> timer > off, 1hour, 2hour, 4hour, 8hour - """Platform to locally control Tuya-based fan devices.""" import logging from functools import partial @@ -237,7 +231,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if value == self._config.get(CONF_FAN_DIRECTION_REV): self._direction = DIRECTION_REVERSE - _LOGGER.debug("Fan current_direction : %s > %s > %s", value, self._direction, self._config.get(CONF_FAN_DIRECTION_FWD)) + _LOGGER.debug("Fan current_direction : %s > %s", value, self._direction) async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) From 81967039c246646e6ae6900e8417c91b63158222 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 2 Sep 2021 10:35:34 +0930 Subject: [PATCH 016/119] fixed logger error --- custom_components/localtuya/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index b4dad12..e1e2b1f 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -149,7 +149,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): )), self._config.get(CONF_FAN_SPEED_CONTROL) ) - _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._ordered_list, percentage)) + _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) async def async_oscillate(self, oscillating: bool) -> None: From 3eacbbda5a8bbcf5b877661c7120912b056417cb Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 5 Oct 2021 15:57:42 +1030 Subject: [PATCH 017/119] spelling error --- custom_components/localtuya/translations/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index de40762..2cb903e 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -77,7 +77,7 @@ "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", - "fan_direction_reverse": "reverser dps string" + "fan_direction_reverse": "reverse dps string" } } } @@ -132,7 +132,7 @@ "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", - "fan_direction_reverse": "reverser dps string" + "fan_direction_reverse": "reverse dps string" } }, "yaml_import": { From 3fca5566bb38d5dfb4131a093a3672b61743faf5 Mon Sep 17 00:00:00 2001 From: Prasad Bankar <22224770+psbankar@users.noreply.github.com> Date: Wed, 6 Oct 2021 21:37:17 +0530 Subject: [PATCH 018/119] Update fan.py Changes: Changed set_speed in turn_on method Changed set_percentage method for better flow Logger changes --- custom_components/localtuya/fan.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index e1e2b1f..43377c9 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -104,35 +104,33 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Return the current percentage.""" return self._percentage - async def async_turn_on(self, speed: str = None, **kwargs) -> None: + async def async_turn_on(self, percentage: str = None, **kwargs) -> None: """Turn on the entity.""" _LOGGER.debug("Fan async_turn_on") await self._device.set_dp(True, self._dp_id) - if speed is not None: - await self.async_set_speed(speed) + if percentage is not None: + await self.async_set_percentage(speed) else: self.schedule_update_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn off the entity.""" - - _LOGGER.debug("Fan async_turn_on") - + _LOGGER.debug("Fan async_turn_off") + await self._device.set_dp(False, self._dp_id) self.schedule_update_ha_state() async def async_set_percentage(self, percentage): """Set the speed of the fan.""" - - _LOGGER.debug("Fan async_set_percentage: %s", percentage) - - if percentage == 0: - return await self.async_turn_off() - if not self.is_on: - await self.async_turn_on() + _LOGGER.debug("Fan async_set_percentage: %s", percentage) + if percentage is not None: + if percentage == 0: + return await self.async_turn_off() + elif not self.is_on: + await self.async_turn_on() if self._use_ordered_list: await self._device.set_dp( str( @@ -150,6 +148,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._config.get(CONF_FAN_SPEED_CONTROL) ) _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) + self.schedule_update_ha_state() async def async_oscillate(self, oscillating: bool) -> None: @@ -230,8 +229,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if value == self._config.get(CONF_FAN_DIRECTION_REV): self._direction = DIRECTION_REVERSE - - _LOGGER.debug("Fan current_direction : %s > %s", value, self._direction) + _LOGGER.debug("Fan current_direction : %s > %s", value, self._direction) async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) From ec223ba73d35d7ea2de64eff11ca647468fb43ae Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 26 Oct 2021 15:32:23 +1030 Subject: [PATCH 019/119] add presets and integer/string option --- custom_components/localtuya/const.py | 4 +- custom_components/localtuya/fan.py | 111 +++++++++++++----- .../localtuya/translations/en.json | 10 +- 3 files changed, 91 insertions(+), 34 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 9662b3b..3d3ac05 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -40,7 +40,9 @@ CONF_FAN_ORDERED_LIST = "fan_speed_ordered_list" CONF_FAN_DIRECTION = "fan_direction" CONF_FAN_DIRECTION_FWD = "fan_direction_forward" CONF_FAN_DIRECTION_REV = "fan_direction_reverse" - +CONF_FAN_SPEED_DPS_TYPE = "fan_speed_dps_type" +CONF_FAN_PRESET_CONTROL = "fan_preset_control" +CONF_FAN_PRESET_LIST = "fan_preset_list" # sensor CONF_SCALING = "scaling" diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 43377c9..4693764 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -24,7 +24,8 @@ from .const import ( CONF_FAN_DIRECTION_REV, CONF_FAN_SPEED_MIN, CONF_FAN_SPEED_MAX, - CONF_FAN_ORDERED_LIST + CONF_FAN_ORDERED_LIST, + CONF_FAN_SPEED_DPS_TYPE ) from homeassistant.util.percentage import ( @@ -44,11 +45,15 @@ def flow_schema(dps): vol.Optional(CONF_FAN_SPEED_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_OSCILLATING_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_DIRECTION): vol.In(dps), + vol.Optional(CONF_FAN_PRESET_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_DIRECTION_FWD, default="forward"): cv.string, vol.Optional(CONF_FAN_DIRECTION_REV, default="reverse"): cv.string, vol.Optional(CONF_FAN_SPEED_MIN, default=1): cv.positive_int, vol.Optional(CONF_FAN_SPEED_MAX, default=9): cv.positive_int, - vol.Optional(CONF_FAN_ORDERED_LIST, default="disabled"): cv.string, + vol.Optional(CONF_FAN_ORDERED_LIST): cv.string, + vol.Optional(CONF_FAN_PRESET_LIST): cv.string, + vol.Optional(CONF_FAN_SPEED_DPS_TYPE, default="string"): vol.In( + ["string", "integer", "list"]), } @@ -68,21 +73,21 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._oscillating = None self._direction = None self._percentage = None + self._preset = None self._speed_range = ( self._config.get(CONF_FAN_SPEED_MIN), self._config.get(CONF_FAN_SPEED_MAX), ) - self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).split(",") - self._ordered_list_mode = None - - if (type(self._ordered_list) is list and len(self._ordered_list) > 1): - self._use_ordered_list = True - _LOGGER.debug("Fan _use_ordered_list: %s > %s", self._use_ordered_list, self._ordered_list) - - else: - self._use_ordered_list = False - _LOGGER.debug("Fan _use_ordered_list: %s", self._use_ordered_list) + self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).replace(" ","").split(",") + self._preset_list = self._config.get(CONF_FAN_PRESET_LIST).replace(" ","").split(",") + self._ordered_speed_dps_type = self._config.get(CONF_FAN_SPEED_DPS_TYPE) + if (self._ordered_speed_dps_type == "list" and type(self._ordered_list) is list + and len(self._ordered_list) > 1): + _LOGGER.debug("Fan _use_ordered_list: %s", self._ordered_list) + else: + _LOGGER.debug("Fan _use_ordered_list: Not a valid list") + @property def oscillating(self): @@ -125,13 +130,32 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): _LOGGER.debug("Fan async_set_percentage: %s", percentage) - + if percentage is not None: if percentage == 0: return await self.async_turn_off() elif not self.is_on: await self.async_turn_on() - if self._use_ordered_list: + + if self._ordered_speed_dps_type == "string": + await self._device.set_dp( + str(math.ceil( + percentage_to_ranged_value(self._speed_range, percentage) + )), + self._config.get(CONF_FAN_SPEED_CONTROL) + ) + _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) + + elif self._ordered_speed_dps_type == "integer": + await self._device.set_dp( + int(math.ceil( + percentage_to_ranged_value(self._speed_range, percentage) + )), + self._config.get(CONF_FAN_SPEED_CONTROL) + ) + _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) + + elif self._ordered_speed_dps_type == "list": await self._device.set_dp( str( percentage_to_ordered_list_item(self._ordered_list, percentage) @@ -140,14 +164,23 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): ) _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) - else: - await self._device.set_dp( - str(math.ceil( - percentage_to_ranged_value(self._speed_range, percentage) - )), - self._config.get(CONF_FAN_SPEED_CONTROL) - ) - _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) + # if self._use_ordered_list: + # await self._device.set_dp( + # str( + # percentage_to_ordered_list_item(self._ordered_list, percentage) + # ), + # self._config.get(CONF_FAN_SPEED_CONTROL) + # ) + # _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) + + # else: + # await self._device.set_dp( + # str(math.ceil( + # percentage_to_ranged_value(self._speed_range, percentage) + # )), + # self._config.get(CONF_FAN_SPEED_CONTROL) + # ) + # _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) self.schedule_update_ha_state() @@ -172,7 +205,15 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): await self._device.set_dp( value, self._config.get(CONF_FAN_DIRECTION) ) - self.schedule_update_ha_state() + self.schedule_update_ha_state() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + _LOGGER.debug("Fan set preset: %s", preset_mode) + await self._device.set_dp( + preset_mode, self._config.get(CONF_FAN_PRESET_CONTROL) + ) + self.schedule_update_ha_state() @property def supported_features(self) -> int: @@ -188,6 +229,9 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if self.has_config(CONF_FAN_DIRECTION): features |= SUPPORT_DIRECTION + if self.has_config(CONF_FAN_PRESET_CONTROL): + features |= SUPPORT_PRESET_MODE + return features @property @@ -203,18 +247,23 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._is_on = self.dps(self._dp_id) + # check for speed and preset. current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) - if self._use_ordered_list: - _LOGGER.debug("Fan current_speed ordered_list_item_to_percentage: %s from %s", current_speed, self._ordered_list) - if current_speed is not None: + if current_speed is not None: + + if current_speed in self._preset_list: + _LOGGER.debug("Fan current_speed in preset list: %s from %s", current_speed, self._preset_list) + self._preset = current_speed + + if self._ordered_speed_dps_type == "list": + _LOGGER.debug("Fan current_speed ordered_list_item_to_percentage: %s from %s", current_speed, self._ordered_list) self._percentage = ordered_list_item_to_percentage(self._ordered_list, current_speed) - - else: - _LOGGER.debug("Fan current_speed ranged_value_to_percentage: %s from %s", current_speed, self._speed_range) - if current_speed is not None: + + elif self._ordered_speed_dps_type == "string" or self._ordered_speed_dps_type == "integer" : + _LOGGER.debug("Fan current_speed ranged_value_to_percentage: %s from %s", current_speed, self._speed_range) self._percentage = ranged_value_to_percentage(self._speed_range, int(current_speed)) - _LOGGER.debug("Fan current_percentage: %s", self._percentage) + _LOGGER.debug("Fan current_percentage: %s", self._percentage) if self.has_config(CONF_FAN_OSCILLATING_CONTROL): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 2cb903e..e465784 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -77,7 +77,10 @@ "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", - "fan_direction_reverse": "reverse dps string" + "fan_direction_reverse": "reverse dps string", + "fan_speed_dps_type": "type of speed dps control. integer/string/list", + "fan_preset_control": "Fan preset dps. CAn be the same as speed", + "fan_preset_list": "FAn preset list. Comma separated" } } } @@ -132,7 +135,10 @@ "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", - "fan_direction_reverse": "reverse dps string" + "fan_direction_reverse": "reverse dps string", + "fan_speed_dps_type": "type of speed dps control. integer/string/list", + "fan_preset_control": "Fan preset dps. CAn be the same as speed", + "fan_preset_list": "FAn preset list. Comma separated" } }, "yaml_import": { From 6f281a08fc0825e6b718f251b7a36ad30c88767f Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 26 Oct 2021 15:35:18 +1030 Subject: [PATCH 020/119] missing imports --- custom_components/localtuya/fan.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 4693764..4aa2f45 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -12,6 +12,7 @@ from homeassistant.components.fan import ( SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, + SUPPORT_PRESET_MODE, FanEntity, ) @@ -25,7 +26,8 @@ from .const import ( CONF_FAN_SPEED_MIN, CONF_FAN_SPEED_MAX, CONF_FAN_ORDERED_LIST, - CONF_FAN_SPEED_DPS_TYPE + CONF_FAN_SPEED_DPS_TYPE, + CONF_FAN_PRESET_LIST ) from homeassistant.util.percentage import ( From 0c59ddc7e0e008de8236658eae51ccf8ac56c485 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 26 Oct 2021 16:09:39 +1030 Subject: [PATCH 021/119] correctly split preset and speed in updates --- custom_components/localtuya/fan.py | 35 ++++++++++++------------------ 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 4aa2f45..89024c3 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -20,6 +20,7 @@ from .common import LocalTuyaEntity, async_setup_entry from .const import ( CONF_FAN_OSCILLATING_CONTROL, CONF_FAN_SPEED_CONTROL, + CONF_FAN_PRESET_CONTROL, CONF_FAN_DIRECTION, CONF_FAN_DIRECTION_FWD, CONF_FAN_DIRECTION_REV, @@ -84,7 +85,8 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._preset_list = self._config.get(CONF_FAN_PRESET_LIST).replace(" ","").split(",") self._ordered_speed_dps_type = self._config.get(CONF_FAN_SPEED_DPS_TYPE) - if (self._ordered_speed_dps_type == "list" and type(self._ordered_list) is list + if (self._ordered_speed_dps_type == "list" + and type(self._ordered_list) is list and len(self._ordered_list) > 1): _LOGGER.debug("Fan _use_ordered_list: %s", self._ordered_list) else: @@ -166,23 +168,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): ) _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) - # if self._use_ordered_list: - # await self._device.set_dp( - # str( - # percentage_to_ordered_list_item(self._ordered_list, percentage) - # ), - # self._config.get(CONF_FAN_SPEED_CONTROL) - # ) - # _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) - - # else: - # await self._device.set_dp( - # str(math.ceil( - # percentage_to_ranged_value(self._speed_range, percentage) - # )), - # self._config.get(CONF_FAN_SPEED_CONTROL) - # ) - # _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) self.schedule_update_ha_state() @@ -249,15 +234,22 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._is_on = self.dps(self._dp_id) - # check for speed and preset. + if self.has_config(CONF_FAN_PRESET_CONTROL): + current_preset = self.dps_conf(CONF_FAN_PRESET_CONTROL) + if current_preset is not None and current_preset in self._preset_list: + _LOGGER.debug("Fan current_preset in preset list: %s from %s", current_preset, self._preset_list) + self._preset = current_preset + current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) if current_speed is not None: - if current_speed in self._preset_list: + if (self.has_config(CONF_FAN_PRESET_CONTROL) + and (CONF_FAN_SPEED_CONTROL == CONF_FAN_PRESET_CONTROL) + and (current_speed in self._preset_list)): _LOGGER.debug("Fan current_speed in preset list: %s from %s", current_speed, self._preset_list) self._preset = current_speed - if self._ordered_speed_dps_type == "list": + elif self._ordered_speed_dps_type == "list": _LOGGER.debug("Fan current_speed ordered_list_item_to_percentage: %s from %s", current_speed, self._ordered_list) self._percentage = ordered_list_item_to_percentage(self._ordered_list, current_speed) @@ -266,6 +258,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._percentage = ranged_value_to_percentage(self._speed_range, int(current_speed)) _LOGGER.debug("Fan current_percentage: %s", self._percentage) + _LOGGER.debug("Fan current_preset: %s", self._preset) if self.has_config(CONF_FAN_OSCILLATING_CONTROL): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) From 7ac4fe5a119c7b1f6ff9c0b835e273a57b3b7362 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 26 Oct 2021 21:41:51 +1030 Subject: [PATCH 022/119] Revert "correctly split preset and speed in updates" This reverts commit 0c59ddc7e0e008de8236658eae51ccf8ac56c485. --- custom_components/localtuya/fan.py | 35 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 89024c3..4aa2f45 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -20,7 +20,6 @@ from .common import LocalTuyaEntity, async_setup_entry from .const import ( CONF_FAN_OSCILLATING_CONTROL, CONF_FAN_SPEED_CONTROL, - CONF_FAN_PRESET_CONTROL, CONF_FAN_DIRECTION, CONF_FAN_DIRECTION_FWD, CONF_FAN_DIRECTION_REV, @@ -85,8 +84,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._preset_list = self._config.get(CONF_FAN_PRESET_LIST).replace(" ","").split(",") self._ordered_speed_dps_type = self._config.get(CONF_FAN_SPEED_DPS_TYPE) - if (self._ordered_speed_dps_type == "list" - and type(self._ordered_list) is list + if (self._ordered_speed_dps_type == "list" and type(self._ordered_list) is list and len(self._ordered_list) > 1): _LOGGER.debug("Fan _use_ordered_list: %s", self._ordered_list) else: @@ -168,6 +166,23 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): ) _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) + # if self._use_ordered_list: + # await self._device.set_dp( + # str( + # percentage_to_ordered_list_item(self._ordered_list, percentage) + # ), + # self._config.get(CONF_FAN_SPEED_CONTROL) + # ) + # _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) + + # else: + # await self._device.set_dp( + # str(math.ceil( + # percentage_to_ranged_value(self._speed_range, percentage) + # )), + # self._config.get(CONF_FAN_SPEED_CONTROL) + # ) + # _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) self.schedule_update_ha_state() @@ -234,22 +249,15 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._is_on = self.dps(self._dp_id) - if self.has_config(CONF_FAN_PRESET_CONTROL): - current_preset = self.dps_conf(CONF_FAN_PRESET_CONTROL) - if current_preset is not None and current_preset in self._preset_list: - _LOGGER.debug("Fan current_preset in preset list: %s from %s", current_preset, self._preset_list) - self._preset = current_preset - + # check for speed and preset. current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) if current_speed is not None: - if (self.has_config(CONF_FAN_PRESET_CONTROL) - and (CONF_FAN_SPEED_CONTROL == CONF_FAN_PRESET_CONTROL) - and (current_speed in self._preset_list)): + if current_speed in self._preset_list: _LOGGER.debug("Fan current_speed in preset list: %s from %s", current_speed, self._preset_list) self._preset = current_speed - elif self._ordered_speed_dps_type == "list": + if self._ordered_speed_dps_type == "list": _LOGGER.debug("Fan current_speed ordered_list_item_to_percentage: %s from %s", current_speed, self._ordered_list) self._percentage = ordered_list_item_to_percentage(self._ordered_list, current_speed) @@ -258,7 +266,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._percentage = ranged_value_to_percentage(self._speed_range, int(current_speed)) _LOGGER.debug("Fan current_percentage: %s", self._percentage) - _LOGGER.debug("Fan current_preset: %s", self._preset) if self.has_config(CONF_FAN_OSCILLATING_CONTROL): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) From 0f43923c2e3d9c652306909313db170b7fa7f0e0 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 26 Oct 2021 21:41:56 +1030 Subject: [PATCH 023/119] Revert "missing imports" This reverts commit 6f281a08fc0825e6b718f251b7a36ad30c88767f. --- custom_components/localtuya/fan.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 4aa2f45..4693764 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -12,7 +12,6 @@ from homeassistant.components.fan import ( SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, - SUPPORT_PRESET_MODE, FanEntity, ) @@ -26,8 +25,7 @@ from .const import ( CONF_FAN_SPEED_MIN, CONF_FAN_SPEED_MAX, CONF_FAN_ORDERED_LIST, - CONF_FAN_SPEED_DPS_TYPE, - CONF_FAN_PRESET_LIST + CONF_FAN_SPEED_DPS_TYPE ) from homeassistant.util.percentage import ( From c8e660dfac689440a4bcbd3a68c45af39b775451 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 26 Oct 2021 21:41:59 +1030 Subject: [PATCH 024/119] Revert "add presets and integer/string option" This reverts commit ec223ba73d35d7ea2de64eff11ca647468fb43ae. --- custom_components/localtuya/const.py | 4 +- custom_components/localtuya/fan.py | 111 +++++------------- .../localtuya/translations/en.json | 10 +- 3 files changed, 34 insertions(+), 91 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 3d3ac05..9662b3b 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -40,9 +40,7 @@ CONF_FAN_ORDERED_LIST = "fan_speed_ordered_list" CONF_FAN_DIRECTION = "fan_direction" CONF_FAN_DIRECTION_FWD = "fan_direction_forward" CONF_FAN_DIRECTION_REV = "fan_direction_reverse" -CONF_FAN_SPEED_DPS_TYPE = "fan_speed_dps_type" -CONF_FAN_PRESET_CONTROL = "fan_preset_control" -CONF_FAN_PRESET_LIST = "fan_preset_list" + # sensor CONF_SCALING = "scaling" diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 4693764..43377c9 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -24,8 +24,7 @@ from .const import ( CONF_FAN_DIRECTION_REV, CONF_FAN_SPEED_MIN, CONF_FAN_SPEED_MAX, - CONF_FAN_ORDERED_LIST, - CONF_FAN_SPEED_DPS_TYPE + CONF_FAN_ORDERED_LIST ) from homeassistant.util.percentage import ( @@ -45,15 +44,11 @@ def flow_schema(dps): vol.Optional(CONF_FAN_SPEED_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_OSCILLATING_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_DIRECTION): vol.In(dps), - vol.Optional(CONF_FAN_PRESET_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_DIRECTION_FWD, default="forward"): cv.string, vol.Optional(CONF_FAN_DIRECTION_REV, default="reverse"): cv.string, vol.Optional(CONF_FAN_SPEED_MIN, default=1): cv.positive_int, vol.Optional(CONF_FAN_SPEED_MAX, default=9): cv.positive_int, - vol.Optional(CONF_FAN_ORDERED_LIST): cv.string, - vol.Optional(CONF_FAN_PRESET_LIST): cv.string, - vol.Optional(CONF_FAN_SPEED_DPS_TYPE, default="string"): vol.In( - ["string", "integer", "list"]), + vol.Optional(CONF_FAN_ORDERED_LIST, default="disabled"): cv.string, } @@ -73,21 +68,21 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._oscillating = None self._direction = None self._percentage = None - self._preset = None self._speed_range = ( self._config.get(CONF_FAN_SPEED_MIN), self._config.get(CONF_FAN_SPEED_MAX), ) - self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).replace(" ","").split(",") - self._preset_list = self._config.get(CONF_FAN_PRESET_LIST).replace(" ","").split(",") - self._ordered_speed_dps_type = self._config.get(CONF_FAN_SPEED_DPS_TYPE) - - if (self._ordered_speed_dps_type == "list" and type(self._ordered_list) is list - and len(self._ordered_list) > 1): - _LOGGER.debug("Fan _use_ordered_list: %s", self._ordered_list) + self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).split(",") + self._ordered_list_mode = None + + if (type(self._ordered_list) is list and len(self._ordered_list) > 1): + self._use_ordered_list = True + _LOGGER.debug("Fan _use_ordered_list: %s > %s", self._use_ordered_list, self._ordered_list) + else: - _LOGGER.debug("Fan _use_ordered_list: Not a valid list") - + self._use_ordered_list = False + _LOGGER.debug("Fan _use_ordered_list: %s", self._use_ordered_list) + @property def oscillating(self): @@ -130,32 +125,13 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): _LOGGER.debug("Fan async_set_percentage: %s", percentage) - + if percentage is not None: if percentage == 0: return await self.async_turn_off() elif not self.is_on: await self.async_turn_on() - - if self._ordered_speed_dps_type == "string": - await self._device.set_dp( - str(math.ceil( - percentage_to_ranged_value(self._speed_range, percentage) - )), - self._config.get(CONF_FAN_SPEED_CONTROL) - ) - _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) - - elif self._ordered_speed_dps_type == "integer": - await self._device.set_dp( - int(math.ceil( - percentage_to_ranged_value(self._speed_range, percentage) - )), - self._config.get(CONF_FAN_SPEED_CONTROL) - ) - _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) - - elif self._ordered_speed_dps_type == "list": + if self._use_ordered_list: await self._device.set_dp( str( percentage_to_ordered_list_item(self._ordered_list, percentage) @@ -164,23 +140,14 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): ) _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) - # if self._use_ordered_list: - # await self._device.set_dp( - # str( - # percentage_to_ordered_list_item(self._ordered_list, percentage) - # ), - # self._config.get(CONF_FAN_SPEED_CONTROL) - # ) - # _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) - - # else: - # await self._device.set_dp( - # str(math.ceil( - # percentage_to_ranged_value(self._speed_range, percentage) - # )), - # self._config.get(CONF_FAN_SPEED_CONTROL) - # ) - # _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) + else: + await self._device.set_dp( + str(math.ceil( + percentage_to_ranged_value(self._speed_range, percentage) + )), + self._config.get(CONF_FAN_SPEED_CONTROL) + ) + _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) self.schedule_update_ha_state() @@ -205,15 +172,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): await self._device.set_dp( value, self._config.get(CONF_FAN_DIRECTION) ) - self.schedule_update_ha_state() - - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set the preset mode of the fan.""" - _LOGGER.debug("Fan set preset: %s", preset_mode) - await self._device.set_dp( - preset_mode, self._config.get(CONF_FAN_PRESET_CONTROL) - ) - self.schedule_update_ha_state() + self.schedule_update_ha_state() @property def supported_features(self) -> int: @@ -229,9 +188,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if self.has_config(CONF_FAN_DIRECTION): features |= SUPPORT_DIRECTION - if self.has_config(CONF_FAN_PRESET_CONTROL): - features |= SUPPORT_PRESET_MODE - return features @property @@ -247,23 +203,18 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._is_on = self.dps(self._dp_id) - # check for speed and preset. current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) - if current_speed is not None: - - if current_speed in self._preset_list: - _LOGGER.debug("Fan current_speed in preset list: %s from %s", current_speed, self._preset_list) - self._preset = current_speed - - if self._ordered_speed_dps_type == "list": - _LOGGER.debug("Fan current_speed ordered_list_item_to_percentage: %s from %s", current_speed, self._ordered_list) + if self._use_ordered_list: + _LOGGER.debug("Fan current_speed ordered_list_item_to_percentage: %s from %s", current_speed, self._ordered_list) + if current_speed is not None: self._percentage = ordered_list_item_to_percentage(self._ordered_list, current_speed) - - elif self._ordered_speed_dps_type == "string" or self._ordered_speed_dps_type == "integer" : - _LOGGER.debug("Fan current_speed ranged_value_to_percentage: %s from %s", current_speed, self._speed_range) + + else: + _LOGGER.debug("Fan current_speed ranged_value_to_percentage: %s from %s", current_speed, self._speed_range) + if current_speed is not None: self._percentage = ranged_value_to_percentage(self._speed_range, int(current_speed)) - _LOGGER.debug("Fan current_percentage: %s", self._percentage) + _LOGGER.debug("Fan current_percentage: %s", self._percentage) if self.has_config(CONF_FAN_OSCILLATING_CONTROL): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index e465784..2cb903e 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -77,10 +77,7 @@ "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", - "fan_direction_reverse": "reverse dps string", - "fan_speed_dps_type": "type of speed dps control. integer/string/list", - "fan_preset_control": "Fan preset dps. CAn be the same as speed", - "fan_preset_list": "FAn preset list. Comma separated" + "fan_direction_reverse": "reverse dps string" } } } @@ -135,10 +132,7 @@ "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", - "fan_direction_reverse": "reverse dps string", - "fan_speed_dps_type": "type of speed dps control. integer/string/list", - "fan_preset_control": "Fan preset dps. CAn be the same as speed", - "fan_preset_list": "FAn preset list. Comma separated" + "fan_direction_reverse": "reverse dps string" } }, "yaml_import": { From ca0183306113696bd302c7360e5ea4dd6f04e3ee Mon Sep 17 00:00:00 2001 From: rikman122 Date: Mon, 8 Nov 2021 20:35:46 +0100 Subject: [PATCH 025/119] lint test fixes --- custom_components/localtuya/__init__.py | 2 +- custom_components/localtuya/vacuum.py | 49 ++++++++++++------------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index c1d77ed..ad879b0 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -53,7 +53,7 @@ localtuya: current: 18 # Optional current_consumption: 19 # Optional voltage: 20 # Optional - + - platform: vacuum friendly_name: Vacuum id: 28 diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py index 7e11f8d..5b365a6 100644 --- a/custom_components/localtuya/vacuum.py +++ b/custom_components/localtuya/vacuum.py @@ -18,7 +18,6 @@ from homeassistant.components.vacuum import ( SUPPORT_STATE, SUPPORT_STATUS, SUPPORT_STOP, - SUPPORT_PAUSE, SUPPORT_LOCATE, StateVacuumEntity, ) @@ -45,10 +44,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CLEAN_TIME = "clean_time" -CLEAN_AREA = "clean_area" -MODES_LIST = "cleaning_mode_list" -MODE = "cleaning_mode" +CLEAN_TIME = "clean_time" +CLEAN_AREA = "clean_area" +MODES_LIST = "cleaning_mode_list" +MODE = "cleaning_mode" DEFAULT_IDLE_STATUS = "standby,sleep" DEFAULT_RETURNING_STATUS = "docking" @@ -59,13 +58,15 @@ DEFAULT_PAUSED_STATE = "paused" DEFAULT_RETURN_MODE = "chargego" DEFAULT_STOP_STATUS = "standby" + def flow_schema(dps): """Return schema used in config flow.""" return { vol.Required(CONF_IDLE_STATUS_VALUE, default=DEFAULT_IDLE_STATUS): str, vol.Required(CONF_POWERGO_DP): vol.In(dps), vol.Required(CONF_DOCKED_STATUS_VALUE, default=DEFAULT_DOCKED_STATUS): str, - vol.Optional(CONF_RETURNING_STATUS_VALUE, default=DEFAULT_RETURNING_STATUS): str, + vol.Optional(CONF_RETURNING_STATUS_VALUE, + default=DEFAULT_RETURNING_STATUS): str, vol.Optional(CONF_BATTERY_DP): vol.In(dps), vol.Optional(CONF_MODE_DP): vol.In(dps), vol.Optional(CONF_MODES, default=DEFAULT_MODES): str, @@ -85,7 +86,7 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): def __init__(self, device, config_entry, switchid, **kwargs): """Initialize a new LocaltuyaVacuum.""" - super().__init__(device, config_entry, switchid ,_LOGGER, **kwargs) + super().__init__(device, config_entry, switchid , _LOGGER, **kwargs) self._state = None self._battery_level = None self._attrs = {} @@ -98,7 +99,7 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): if self.has_config(CONF_MODES): self._modes_list = self._config[CONF_MODES].split(",") self._attrs[MODES_LIST] = self._modes_list - + self._docked_status_list = [] if self.has_config(CONF_DOCKED_STATUS_VALUE): self._docked_status_list = self._config[CONF_DOCKED_STATUS_VALUE].split(",") @@ -115,7 +116,8 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): @property def supported_features(self): """Flag supported features.""" - supported_features = SUPPORT_START | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_STATUS | SUPPORT_STATE + supported_features = (SUPPORT_START | SUPPORT_PAUSE + | SUPPORT_STOP | SUPPORT_STATUS | SUPPORT_STATE) if self.has_config(CONF_RETURN_MODE): supported_features = supported_features | SUPPORT_RETURN_HOME @@ -153,11 +155,6 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): """Return the list of available fan speeds.""" return self._fan_speed_list - @property - def error(self): - """Return error message.""" - return "" - async def async_start(self, **kwargs): """Turn the vacuum on and start cleaning.""" await self._device.set_dp(True, self._config[CONF_POWERGO_DP]) @@ -169,14 +166,16 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" if self.has_config(CONF_RETURN_MODE): - await self._device.set_dp(self._config[CONF_RETURN_MODE], self._config[CONF_MODE_DP]) + await self._device.set_dp(self._config[CONF_RETURN_MODE], + self._config[CONF_MODE_DP]) else: _LOGGER.error("Missing command for return home in commands set.") async def async_stop(self, **kwargs): - """Turn the vacuum off stopping the cleaning""" + """Turn the vacuum off stopping the cleaning.""" if self.has_config(CONF_STOP_STATUS): - await self._device.set_dp(self._config[CONF_STOP_STATUS], self._config[CONF_MODE_DP]) + await self._device.set_dp(self._config[CONF_STOP_STATUS], + self._config[CONF_MODE_DP]) else: _LOGGER.error("Missing command for stop in commands set.") @@ -189,9 +188,8 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): if self.has_config(CONF_LOCATE_DP): await self._device.set_dp('', self._config[CONF_LOCATE_DP]) - async def async_set_fan_speed(self, **kwargs): + async def async_set_fan_speed(self, fan_speed, **kwargs): """Set the fan speed.""" - fan_speed = kwargs["fan_speed"] await self._device.set_dp(fan_speed, self._config[CONF_FAN_SPEED_DP]) async def async_send_command(self, command, params=None, **kwargs): @@ -216,20 +214,21 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): if self.has_config(CONF_BATTERY_DP): self._battery_level = self.dps_conf(CONF_BATTERY_DP) - + self._cleaning_mode = "" if self.has_config(CONF_MODES): self._cleaning_mode = self.dps_conf(CONF_MODE_DP) self._attrs[MODE] = self._cleaning_mode - + self._fan_speed = "" if self.has_config(CONF_FAN_SPEEDS): self._fan_speed = self.dps_conf(CONF_FAN_SPEED_DP) - + if self.has_config(CONF_CLEAN_TIME_DP): self._attrs[CLEAN_TIME] = self.dps_conf(CONF_CLEAN_TIME_DP) - + if self.has_config(CONF_CLEAN_AREA_DP): self._attrs[CLEAN_AREA] = self.dps_conf(CONF_CLEAN_AREA_DP) - -async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaVacuum, flow_schema) \ No newline at end of file + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaVacuum, flow_schema) From c99a93bd6c3f43ba17107720a97bd289a79c59e6 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 10 Nov 2021 06:59:58 +1030 Subject: [PATCH 026/119] Fix speed variable error --- custom_components/localtuya/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 43377c9..37ff370 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -109,7 +109,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): _LOGGER.debug("Fan async_turn_on") await self._device.set_dp(True, self._dp_id) if percentage is not None: - await self.async_set_percentage(speed) + await self.async_set_percentage(percentage) else: self.schedule_update_ha_state() From 9bf9b9f55dc1b4942fdfcc48dc3d6524a7f0aabe Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 11:12:43 +1030 Subject: [PATCH 027/119] fix formatting errors --- .vscode/settings.json | 5 ++++ custom_components/localtuya/fan.py | 40 ++++++++++++++++-------------- 2 files changed, 26 insertions(+), 19 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a04b218 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.yaml": "home-assistant" + } +} \ No newline at end of file diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 37ff370..1a5b8d8 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -69,7 +69,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._direction = None self._percentage = None self._speed_range = ( - self._config.get(CONF_FAN_SPEED_MIN), + self._config.get(CONF_FAN_SPEED_MIN), self._config.get(CONF_FAN_SPEED_MAX), ) self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).split(",") @@ -77,12 +77,12 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if (type(self._ordered_list) is list and len(self._ordered_list) > 1): self._use_ordered_list = True - _LOGGER.debug("Fan _use_ordered_list: %s > %s", self._use_ordered_list, self._ordered_list) + _LOGGER.debug("Fan _use_ordered_list: %s > %s", + self._use_ordered_list, self._ordered_list) else: self._use_ordered_list = False _LOGGER.debug("Fan _use_ordered_list: %s", self._use_ordered_list) - @property def oscillating(self): @@ -125,7 +125,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): _LOGGER.debug("Fan async_set_percentage: %s", percentage) - if percentage is not None: if percentage == 0: return await self.async_turn_off() @@ -137,8 +136,9 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): percentage_to_ordered_list_item(self._ordered_list, percentage) ), self._config.get(CONF_FAN_SPEED_CONTROL) - ) - _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) + ) + _LOGGER.debug("Fan async_set_percentage: %s > %s", + percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) else: await self._device.set_dp( @@ -146,11 +146,11 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): percentage_to_ranged_value(self._speed_range, percentage) )), self._config.get(CONF_FAN_SPEED_CONTROL) - ) - _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) + ) + _LOGGER.debug("Fan async_set_percentage: %s > %s", + percentage, percentage_to_ranged_value(self._speed_range, percentage)) self.schedule_update_ha_state() - async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation.""" _LOGGER.debug("Fan async_oscillate: %s", oscillating) @@ -165,14 +165,14 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if direction == DIRECTION_FORWARD: value = self._config.get(CONF_FAN_DIRECTION_FWD) - + if direction == DIRECTION_REVERSE: value = self._config.get(CONF_FAN_DIRECTION_REV) await self._device.set_dp( value, self._config.get(CONF_FAN_DIRECTION) ) - self.schedule_update_ha_state() + self.schedule_update_ha_state() @property def supported_features(self) -> int: @@ -188,7 +188,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if self.has_config(CONF_FAN_DIRECTION): features |= SUPPORT_DIRECTION - return features + return features @property def speed_count(self) -> int: @@ -197,7 +197,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): _LOGGER.debug("Fan speed_count: %s", speed_count) return speed_count - def status_updated(self): """Get state of Tuya fan.""" @@ -205,12 +204,17 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) if self._use_ordered_list: - _LOGGER.debug("Fan current_speed ordered_list_item_to_percentage: %s from %s", current_speed, self._ordered_list) + _LOGGER.debug( + "Fan current_speed ordered_list_item_to_percentage: %s from %s", + current_speed, self._ordered_list + ) if current_speed is not None: - self._percentage = ordered_list_item_to_percentage(self._ordered_list, current_speed) + self._percentage = ordered_list_item_to_percentage( + self._ordered_list, current_speed) else: - _LOGGER.debug("Fan current_speed ranged_value_to_percentage: %s from %s", current_speed, self._speed_range) + _LOGGER.debug("Fan current_speed ranged_value_to_percentage: %s from %s", + current_speed, self._speed_range) if current_speed is not None: self._percentage = ranged_value_to_percentage(self._speed_range, int(current_speed)) @@ -220,7 +224,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) _LOGGER.debug("Fan current_oscillating : %s", self._oscillating) - if self.has_config(CONF_FAN_DIRECTION): value = self.dps_conf(CONF_FAN_DIRECTION) if value is not None: @@ -230,6 +233,5 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if value == self._config.get(CONF_FAN_DIRECTION_REV): self._direction = DIRECTION_REVERSE _LOGGER.debug("Fan current_direction : %s > %s", value, self._direction) - -async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) \ No newline at end of file From af9f11ad10707d1e1dbaf686ea930727cfedf8f5 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 11:12:43 +1030 Subject: [PATCH 028/119] fix formatting errors --- .vscode/settings.json | 5 ++ custom_components/localtuya/fan.py | 114 +++++++++++++++++------------ 2 files changed, 71 insertions(+), 48 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a04b218 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.yaml": "home-assistant" + } +} \ No newline at end of file diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 37ff370..51a2191 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -1,38 +1,37 @@ """Platform to locally control Tuya-based fan devices.""" import logging -from functools import partial import math +from functools import partial import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.components.fan import ( - DOMAIN, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity, ) - -from .common import LocalTuyaEntity, async_setup_entry -from .const import ( - CONF_FAN_OSCILLATING_CONTROL, - CONF_FAN_SPEED_CONTROL, - CONF_FAN_DIRECTION, - CONF_FAN_DIRECTION_FWD, - CONF_FAN_DIRECTION_REV, - CONF_FAN_SPEED_MIN, - CONF_FAN_SPEED_MAX, - CONF_FAN_ORDERED_LIST -) - from homeassistant.util.percentage import ( + int_states_in_range, ordered_list_item_to_percentage, percentage_to_ordered_list_item, percentage_to_ranged_value, ranged_value_to_percentage, - int_states_in_range +) + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + CONF_FAN_DIRECTION, + CONF_FAN_DIRECTION_FWD, + CONF_FAN_DIRECTION_REV, + CONF_FAN_ORDERED_LIST, + CONF_FAN_OSCILLATING_CONTROL, + CONF_FAN_SPEED_CONTROL, + CONF_FAN_SPEED_MAX, + CONF_FAN_SPEED_MIN, ) _LOGGER = logging.getLogger(__name__) @@ -69,20 +68,23 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._direction = None self._percentage = None self._speed_range = ( - self._config.get(CONF_FAN_SPEED_MIN), + self._config.get(CONF_FAN_SPEED_MIN), self._config.get(CONF_FAN_SPEED_MAX), ) self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).split(",") self._ordered_list_mode = None - if (type(self._ordered_list) is list and len(self._ordered_list) > 1): + if type(self._ordered_list) is list and len(self._ordered_list) > 1: self._use_ordered_list = True - _LOGGER.debug("Fan _use_ordered_list: %s > %s", self._use_ordered_list, self._ordered_list) + _LOGGER.debug( + "Fan _use_ordered_list: %s > %s", + self._use_ordered_list, + self._ordered_list, + ) else: self._use_ordered_list = False _LOGGER.debug("Fan _use_ordered_list: %s", self._use_ordered_list) - @property def oscillating(self): @@ -125,7 +127,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): _LOGGER.debug("Fan async_set_percentage: %s", percentage) - if percentage is not None: if percentage == 0: return await self.async_turn_off() @@ -135,22 +136,31 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): await self._device.set_dp( str( percentage_to_ordered_list_item(self._ordered_list, percentage) - ), - self._config.get(CONF_FAN_SPEED_CONTROL) - ) - _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ordered_list_item(self._ordered_list, percentage)) + ), + self._config.get(CONF_FAN_SPEED_CONTROL), + ) + _LOGGER.debug( + "Fan async_set_percentage: %s > %s", + percentage, + percentage_to_ordered_list_item(self._ordered_list, percentage), + ) else: await self._device.set_dp( - str(math.ceil( - percentage_to_ranged_value(self._speed_range, percentage) - )), - self._config.get(CONF_FAN_SPEED_CONTROL) - ) - _LOGGER.debug("Fan async_set_percentage: %s > %s", percentage, percentage_to_ranged_value(self._speed_range, percentage)) + str( + math.ceil( + percentage_to_ranged_value(self._speed_range, percentage) + ) + ), + self._config.get(CONF_FAN_SPEED_CONTROL), + ) + _LOGGER.debug( + "Fan async_set_percentage: %s > %s", + percentage, + percentage_to_ranged_value(self._speed_range, percentage), + ) self.schedule_update_ha_state() - async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation.""" _LOGGER.debug("Fan async_oscillate: %s", oscillating) @@ -165,14 +175,12 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if direction == DIRECTION_FORWARD: value = self._config.get(CONF_FAN_DIRECTION_FWD) - + if direction == DIRECTION_REVERSE: value = self._config.get(CONF_FAN_DIRECTION_REV) - await self._device.set_dp( - value, self._config.get(CONF_FAN_DIRECTION) - ) - self.schedule_update_ha_state() + await self._device.set_dp(value, self._config.get(CONF_FAN_DIRECTION)) + self.schedule_update_ha_state() @property def supported_features(self) -> int: @@ -188,8 +196,8 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if self.has_config(CONF_FAN_DIRECTION): features |= SUPPORT_DIRECTION - return features - + return features + @property def speed_count(self) -> int: """Speed count for the fan.""" @@ -197,7 +205,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): _LOGGER.debug("Fan speed_count: %s", speed_count) return speed_count - def status_updated(self): """Get state of Tuya fan.""" @@ -205,14 +212,26 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) if self._use_ordered_list: - _LOGGER.debug("Fan current_speed ordered_list_item_to_percentage: %s from %s", current_speed, self._ordered_list) + _LOGGER.debug( + "Fan current_speed ordered_list_item_to_percentage: %s from %s", + current_speed, + self._ordered_list, + ) if current_speed is not None: - self._percentage = ordered_list_item_to_percentage(self._ordered_list, current_speed) - + self._percentage = ordered_list_item_to_percentage( + self._ordered_list, current_speed + ) + else: - _LOGGER.debug("Fan current_speed ranged_value_to_percentage: %s from %s", current_speed, self._speed_range) + _LOGGER.debug( + "Fan current_speed ranged_value_to_percentage: %s from %s", + current_speed, + self._speed_range, + ) if current_speed is not None: - self._percentage = ranged_value_to_percentage(self._speed_range, int(current_speed)) + self._percentage = ranged_value_to_percentage( + self._speed_range, int(current_speed) + ) _LOGGER.debug("Fan current_percentage: %s", self._percentage) @@ -220,16 +239,15 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) _LOGGER.debug("Fan current_oscillating : %s", self._oscillating) - if self.has_config(CONF_FAN_DIRECTION): value = self.dps_conf(CONF_FAN_DIRECTION) if value is not None: if value == self._config.get(CONF_FAN_DIRECTION_FWD): self._direction = DIRECTION_FORWARD - + if value == self._config.get(CONF_FAN_DIRECTION_REV): self._direction = DIRECTION_REVERSE _LOGGER.debug("Fan current_direction : %s > %s", value, self._direction) - + async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) From 77f91590048b0e5b35aca36ce0628d9d092c24dc Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 11:54:26 +1030 Subject: [PATCH 029/119] workflow_dispatch --- .github/workflows/tox.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 40ca8d9..8fc6768 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -1,6 +1,6 @@ name: Tox PR CI -on: [pull_request] +on: [pull_request, workflow_dispatch] jobs: build: From eda45fc896f52bcb357977d316901fef9631d21f Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 12:14:49 +1030 Subject: [PATCH 030/119] fix tox errors --- custom_components/localtuya/fan.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 1a5b8d8..e02fd25 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -122,7 +122,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): async def async_set_percentage(self, percentage): """Set the speed of the fan.""" - _LOGGER.debug("Fan async_set_percentage: %s", percentage) if percentage is not None: @@ -199,7 +198,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): def status_updated(self): """Get state of Tuya fan.""" - self._is_on = self.dps(self._dp_id) current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) From f1753945ca5f6d26a6dc5fc281593e015a0106ce Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 13:08:32 +1030 Subject: [PATCH 031/119] checks updates and tox fixes --- requirements_test.txt | 6 +++--- tox.ini | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index df62e77..8a2a53d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,9 +1,9 @@ black==21.4b0 codespell==2.0.0 flake8==3.9.2 -mypy==0.901 +mypy==0.910 pydocstyle==6.1.1 cryptography==3.2 -pylint==2.8.2 +pylint==2.11.1 pylint-strict-informational==0.1 -homeassistant==2021.1.4 +homeassistant==2021.11.2 diff --git a/tox.ini b/tox.ini index 49ccccb..75c6e56 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] skipsdist = true -envlist = py{37,38}, lint, typing +envlist = py{38,39}, lint, typing skip_missing_interpreters = True cs_exclude_words = hass,unvalid [gh-actions] python = - 3.7: clean, py37, lint, typing 3.8: clean, py38, lint, typing + 3.9: clean, py38, lint, typing [testenv] passenv = TOXENV CI From c30c68dbf540a70691845b0754853bf83cb0ad96 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 13:09:01 +1030 Subject: [PATCH 032/119] tox --- .github/workflows/tox.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 8fc6768..67c0209 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -14,8 +14,8 @@ jobs: platform: - ubuntu-latest python-version: - - 3.7 - 3.8 + - 3.9 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} From 01b9b00050c5970654aab7a2d03959fba169a074 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 13:12:31 +1030 Subject: [PATCH 033/119] fix requirements --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 8a2a53d..e65a9a5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,7 +3,7 @@ codespell==2.0.0 flake8==3.9.2 mypy==0.910 pydocstyle==6.1.1 -cryptography==3.2 +cryptography==3.4.8 pylint==2.11.1 pylint-strict-informational==0.1 homeassistant==2021.11.2 From 6e6cd0861fa79d89cc66cecf1cdf1bfa5cb5b214 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 13:15:09 +1030 Subject: [PATCH 034/119] fix tox errors --- custom_components/localtuya/fan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 31076a3..6a1e219 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -245,4 +245,5 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._direction = DIRECTION_REVERSE _LOGGER.debug("Fan current_direction : %s > %s", value, self._direction) -async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) \ No newline at end of file + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) From 95ef271dab36fcd7b204aec952e15796d895f7fc Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 13:50:18 +1030 Subject: [PATCH 035/119] more tox fixes --- custom_components/localtuya/fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 6a1e219..51d9d5e 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -74,7 +74,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).split(",") self._ordered_list_mode = None - if type(self._ordered_list) is list and len(self._ordered_list) > 1: + if isinstance(self._ordered_list, list) and len(self._ordered_list) > 1: self._use_ordered_list = True _LOGGER.debug( "Fan _use_ordered_list: %s > %s", @@ -105,7 +105,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Return the current percentage.""" return self._percentage - async def async_turn_on(self, percentage: str = None, **kwargs) -> None: + async def async_turn_on(self, percentage: str = None, preset_mode: str = None, **kwargs: Any) -> None: """Turn on the entity.""" _LOGGER.debug("Fan async_turn_on") await self._device.set_dp(True, self._dp_id) From a171430cc877051582196a3456ecdaecdca864ec Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 13:53:51 +1030 Subject: [PATCH 036/119] fix error --- custom_components/localtuya/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 51d9d5e..17f0c32 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -105,7 +105,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Return the current percentage.""" return self._percentage - async def async_turn_on(self, percentage: str = None, preset_mode: str = None, **kwargs: Any) -> None: + async def async_turn_on(self, percentage: str = None, preset_mode: str = None, **kwargs) -> None: """Turn on the entity.""" _LOGGER.debug("Fan async_turn_on") await self._device.set_dp(True, self._dp_id) From e55cd2013faf9d37924cda0b51bf7c4d4283630d Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 13:57:18 +1030 Subject: [PATCH 037/119] line length issue --- custom_components/localtuya/fan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 17f0c32..553f577 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -105,7 +105,8 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Return the current percentage.""" return self._percentage - async def async_turn_on(self, percentage: str = None, preset_mode: str = None, **kwargs) -> None: + async def async_turn_on(self, percentage: str = None, + preset_mode: str = None, **kwargs) -> None: """Turn on the entity.""" _LOGGER.debug("Fan async_turn_on") await self._device.set_dp(True, self._dp_id) From 8d5da3d73f7d3cbbca197afbd3bc1b3d8e516be3 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 21:34:00 +1030 Subject: [PATCH 038/119] remove dev tool changes fro branch --- .github/workflows/tox.yaml | 4 ++-- .vscode/settings.json | 5 ----- requirements_test.txt | 8 ++++---- tox.ini | 6 +++--- 4 files changed, 9 insertions(+), 14 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 67c0209..40ca8d9 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -1,6 +1,6 @@ name: Tox PR CI -on: [pull_request, workflow_dispatch] +on: [pull_request] jobs: build: @@ -14,8 +14,8 @@ jobs: platform: - ubuntu-latest python-version: + - 3.7 - 3.8 - - 3.9 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index a04b218..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "files.associations": { - "*.yaml": "home-assistant" - } -} \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index e65a9a5..d388fd6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,9 +1,9 @@ black==21.4b0 codespell==2.0.0 flake8==3.9.2 -mypy==0.910 +mypy==0.901 pydocstyle==6.1.1 -cryptography==3.4.8 -pylint==2.11.1 +cryptography==3.2 +pylint==2.8.2 pylint-strict-informational==0.1 -homeassistant==2021.11.2 +homeassistant==2021.1.4 \ No newline at end of file diff --git a/tox.ini b/tox.ini index 75c6e56..1c72213 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] skipsdist = true -envlist = py{38,39}, lint, typing +envlist = py{37,38}, lint, typing skip_missing_interpreters = True cs_exclude_words = hass,unvalid [gh-actions] python = + 3.7: clean, py37, lint, typing 3.8: clean, py38, lint, typing - 3.9: clean, py38, lint, typing [testenv] passenv = TOXENV CI @@ -35,4 +35,4 @@ commands = [testenv:typing] commands = - mypy --ignore-missing-imports --follow-imports=skip custom_components + mypy --ignore-missing-imports --follow-imports=skip custom_components \ No newline at end of file From c9c96ad6359c9a2c3fb15276e35a8ab260ac672f Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 21:37:57 +1030 Subject: [PATCH 039/119] revert dev tool config files for master branch --- requirements_test.txt | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index d388fd6..df62e77 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,4 +6,4 @@ pydocstyle==6.1.1 cryptography==3.2 pylint==2.8.2 pylint-strict-informational==0.1 -homeassistant==2021.1.4 \ No newline at end of file +homeassistant==2021.1.4 diff --git a/tox.ini b/tox.ini index 1c72213..49ccccb 100644 --- a/tox.ini +++ b/tox.ini @@ -35,4 +35,4 @@ commands = [testenv:typing] commands = - mypy --ignore-missing-imports --follow-imports=skip custom_components \ No newline at end of file + mypy --ignore-missing-imports --follow-imports=skip custom_components From e979a4b5cda15670b9f7127c3eb651f54f507d02 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 22:02:36 +1030 Subject: [PATCH 040/119] update dev checker tools --- .github/workflows/tox.yaml | 4 ++-- requirements_test.txt | 8 ++++---- tox.ini | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 40ca8d9..67c0209 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -1,6 +1,6 @@ name: Tox PR CI -on: [pull_request] +on: [pull_request, workflow_dispatch] jobs: build: @@ -14,8 +14,8 @@ jobs: platform: - ubuntu-latest python-version: - - 3.7 - 3.8 + - 3.9 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/requirements_test.txt b/requirements_test.txt index df62e77..e65a9a5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,9 +1,9 @@ black==21.4b0 codespell==2.0.0 flake8==3.9.2 -mypy==0.901 +mypy==0.910 pydocstyle==6.1.1 -cryptography==3.2 -pylint==2.8.2 +cryptography==3.4.8 +pylint==2.11.1 pylint-strict-informational==0.1 -homeassistant==2021.1.4 +homeassistant==2021.11.2 diff --git a/tox.ini b/tox.ini index 49ccccb..75c6e56 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] skipsdist = true -envlist = py{37,38}, lint, typing +envlist = py{38,39}, lint, typing skip_missing_interpreters = True cs_exclude_words = hass,unvalid [gh-actions] python = - 3.7: clean, py37, lint, typing 3.8: clean, py38, lint, typing + 3.9: clean, py38, lint, typing [testenv] passenv = TOXENV CI From 445b62efd8a247bacf89ebc5a425426808add541 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 22:11:34 +1030 Subject: [PATCH 041/119] tox.yaml --- .github/workflows/tox.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 67c0209..afa1588 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -1,6 +1,6 @@ name: Tox PR CI -on: [pull_request, workflow_dispatch] +on: [push, pull_request, workflow_dispatch] jobs: build: From 1c2f4ba0843c76deafe2b05efd21ea10501ce95e Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 22:30:57 +1030 Subject: [PATCH 042/119] Delete settings.json --- .vscode/settings.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index a04b218..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "files.associations": { - "*.yaml": "home-assistant" - } -} \ No newline at end of file From e8aa5ac9a8852a0e0f971ef247676eb80c6fe8c7 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 22:46:55 +1030 Subject: [PATCH 043/119] integration inputs --- custom_components/localtuya/const.py | 3 + custom_components/localtuya/fan.py | 197 ++++++++++-------- .../localtuya/translations/en.json | 10 +- 3 files changed, 123 insertions(+), 87 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 9662b3b..36db866 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -40,6 +40,9 @@ CONF_FAN_ORDERED_LIST = "fan_speed_ordered_list" CONF_FAN_DIRECTION = "fan_direction" CONF_FAN_DIRECTION_FWD = "fan_direction_forward" CONF_FAN_DIRECTION_REV = "fan_direction_reverse" +CONF_FAN_SPEED_DPS_TYPE = "fan_speed_dps_type" +CONF_FAN_PRESET_CONTROL = "fan_preset_control" +CONF_FAN_PRESET_LIST = "fan_preset_list" # sensor CONF_SCALING = "scaling" diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 553f577..efd2058 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -1,56 +1,63 @@ """Platform to locally control Tuya-based fan devices.""" import logging -import math from functools import partial +import math import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.components.fan import ( + DOMAIN, DIRECTION_FORWARD, DIRECTION_REVERSE, - DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, + SUPPORT_PRESET_MODE, FanEntity, ) -from homeassistant.util.percentage import ( - int_states_in_range, - ordered_list_item_to_percentage, - percentage_to_ordered_list_item, - percentage_to_ranged_value, - ranged_value_to_percentage, -) from .common import LocalTuyaEntity, async_setup_entry from .const import ( + CONF_FAN_OSCILLATING_CONTROL, + CONF_FAN_SPEED_CONTROL, + CONF_FAN_PRESET_CONTROL, CONF_FAN_DIRECTION, CONF_FAN_DIRECTION_FWD, CONF_FAN_DIRECTION_REV, - CONF_FAN_ORDERED_LIST, - CONF_FAN_OSCILLATING_CONTROL, - CONF_FAN_SPEED_CONTROL, - CONF_FAN_SPEED_MAX, CONF_FAN_SPEED_MIN, + CONF_FAN_SPEED_MAX, + CONF_FAN_ORDERED_LIST, + CONF_FAN_SPEED_DPS_TYPE, + CONF_FAN_PRESET_LIST +) + +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, + percentage_to_ranged_value, + ranged_value_to_percentage, + int_states_in_range ) _LOGGER = logging.getLogger(__name__) - def flow_schema(dps): """Return schema used in config flow.""" return { vol.Optional(CONF_FAN_SPEED_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_OSCILLATING_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_DIRECTION): vol.In(dps), + vol.Optional(CONF_FAN_PRESET_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_DIRECTION_FWD, default="forward"): cv.string, vol.Optional(CONF_FAN_DIRECTION_REV, default="reverse"): cv.string, vol.Optional(CONF_FAN_SPEED_MIN, default=1): cv.positive_int, vol.Optional(CONF_FAN_SPEED_MAX, default=9): cv.positive_int, - vol.Optional(CONF_FAN_ORDERED_LIST, default="disabled"): cv.string, + vol.Optional(CONF_FAN_ORDERED_LIST): cv.string, + vol.Optional(CONF_FAN_PRESET_LIST): cv.string, + vol.Optional(CONF_FAN_SPEED_DPS_TYPE, default="string"): vol.In( + ["string", "integer", "list"]), } - class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Representation of a Tuya fan.""" @@ -67,24 +74,22 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._oscillating = None self._direction = None self._percentage = None + self._preset = None self._speed_range = ( - self._config.get(CONF_FAN_SPEED_MIN), + self._config.get(CONF_FAN_SPEED_MIN), self._config.get(CONF_FAN_SPEED_MAX), ) - self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).split(",") - self._ordered_list_mode = None - - if isinstance(self._ordered_list, list) and len(self._ordered_list) > 1: - self._use_ordered_list = True - _LOGGER.debug( - "Fan _use_ordered_list: %s > %s", - self._use_ordered_list, - self._ordered_list, - ) + self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).replace(" ","").split(",") + self._preset_list = self._config.get(CONF_FAN_PRESET_LIST).replace(" ","").split(",") + self._ordered_speed_dps_type = self._config.get(CONF_FAN_SPEED_DPS_TYPE) + + if (self._ordered_speed_dps_type == "list" + and isinstance(self._ordered_list, list) + and len(self._ordered_list) > 1): + _LOGGER.debug("Fan _use_ordered_list: %s", self._ordered_list) else: - self._use_ordered_list = False - _LOGGER.debug("Fan _use_ordered_list: %s", self._use_ordered_list) - + _LOGGER.debug("Fan _use_ordered_list: Not a valid list") + @property def oscillating(self): """Return current oscillating status.""" @@ -105,13 +110,12 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Return the current percentage.""" return self._percentage - async def async_turn_on(self, percentage: str = None, - preset_mode: str = None, **kwargs) -> None: + async def async_turn_on(self, percentage: str = None, **kwargs) -> None: """Turn on the entity.""" _LOGGER.debug("Fan async_turn_on") await self._device.set_dp(True, self._dp_id) if percentage is not None: - await self.async_set_percentage(percentage) + await self.async_set_percentage(speed) else: self.schedule_update_ha_state() @@ -124,42 +128,44 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): async def async_set_percentage(self, percentage): """Set the speed of the fan.""" - _LOGGER.debug("Fan async_set_percentage: %s", percentage) + _LOGGER.debug("Fan async_set_percentage: %s", percentage) + if percentage is not None: if percentage == 0: return await self.async_turn_off() - elif not self.is_on: + + if not self.is_on: await self.async_turn_on() - if self._use_ordered_list: + + if self._ordered_speed_dps_type == "string": + send_speed = str(math.ceil( + percentage_to_ranged_value(self._speed_range, percentage))) await self._device.set_dp( - str( - percentage_to_ordered_list_item(self._ordered_list, percentage) - ), - self._config.get(CONF_FAN_SPEED_CONTROL), - ) - _LOGGER.debug( - "Fan async_set_percentage: %s > %s", - percentage, - percentage_to_ordered_list_item(self._ordered_list, percentage), - ) + send_speed, self._config.get(CONF_FAN_SPEED_CONTROL)) + _LOGGER.debug("Fan async_set_percentage: %s > %s", + percentage, send_speed) + + elif self._ordered_speed_dps_type == "integer": + send_speed = int(math.ceil( + percentage_to_ranged_value(self._speed_range, percentage))) + await self._device.set_dp( + send_speed, self._config.get(CONF_FAN_SPEED_CONTROL)) + _LOGGER.debug("Fan async_set_percentage: %s > %s", + percentage, send_speed) - else: - await self._device.set_dp( - str( - math.ceil( - percentage_to_ranged_value(self._speed_range, percentage) + elif self._ordered_speed_dps_type == "list": + send_speed = str( + percentage_to_ordered_list_item(self._ordered_list, percentage) ) - ), - self._config.get(CONF_FAN_SPEED_CONTROL), - ) - _LOGGER.debug( - "Fan async_set_percentage: %s > %s", - percentage, - percentage_to_ranged_value(self._speed_range, percentage), - ) + await self._device.set_dp( + send_speed, self._config.get(CONF_FAN_SPEED_CONTROL)) + _LOGGER.debug("Fan async_set_percentage: %s > %s", + percentage, send_speed) + self.schedule_update_ha_state() + async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation.""" _LOGGER.debug("Fan async_oscillate: %s", oscillating) @@ -174,10 +180,21 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if direction == DIRECTION_FORWARD: value = self._config.get(CONF_FAN_DIRECTION_FWD) - + if direction == DIRECTION_REVERSE: value = self._config.get(CONF_FAN_DIRECTION_REV) - await self._device.set_dp(value, self._config.get(CONF_FAN_DIRECTION)) + + await self._device.set_dp( + value, self._config.get(CONF_FAN_DIRECTION) + ) + self.schedule_update_ha_state() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + _LOGGER.debug("Fan set preset: %s", preset_mode) + await self._device.set_dp( + preset_mode, self._config.get(CONF_FAN_PRESET_CONTROL) + ) self.schedule_update_ha_state() @property @@ -194,8 +211,11 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if self.has_config(CONF_FAN_DIRECTION): features |= SUPPORT_DIRECTION - return features + if self.has_config(CONF_FAN_PRESET_CONTROL): + features |= SUPPORT_PRESET_MODE + return features + @property def speed_count(self) -> int: """Speed count for the fan.""" @@ -203,34 +223,41 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): _LOGGER.debug("Fan speed_count: %s", speed_count) return speed_count + def status_updated(self): """Get state of Tuya fan.""" + self._is_on = self.dps(self._dp_id) + if self.has_config(CONF_FAN_PRESET_CONTROL): + current_preset = self.dps_conf(CONF_FAN_PRESET_CONTROL) + if current_preset is not None and current_preset in self._preset_list: + _LOGGER.debug("Fan current_preset in preset list: %s from %s", + current_preset, self._preset_list) + self._preset = current_preset + current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) - if self._use_ordered_list: - _LOGGER.debug( - "Fan current_speed ordered_list_item_to_percentage: %s from %s", - current_speed, - self._ordered_list, - ) - if current_speed is not None: - self._percentage = ordered_list_item_to_percentage( - self._ordered_list, current_speed - ) + if current_speed is not None: - else: - _LOGGER.debug( - "Fan current_speed ranged_value_to_percentage: %s from %s", - current_speed, - self._speed_range, - ) - if current_speed is not None: - self._percentage = ranged_value_to_percentage( - self._speed_range, int(current_speed) - ) + if (self.has_config(CONF_FAN_PRESET_CONTROL) + and (CONF_FAN_SPEED_CONTROL == CONF_FAN_PRESET_CONTROL) + and (current_speed in self._preset_list)): + _LOGGER.debug("Fan current_speed in preset list: %s from %s", + current_speed, self._preset_list) + self._preset = current_speed - _LOGGER.debug("Fan current_percentage: %s", self._percentage) + elif self._ordered_speed_dps_type == "list": + _LOGGER.debug("Fan current_speed ordered_list_item_to_percentage: %s from %s", + current_speed, self._ordered_list) + self._percentage = ordered_list_item_to_percentage(self._ordered_list, current_speed) + + elif self._ordered_speed_dps_type == "string" or self._ordered_speed_dps_type == "integer" : + _LOGGER.debug("Fan current_speed ranged_value_to_percentage: %s from %s", + current_speed, self._speed_range) + self._percentage = ranged_value_to_percentage(self._speed_range, int(current_speed)) + + _LOGGER.debug("Fan current_percentage: %s", self._percentage) + _LOGGER.debug("Fan current_preset: %s", self._preset) if self.has_config(CONF_FAN_OSCILLATING_CONTROL): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) @@ -241,10 +268,10 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if value is not None: if value == self._config.get(CONF_FAN_DIRECTION_FWD): self._direction = DIRECTION_FORWARD - + if value == self._config.get(CONF_FAN_DIRECTION_REV): self._direction = DIRECTION_REVERSE _LOGGER.debug("Fan current_direction : %s > %s", value, self._direction) - + async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 2cb903e..2014b0b 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -77,7 +77,10 @@ "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", - "fan_direction_reverse": "reverse dps string" + "fan_direction_reverse": "reverse dps string", + "fan_speed_dps_type": "type of speed dps control. integer/string/list", + "fan_preset_control": "Fan preset dps. Can be the same as speed", + "fan_preset_list": "Fan preset list. Comma separated" } } } @@ -132,7 +135,10 @@ "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", - "fan_direction_reverse": "reverse dps string" + "fan_direction_reverse": "reverse dps string", + "fan_speed_dps_type": "type of speed dps control. integer/string/list", + "fan_preset_control": "Fan preset dps. CAn be the same as speed", + "fan_preset_list": "Fan preset list. Comma separated" } }, "yaml_import": { From d600cba89cffec158c2d6d1049e5802ab76c7e63 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 11 Nov 2021 22:56:34 +1030 Subject: [PATCH 044/119] fix tox errors --- custom_components/localtuya/fan.py | 187 +++++++++++++++++------------ 1 file changed, 109 insertions(+), 78 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index efd2058..6896831 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -1,46 +1,46 @@ """Platform to locally control Tuya-based fan devices.""" import logging -from functools import partial import math +from functools import partial import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.components.fan import ( - DOMAIN, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, - SUPPORT_SET_SPEED, SUPPORT_PRESET_MODE, + SUPPORT_SET_SPEED, FanEntity, ) - -from .common import LocalTuyaEntity, async_setup_entry -from .const import ( - CONF_FAN_OSCILLATING_CONTROL, - CONF_FAN_SPEED_CONTROL, - CONF_FAN_PRESET_CONTROL, - CONF_FAN_DIRECTION, - CONF_FAN_DIRECTION_FWD, - CONF_FAN_DIRECTION_REV, - CONF_FAN_SPEED_MIN, - CONF_FAN_SPEED_MAX, - CONF_FAN_ORDERED_LIST, - CONF_FAN_SPEED_DPS_TYPE, - CONF_FAN_PRESET_LIST -) - from homeassistant.util.percentage import ( + int_states_in_range, ordered_list_item_to_percentage, percentage_to_ordered_list_item, percentage_to_ranged_value, ranged_value_to_percentage, - int_states_in_range +) + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + CONF_FAN_DIRECTION, + CONF_FAN_DIRECTION_FWD, + CONF_FAN_DIRECTION_REV, + CONF_FAN_ORDERED_LIST, + CONF_FAN_OSCILLATING_CONTROL, + CONF_FAN_PRESET_CONTROL, + CONF_FAN_PRESET_LIST, + CONF_FAN_SPEED_CONTROL, + CONF_FAN_SPEED_DPS_TYPE, + CONF_FAN_SPEED_MAX, + CONF_FAN_SPEED_MIN, ) _LOGGER = logging.getLogger(__name__) + def flow_schema(dps): """Return schema used in config flow.""" return { @@ -55,9 +55,11 @@ def flow_schema(dps): vol.Optional(CONF_FAN_ORDERED_LIST): cv.string, vol.Optional(CONF_FAN_PRESET_LIST): cv.string, vol.Optional(CONF_FAN_SPEED_DPS_TYPE, default="string"): vol.In( - ["string", "integer", "list"]), + ["string", "integer", "list"] + ), } + class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Representation of a Tuya fan.""" @@ -76,20 +78,26 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._percentage = None self._preset = None self._speed_range = ( - self._config.get(CONF_FAN_SPEED_MIN), + self._config.get(CONF_FAN_SPEED_MIN), self._config.get(CONF_FAN_SPEED_MAX), ) - self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).replace(" ","").split(",") - self._preset_list = self._config.get(CONF_FAN_PRESET_LIST).replace(" ","").split(",") + self._ordered_list = ( + self._config.get(CONF_FAN_ORDERED_LIST).replace(" ", "").split(",") + ) + self._preset_list = ( + self._config.get(CONF_FAN_PRESET_LIST).replace(" ", "").split(",") + ) self._ordered_speed_dps_type = self._config.get(CONF_FAN_SPEED_DPS_TYPE) - - if (self._ordered_speed_dps_type == "list" - and isinstance(self._ordered_list, list) - and len(self._ordered_list) > 1): + + if ( + self._ordered_speed_dps_type == "list" + and isinstance(self._ordered_list, list) + and len(self._ordered_list) > 1 + ): _LOGGER.debug("Fan _use_ordered_list: %s", self._ordered_list) else: _LOGGER.debug("Fan _use_ordered_list: Not a valid list") - + @property def oscillating(self): """Return current oscillating status.""" @@ -115,7 +123,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): _LOGGER.debug("Fan async_turn_on") await self._device.set_dp(True, self._dp_id) if percentage is not None: - await self.async_set_percentage(speed) + await self.async_set_percentage(percentage) else: self.schedule_update_ha_state() @@ -128,44 +136,50 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): async def async_set_percentage(self, percentage): """Set the speed of the fan.""" - _LOGGER.debug("Fan async_set_percentage: %s", percentage) - + if percentage is not None: if percentage == 0: return await self.async_turn_off() - + if not self.is_on: await self.async_turn_on() - + if self._ordered_speed_dps_type == "string": - send_speed = str(math.ceil( - percentage_to_ranged_value(self._speed_range, percentage))) + send_speed = str( + math.ceil(percentage_to_ranged_value(self._speed_range, percentage)) + ) await self._device.set_dp( - send_speed, self._config.get(CONF_FAN_SPEED_CONTROL)) - _LOGGER.debug("Fan async_set_percentage: %s > %s", - percentage, send_speed) - + send_speed, self._config.get(CONF_FAN_SPEED_CONTROL) + ) + _LOGGER.debug( + "Fan async_set_percentage: %s > %s", percentage, send_speed + ) + elif self._ordered_speed_dps_type == "integer": - send_speed = int(math.ceil( - percentage_to_ranged_value(self._speed_range, percentage))) + send_speed = int( + math.ceil(percentage_to_ranged_value(self._speed_range, percentage)) + ) await self._device.set_dp( - send_speed, self._config.get(CONF_FAN_SPEED_CONTROL)) - _LOGGER.debug("Fan async_set_percentage: %s > %s", - percentage, send_speed) + send_speed, self._config.get(CONF_FAN_SPEED_CONTROL) + ) + _LOGGER.debug( + "Fan async_set_percentage: %s > %s", percentage, send_speed + ) elif self._ordered_speed_dps_type == "list": send_speed = str( - percentage_to_ordered_list_item(self._ordered_list, percentage) - ) + percentage_to_ordered_list_item(self._ordered_list, percentage) + ) await self._device.set_dp( - send_speed, self._config.get(CONF_FAN_SPEED_CONTROL)) - _LOGGER.debug("Fan async_set_percentage: %s > %s", - percentage, send_speed) + send_speed, self._config.get(CONF_FAN_SPEED_CONTROL) + ) + _LOGGER.debug( + "Fan async_set_percentage: %s > %s", percentage, send_speed + ) self.schedule_update_ha_state() - async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation.""" _LOGGER.debug("Fan async_oscillate: %s", oscillating) @@ -180,17 +194,15 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if direction == DIRECTION_FORWARD: value = self._config.get(CONF_FAN_DIRECTION_FWD) - + if direction == DIRECTION_REVERSE: value = self._config.get(CONF_FAN_DIRECTION_REV) - await self._device.set_dp( - value, self._config.get(CONF_FAN_DIRECTION) - ) - self.schedule_update_ha_state() + await self._device.set_dp(value, self._config.get(CONF_FAN_DIRECTION)) + self.schedule_update_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set the preset mode of the fan.""" + """Set the preset mode of the fan.""" _LOGGER.debug("Fan set preset: %s", preset_mode) await self._device.set_dp( preset_mode, self._config.get(CONF_FAN_PRESET_CONTROL) @@ -214,8 +226,8 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if self.has_config(CONF_FAN_PRESET_CONTROL): features |= SUPPORT_PRESET_MODE - return features - + return features + @property def speed_count(self) -> int: """Speed count for the fan.""" @@ -223,38 +235,57 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): _LOGGER.debug("Fan speed_count: %s", speed_count) return speed_count - def status_updated(self): """Get state of Tuya fan.""" - self._is_on = self.dps(self._dp_id) if self.has_config(CONF_FAN_PRESET_CONTROL): current_preset = self.dps_conf(CONF_FAN_PRESET_CONTROL) if current_preset is not None and current_preset in self._preset_list: - _LOGGER.debug("Fan current_preset in preset list: %s from %s", - current_preset, self._preset_list) + _LOGGER.debug( + "Fan current_preset in preset list: %s from %s", + current_preset, + self._preset_list, + ) self._preset = current_preset current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) if current_speed is not None: - if (self.has_config(CONF_FAN_PRESET_CONTROL) - and (CONF_FAN_SPEED_CONTROL == CONF_FAN_PRESET_CONTROL) - and (current_speed in self._preset_list)): - _LOGGER.debug("Fan current_speed in preset list: %s from %s", - current_speed, self._preset_list) + if ( + self.has_config(CONF_FAN_PRESET_CONTROL) + and (CONF_FAN_SPEED_CONTROL == CONF_FAN_PRESET_CONTROL) + and (current_speed in self._preset_list) + ): + _LOGGER.debug( + "Fan current_speed in preset list: %s from %s", + current_speed, + self._preset_list, + ) self._preset = current_speed elif self._ordered_speed_dps_type == "list": - _LOGGER.debug("Fan current_speed ordered_list_item_to_percentage: %s from %s", - current_speed, self._ordered_list) - self._percentage = ordered_list_item_to_percentage(self._ordered_list, current_speed) - - elif self._ordered_speed_dps_type == "string" or self._ordered_speed_dps_type == "integer" : - _LOGGER.debug("Fan current_speed ranged_value_to_percentage: %s from %s", - current_speed, self._speed_range) - self._percentage = ranged_value_to_percentage(self._speed_range, int(current_speed)) + _LOGGER.debug( + "Fan current_speed ordered_list_item_to_percentage: %s from %s", + current_speed, + self._ordered_list, + ) + self._percentage = ordered_list_item_to_percentage( + self._ordered_list, current_speed + ) + + elif ( + self._ordered_speed_dps_type == "string" + or self._ordered_speed_dps_type == "integer" + ): + _LOGGER.debug( + "Fan current_speed ranged_value_to_percentage: %s from %s", + current_speed, + self._speed_range, + ) + self._percentage = ranged_value_to_percentage( + self._speed_range, int(current_speed) + ) _LOGGER.debug("Fan current_percentage: %s", self._percentage) _LOGGER.debug("Fan current_preset: %s", self._preset) @@ -268,10 +299,10 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if value is not None: if value == self._config.get(CONF_FAN_DIRECTION_FWD): self._direction = DIRECTION_FORWARD - + if value == self._config.get(CONF_FAN_DIRECTION_REV): self._direction = DIRECTION_REVERSE _LOGGER.debug("Fan current_direction : %s > %s", value, self._direction) - + async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaFan, flow_schema) From 3d9316b14a2a0eb80e1bf625176d42f648bd0886 Mon Sep 17 00:00:00 2001 From: rikman122 Date: Tue, 23 Nov 2021 16:50:19 +0100 Subject: [PATCH 045/119] style fixes --- custom_components/localtuya/vacuum.py | 32 +++++++++++++++++---------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py index 5b365a6..cc97e25 100644 --- a/custom_components/localtuya/vacuum.py +++ b/custom_components/localtuya/vacuum.py @@ -65,8 +65,9 @@ def flow_schema(dps): vol.Required(CONF_IDLE_STATUS_VALUE, default=DEFAULT_IDLE_STATUS): str, vol.Required(CONF_POWERGO_DP): vol.In(dps), vol.Required(CONF_DOCKED_STATUS_VALUE, default=DEFAULT_DOCKED_STATUS): str, - vol.Optional(CONF_RETURNING_STATUS_VALUE, - default=DEFAULT_RETURNING_STATUS): str, + vol.Optional( + CONF_RETURNING_STATUS_VALUE, default=DEFAULT_RETURNING_STATUS + ): str, vol.Optional(CONF_BATTERY_DP): vol.In(dps), vol.Optional(CONF_MODE_DP): vol.In(dps), vol.Optional(CONF_MODES, default=DEFAULT_MODES): str, @@ -86,7 +87,7 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): def __init__(self, device, config_entry, switchid, **kwargs): """Initialize a new LocaltuyaVacuum.""" - super().__init__(device, config_entry, switchid , _LOGGER, **kwargs) + super().__init__(device, config_entry, switchid, _LOGGER, **kwargs) self._state = None self._battery_level = None self._attrs = {} @@ -116,8 +117,13 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): @property def supported_features(self): """Flag supported features.""" - supported_features = (SUPPORT_START | SUPPORT_PAUSE - | SUPPORT_STOP | SUPPORT_STATUS | SUPPORT_STATE) + supported_features = ( + SUPPORT_START + | SUPPORT_PAUSE + | SUPPORT_STOP + | SUPPORT_STATUS + | SUPPORT_STATE + ) if self.has_config(CONF_RETURN_MODE): supported_features = supported_features | SUPPORT_RETURN_HOME @@ -166,16 +172,18 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" if self.has_config(CONF_RETURN_MODE): - await self._device.set_dp(self._config[CONF_RETURN_MODE], - self._config[CONF_MODE_DP]) + await self._device.set_dp( + self._config[CONF_RETURN_MODE], self._config[CONF_MODE_DP] + ) else: _LOGGER.error("Missing command for return home in commands set.") async def async_stop(self, **kwargs): """Turn the vacuum off stopping the cleaning.""" if self.has_config(CONF_STOP_STATUS): - await self._device.set_dp(self._config[CONF_STOP_STATUS], - self._config[CONF_MODE_DP]) + await self._device.set_dp( + self._config[CONF_STOP_STATUS], self._config[CONF_MODE_DP] + ) else: _LOGGER.error("Missing command for stop in commands set.") @@ -186,7 +194,7 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): async def async_locate(self, **kwargs): """Locate the vacuum cleaner.""" if self.has_config(CONF_LOCATE_DP): - await self._device.set_dp('', self._config[CONF_LOCATE_DP]) + await self._device.set_dp("", self._config[CONF_LOCATE_DP]) async def async_set_fan_speed(self, fan_speed, **kwargs): """Set the fan speed.""" @@ -194,8 +202,8 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): async def async_send_command(self, command, params=None, **kwargs): """Send a command to a vacuum cleaner.""" - if command == "set_mode" and 'mode' in params: - mode = params['mode'] + if command == "set_mode" and "mode" in params: + mode = params["mode"] await self._device.set_dp(mode, self._config[CONF_MODE_DP]) def status_updated(self): From 0985ad64f29dbf5b526b28415d5a9bed5c7de4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 10 Aug 2021 19:17:56 +0100 Subject: [PATCH 046/119] add climate implementation --- custom_components/localtuya/climate.py | 371 ++++++++++++++++++ custom_components/localtuya/const.py | 21 +- .../localtuya/translations/en.json | 40 +- 3 files changed, 429 insertions(+), 3 deletions(-) create mode 100644 custom_components/localtuya/climate.py diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py new file mode 100644 index 0000000..fe4eab3 --- /dev/null +++ b/custom_components/localtuya/climate.py @@ -0,0 +1,371 @@ +"""Platform to locally control Tuya-based climate devices.""" +import asyncio +import logging +import json +from functools import partial +import json + +import voluptuous as vol +from homeassistant.components.climate import ( + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + DOMAIN, + ClimateEntity, +) +from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, + CURRENT_HVAC_OFF, + CURRENT_HVAC_HEAT, + PRESET_NONE, + PRESET_ECO, + PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_HOME, + PRESET_SLEEP, + PRESET_ACTIVITY, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_TEMPERATURE_UNIT, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) + +from .common import LocalTuyaEntity, async_setup_entry +from .const import ( + CONF_CURRENT_TEMPERATURE_DP, + CONF_FAN_MODE_DP, + CONF_MAX_TEMP_DP, + CONF_MIN_TEMP_DP, + CONF_PRECISION, + CONF_TARGET_PRECISION, + CONF_TARGET_TEMPERATURE_DP, + CONF_TEMPERATURE_STEP, + CONF_HVAC_MODE_DP, + CONF_HVAC_MODE_SET, + CONF_EURISTIC_ACTION, + CONF_HVAC_ACTION_DP, + CONF_HVAC_ACTION_SET, + CONF_ECO_DP, + CONF_ECO_VALUE, + CONF_PRESET_DP, + CONF_PRESET_SET, +) + +from . import pytuya + +_LOGGER = logging.getLogger(__name__) + +HVAC_MODE_SETS = { + "manual/auto": { + HVAC_MODE_HEAT: "manual", + HVAC_MODE_AUTO: "auto", + }, + "Manual/Auto": { + HVAC_MODE_HEAT: "Manual", + HVAC_MODE_AUTO: "Auto", + }, +} +HVAC_ACTION_SETS = { + "True/False": { + CURRENT_HVAC_HEAT: True, + CURRENT_HVAC_OFF: False, + }, + "open/close": { + CURRENT_HVAC_HEAT: "open", + CURRENT_HVAC_OFF: "close", + }, + "heating/no_heating": { + CURRENT_HVAC_HEAT: "heating", + CURRENT_HVAC_OFF: "no_heating", + }, +} +PRESET_SETS = { + "Manual/Holiday/Program": { + PRESET_NONE: "Manual", + PRESET_AWAY: "Holiday", + PRESET_HOME: "Program", + }, +} + +TEMPERATURE_CELSIUS = "celsius" +TEMPERATURE_FAHRENHEIT = "fahrenheit" +DEFAULT_TEMPERATURE_UNIT = TEMPERATURE_CELSIUS +DEFAULT_PRECISION = PRECISION_TENTHS +DEFAULT_TEMPERATURE_STEP = PRECISION_HALVES +MODE_WAIT = 0.1 + +def flow_schema(dps): + """Return schema used in config flow.""" + return { + vol.Optional(CONF_TARGET_TEMPERATURE_DP): vol.In(dps), + vol.Optional(CONF_CURRENT_TEMPERATURE_DP): vol.In(dps), + vol.Optional(CONF_TEMPERATURE_STEP): vol.In( + [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] + ), + vol.Optional(CONF_MAX_TEMP_DP): vol.In(dps), + vol.Optional(CONF_MIN_TEMP_DP): vol.In(dps), + vol.Optional(CONF_PRECISION): vol.In( + [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] + ), + vol.Optional(CONF_HVAC_MODE_DP): vol.In(dps), + vol.Optional(CONF_HVAC_MODE_SET): vol.In( + list(HVAC_MODE_SETS.keys()) + ), + vol.Optional(CONF_HVAC_ACTION_DP): vol.In(dps), + vol.Optional(CONF_HVAC_ACTION_SET): vol.In( + list(HVAC_ACTION_SETS.keys()) + ), + vol.Optional(CONF_ECO_DP): vol.In(dps), + vol.Optional(CONF_ECO_VALUE): str, + vol.Optional(CONF_PRESET_DP): vol.In(dps), + vol.Optional(CONF_PRESET_SET): vol.In( + list(PRESET_SETS.keys()) + ), + vol.Optional(CONF_TEMPERATURE_UNIT): vol.In( + [TEMPERATURE_CELSIUS, TEMPERATURE_FAHRENHEIT] + ), + vol.Optional(CONF_TARGET_PRECISION): vol.In( + [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] + ), + vol.Optional(CONF_EURISTIC_ACTION, default=False): bool, + vol.Optional(CONF_FAN_MODE_DP): vol.In(dps), + } + + +class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): + """Tuya climate device.""" + + def __init__( + self, + device, + config_entry, + switchid, + **kwargs, + ): + """Initialize a new LocaltuyaClimate.""" + super().__init__(device, config_entry, switchid, _LOGGER, **kwargs) + self._state = None + self._target_temperature = None + self._current_temperature = None + self._hvac_mode = None + self._preset_mode = None + self._hvac_action = None + self._precision = self._config.get(CONF_PRECISION, DEFAULT_PRECISION) + self._target_precision = self._config.get(CONF_TARGET_PRECISION, self._precision) + self._conf_hvac_mode_dp = self._config.get(CONF_HVAC_MODE_DP) + self._conf_hvac_mode_set = HVAC_MODE_SETS.get(self._config.get(CONF_HVAC_MODE_SET), {}) + self._conf_preset_dp = self._config.get(CONF_PRESET_DP) + self._conf_preset_set = PRESET_SETS.get(self._config.get(CONF_PRESET_SET), {}) + self._conf_hvac_action_dp = self._config.get(CONF_HVAC_ACTION_DP) + self._conf_hvac_action_set = HVAC_ACTION_SETS.get(self._config.get(CONF_HVAC_ACTION_SET), {}) + self._conf_eco_dp = self._config.get(CONF_ECO_DP) + self._conf_eco_value = self._config.get(CONF_ECO_VALUE, "ECO") + self._has_presets = self.has_config(CONF_ECO_DP) or self.has_config(CONF_PRESET_DP) + print("Initialized climate [{}]".format(self.name)) + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = 0 + if self.has_config(CONF_TARGET_TEMPERATURE_DP): + supported_features = supported_features | SUPPORT_TARGET_TEMPERATURE + if self.has_config(CONF_MAX_TEMP_DP): + supported_features = supported_features | SUPPORT_TARGET_TEMPERATURE_RANGE + if self.has_config(CONF_FAN_MODE_DP): + supported_features = supported_features | SUPPORT_FAN_MODE + if self.has_config(CONF_PRESET_DP) or self.has_config(CONF_ECO_DP): + supported_features = supported_features | SUPPORT_PRESET_MODE + return supported_features + + @property + def precision(self): + """Return the precision of the system.""" + return self._precision + + @property + def target_recision(self): + """Return the precision of the target.""" + return self._target_precision + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + if ( + self._config.get(CONF_TEMPERATURE_UNIT, DEFAULT_TEMPERATURE_UNIT) + == TEMPERATURE_FAHRENHEIT + ): + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + @property + def hvac_mode(self): + """Return current operation ie. heat, cool, idle.""" + return self._hvac_mode + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + if not self.has_config(CONF_HVAC_MODE_DP): + return None + return list(self._conf_hvac_mode_set) + [HVAC_MODE_OFF] + + @property + def hvac_action(self): + """Return the current running hvac operation if supported. + Need to be one of CURRENT_HVAC_*. + """ + if self._config[CONF_EURISTIC_ACTION]: + if self._hvac_mode == HVAC_MODE_HEAT: + if self._current_temperature < (self._target_temperature - self._precision): + self._hvac_action = CURRENT_HVAC_HEAT + if self._current_temperature == (self._target_temperature - self._precision): + if self._hvac_action == CURRENT_HVAC_HEAT: + self._hvac_action = CURRENT_HVAC_HEAT + if self._hvac_action == CURRENT_HVAC_OFF: + self._hvac_action = CURRENT_HVAC_OFF + if (self._current_temperature + self._precision) > self._target_temperature: + self._hvac_action = CURRENT_HVAC_OFF + return self._hvac_action + return self._hvac_action + + @property + def preset_mode(self): + """Return current preset""" + return self._preset_mode + + @property + def preset_modes(self): + """Return the list of available presets modes.""" + if not self._has_presets: + return None + presets = list(self._conf_preset_set) + if self._conf_eco_dp: + presets.append(PRESET_ECO) + return presets + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self._config.get(CONF_TEMPERATURE_STEP, DEFAULT_TEMPERATURE_STEP) + + @property + def fan_mode(self): + """Return the fan setting.""" + return NotImplementedError() + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + return NotImplementedError() + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if ATTR_TEMPERATURE in kwargs and self.has_config(CONF_TARGET_TEMPERATURE_DP): + temperature = round(kwargs[ATTR_TEMPERATURE] / self._target_precision) + await self._device.set_dp(temperature, self._config[CONF_TARGET_TEMPERATURE_DP]) + + def set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + return NotImplementedError() + + async def async_set_hvac_mode(self, hvac_mode): + """Set new target operation mode.""" + if hvac_mode == HVAC_MODE_OFF: + await self._device.set_dp(False, self._dp_id) + return + if not self._state: + await self._device.set_dp(True, self._dp_id) + await asyncio.sleep(MODE_WAIT) + await self._device.set_dp(self._conf_hvac_mode_set[hvac_mode], self._conf_hvac_mode_dp) + + async def async_set_preset_mode(self, preset_mode): + """Set new target preset mode.""" + if preset_mode == PRESET_ECO: + await self._device.set_dp(self._conf_eco_value, self._conf_eco_dp) + return + await self._device.set_dp(self._conf_preset_set[preset_mode], self._conf_preset_dp) + + @property + def min_temp(self): + """Return the minimum temperature.""" + if self.has_config(CONF_MIN_TEMP_DP): + return self.dps_conf(CONF_MIN_TEMP_DP) + return DEFAULT_MIN_TEMP + + @property + def max_temp(self): + """Return the maximum temperature.""" + if self.has_config(CONF_MAX_TEMP_DP): + return self.dps_conf(CONF_MAX_TEMP_DP) + return DEFAULT_MAX_TEMP + + def status_updated(self): + """Device status was updated.""" + self._state = self.dps(self._dp_id) + + if self.has_config(CONF_TARGET_TEMPERATURE_DP): + self._target_temperature = ( + self.dps_conf(CONF_TARGET_TEMPERATURE_DP) * self._target_precision + ) + + if self.has_config(CONF_CURRENT_TEMPERATURE_DP): + self._current_temperature = ( + self.dps_conf(CONF_CURRENT_TEMPERATURE_DP) * self._precision + ) + + #_LOGGER.debug("the test is %s", test)he preset status""" + if self._has_presets: + if self.has_config(CONF_ECO_DP) and self.dps_conf(CONF_ECO_DP) == self._conf_eco_value: + self._preset_mode = PRESET_ECO + else: + for preset,value in self._conf_preset_set.items(): # todo remove + if self.dps_conf(CONF_PRESET_DP) == value: + self._preset_mode = preset + break + else: + self._preset_mode = PRESET_NONE + + """Update the HVAC status""" + if self.has_config(CONF_HVAC_MODE_DP): + if not self._state: + self._hvac_mode = HVAC_MODE_OFF + else: + for mode,value in self._conf_hvac_mode_set.items(): + if self.dps_conf(CONF_HVAC_MODE_DP) == value: + self._hvac_mode = mode + break + else: + # in case hvac mode and preset share the same dp + self._hvac_mode = HVAC_MODE_AUTO + + """Update the current action""" + for action,value in self._conf_hvac_action_set.items(): + if self.dps_conf(CONF_HVAC_ACTION_DP) == value: + self._hvac_action = action + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaClimate, flow_schema) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index bd8a5d3..6c03420 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -41,11 +41,30 @@ CONF_FAN_SPEED_HIGH = "fan_speed_high" # sensor CONF_SCALING = "scaling" +# climate +CONF_TARGET_TEMPERATURE_DP = "target_temperature_dp" +CONF_CURRENT_TEMPERATURE_DP = "current_temperature_dp" +CONF_TEMPERATURE_STEP = "temperature_step" +CONF_MAX_TEMP_DP = "max_temperature_dp" +CONF_MIN_TEMP_DP = "min_temperature_dp" +CONF_FAN_MODE_DP = "fan_mode_dp" +CONF_PRECISION = "precision" +CONF_TARGET_PRECISION = "target_precision" +CONF_HVAC_MODE_DP = "hvac_mode_dp" +CONF_HVAC_MODE_SET = "hvac_mode_set" +CONF_PRESET_DP = "preset_dp" +CONF_PRESET_SET = "preset_set" +CONF_EURISTIC_ACTION = "euristic_action" +CONF_HVAC_ACTION_DP = "hvac_action_dp" +CONF_HVAC_ACTION_SET = "hvac_action_set" +CONF_ECO_DP = "eco_dp" +CONF_ECO_VALUE = "eco_value" + DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "climate", "fan", "light", "sensor", "switch"] TUYA_DEVICE = "tuya_device" diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 6ce233a..8ede6fe 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -74,7 +74,25 @@ "fan_oscillating_control": "Fan Oscillating Control", "fan_speed_low": "Fan Low Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_speed_high": "Fan High Speed Setting", + "current_temperature_dp": "Current Temperature", + "target_temperature_dp": "Target Temperature", + "fan_mode_dp": "Fan Mode (optional)", + "temperature_step": "Temperature Step (optional)", + "max_temperature_dp": "Max Temperature (optional)", + "min_temperature_dp": "Min Temperature (optional)", + "precision": "Precision (optional, for DPs values)", + "target_precision": "Target Precision (optional, for DPs values)", + "temperature_unit": "Temperature Unit (optional)", + "hvac_mode_dp": "HVAC Mode DP (optional)", + "hvac_mode_set": "HVAC Mode Set (optional)", + "hvac_action_dp": "HVAC Current Action DP (optional)", + "hvac_action_set": "HVAC Current Action Set (optional)", + "preset_dp": "Presets DP (optional)", + "preset_set": "Presets Set (optional)", + "eco_dp": "Eco DP (optional)", + "eco_value": "Eco value (optional)", + "euristic_action": "Enable euristic action (optional)" } } } @@ -126,7 +144,25 @@ "fan_oscillating_control": "Fan Oscillating Control", "fan_speed_low": "Fan Low Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_speed_high": "Fan High Speed Setting", + "current_temperature_dp": "Current Temperature", + "target_temperature_dp": "Target Temperature", + "fan_mode_dp": "Fan Mode (optional)", + "temperature_step": "Temperature Step (optional)", + "max_temperature_dp": "Max Temperature (optional)", + "min_temperature_dp": "Min Temperature (optional)", + "precision": "Precision (optional, for DPs values)", + "target_precision": "Target Precision (optional, for DPs values)", + "temperature_unit": "Temperature Unit (optional)", + "hvac_mode_dp": "HVAC Mode DP (optional)", + "hvac_mode_set": "HVAC Mode Set (optional)", + "hvac_action_dp": "HVAC Current Action DP (optional)", + "hvac_action_set": "HVAC Current Action Set (optional)", + "preset_dp": "Presets DP (optional)", + "preset_set": "Presets Set (optional)", + "eco_dp": "Eco DP (optional)", + "eco_value": "Eco value (optional)", + "euristic_action": "Enable euristic action (optional)" } }, "yaml_import": { From d9f0d1ecab10a5245122e09c668ce780bdee968d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Mon, 16 Aug 2021 17:51:44 +0100 Subject: [PATCH 047/119] add simple on/off mode --- custom_components/localtuya/climate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index fe4eab3..b8221c9 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -79,6 +79,9 @@ HVAC_MODE_SETS = { HVAC_MODE_HEAT: "Manual", HVAC_MODE_AUTO: "Auto", }, + "True/False": { + HVAC_MODE_HEAT: True, + }, } HVAC_ACTION_SETS = { "True/False": { @@ -298,7 +301,7 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): if hvac_mode == HVAC_MODE_OFF: await self._device.set_dp(False, self._dp_id) return - if not self._state: + if not self._state and self._conf_hvac_mode_dp != self._dp_id: await self._device.set_dp(True, self._dp_id) await asyncio.sleep(MODE_WAIT) await self._device.set_dp(self._conf_hvac_mode_set[hvac_mode], self._conf_hvac_mode_dp) From 2335bb12fa6e8b9cc0f6140095c2317d250f9793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 2 Nov 2021 14:48:14 +0000 Subject: [PATCH 048/119] reorder imports Co-authored-by: Jelle Spijker --- custom_components/localtuya/climate.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index b8221c9..ab27462 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -13,27 +13,27 @@ from homeassistant.components.climate import ( ClimateEntity, ) from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_OFF, HVAC_MODE_AUTO, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_ACTIVITY, + PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, + PRESET_HOME, + PRESET_NONE, + PRESET_SLEEP, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - CURRENT_HVAC_OFF, - CURRENT_HVAC_HEAT, - PRESET_NONE, - PRESET_ECO, - PRESET_AWAY, - PRESET_BOOST, - PRESET_COMFORT, - PRESET_HOME, - PRESET_SLEEP, - PRESET_ACTIVITY, ) from homeassistant.const import ( ATTR_TEMPERATURE, From ea5244de74b1bc70dc02d23da5a9ad2c3303e291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 2 Nov 2021 14:48:36 +0000 Subject: [PATCH 049/119] reorder imports2 Co-authored-by: Jelle Spijker --- custom_components/localtuya/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index ab27462..2186164 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -7,10 +7,10 @@ import json import voluptuous as vol from homeassistant.components.climate import ( + ClimateEntity, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, - ClimateEntity, ) from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, From e184942c11d92926d6bd10e7f302a5af21b17c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 2 Nov 2021 14:48:53 +0000 Subject: [PATCH 050/119] reorder dict Co-authored-by: Jelle Spijker --- custom_components/localtuya/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 2186164..5c8894c 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -72,8 +72,8 @@ _LOGGER = logging.getLogger(__name__) HVAC_MODE_SETS = { "manual/auto": { - HVAC_MODE_HEAT: "manual", HVAC_MODE_AUTO: "auto", + HVAC_MODE_HEAT: "manual", }, "Manual/Auto": { HVAC_MODE_HEAT: "Manual", From 39b6ffa616647eaa6a566414f02c3e1840198a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 2 Nov 2021 14:49:07 +0000 Subject: [PATCH 051/119] reorder dict2 Co-authored-by: Jelle Spijker --- custom_components/localtuya/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 5c8894c..69b32ec 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -76,8 +76,8 @@ HVAC_MODE_SETS = { HVAC_MODE_HEAT: "manual", }, "Manual/Auto": { - HVAC_MODE_HEAT: "Manual", HVAC_MODE_AUTO: "Auto", + HVAC_MODE_HEAT: "Manual", }, "True/False": { HVAC_MODE_HEAT: True, From b6fe88211a3fd8beafa8d04f0b3b521244c00a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 2 Nov 2021 14:50:02 +0000 Subject: [PATCH 052/119] reorder imports3 Co-authored-by: Jelle Spijker --- custom_components/localtuya/climate.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 69b32ec..a4f6a73 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -48,22 +48,22 @@ from homeassistant.const import ( from .common import LocalTuyaEntity, async_setup_entry from .const import ( CONF_CURRENT_TEMPERATURE_DP, + CONF_ECO_DP, + CONF_ECO_VALUE, + CONF_EURISTIC_ACTION, CONF_FAN_MODE_DP, + CONF_HVAC_ACTION_DP, + CONF_HVAC_ACTION_SET, + CONF_HVAC_MODE_DP, + CONF_HVAC_MODE_SET, CONF_MAX_TEMP_DP, CONF_MIN_TEMP_DP, CONF_PRECISION, + CONF_PRESET_DP, + CONF_PRESET_SET, CONF_TARGET_PRECISION, CONF_TARGET_TEMPERATURE_DP, CONF_TEMPERATURE_STEP, - CONF_HVAC_MODE_DP, - CONF_HVAC_MODE_SET, - CONF_EURISTIC_ACTION, - CONF_HVAC_ACTION_DP, - CONF_HVAC_ACTION_SET, - CONF_ECO_DP, - CONF_ECO_VALUE, - CONF_PRESET_DP, - CONF_PRESET_SET, ) from . import pytuya From 0550f54fe33003353cb49630da0ce2b416bd6518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 2 Nov 2021 14:50:39 +0000 Subject: [PATCH 053/119] format comment Co-authored-by: Jelle Spijker --- custom_components/localtuya/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index a4f6a73..a28b028 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -353,7 +353,7 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): else: self._preset_mode = PRESET_NONE - """Update the HVAC status""" + # Update the HVAC status if self.has_config(CONF_HVAC_MODE_DP): if not self._state: self._hvac_mode = HVAC_MODE_OFF From 6dcb5a5056c26e86a57c6a5521191669e353f47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 2 Nov 2021 14:50:49 +0000 Subject: [PATCH 054/119] format comment2 Co-authored-by: Jelle Spijker --- custom_components/localtuya/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index a28b028..3410deb 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -366,7 +366,7 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): # in case hvac mode and preset share the same dp self._hvac_mode = HVAC_MODE_AUTO - """Update the current action""" + # Update the current action for action,value in self._conf_hvac_action_set.items(): if self.dps_conf(CONF_HVAC_ACTION_DP) == value: self._hvac_action = action From 7251d62035c17d9c0962514f9fc1897bc8b158e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 2 Nov 2021 16:57:30 +0000 Subject: [PATCH 055/119] use _LOGGER Co-authored-by: Jelle Spijker --- custom_components/localtuya/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 3410deb..7c3d3f1 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -179,7 +179,7 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): self._conf_eco_dp = self._config.get(CONF_ECO_DP) self._conf_eco_value = self._config.get(CONF_ECO_VALUE, "ECO") self._has_presets = self.has_config(CONF_ECO_DP) or self.has_config(CONF_PRESET_DP) - print("Initialized climate [{}]".format(self.name)) + _LOGGER.debug(f"Initialized climate [{self.name}]") @property def supported_features(self): From ae3f305db032dbb10aabd10b04b4968662591da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 23 Nov 2021 19:59:38 +0000 Subject: [PATCH 056/119] addressed all pending suggestions --- custom_components/localtuya/climate.py | 66 +++++++++---------- custom_components/localtuya/const.py | 1 - .../localtuya/translations/en.json | 2 - 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 7c3d3f1..3deea4f 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -7,33 +7,32 @@ import json import voluptuous as vol from homeassistant.components.climate import ( - ClimateEntity, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, + ClimateEntity, ) from homeassistant.components.climate.const import ( - CURRENT_HVAC_HEAT, - CURRENT_HVAC_OFF, HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, - PRESET_ACTIVITY, - PRESET_AWAY, - PRESET_BOOST, - PRESET_COMFORT, - PRESET_ECO, - PRESET_HOME, - PRESET_NONE, - PRESET_SLEEP, - SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, + CURRENT_HVAC_OFF, + CURRENT_HVAC_HEAT, + PRESET_NONE, + PRESET_ECO, + PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_HOME, + PRESET_SLEEP, + PRESET_ACTIVITY, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -48,22 +47,21 @@ from homeassistant.const import ( from .common import LocalTuyaEntity, async_setup_entry from .const import ( CONF_CURRENT_TEMPERATURE_DP, - CONF_ECO_DP, - CONF_ECO_VALUE, - CONF_EURISTIC_ACTION, - CONF_FAN_MODE_DP, - CONF_HVAC_ACTION_DP, - CONF_HVAC_ACTION_SET, - CONF_HVAC_MODE_DP, - CONF_HVAC_MODE_SET, CONF_MAX_TEMP_DP, CONF_MIN_TEMP_DP, CONF_PRECISION, - CONF_PRESET_DP, - CONF_PRESET_SET, CONF_TARGET_PRECISION, CONF_TARGET_TEMPERATURE_DP, CONF_TEMPERATURE_STEP, + CONF_HVAC_MODE_DP, + CONF_HVAC_MODE_SET, + CONF_EURISTIC_ACTION, + CONF_HVAC_ACTION_DP, + CONF_HVAC_ACTION_SET, + CONF_ECO_DP, + CONF_ECO_VALUE, + CONF_PRESET_DP, + CONF_PRESET_SET, ) from . import pytuya @@ -72,12 +70,12 @@ _LOGGER = logging.getLogger(__name__) HVAC_MODE_SETS = { "manual/auto": { - HVAC_MODE_AUTO: "auto", HVAC_MODE_HEAT: "manual", + HVAC_MODE_AUTO: "auto", }, "Manual/Auto": { - HVAC_MODE_AUTO: "Auto", HVAC_MODE_HEAT: "Manual", + HVAC_MODE_AUTO: "Auto", }, "True/False": { HVAC_MODE_HEAT: True, @@ -99,9 +97,9 @@ HVAC_ACTION_SETS = { } PRESET_SETS = { "Manual/Holiday/Program": { - PRESET_NONE: "Manual", PRESET_AWAY: "Holiday", PRESET_HOME: "Program", + PRESET_NONE: "Manual", }, } @@ -110,6 +108,7 @@ TEMPERATURE_FAHRENHEIT = "fahrenheit" DEFAULT_TEMPERATURE_UNIT = TEMPERATURE_CELSIUS DEFAULT_PRECISION = PRECISION_TENTHS DEFAULT_TEMPERATURE_STEP = PRECISION_HALVES +# Empirically tested to work for AVATTO thermostat MODE_WAIT = 0.1 def flow_schema(dps): @@ -146,7 +145,6 @@ def flow_schema(dps): [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] ), vol.Optional(CONF_EURISTIC_ACTION, default=False): bool, - vol.Optional(CONF_FAN_MODE_DP): vol.In(dps), } @@ -179,7 +177,7 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): self._conf_eco_dp = self._config.get(CONF_ECO_DP) self._conf_eco_value = self._config.get(CONF_ECO_VALUE, "ECO") self._has_presets = self.has_config(CONF_ECO_DP) or self.has_config(CONF_PRESET_DP) - _LOGGER.debug(f"Initialized climate [{self.name}]") + print("Initialized climate [{}]".format(self.name)) @property def supported_features(self): @@ -189,8 +187,6 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): supported_features = supported_features | SUPPORT_TARGET_TEMPERATURE if self.has_config(CONF_MAX_TEMP_DP): supported_features = supported_features | SUPPORT_TARGET_TEMPERATURE_RANGE - if self.has_config(CONF_FAN_MODE_DP): - supported_features = supported_features | SUPPORT_FAN_MODE if self.has_config(CONF_PRESET_DP) or self.has_config(CONF_ECO_DP): supported_features = supported_features | SUPPORT_PRESET_MODE return supported_features @@ -303,6 +299,7 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): return if not self._state and self._conf_hvac_mode_dp != self._dp_id: await self._device.set_dp(True, self._dp_id) + # Some thermostats need a small wait before sending another update await asyncio.sleep(MODE_WAIT) await self._device.set_dp(self._conf_hvac_mode_set[hvac_mode], self._conf_hvac_mode_dp) @@ -341,7 +338,6 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): self.dps_conf(CONF_CURRENT_TEMPERATURE_DP) * self._precision ) - #_LOGGER.debug("the test is %s", test)he preset status""" if self._has_presets: if self.has_config(CONF_ECO_DP) and self.dps_conf(CONF_ECO_DP) == self._conf_eco_value: self._preset_mode = PRESET_ECO @@ -353,7 +349,7 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): else: self._preset_mode = PRESET_NONE - # Update the HVAC status + """Update the HVAC status""" if self.has_config(CONF_HVAC_MODE_DP): if not self._state: self._hvac_mode = HVAC_MODE_OFF @@ -366,7 +362,7 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): # in case hvac mode and preset share the same dp self._hvac_mode = HVAC_MODE_AUTO - # Update the current action + """Update the current action""" for action,value in self._conf_hvac_action_set.items(): if self.dps_conf(CONF_HVAC_ACTION_DP) == value: self._hvac_action = action diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 6c03420..131891c 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -47,7 +47,6 @@ CONF_CURRENT_TEMPERATURE_DP = "current_temperature_dp" CONF_TEMPERATURE_STEP = "temperature_step" CONF_MAX_TEMP_DP = "max_temperature_dp" CONF_MIN_TEMP_DP = "min_temperature_dp" -CONF_FAN_MODE_DP = "fan_mode_dp" CONF_PRECISION = "precision" CONF_TARGET_PRECISION = "target_precision" CONF_HVAC_MODE_DP = "hvac_mode_dp" diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 8ede6fe..0c39ff4 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -77,7 +77,6 @@ "fan_speed_high": "Fan High Speed Setting", "current_temperature_dp": "Current Temperature", "target_temperature_dp": "Target Temperature", - "fan_mode_dp": "Fan Mode (optional)", "temperature_step": "Temperature Step (optional)", "max_temperature_dp": "Max Temperature (optional)", "min_temperature_dp": "Min Temperature (optional)", @@ -147,7 +146,6 @@ "fan_speed_high": "Fan High Speed Setting", "current_temperature_dp": "Current Temperature", "target_temperature_dp": "Target Temperature", - "fan_mode_dp": "Fan Mode (optional)", "temperature_step": "Temperature Step (optional)", "max_temperature_dp": "Max Temperature (optional)", "min_temperature_dp": "Min Temperature (optional)", From 20369577f3403b99dab298979d3c35398640cff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 23 Nov 2021 20:37:58 +0000 Subject: [PATCH 057/119] fix linter issues --- custom_components/localtuya/climate.py | 90 ++++++++++--------- custom_components/localtuya/const.py | 2 +- .../localtuya/translations/en.json | 4 +- 3 files changed, 52 insertions(+), 44 deletions(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 3deea4f..eaf6903 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -1,9 +1,7 @@ """Platform to locally control Tuya-based climate devices.""" import asyncio import logging -import json from functools import partial -import json import voluptuous as vol from homeassistant.components.climate import ( @@ -16,10 +14,6 @@ from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -28,11 +22,7 @@ from homeassistant.components.climate.const import ( PRESET_NONE, PRESET_ECO, PRESET_AWAY, - PRESET_BOOST, - PRESET_COMFORT, PRESET_HOME, - PRESET_SLEEP, - PRESET_ACTIVITY, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -55,7 +45,7 @@ from .const import ( CONF_TEMPERATURE_STEP, CONF_HVAC_MODE_DP, CONF_HVAC_MODE_SET, - CONF_EURISTIC_ACTION, + CONF_HEURISTIC_ACTION, CONF_HVAC_ACTION_DP, CONF_HVAC_ACTION_SET, CONF_ECO_DP, @@ -64,8 +54,6 @@ from .const import ( CONF_PRESET_SET, ) -from . import pytuya - _LOGGER = logging.getLogger(__name__) HVAC_MODE_SETS = { @@ -111,6 +99,7 @@ DEFAULT_TEMPERATURE_STEP = PRECISION_HALVES # Empirically tested to work for AVATTO thermostat MODE_WAIT = 0.1 + def flow_schema(dps): """Return schema used in config flow.""" return { @@ -125,26 +114,20 @@ def flow_schema(dps): [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] ), vol.Optional(CONF_HVAC_MODE_DP): vol.In(dps), - vol.Optional(CONF_HVAC_MODE_SET): vol.In( - list(HVAC_MODE_SETS.keys()) - ), + vol.Optional(CONF_HVAC_MODE_SET): vol.In(list(HVAC_MODE_SETS.keys())), vol.Optional(CONF_HVAC_ACTION_DP): vol.In(dps), - vol.Optional(CONF_HVAC_ACTION_SET): vol.In( - list(HVAC_ACTION_SETS.keys()) - ), + vol.Optional(CONF_HVAC_ACTION_SET): vol.In(list(HVAC_ACTION_SETS.keys())), vol.Optional(CONF_ECO_DP): vol.In(dps), vol.Optional(CONF_ECO_VALUE): str, vol.Optional(CONF_PRESET_DP): vol.In(dps), - vol.Optional(CONF_PRESET_SET): vol.In( - list(PRESET_SETS.keys()) - ), + vol.Optional(CONF_PRESET_SET): vol.In(list(PRESET_SETS.keys())), vol.Optional(CONF_TEMPERATURE_UNIT): vol.In( [TEMPERATURE_CELSIUS, TEMPERATURE_FAHRENHEIT] ), vol.Optional(CONF_TARGET_PRECISION): vol.In( [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS] ), - vol.Optional(CONF_EURISTIC_ACTION, default=False): bool, + vol.Optional(CONF_HEURISTIC_ACTION): bool, } @@ -167,16 +150,24 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): self._preset_mode = None self._hvac_action = None self._precision = self._config.get(CONF_PRECISION, DEFAULT_PRECISION) - self._target_precision = self._config.get(CONF_TARGET_PRECISION, self._precision) + self._target_precision = self._config.get( + CONF_TARGET_PRECISION, self._precision + ) self._conf_hvac_mode_dp = self._config.get(CONF_HVAC_MODE_DP) - self._conf_hvac_mode_set = HVAC_MODE_SETS.get(self._config.get(CONF_HVAC_MODE_SET), {}) + self._conf_hvac_mode_set = HVAC_MODE_SETS.get( + self._config.get(CONF_HVAC_MODE_SET), {} + ) self._conf_preset_dp = self._config.get(CONF_PRESET_DP) self._conf_preset_set = PRESET_SETS.get(self._config.get(CONF_PRESET_SET), {}) self._conf_hvac_action_dp = self._config.get(CONF_HVAC_ACTION_DP) - self._conf_hvac_action_set = HVAC_ACTION_SETS.get(self._config.get(CONF_HVAC_ACTION_SET), {}) + self._conf_hvac_action_set = HVAC_ACTION_SETS.get( + self._config.get(CONF_HVAC_ACTION_SET), {} + ) self._conf_eco_dp = self._config.get(CONF_ECO_DP) self._conf_eco_value = self._config.get(CONF_ECO_VALUE, "ECO") - self._has_presets = self.has_config(CONF_ECO_DP) or self.has_config(CONF_PRESET_DP) + self._has_presets = self.has_config(CONF_ECO_DP) or self.has_config( + CONF_PRESET_DP + ) print("Initialized climate [{}]".format(self.name)) @property @@ -226,25 +217,32 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): @property def hvac_action(self): """Return the current running hvac operation if supported. + Need to be one of CURRENT_HVAC_*. """ - if self._config[CONF_EURISTIC_ACTION]: + if self._config.get(CONF_HEURISTIC_ACTION, False): if self._hvac_mode == HVAC_MODE_HEAT: - if self._current_temperature < (self._target_temperature - self._precision): + if self._current_temperature < ( + self._target_temperature - self._precision + ): self._hvac_action = CURRENT_HVAC_HEAT - if self._current_temperature == (self._target_temperature - self._precision): + if self._current_temperature == ( + self._target_temperature - self._precision + ): if self._hvac_action == CURRENT_HVAC_HEAT: self._hvac_action = CURRENT_HVAC_HEAT if self._hvac_action == CURRENT_HVAC_OFF: self._hvac_action = CURRENT_HVAC_OFF - if (self._current_temperature + self._precision) > self._target_temperature: + if ( + self._current_temperature + self._precision + ) > self._target_temperature: self._hvac_action = CURRENT_HVAC_OFF return self._hvac_action return self._hvac_action @property def preset_mode(self): - """Return current preset""" + """Return current preset.""" return self._preset_mode @property @@ -286,7 +284,9 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): """Set new target temperature.""" if ATTR_TEMPERATURE in kwargs and self.has_config(CONF_TARGET_TEMPERATURE_DP): temperature = round(kwargs[ATTR_TEMPERATURE] / self._target_precision) - await self._device.set_dp(temperature, self._config[CONF_TARGET_TEMPERATURE_DP]) + await self._device.set_dp( + temperature, self._config[CONF_TARGET_TEMPERATURE_DP] + ) def set_fan_mode(self, fan_mode): """Set new target fan mode.""" @@ -301,14 +301,18 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): await self._device.set_dp(True, self._dp_id) # Some thermostats need a small wait before sending another update await asyncio.sleep(MODE_WAIT) - await self._device.set_dp(self._conf_hvac_mode_set[hvac_mode], self._conf_hvac_mode_dp) + await self._device.set_dp( + self._conf_hvac_mode_set[hvac_mode], self._conf_hvac_mode_dp + ) async def async_set_preset_mode(self, preset_mode): """Set new target preset mode.""" if preset_mode == PRESET_ECO: await self._device.set_dp(self._conf_eco_value, self._conf_eco_dp) return - await self._device.set_dp(self._conf_preset_set[preset_mode], self._conf_preset_dp) + await self._device.set_dp( + self._conf_preset_set[preset_mode], self._conf_preset_dp + ) @property def min_temp(self): @@ -339,22 +343,25 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): ) if self._has_presets: - if self.has_config(CONF_ECO_DP) and self.dps_conf(CONF_ECO_DP) == self._conf_eco_value: + if ( + self.has_config(CONF_ECO_DP) + and self.dps_conf(CONF_ECO_DP) == self._conf_eco_value + ): self._preset_mode = PRESET_ECO else: - for preset,value in self._conf_preset_set.items(): # todo remove + for preset, value in self._conf_preset_set.items(): # todo remove if self.dps_conf(CONF_PRESET_DP) == value: self._preset_mode = preset break else: self._preset_mode = PRESET_NONE - """Update the HVAC status""" + # Update the HVAC status if self.has_config(CONF_HVAC_MODE_DP): if not self._state: self._hvac_mode = HVAC_MODE_OFF else: - for mode,value in self._conf_hvac_mode_set.items(): + for mode, value in self._conf_hvac_mode_set.items(): if self.dps_conf(CONF_HVAC_MODE_DP) == value: self._hvac_mode = mode break @@ -362,9 +369,10 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): # in case hvac mode and preset share the same dp self._hvac_mode = HVAC_MODE_AUTO - """Update the current action""" - for action,value in self._conf_hvac_action_set.items(): + # Update the current action + for action, value in self._conf_hvac_action_set.items(): if self.dps_conf(CONF_HVAC_ACTION_DP) == value: self._hvac_action = action + async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaClimate, flow_schema) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 131891c..8bc26cb 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -53,7 +53,7 @@ CONF_HVAC_MODE_DP = "hvac_mode_dp" CONF_HVAC_MODE_SET = "hvac_mode_set" CONF_PRESET_DP = "preset_dp" CONF_PRESET_SET = "preset_set" -CONF_EURISTIC_ACTION = "euristic_action" +CONF_HEURISTIC_ACTION = "heuristic_action" CONF_HVAC_ACTION_DP = "hvac_action_dp" CONF_HVAC_ACTION_SET = "hvac_action_set" CONF_ECO_DP = "eco_dp" diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 0c39ff4..a7fe191 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -91,7 +91,7 @@ "preset_set": "Presets Set (optional)", "eco_dp": "Eco DP (optional)", "eco_value": "Eco value (optional)", - "euristic_action": "Enable euristic action (optional)" + "heuristic_action": "Enable heuristic action (optional)" } } } @@ -160,7 +160,7 @@ "preset_set": "Presets Set (optional)", "eco_dp": "Eco DP (optional)", "eco_value": "Eco value (optional)", - "euristic_action": "Enable euristic action (optional)" + "heuristic_action": "Enable heuristic action (optional)" } }, "yaml_import": { From e13f02a88119ce8025e1561f95ca3b6aa063678e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Fri, 27 Aug 2021 02:04:48 +0100 Subject: [PATCH 058/119] send update dps command with heartbeat --- .../localtuya/pytuya/__init__.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 789091c..2c613b4 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -61,6 +61,7 @@ TuyaMessage = namedtuple("TuyaMessage", "seqno cmd retcode payload crc") SET = "set" STATUS = "status" HEARTBEAT = "heartbeat" +UPDATEDPS = "updatedps" # Request refresh of DPS PROTOCOL_VERSION_BYTES_31 = b"3.1" PROTOCOL_VERSION_BYTES_33 = b"3.3" @@ -90,11 +91,13 @@ PAYLOAD_DICT = { STATUS: {"hexByte": 0x0A, "command": {"gwId": "", "devId": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, }, "type_0d": { STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, }, } @@ -379,6 +382,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): while True: try: await self.heartbeat() + await self.updatedps() await asyncio.sleep(HEARTBEAT_INTERVAL) except asyncio.CancelledError: self.debug("Stopped heartbeat loop") @@ -478,6 +482,16 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): """Send a heartbeat message.""" return await self.exchange(HEARTBEAT) + async def updatedps(self): + """ + Request device to update index. + Args: + index(array): list of dps to update (ex. [4, 5, 6, 18, 19, 20]) + """ + self.debug('updatedps() entry (dev_type is %s)', self.dev_type) + payload = self._generate_payload(UPDATEDPS) + self.transport.write(payload) + async def set_dp(self, value, dp_index): """ Set value (may be any type: bool, int or string) of any dps index. @@ -582,7 +596,10 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): json_data["t"] = str(int(time.time())) if data is not None: - json_data["dps"] = data + if "dpId" in json_data: + json_data["dpId"] = data + else: + json_data["dps"] = data elif command_hb == 0x0D: json_data["dps"] = self.dps_to_request @@ -591,7 +608,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): if self.version == 3.3: payload = self.cipher.encrypt(payload, False) - if command_hb != 0x0A: + if command_hb != 0x0A and command_hb != 0x12: # add the 3.3 header payload = PROTOCOL_33_HEADER + payload elif command == SET: From 24152569e7393a70403052df82d1566244866d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 31 Aug 2021 13:49:39 +0100 Subject: [PATCH 059/119] update state only on changes --- custom_components/localtuya/common.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 88b434f..2573b1a 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -234,13 +234,13 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): def _update_handler(status): """Update entity state when status was updated.""" - if status is not None: - self._status = status - self.status_updated() - else: - self._status = {} - - self.schedule_update_ha_state() + if status is None: + status = {} + if self._status != status: + self._status = status.copy() + if status: + self.status_updated() + self.schedule_update_ha_state() signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}" self.async_on_remove( From 47c4edc20d6e4ed6b2848b8df9becf83a6fc8b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 23 Nov 2021 21:20:43 +0000 Subject: [PATCH 060/119] fix lint issues --- custom_components/localtuya/pytuya/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 2c613b4..90ac8ed 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -61,7 +61,7 @@ TuyaMessage = namedtuple("TuyaMessage", "seqno cmd retcode payload crc") SET = "set" STATUS = "status" HEARTBEAT = "heartbeat" -UPDATEDPS = "updatedps" # Request refresh of DPS +UPDATEDPS = "updatedps" # Request refresh of DPS PROTOCOL_VERSION_BYTES_31 = b"3.1" PROTOCOL_VERSION_BYTES_33 = b"3.3" @@ -485,10 +485,11 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): async def updatedps(self): """ Request device to update index. + Args: index(array): list of dps to update (ex. [4, 5, 6, 18, 19, 20]) """ - self.debug('updatedps() entry (dev_type is %s)', self.dev_type) + self.debug("updatedps() entry (dev_type is %s)", self.dev_type) payload = self._generate_payload(UPDATEDPS) self.transport.write(payload) @@ -608,7 +609,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): if self.version == 3.3: payload = self.cipher.encrypt(payload, False) - if command_hb != 0x0A and command_hb != 0x12: + if command_hb not in [0x0A, 0x12]: # add the 3.3 header payload = PROTOCOL_33_HEADER + payload elif command == SET: From 515b3303e9042322fe185db11ae5cac85fe5dfae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 23 Nov 2021 22:16:30 +0000 Subject: [PATCH 061/119] add polling option --- custom_components/localtuya/common.py | 21 ++++++++++++++++++- custom_components/localtuya/config_flow.py | 4 ++++ .../localtuya/pytuya/__init__.py | 1 - custom_components/localtuya/strings.json | 3 ++- .../localtuya/translations/en.json | 4 +++- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 2573b1a..30028af 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -1,6 +1,7 @@ """Code shared between all platforms.""" import asyncio import logging +from datetime import timedelta from homeassistant.const import ( CONF_DEVICE_ID, @@ -9,8 +10,10 @@ from homeassistant.const import ( CONF_HOST, CONF_ID, CONF_PLATFORM, + CONF_SCAN_INTERVAL, ) from homeassistant.core import callback +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -116,6 +119,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self.dps_to_request = {} self._is_closing = False self._connect_task = None + self._unsub_interval = None self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID]) # This has to be done in case the device type is type_0d @@ -151,6 +155,15 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): raise Exception("Failed to retrieve status") self.status_updated(status) + if ( + CONF_SCAN_INTERVAL in self._config_entry + and self._config_entry[CONF_SCAN_INTERVAL] > 0 + ): + self._unsub_interval = async_track_time_interval( + self._hass, + self._async_refresh, + timedelta(seconds=self._config_entry[CONF_SCAN_INTERVAL]), + ) except Exception: # pylint: disable=broad-except self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed") if self._interface is not None: @@ -158,6 +171,10 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self._interface = None self._connect_task = None + async def _async_refresh(self, _now): + if self._interface is not None: + await self._interface.updatedps() + async def close(self): """Close connection and stop re-connect loop.""" self._is_closing = True @@ -204,7 +221,9 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): """Device disconnected.""" signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}" async_dispatcher_send(self._hass, signal, None) - + if self._unsub_interval is not None: + self._unsub_interval() + self._unsub_interval = None self._interface = None self.debug("Disconnected - waiting for discovery broadcast") diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index 731b60a..03bf80d 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_HOST, CONF_ID, CONF_PLATFORM, + CONF_SCAN_INTERVAL, ) from homeassistant.core import callback @@ -44,6 +45,7 @@ BASIC_INFO_SCHEMA = vol.Schema( vol.Required(CONF_HOST): str, vol.Required(CONF_DEVICE_ID): str, vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), + vol.Optional(CONF_SCAN_INTERVAL): int, } ) @@ -55,6 +57,7 @@ DEVICE_SCHEMA = vol.Schema( vol.Required(CONF_LOCAL_KEY): cv.string, vol.Required(CONF_FRIENDLY_NAME): cv.string, vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), + vol.Optional(CONF_SCAN_INTERVAL): int, } ) @@ -90,6 +93,7 @@ def options_schema(entities): vol.Required(CONF_HOST): str, vol.Required(CONF_LOCAL_KEY): str, vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), + vol.Optional(CONF_SCAN_INTERVAL): int, vol.Required( CONF_ENTITIES, description={"suggested_value": entity_names} ): cv.multi_select(entity_names), diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 90ac8ed..f033757 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -382,7 +382,6 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): while True: try: await self.heartbeat() - await self.updatedps() await asyncio.sleep(HEARTBEAT_INTERVAL) except asyncio.CancelledError: self.debug("Stopped heartbeat loop") diff --git a/custom_components/localtuya/strings.json b/custom_components/localtuya/strings.json index 898b99d..8a2c03f 100644 --- a/custom_components/localtuya/strings.json +++ b/custom_components/localtuya/strings.json @@ -20,6 +20,7 @@ "device_id": "Device ID", "local_key": "Local key", "protocol_version": "Protocol Version", + "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)", "device_type": "Device type" } }, @@ -39,4 +40,4 @@ } }, "title": "LocalTuya" -} \ No newline at end of file +} diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 6ce233a..34826c4 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -29,7 +29,8 @@ "host": "Host", "device_id": "Device ID", "local_key": "Local key", - "protocol_version": "Protocol Version" + "protocol_version": "Protocol Version", + "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)" } }, "pick_entity_type": { @@ -89,6 +90,7 @@ "host": "Host", "local_key": "Local key", "protocol_version": "Protocol Version", + "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)", "entities": "Entities (uncheck an entity to remove it)" } }, From 9c6de40731716baa2b51854b292095db5bf77887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Fri, 26 Nov 2021 23:47:00 +0000 Subject: [PATCH 062/119] handle updatedps response --- .../localtuya/pytuya/__init__.py | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index f033757..ccde438 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -91,13 +91,13 @@ PAYLOAD_DICT = { STATUS: {"hexByte": 0x0A, "command": {"gwId": "", "devId": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, - UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [4, 5, 6, 18, 19, 20]}}, }, "type_0d": { STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, - UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [4, 5, 6, 18, 19, 20]}}, }, } @@ -210,9 +210,11 @@ class AESCipher: class MessageDispatcher(ContextualLogger): """Buffer and dispatcher for Tuya messages.""" - # Heartbeats always respond with sequence number 0, so they can't be waited for like - # other messages. This is a hack to allow waiting for heartbeats. + # Heartbeats and updatedps always respond with sequence number 0, + # so they can't be waited for like other messages. + # This is a hack to allow waiting for them. HEARTBEAT_SEQNO = -100 + UPDATEDPS_SEQNO = -101 def __init__(self, dev_id, listener): """Initialize a new MessageBuffer.""" @@ -295,9 +297,28 @@ class MessageDispatcher(ContextualLogger): sem = self.listeners[self.HEARTBEAT_SEQNO] self.listeners[self.HEARTBEAT_SEQNO] = msg sem.release() + elif msg.cmd == 0x12: + self.debug("Got normal updatedps response") + if self.UPDATEDPS_SEQNO in self.listeners: + sem = self.listeners[self.UPDATEDPS_SEQNO] + self.listeners[self.UPDATEDPS_SEQNO] = msg + if isinstance(sem, asyncio.Semaphore): + sem.release() elif msg.cmd == 0x08: - self.debug("Got status update") - self.listener(msg) + # If we have an open updatedps call then this is for it. + # Some devices send 0x12 and 0x08 in response to a updatedps. + # Empty DPS responses here are always for updatedps + # but hey we haven't decoded yet to know + if self.UPDATEDPS_SEQNO in self.listeners and isinstance( + self.listeners[self.UPDATEDPS_SEQNO], asyncio.Semaphore + ): + self.debug("Got status type updatedps response") + sem = self.listeners[self.UPDATEDPS_SEQNO] + self.listeners[self.UPDATEDPS_SEQNO] = msg + sem.release() + else: + self.debug("Got status update") + self.listener(msg) else: self.debug( "Got message type %d for unknown listener %d: %s", @@ -443,12 +464,13 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): payload = self._generate_payload(command, dps) dev_type = self.dev_type - # Wait for special sequence number if heartbeat - seqno = ( - MessageDispatcher.HEARTBEAT_SEQNO - if command == HEARTBEAT - else (self.seqno - 1) - ) + # Wait for special sequence number if heartbeat or updatedps + if command == HEARTBEAT: + seqno = MessageDispatcher.HEARTBEAT_SEQNO + elif command == UPDATEDPS: + seqno = MessageDispatcher.UPDATEDPS_SEQNO + else: + seqno = self.seqno - 1 self.transport.write(payload) msg = await self.dispatcher.wait_for(seqno) @@ -482,15 +504,10 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): return await self.exchange(HEARTBEAT) async def updatedps(self): - """ - Request device to update index. - - Args: - index(array): list of dps to update (ex. [4, 5, 6, 18, 19, 20]) - """ - self.debug("updatedps() entry (dev_type is %s)", self.dev_type) - payload = self._generate_payload(UPDATEDPS) - self.transport.write(payload) + """Request device to update index.""" + if self.version == 3.3: + return await self.exchange(UPDATEDPS) + return True async def set_dp(self, value, dp_index): """ From e292524793c10bb3d5f78dfc1b60ca32aa303def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Sat, 27 Nov 2021 17:30:15 +0000 Subject: [PATCH 063/119] shorten label text --- custom_components/localtuya/strings.json | 2 +- custom_components/localtuya/translations/en.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/strings.json b/custom_components/localtuya/strings.json index 8a2c03f..b8bedc8 100644 --- a/custom_components/localtuya/strings.json +++ b/custom_components/localtuya/strings.json @@ -20,7 +20,7 @@ "device_id": "Device ID", "local_key": "Local key", "protocol_version": "Protocol Version", - "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)", + "scan_interval": "Scan interval (seconds, only when not updating automatically)", "device_type": "Device type" } }, diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 34826c4..8a03279 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -30,7 +30,7 @@ "device_id": "Device ID", "local_key": "Local key", "protocol_version": "Protocol Version", - "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)" + "scan_interval": "Scan interval (seconds, only when not updating automatically)" } }, "pick_entity_type": { @@ -90,7 +90,7 @@ "host": "Host", "local_key": "Local key", "protocol_version": "Protocol Version", - "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)", + "scan_interval": "Scan interval (seconds, only when not updating automatically)", "entities": "Entities (uncheck an entity to remove it)" } }, From d6e7c7dec433c1c89e33efe6cb100da8f399d35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Sun, 28 Nov 2021 01:46:12 +0000 Subject: [PATCH 064/119] send updatedps with all detected dps by default --- custom_components/localtuya/common.py | 2 +- custom_components/localtuya/pytuya/__init__.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 30028af..badfabc 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -173,7 +173,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): async def _async_refresh(self, _now): if self._interface is not None: - await self._interface.updatedps() + await self._interface.update_dps() async def close(self): """Close connection and stop re-connect loop.""" diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index ccde438..24b0268 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -21,6 +21,7 @@ Functions json = status() # returns json payload set_version(version) # 3.1 [default] or 3.3 detect_available_dps() # returns a list of available dps provided by the device + update_dps(dps) # sends update dps command add_dps_to_request(dp_index) # adds dp_index to the list of dps used by the # device (to be queried in the payload) set_dp(on, dp_index) # Set value of any dps index. @@ -503,10 +504,20 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): """Send a heartbeat message.""" return await self.exchange(HEARTBEAT) - async def updatedps(self): - """Request device to update index.""" + async def update_dps(self, dps=None): + """ + Request device to update index. + + Args: + dps([int]): list of dps to update, default=all detected + """ if self.version == 3.3: - return await self.exchange(UPDATEDPS) + if dps is None: + if not self.dps_cache: + await self.detect_available_dps() + if self.dps_cache: + dps = [int(dp) for dp in self.dps_cache][:255] + return await self.exchange(UPDATEDPS, dps) return True async def set_dp(self, value, dp_index): From 23e48b791a995f5757155c63953861074c0571e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Sun, 28 Nov 2021 10:46:28 +0000 Subject: [PATCH 065/119] revert defaulting to all dps --- custom_components/localtuya/pytuya/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 24b0268..ecd0b7d 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -509,14 +509,9 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): Request device to update index. Args: - dps([int]): list of dps to update, default=all detected + dps([int]): list of dps to update """ if self.version == 3.3: - if dps is None: - if not self.dps_cache: - await self.detect_available_dps() - if self.dps_cache: - dps = [int(dp) for dp in self.dps_cache][:255] return await self.exchange(UPDATEDPS, dps) return True From 3cbe6751d3ae9f26136c82811cb2b7a5e23478e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Mon, 29 Nov 2021 03:39:22 +0000 Subject: [PATCH 066/119] revert changes, dont wait for response but use all detected dps --- .../localtuya/pytuya/__init__.py | 55 +++++++------------ 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index ecd0b7d..16ae271 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -92,13 +92,13 @@ PAYLOAD_DICT = { STATUS: {"hexByte": 0x0A, "command": {"gwId": "", "devId": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, - UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [4, 5, 6, 18, 19, 20]}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, }, "type_0d": { STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, - UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [4, 5, 6, 18, 19, 20]}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, }, } @@ -211,11 +211,9 @@ class AESCipher: class MessageDispatcher(ContextualLogger): """Buffer and dispatcher for Tuya messages.""" - # Heartbeats and updatedps always respond with sequence number 0, - # so they can't be waited for like other messages. - # This is a hack to allow waiting for them. + # Heartbeats always respond with sequence number 0, so they can't be waited for like + # other messages. This is a hack to allow waiting for heartbeats. HEARTBEAT_SEQNO = -100 - UPDATEDPS_SEQNO = -101 def __init__(self, dev_id, listener): """Initialize a new MessageBuffer.""" @@ -300,26 +298,9 @@ class MessageDispatcher(ContextualLogger): sem.release() elif msg.cmd == 0x12: self.debug("Got normal updatedps response") - if self.UPDATEDPS_SEQNO in self.listeners: - sem = self.listeners[self.UPDATEDPS_SEQNO] - self.listeners[self.UPDATEDPS_SEQNO] = msg - if isinstance(sem, asyncio.Semaphore): - sem.release() elif msg.cmd == 0x08: - # If we have an open updatedps call then this is for it. - # Some devices send 0x12 and 0x08 in response to a updatedps. - # Empty DPS responses here are always for updatedps - # but hey we haven't decoded yet to know - if self.UPDATEDPS_SEQNO in self.listeners and isinstance( - self.listeners[self.UPDATEDPS_SEQNO], asyncio.Semaphore - ): - self.debug("Got status type updatedps response") - sem = self.listeners[self.UPDATEDPS_SEQNO] - self.listeners[self.UPDATEDPS_SEQNO] = msg - sem.release() - else: - self.debug("Got status update") - self.listener(msg) + self.debug("Got status update") + self.listener(msg) else: self.debug( "Got message type %d for unknown listener %d: %s", @@ -465,13 +446,12 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): payload = self._generate_payload(command, dps) dev_type = self.dev_type - # Wait for special sequence number if heartbeat or updatedps - if command == HEARTBEAT: - seqno = MessageDispatcher.HEARTBEAT_SEQNO - elif command == UPDATEDPS: - seqno = MessageDispatcher.UPDATEDPS_SEQNO - else: - seqno = self.seqno - 1 + # Wait for special sequence number if heartbeat + seqno = ( + MessageDispatcher.HEARTBEAT_SEQNO + if command == HEARTBEAT + else (self.seqno - 1) + ) self.transport.write(payload) msg = await self.dispatcher.wait_for(seqno) @@ -509,10 +489,17 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): Request device to update index. Args: - dps([int]): list of dps to update + dps([int]): list of dps to update, default=all detected """ if self.version == 3.3: - return await self.exchange(UPDATEDPS, dps) + if dps is None: + if not self.dps_cache: + await self.detect_available_dps() + if self.dps_cache: + dps = [int(dp) for dp in self.dps_cache][:255] + self.debug("updatedps() entry (dps %s)", dps) + payload = self._generate_payload(UPDATEDPS, dps) + self.transport.write(payload) return True async def set_dp(self, value, dp_index): From 943bfa532e1af6ac8c2b3d418fe944db772a77a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 30 Nov 2021 17:08:47 +0000 Subject: [PATCH 067/119] log dps_cache --- custom_components/localtuya/pytuya/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 16ae271..f9618e0 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -497,7 +497,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): await self.detect_available_dps() if self.dps_cache: dps = [int(dp) for dp in self.dps_cache][:255] - self.debug("updatedps() entry (dps %s)", dps) + self.debug("updatedps() entry (dps_cache %s)", self.dps_cache) payload = self._generate_payload(UPDATEDPS, dps) self.transport.write(payload) return True From 0db320ee36e30ea5cc8724c393bb19c68fe66968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Wed, 1 Dec 2021 15:36:03 +0000 Subject: [PATCH 068/119] add whitelist with 18,19,20 --- custom_components/localtuya/pytuya/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index f9618e0..b7645ec 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -78,6 +78,9 @@ SUFFIX_VALUE = 0x0000AA55 HEARTBEAT_INTERVAL = 10 +# DPS that are known to be safe to use with update_dps (0x12) command +UPDATE_DPS_WHITELIST = [18, 19, 20] # Socket (Wi-Fi) + # This is intended to match requests.json payload at # https://github.com/codetheweb/tuyapi : # type_0a devices require the 0a command as the status request @@ -489,15 +492,17 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): Request device to update index. Args: - dps([int]): list of dps to update, default=all detected + dps([int]): list of dps to update, default=detected&whitelisted """ if self.version == 3.3: if dps is None: if not self.dps_cache: await self.detect_available_dps() if self.dps_cache: - dps = [int(dp) for dp in self.dps_cache][:255] - self.debug("updatedps() entry (dps_cache %s)", self.dps_cache) + dps = [int(dp) for dp in self.dps_cache] + # filter non whitelisted dps + dps = list(set(dps).intersection(set(UPDATE_DPS_WHITELIST))) + self.debug("updatedps() entry (dps %s, dps_cache %s)", dps, self.dps_cache) payload = self._generate_payload(UPDATEDPS, dps) self.transport.write(payload) return True From 678bf816df2d2bae451d64652d3f72e662e0a8b6 Mon Sep 17 00:00:00 2001 From: Thales Silva Date: Tue, 7 Dec 2021 02:34:07 -0300 Subject: [PATCH 069/119] Update vacuum.py --- custom_components/localtuya/vacuum.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py index cc97e25..b1de91d 100644 --- a/custom_components/localtuya/vacuum.py +++ b/custom_components/localtuya/vacuum.py @@ -209,6 +209,9 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): def status_updated(self): """Device status was updated.""" state_value = str(self.dps(self._dp_id)) + + _LOGGER.info(f"Device {state_value}") + if state_value in self._idle_status_list: self._state = STATE_IDLE elif state_value in self._docked_status_list: From 1a75c2f242e38ebdbe378044fe46e9c3693eded2 Mon Sep 17 00:00:00 2001 From: Thales Silva Date: Wed, 8 Dec 2021 15:00:02 -0300 Subject: [PATCH 070/119] Report fault and fault code. Report cleaning record. --- custom_components/localtuya/vacuum.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py index b1de91d..15a1d6b 100644 --- a/custom_components/localtuya/vacuum.py +++ b/custom_components/localtuya/vacuum.py @@ -10,6 +10,7 @@ from homeassistant.components.vacuum import ( STATE_IDLE, STATE_RETURNING, STATE_PAUSED, + STATE_ERROR, SUPPORT_BATTERY, SUPPORT_FAN_SPEED, SUPPORT_PAUSE, @@ -36,7 +37,9 @@ from .const import ( CONF_FAN_SPEEDS, CONF_CLEAN_TIME_DP, CONF_CLEAN_AREA_DP, + CONF_CLEAN_RECORD_DP, CONF_LOCATE_DP, + CONF_FAULT_DP, CONF_PAUSED_STATE, CONF_RETURN_MODE, CONF_STOP_STATUS, @@ -46,8 +49,10 @@ _LOGGER = logging.getLogger(__name__) CLEAN_TIME = "clean_time" CLEAN_AREA = "clean_area" +CLEAN_RECORD = "clean_record" MODES_LIST = "cleaning_mode_list" MODE = "cleaning_mode" +FAULT = "fault" DEFAULT_IDLE_STATUS = "standby,sleep" DEFAULT_RETURNING_STATUS = "docking" @@ -76,7 +81,9 @@ def flow_schema(dps): vol.Optional(CONF_FAN_SPEEDS, default=DEFAULT_FAN_SPEEDS): str, vol.Optional(CONF_CLEAN_TIME_DP): vol.In(dps), vol.Optional(CONF_CLEAN_AREA_DP): vol.In(dps), + vol.Optional(CONF_CLEAN_RECORD_DP): vol.In(dps), vol.Optional(CONF_LOCATE_DP): vol.In(dps), + vol.Optional(CONF_FAULT_DP): vol.In(dps), vol.Optional(CONF_PAUSED_STATE, default=DEFAULT_PAUSED_STATE): str, vol.Optional(CONF_STOP_STATUS, default=DEFAULT_STOP_STATUS): str, } @@ -209,9 +216,7 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): def status_updated(self): """Device status was updated.""" state_value = str(self.dps(self._dp_id)) - - _LOGGER.info(f"Device {state_value}") - + if state_value in self._idle_status_list: self._state = STATE_IDLE elif state_value in self._docked_status_list: @@ -241,5 +246,13 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): if self.has_config(CONF_CLEAN_AREA_DP): self._attrs[CLEAN_AREA] = self.dps_conf(CONF_CLEAN_AREA_DP) + if self.has_config(CONF_CLEAN_RECORD_DP): + self._attrs[CLEAN_RECORD] = self.dps_conf(CONF_CLEAN_RECORD_DP) + + if self.has_config(CONF_FAULT_DP): + self._attrs[FAULT] = self.dps_conf(CONF_FAULT_DP) + if self._attrs[FAULT] != 0: + self._state = STATE_ERROR + async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaVacuum, flow_schema) From 52eafcf38345fecf51613843ea0396a9f696cf4e Mon Sep 17 00:00:00 2001 From: Thales Silva Date: Wed, 8 Dec 2021 15:00:35 -0300 Subject: [PATCH 071/119] Report fault and fault code. Cleaning Record. --- custom_components/localtuya/translations/en.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index e268d4d..0513276 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -64,6 +64,7 @@ "idle_status_value": "Idle Status (comma-separated)", "returning_status_value": "Returning Status", "docked_status_value": "Docked Status (comma-separated)", + "fault_dp": "Fault DP (Usually 11)", "battery_dp": "Battery status DP (Usually 14)", "mode_dp": "Mode DP (Usually 27)", "modes": "Modes list", @@ -72,6 +73,7 @@ "fan_speeds": "Fan speeds list (comma-separated)", "clean_time_dp": "Clean Time DP (Usually 33)", "clean_area_dp": "Clean Area DP (Usually 32)", + "clean_record_dp": "Clean Record DP (Usually 34)", "locate_dp": "Locate DP (Usually 31)", "paused_state": "Pause state (pause, paused, etc)", "stop_status": "Stop status", @@ -131,6 +133,7 @@ "idle_status_value": "Idle Status (comma-separated)", "returning_status_value": "Returning Status", "docked_status_value": "Docked Status (comma-separated)", + "fault_dp": "Fault DP (Usually 11)", "battery_dp": "Battery status DP (Usually 14)", "mode_dp": "Mode DP (Usually 27)", "modes": "Modes list", @@ -139,6 +142,7 @@ "fan_speeds": "Fan speeds list (comma-separated)", "clean_time_dp": "Clean Time DP (Usually 33)", "clean_area_dp": "Clean Area DP (Usually 32)", + "clean_record_dp": "Clean Record DP (Usually 34)", "locate_dp": "Locate DP (Usually 31)", "paused_state": "Pause state (pause, paused, etc)", "stop_status": "Stop status", From a6b3d49853ee1e1851b2945c192776841e18d99a Mon Sep 17 00:00:00 2001 From: Thales Silva Date: Wed, 8 Dec 2021 15:01:03 -0300 Subject: [PATCH 072/119] Report fault and fault code. Cleaning record. --- custom_components/localtuya/const.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 80dc4d6..faf6f35 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -53,7 +53,9 @@ CONF_FAN_SPEED_DP = "fan_speed_dp" CONF_FAN_SPEEDS = "fan_speeds" CONF_CLEAN_TIME_DP = "clean_time_dp" CONF_CLEAN_AREA_DP = "clean_area_dp" +CONF_CLEAN_RECORD_DP = "clean_record_dp" CONF_LOCATE_DP = "locate_dp" +CONF_FAULT_DP = "fault_dp" CONF_PAUSED_STATE = "paused_state" CONF_RETURN_MODE = "return_mode" CONF_STOP_STATUS = "stop_status" From d271da1b503d22418fe84f4803f367ad4745e13f Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 21:54:57 +1100 Subject: [PATCH 073/119] Implement optional light variable to reverse color temp --- custom_components/localtuya/const.py | 1 + custom_components/localtuya/light.py | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index bd8a5d3..f981582 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -16,6 +16,7 @@ CONF_COLOR = "color" CONF_COLOR_MODE = "color_mode" CONF_COLOR_TEMP_MIN_KELVIN = "color_temp_min_kelvin" CONF_COLOR_TEMP_MAX_KELVIN = "color_temp_max_kelvin" +CONF_COLOR_TEMP_REVERSE = "color_temp_reverse" CONF_MUSIC_MODE = "music_mode" # switch diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 49fade8..a910997 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -27,6 +27,7 @@ from .const import ( CONF_COLOR_MODE, CONF_COLOR_TEMP_MAX_KELVIN, CONF_COLOR_TEMP_MIN_KELVIN, + CONF_COLOR_TEMP_REVERSE, CONF_MUSIC_MODE, ) @@ -35,6 +36,7 @@ _LOGGER = logging.getLogger(__name__) MIRED_TO_KELVIN_CONST = 1000000 DEFAULT_MIN_KELVIN = 2700 # MIRED 370 DEFAULT_MAX_KELVIN = 6500 # MIRED 153 +DEFAULT_COLOR_TEMP_REVERSE = False DEFAULT_LOWER_BRIGHTNESS = 29 DEFAULT_UPPER_BRIGHTNESS = 1000 @@ -117,6 +119,9 @@ def flow_schema(dps): vol.Optional(CONF_COLOR_TEMP_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( vol.Coerce(int), vol.Range(min=1500, max=8000) ), + vol.Optional( + CONF_COLOR_TEMP_REVERSE, default=False, description={"suggested_value": False} + ): bool, vol.Optional(CONF_SCENE): vol.In(dps), vol.Optional( CONF_MUSIC_MODE, default=False, description={"suggested_value": False} @@ -154,6 +159,9 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): MIRED_TO_KELVIN_CONST / self._config.get(CONF_COLOR_TEMP_MAX_KELVIN, DEFAULT_MAX_KELVIN) ) + self._color_temp_reverse = self._config.get( + CONF_COLOR_TEMP_REVERSE, DEFAULT_COLOR_TEMP_REVERSE + ) self._hs = None self._effect = None self._effect_list = [] @@ -199,13 +207,15 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): def color_temp(self): """Return the color_temp of the light.""" if self.has_config(CONF_COLOR_TEMP) and self.is_white_mode: - return int( + color_temp_value = self._upper_color_temp - self._color_temp if self._color_temp_reverse else self._color_temp + color_temp_scaled = int( self._max_mired - ( ((self._max_mired - self._min_mired) / self._upper_color_temp) - * self._color_temp + * color_temp_value ) ) + return color_temp_scaled return None @property @@ -364,14 +374,16 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP): if brightness is None: brightness = self._brightness - color_temp = int( + + color_temp_value = (self._max_mired - self._min_mired) - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) if self._color_temp_reverse else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + color_temp_scaled = int( self._upper_color_temp - (self._upper_color_temp / (self._max_mired - self._min_mired)) - * (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + * color_temp_value ) states[self._config.get(CONF_COLOR_MODE)] = MODE_WHITE states[self._config.get(CONF_BRIGHTNESS)] = brightness - states[self._config.get(CONF_COLOR_TEMP)] = color_temp + states[self._config.get(CONF_COLOR_TEMP)] = color_temp_scaled await self._device.set_dps(states) async def async_turn_off(self, **kwargs): From b79c9d4841797496ea673a7ade5e29dbdc3fe5f2 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 22:07:42 +1100 Subject: [PATCH 074/119] update light config info --- info.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/info.md b/info.md index 3c69026..9e4c1d5 100644 --- a/info.md +++ b/info.md @@ -66,6 +66,13 @@ localtuya: - platform: light friendly_name: Device Light + brightness: 10 # Optional + brightness_lower: 0 # Optional + brightness_upper: 100 # Optional + color_temp: 11 # Optional + color_temp_reverse: false # Optional + color_temp_min_kelvin: 2700 # Optional + color_temp_max_kelvin: 6500 # Optional id: 4 - platform: sensor From 65758ba6e2de53db45ca7a7b83e3df97a690a3e4 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 22:19:52 +1100 Subject: [PATCH 075/119] Formatting and variables --- custom_components/localtuya/light.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index a910997..8e76dca 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -36,6 +36,7 @@ _LOGGER = logging.getLogger(__name__) MIRED_TO_KELVIN_CONST = 1000000 DEFAULT_MIN_KELVIN = 2700 # MIRED 370 DEFAULT_MAX_KELVIN = 6500 # MIRED 153 + DEFAULT_COLOR_TEMP_REVERSE = False DEFAULT_LOWER_BRIGHTNESS = 29 @@ -120,7 +121,9 @@ def flow_schema(dps): vol.Coerce(int), vol.Range(min=1500, max=8000) ), vol.Optional( - CONF_COLOR_TEMP_REVERSE, default=False, description={"suggested_value": False} + CONF_COLOR_TEMP_REVERSE, + default=DEFAULT_COLOR_TEMP_REVERSE, + description={"suggested_value": DEFAULT_COLOR_TEMP_REVERSE}, ): bool, vol.Optional(CONF_SCENE): vol.In(dps), vol.Optional( @@ -207,7 +210,11 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): def color_temp(self): """Return the color_temp of the light.""" if self.has_config(CONF_COLOR_TEMP) and self.is_white_mode: - color_temp_value = self._upper_color_temp - self._color_temp if self._color_temp_reverse else self._color_temp + color_temp_value = ( + self._upper_color_temp - self._color_temp + if self._color_temp_reverse + else self._color_temp + ) color_temp_scaled = int( self._max_mired - ( @@ -374,8 +381,13 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP): if brightness is None: brightness = self._brightness - - color_temp_value = (self._max_mired - self._min_mired) - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) if self._color_temp_reverse else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + + color_temp_value = ( + (self._max_mired - self._min_mired) + - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + if self._color_temp_reverse + else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + ) color_temp_scaled = int( self._upper_color_temp - (self._upper_color_temp / (self._max_mired - self._min_mired)) From f5ee4202233c5fdd53f625902b65862ae14eb11c Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 22:22:45 +1100 Subject: [PATCH 076/119] update readme and info --- README.md | 2 +- info.md | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0030cf3..a5faa83 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ localtuya: color_mode: 21 # Optional, usually 2 or 21, default: "none" brightness: 22 # Optional, usually 3 or 22, default: "none" color_temp: 23 # Optional, usually 4 or 23, default: "none" + color_temp_reverse: false # Optional, default: false color: 24 # Optional, usually 5 (RGB_HSV) or 24 (HSV), default: "none" brightness_lower: 29 # Optional, usually 0 or 29, default: 29 brightness_upper: 1000 # Optional, usually 255 or 1000, default: 1000 @@ -79,7 +80,6 @@ localtuya: scene: 25 # Optional, usually 6 (RGB_HSV) or 25 (HSV), default: "none" music_mode: False # Optional, some use internal mic, others, phone mic. Only internal mic is supported, default: "False" - - platform: sensor friendly_name: Plug Voltage id: 20 diff --git a/info.md b/info.md index 9e4c1d5..1954f9d 100644 --- a/info.md +++ b/info.md @@ -66,14 +66,18 @@ localtuya: - platform: light friendly_name: Device Light - brightness: 10 # Optional - brightness_lower: 0 # Optional - brightness_upper: 100 # Optional - color_temp: 11 # Optional - color_temp_reverse: false # Optional - color_temp_min_kelvin: 2700 # Optional - color_temp_max_kelvin: 6500 # Optional - id: 4 + id: 4 # Usually 1 or 20 + color_mode: 21 # Optional, usually 2 or 21, default: "none" + brightness: 22 # Optional, usually 3 or 22, default: "none" + color_temp: 23 # Optional, usually 4 or 23, default: "none" + color_temp_reverse: false # Optional, default: false + color: 24 # Optional, usually 5 (RGB_HSV) or 24 (HSV), default: "none" + brightness_lower: 29 # Optional, usually 0 or 29, default: 29 + brightness_upper: 1000 # Optional, usually 255 or 1000, default: 1000 + color_temp_min_kelvin: 2700 # Optional, default: 2700 + color_temp_max_kelvin: 6500 # Optional, default: 6500 + scene: 25 # Optional, usually 6 (RGB_HSV) or 25 (HSV), default: "none" + music_mode: False # Optional, some use internal mic, others, phone mic. Only internal mic is supported, default: "False" - platform: sensor friendly_name: Plug Voltage From e45be52a9e7845c5ab4e3a460528ed829a9b07ea Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 13 Dec 2021 11:09:13 +1100 Subject: [PATCH 077/119] rename variables --- custom_components/localtuya/light.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 8e76dca..e99414e 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -215,14 +215,13 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if self._color_temp_reverse else self._color_temp ) - color_temp_scaled = int( + return int( self._max_mired - ( ((self._max_mired - self._min_mired) / self._upper_color_temp) * color_temp_value ) ) - return color_temp_scaled return None @property @@ -381,21 +380,20 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP): if brightness is None: brightness = self._brightness - color_temp_value = ( (self._max_mired - self._min_mired) - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) if self._color_temp_reverse else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) ) - color_temp_scaled = int( + color_temp = int( self._upper_color_temp - (self._upper_color_temp / (self._max_mired - self._min_mired)) * color_temp_value ) states[self._config.get(CONF_COLOR_MODE)] = MODE_WHITE states[self._config.get(CONF_BRIGHTNESS)] = brightness - states[self._config.get(CONF_COLOR_TEMP)] = color_temp_scaled + states[self._config.get(CONF_COLOR_TEMP)] = color_temp await self._device.set_dps(states) async def async_turn_off(self, **kwargs): From c43240513cc3017ec5525413ffc4a65bfc523786 Mon Sep 17 00:00:00 2001 From: rikman122 Date: Tue, 14 Dec 2021 13:04:55 +0100 Subject: [PATCH 078/119] fix extra_state_attributes warning --- custom_components/localtuya/vacuum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py index 15a1d6b..9a14399 100644 --- a/custom_components/localtuya/vacuum.py +++ b/custom_components/localtuya/vacuum.py @@ -154,7 +154,7 @@ class LocaltuyaVacuum(LocalTuyaEntity, StateVacuumEntity): return self._battery_level @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return the specific state attributes of this vacuum cleaner.""" return self._attrs From e1bb45f2a27e97d724a53579f881fb1e11c82bbf Mon Sep 17 00:00:00 2001 From: sibowler Date: Sat, 16 Oct 2021 07:41:31 +1100 Subject: [PATCH 079/119] Adding Number and Select as Tuya platforms. Provides support for a wider range of devices. --- custom_components/localtuya/const.py | 2 +- custom_components/localtuya/number.py | 86 +++++++++++++++++ custom_components/localtuya/select.py | 95 +++++++++++++++++++ .../localtuya/translations/en.json | 14 ++- 4 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 custom_components/localtuya/number.py create mode 100644 custom_components/localtuya/select.py diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index bd8a5d3..1bd74ca 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -46,6 +46,6 @@ DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch", "number", "select"] TUYA_DEVICE = "tuya_device" diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py new file mode 100644 index 0000000..50c1919 --- /dev/null +++ b/custom_components/localtuya/number.py @@ -0,0 +1,86 @@ +"""Platform to present any Tuya DP as a number.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.number import DOMAIN, NumberEntity +from homeassistant.const import ( + CONF_DEVICE_CLASS, + STATE_UNKNOWN, +) + +from .common import LocalTuyaEntity, async_setup_entry + +_LOGGER = logging.getLogger(__name__) + +CONF_MIN_VALUE = "min_value" +CONF_MAX_VALUE = "max_value" + +DEFAULT_MIN = 0 +DEFAULT_MAX = 100000 + +def flow_schema(dps): +# """Return schema used in config flow.""" + return { + vol.Optional(CONF_MIN_VALUE, default=DEFAULT_MIN): vol.All( + vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0), + ), + vol.Required(CONF_MAX_VALUE, default=DEFAULT_MAX): vol.All( + vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0), + ), + } + + +class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): + """Representation of a Tuya Number.""" + + def __init__( + self, + device, + config_entry, + sensorid, + **kwargs, + ): + """Initialize the Tuya sensor.""" + super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) + self._state = STATE_UNKNOWN + + self._minValue = DEFAULT_MIN + if (CONF_MIN_VALUE in self._config): + self._minValue = self._config.get(CONF_MIN_VALUE) + + self._maxValue = self._config.get(CONF_MAX_VALUE) + + @property + def value(self) -> float: + """Return sensor state.""" + return self._state + + @property + def min_value(self) -> float: + """Return the minimum value.""" + + return self._minValue + + @property + def max_value(self) -> float: + """Return the maximum value.""" + return self._maxValue + + @property + def device_class(self): + """Return the class of this device.""" + return self._config.get(CONF_DEVICE_CLASS) + + async def async_set_value(self, value: float) -> None: + """Update the current value.""" + await self._device.set_dp(value, self._dp_id) + + + def status_updated(self): + """Device status was updated.""" + state = self.dps(self._dp_id) + self._state = state + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaNumber, flow_schema) diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py new file mode 100644 index 0000000..2f70d99 --- /dev/null +++ b/custom_components/localtuya/select.py @@ -0,0 +1,95 @@ +"""Platform to present any Tuya DP as an enumeration.""" +import logging +from functools import partial + +import voluptuous as vol +from homeassistant.components.select import DOMAIN, SelectEntity +from homeassistant.const import ( + CONF_DEVICE_CLASS, + STATE_UNKNOWN, +) + +from .common import LocalTuyaEntity, async_setup_entry + +_LOGGER = logging.getLogger(__name__) + +CONF_OPTIONS = "select_options" +CONF_OPTIONS_FRIENDLY = "select_options_friendly" + + +def flow_schema(dps): +# """Return schema used in config flow.""" + return { + vol.Required(CONF_OPTIONS): str, + vol.Optional(CONF_OPTIONS_FRIENDLY): str, + } + + +class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): + """Representation of a Tuya Enumeration.""" + + def __init__( + self, + device, + config_entry, + sensorid, + **kwargs, + ): + """Initialize the Tuya sensor.""" + super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) + self._state = STATE_UNKNOWN + self._validOptions = self._config.get(CONF_OPTIONS).split(';') + + #Set Display options + self._displayOptions = [] + displayOptionsStr = "" + if (CONF_OPTIONS_FRIENDLY in self._config): + displayOptionsStr = self._config.get(CONF_OPTIONS_FRIENDLY).strip() + _LOGGER.debug("Display Options Configured: " + displayOptionsStr) + + if (displayOptionsStr.find(";") >= 0): + self._displayOptions = displayOptionsStr.split(';') + elif (len(displayOptionsStr.strip()) > 0): + self._displayOptions.append(displayOptionsStr) + else: + #Default display string to raw string + _LOGGER.debug("No Display options configured - defaulting to raw values") + self._displayOptions = self._validOptions + + _LOGGER.debug("Total Raw Options: " + str(len(self._validOptions)) + " - Total Display Options: " + str(len(self._displayOptions))) + if (len(self._validOptions) > len(self._displayOptions)): + #If list of display items smaller than list of valid items, then default remaining items to be the raw value + _LOGGER.debug("Valid options is larger than display options - filling up with raw values") + for i in range(len(self._displayOptions), len(self._validOptions)): + self._displayOptions.append(self._validOptions[i]) + + @property + def current_option(self) -> str: + """Return the current value.""" + return self._stateFriendly + + @property + def options(self) -> list: + """Return the list of values.""" + return self._displayOptions + + @property + def device_class(self): + """Return the class of this device.""" + return self._config.get(CONF_DEVICE_CLASS) + + async def async_select_option(self, option: str) -> None: + """Update the current value.""" + optionValue = self._validOptions[self._displayOptions.index(option)] + _LOGGER.debug("Sending Option: " + option + " -> " + optionValue) + await self._device.set_dp(optionValue, self._dp_id) + + + def status_updated(self): + """Device status was updated.""" + state = self.dps(self._dp_id) + self._stateFriendly = self._displayOptions[self._validOptions.index(state)] + self._state = state + + +async_setup_entry = partial(async_setup_entry, DOMAIN, LocaltuyaSelect, flow_schema) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 6ce233a..a833f73 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -74,7 +74,11 @@ "fan_oscillating_control": "Fan Oscillating Control", "fan_speed_low": "Fan Low Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_speed_high": "Fan High Speed Setting", + "max_value": "Maximum Value", + "min_value": "Minimum Value", + "select_options": "Valid entries, separate entries by a ;", + "select_options_friendly": "User Friendly options, separate entries by a ;" } } } @@ -124,9 +128,13 @@ "scene": "Scene", "fan_speed_control": "Fan Speed Control", "fan_oscillating_control": "Fan Oscillating Control", - "fan_speed_low": "Fan Low Speed Setting", + "fan_speed_low": "Fan Low Speed Settingaa", "fan_speed_medium": "Fan Medium Speed Setting", - "fan_speed_high": "Fan High Speed Setting" + "fan_speed_high": "Fan High Speed Setting", + "max_value": "Maximum Value", + "min_value": "Minimum Value", + "select_options": "Valid entries, separate entries by a ;", + "select_options_friendly": "User Friendly options, separate entries by a ;" } }, "yaml_import": { From 23a16babad08ba3b43f3e564ac2e673331d832c5 Mon Sep 17 00:00:00 2001 From: sibowler Date: Sat, 16 Oct 2021 07:43:33 +1100 Subject: [PATCH 080/119] Fix for typo introduced during debugging. --- custom_components/localtuya/translations/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index a833f73..f12c4ab 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -128,7 +128,7 @@ "scene": "Scene", "fan_speed_control": "Fan Speed Control", "fan_oscillating_control": "Fan Oscillating Control", - "fan_speed_low": "Fan Low Speed Settingaa", + "fan_speed_low": "Fan Low Speed Setting", "fan_speed_medium": "Fan Medium Speed Setting", "fan_speed_high": "Fan High Speed Setting", "max_value": "Maximum Value", From fad7e0b1ac1e647f2f545a29745ecb5c671994c7 Mon Sep 17 00:00:00 2001 From: sibowler Date: Sat, 16 Oct 2021 07:46:56 +1100 Subject: [PATCH 081/119] Reordering platforms to be alphabetical --- custom_components/localtuya/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 1bd74ca..af67876 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -46,6 +46,6 @@ DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "sensor", "switch", "number", "select"] +PLATFORMS = ["binary_sensor", "cover", "fan", "light", "number", "select", "sensor", "switch"] TUYA_DEVICE = "tuya_device" From 61896605b852a3e4f73b960edd7e2245be035b58 Mon Sep 17 00:00:00 2001 From: sibowler Date: Mon, 13 Dec 2021 06:08:53 +1100 Subject: [PATCH 082/119] Fixing up style issues --- custom_components/localtuya/const.py | 3 ++- custom_components/localtuya/number.py | 5 ++--- custom_components/localtuya/select.py | 16 +++++++++------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index af67876..b4b5817 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -46,6 +46,7 @@ DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "number", "select", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "fan", "light", "number", + "select", "sensor", "switch"] TUYA_DEVICE = "tuya_device" diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py index 50c1919..1bd7ec4 100644 --- a/custom_components/localtuya/number.py +++ b/custom_components/localtuya/number.py @@ -19,8 +19,9 @@ CONF_MAX_VALUE = "max_value" DEFAULT_MIN = 0 DEFAULT_MAX = 100000 + def flow_schema(dps): -# """Return schema used in config flow.""" + """Return schema used in config flow.""" return { vol.Optional(CONF_MIN_VALUE, default=DEFAULT_MIN): vol.All( vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0), @@ -59,7 +60,6 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): @property def min_value(self) -> float: """Return the minimum value.""" - return self._minValue @property @@ -76,7 +76,6 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): """Update the current value.""" await self._device.set_dp(value, self._dp_id) - def status_updated(self): """Device status was updated.""" state = self.dps(self._dp_id) diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py index 2f70d99..90187b8 100644 --- a/custom_components/localtuya/select.py +++ b/custom_components/localtuya/select.py @@ -18,7 +18,7 @@ CONF_OPTIONS_FRIENDLY = "select_options_friendly" def flow_schema(dps): -# """Return schema used in config flow.""" + """Return schema used in config flow.""" return { vol.Required(CONF_OPTIONS): str, vol.Optional(CONF_OPTIONS_FRIENDLY): str, @@ -40,7 +40,7 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): self._state = STATE_UNKNOWN self._validOptions = self._config.get(CONF_OPTIONS).split(';') - #Set Display options + # Set Display options self._displayOptions = [] displayOptionsStr = "" if (CONF_OPTIONS_FRIENDLY in self._config): @@ -52,14 +52,17 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): elif (len(displayOptionsStr.strip()) > 0): self._displayOptions.append(displayOptionsStr) else: - #Default display string to raw string + # Default display string to raw string _LOGGER.debug("No Display options configured - defaulting to raw values") self._displayOptions = self._validOptions - _LOGGER.debug("Total Raw Options: " + str(len(self._validOptions)) + " - Total Display Options: " + str(len(self._displayOptions))) + _LOGGER.debug("Total Raw Options: " + str(len(self._validOptions)) + + " - Total Display Options: " + str(len(self._displayOptions))) if (len(self._validOptions) > len(self._displayOptions)): - #If list of display items smaller than list of valid items, then default remaining items to be the raw value - _LOGGER.debug("Valid options is larger than display options - filling up with raw values") + # If list of display items smaller than list of valid items, + # then default remaining items to be the raw value + _LOGGER.debug("Valid options is larger than display options - \ + filling up with raw values") for i in range(len(self._displayOptions), len(self._validOptions)): self._displayOptions.append(self._validOptions[i]) @@ -84,7 +87,6 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): _LOGGER.debug("Sending Option: " + option + " -> " + optionValue) await self._device.set_dp(optionValue, self._dp_id) - def status_updated(self): """Device status was updated.""" state = self.dps(self._dp_id) From a0fff6ee2f9e0982bb14cb338b88765f4117f34d Mon Sep 17 00:00:00 2001 From: sibowler Date: Tue, 14 Dec 2021 06:16:31 +1100 Subject: [PATCH 083/119] Fixing up style errors in line with tox. --- custom_components/localtuya/const.py | 12 +++++- custom_components/localtuya/number.py | 18 +++++---- custom_components/localtuya/select.py | 56 +++++++++++++++------------ 3 files changed, 51 insertions(+), 35 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index b4b5817..12fa66c 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -46,7 +46,15 @@ DATA_DISCOVERY = "discovery" DOMAIN = "localtuya" # Platforms in this list must support config flows -PLATFORMS = ["binary_sensor", "cover", "fan", "light", "number", - "select", "sensor", "switch"] +PLATFORMS = [ + "binary_sensor", + "cover", + "fan", + "light", + "number", + "select", + "sensor", + "switch", +] TUYA_DEVICE = "tuya_device" diff --git a/custom_components/localtuya/number.py b/custom_components/localtuya/number.py index 1bd7ec4..328f6a2 100644 --- a/custom_components/localtuya/number.py +++ b/custom_components/localtuya/number.py @@ -24,10 +24,12 @@ def flow_schema(dps): """Return schema used in config flow.""" return { vol.Optional(CONF_MIN_VALUE, default=DEFAULT_MIN): vol.All( - vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0), + vol.Coerce(float), + vol.Range(min=-1000000.0, max=1000000.0), ), vol.Required(CONF_MAX_VALUE, default=DEFAULT_MAX): vol.All( - vol.Coerce(float), vol.Range(min=-1000000.0, max=1000000.0), + vol.Coerce(float), + vol.Range(min=-1000000.0, max=1000000.0), ), } @@ -46,11 +48,11 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) self._state = STATE_UNKNOWN - self._minValue = DEFAULT_MIN - if (CONF_MIN_VALUE in self._config): - self._minValue = self._config.get(CONF_MIN_VALUE) + self._min_value = DEFAULT_MIN + if CONF_MIN_VALUE in self._config: + self._min_value = self._config.get(CONF_MIN_VALUE) - self._maxValue = self._config.get(CONF_MAX_VALUE) + self._max_value = self._config.get(CONF_MAX_VALUE) @property def value(self) -> float: @@ -60,12 +62,12 @@ class LocaltuyaNumber(LocalTuyaEntity, NumberEntity): @property def min_value(self) -> float: """Return the minimum value.""" - return self._minValue + return self._min_value @property def max_value(self) -> float: """Return the maximum value.""" - return self._maxValue + return self._max_value @property def device_class(self): diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py index 90187b8..4d9ce54 100644 --- a/custom_components/localtuya/select.py +++ b/custom_components/localtuya/select.py @@ -38,43 +38,49 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): """Initialize the Tuya sensor.""" super().__init__(device, config_entry, sensorid, _LOGGER, **kwargs) self._state = STATE_UNKNOWN - self._validOptions = self._config.get(CONF_OPTIONS).split(';') + self._state_friendly = "" + self._valid_options = self._config.get(CONF_OPTIONS).split(";") # Set Display options - self._displayOptions = [] - displayOptionsStr = "" - if (CONF_OPTIONS_FRIENDLY in self._config): - displayOptionsStr = self._config.get(CONF_OPTIONS_FRIENDLY).strip() - _LOGGER.debug("Display Options Configured: " + displayOptionsStr) + self._display_options = [] + display_options_str = "" + if CONF_OPTIONS_FRIENDLY in self._config: + display_options_str = self._config.get(CONF_OPTIONS_FRIENDLY).strip() + _LOGGER.debug("Display Options Configured: %s", display_options_str) - if (displayOptionsStr.find(";") >= 0): - self._displayOptions = displayOptionsStr.split(';') - elif (len(displayOptionsStr.strip()) > 0): - self._displayOptions.append(displayOptionsStr) + if display_options_str.find(";") >= 0: + self._display_options = display_options_str.split(";") + elif len(display_options_str.strip()) > 0: + self._display_options.append(display_options_str) else: # Default display string to raw string _LOGGER.debug("No Display options configured - defaulting to raw values") - self._displayOptions = self._validOptions + self._display_options = self._valid_options - _LOGGER.debug("Total Raw Options: " + str(len(self._validOptions)) + - " - Total Display Options: " + str(len(self._displayOptions))) - if (len(self._validOptions) > len(self._displayOptions)): - # If list of display items smaller than list of valid items, + _LOGGER.debug( + "Total Raw Options: %s - Total Display Options: %s", + str(len(self._valid_options)), + str(len(self._display_options)) + ) + if len(self._valid_options) > len(self._display_options): + # If list of display items smaller than list of valid items, # then default remaining items to be the raw value - _LOGGER.debug("Valid options is larger than display options - \ - filling up with raw values") - for i in range(len(self._displayOptions), len(self._validOptions)): - self._displayOptions.append(self._validOptions[i]) + _LOGGER.debug( + "Valid options is larger than display options - \ + filling up with raw values" + ) + for i in range(len(self._display_options), len(self._valid_options)): + self._display_options.append(self._valid_options[i]) @property def current_option(self) -> str: """Return the current value.""" - return self._stateFriendly + return self._state_friendly @property def options(self) -> list: """Return the list of values.""" - return self._displayOptions + return self._display_options @property def device_class(self): @@ -83,14 +89,14 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Update the current value.""" - optionValue = self._validOptions[self._displayOptions.index(option)] - _LOGGER.debug("Sending Option: " + option + " -> " + optionValue) - await self._device.set_dp(optionValue, self._dp_id) + option_value = self._valid_options[self._display_options.index(option)] + _LOGGER.debug("Sending Option: " + option + " -> " + option_value) + await self._device.set_dp(option_value, self._dp_id) def status_updated(self): """Device status was updated.""" state = self.dps(self._dp_id) - self._stateFriendly = self._displayOptions[self._validOptions.index(state)] + self._state_friendly = self._display_options[self._valid_options.index(state)] self._state = state From 1b53c227918bde30107c0874426eaad6fcf1183c Mon Sep 17 00:00:00 2001 From: sibowler Date: Tue, 14 Dec 2021 08:51:53 +1100 Subject: [PATCH 084/119] black styling --- custom_components/localtuya/select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/select.py b/custom_components/localtuya/select.py index 4d9ce54..43b32c9 100644 --- a/custom_components/localtuya/select.py +++ b/custom_components/localtuya/select.py @@ -60,7 +60,7 @@ class LocaltuyaSelect(LocalTuyaEntity, SelectEntity): _LOGGER.debug( "Total Raw Options: %s - Total Display Options: %s", str(len(self._valid_options)), - str(len(self._display_options)) + str(len(self._display_options)), ) if len(self._valid_options) > len(self._display_options): # If list of display items smaller than list of valid items, From 5a4167fe5b18cb1e422f1f363bafd70a59b9b485 Mon Sep 17 00:00:00 2001 From: sibowler Date: Tue, 14 Dec 2021 19:49:10 +1100 Subject: [PATCH 085/119] Fix dependencies to account for Select type. Also fixup errors in Fan due to movement in dependencies. --- custom_components/localtuya/fan.py | 8 +++++++- requirements_test.txt | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 834e199..0c65dfa 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -79,7 +79,13 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Get the list of available speeds.""" return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - async def async_turn_on(self, speed: str = None, **kwargs) -> None: + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: """Turn on the entity.""" await self._device.set_dp(True, self._dp_id) if speed is not None: diff --git a/requirements_test.txt b/requirements_test.txt index df62e77..a4e209a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -3,7 +3,7 @@ codespell==2.0.0 flake8==3.9.2 mypy==0.901 pydocstyle==6.1.1 -cryptography==3.2 +cryptography==3.3.2 pylint==2.8.2 pylint-strict-informational==0.1 -homeassistant==2021.1.4 +homeassistant==2021.7.1 From a6738691202a6b59eecf7c4541440c10223bf47c Mon Sep 17 00:00:00 2001 From: sibowler Date: Tue, 14 Dec 2021 19:50:17 +1100 Subject: [PATCH 086/119] Adding Select and Number dependencies --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2d94fd3..562dc77 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,7 @@ warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true -[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*] +[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,homeassistant.components.select.*,homeassistant.components.number.*] strict = true ignore_errors = false warn_unreachable = true From 76b86eb6e11452d6cffea541a63805b24a1ee718 Mon Sep 17 00:00:00 2001 From: sibowler Date: Tue, 14 Dec 2021 19:51:39 +1100 Subject: [PATCH 087/119] Adding number and select types to hacs definition. --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index 4b6ec05..3c5a41a 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "Local Tuya", - "domains": ["climate", "cover", "fan", "light", "sensor", "switch"], + "domains": ["climate", "cover", "fan", "light", "number", "select", "sensor", "switch"], "homeassistant": "0.116.0", "iot_class": ["Local Push"] } From bfba2d7f722b2bacc221f9da3d4beba4b7e1417a Mon Sep 17 00:00:00 2001 From: Sefi Ninio Date: Thu, 16 Dec 2021 18:10:31 +0200 Subject: [PATCH 088/119] fix deprecated device_state_attributes Replace `device_state_attributes` with `extra_state_attributes` This fixes #662 --- custom_components/localtuya/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/switch.py b/custom_components/localtuya/switch.py index d3e08f5..f43d910 100644 --- a/custom_components/localtuya/switch.py +++ b/custom_components/localtuya/switch.py @@ -48,7 +48,7 @@ class LocaltuyaSwitch(LocalTuyaEntity, SwitchEntity): return self._state @property - def device_state_attributes(self): + def extra_state_attributes(self): """Return device state attributes.""" attrs = {} if self.has_config(CONF_CURRENT): From 28554a1849746d9d210cff87b612acece2f47a7e Mon Sep 17 00:00:00 2001 From: jeremysherriff Date: Sun, 19 Dec 2021 17:38:30 +1300 Subject: [PATCH 089/119] Add scan interval information to README To accompany PR rospogrigio/localtuya#549 --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4ccf04..20ff98f 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ localtuya: local_key: xxxxx friendly_name: Tuya Device protocol_version: "3.3" + scan_interval: # optional, only needed if energy monitoring values are not updating + seconds: 30 # Values less than 10 seconds may cause stability issues entities: - platform: binary_sensor friendly_name: Plug Status @@ -112,9 +114,13 @@ select one of these, or manually input all the parameters. ![discovery](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/1-discovery.png) If you have selected one entry, you only need to input the device's Friendly Name and the localKey. + +Setting the scan interval is optional, only needed if energy/power values are not updating frequently enough by default. Values less than 10 seconds may cause stability issues. + Once you press "Submit", the connection is tested to check that everything works. -![device](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/2-device.png) +![image](https://user-images.githubusercontent.com/1082213/146663895-41e1902b-4f09-4b21-b9d7-067d9cd67069.png) + Then, it's time to add the entities: this step will take place several times. First, select the entity type from the drop-down menu to set it up. After you have defined all the needed entities, leave the "Do not add more entities" checkbox checked: this will complete the procedure. @@ -140,6 +146,7 @@ You can obtain Energy monitoring (voltage, current) in two different ways: Note: Voltage and Consumption usually include the first decimal. You will need to scale the parament by 0.1 to get the correct values. 1) Access the voltage/current/current_consumption attributes of a switch, and define template sensors Note: these values are already divided by 10 for Voltage and Consumption +1) On some devices, you may find that the energy values are not updating frequently enough by default. If so, set the scan interval (see above) to an appropriate value. Settings below 10 seconds may cause stability issues, 30 seconds is recommended. ``` sensor: From 0d14df976d8cfa28f3b934bc5e0fbca6aa015486 Mon Sep 17 00:00:00 2001 From: jeremysherriff Date: Sun, 19 Dec 2021 17:41:02 +1300 Subject: [PATCH 090/119] Tweak image sizing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20ff98f..7e4d690 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Setting the scan interval is optional, only needed if energy/power values are no Once you press "Submit", the connection is tested to check that everything works. -![image](https://user-images.githubusercontent.com/1082213/146663895-41e1902b-4f09-4b21-b9d7-067d9cd67069.png) +![image](https://user-images.githubusercontent.com/1082213/146664103-ac40319e-f934-4933-90cf-2beaff1e6bac.png) Then, it's time to add the entities: this step will take place several times. First, select the entity type from the drop-down menu to set it up. From 5542d83e31d1e13234264517843b02b60c685de5 Mon Sep 17 00:00:00 2001 From: jeremysherriff Date: Sun, 19 Dec 2021 17:46:34 +1300 Subject: [PATCH 091/119] Add scan interval information to info.md To accompany PR rospogrigio/localtuya#549 --- info.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/info.md b/info.md index 3c69026..b59ba35 100644 --- a/info.md +++ b/info.md @@ -43,6 +43,8 @@ localtuya: local_key: xxxxx friendly_name: Tuya Device protocol_version: "3.3" + scan_interval: # optional, only needed if energy monitoring values are not updating + seconds: 30 # Values less than 10 seconds may cause stability issues entities: - platform: binary_sensor friendly_name: Plug Status @@ -98,9 +100,12 @@ select one of these, or manually input all the parameters. ![discovery](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/1-discovery.png) If you have selected one entry, you just have to input the Friendly Name of the Device, and the localKey. -Once you press "Submit", the connection will be tested to check that everything works, in order to proceed. -![device](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/2-device.png) +Setting the scan interval is optional, only needed if energy/power values are not updating frequently enough by default. Values less than 10 seconds may cause stability issues. + +Once you press "Submit", the connection is tested to check that everything works. + +![image](https://user-images.githubusercontent.com/1082213/146663895-41e1902b-4f09-4b21-b9d7-067d9cd67069.png) Then, it's time to add the entities: this step will take place several times. Select the entity type from the drop-down menu to set it up. After you have defined all the needed entities leave the "Do not add more entities" checkbox checked: this will complete the procedure. @@ -122,7 +127,8 @@ After all the entities have been configured, the procedure is complete, and the Energy monitoring (voltage, current...) values can be obtained in two different ways: 1) creating individual sensors, each one with the desired name. Note: Voltage and Consumption usually include the first decimal, so 0.1 as "scaling" parameter shall be used in order to get the correct values. -2) accessing the voltage/current/current_consumption attributes of a switch, and then defining template sensors like this (please note that in this case the values are already divided by 10 for Voltage and Consumption): +2) accessing the voltage/current/current_consumption attributes of a switch, and then defining template sensors like this (please note that in this case the values are already divided by 10 for Voltage and Consumption) +3) On some devices, you may find that the energy values are not updating frequently enough by default. If so, set the scan interval (see above) to an appropriate value. Settings below 10 seconds may cause stability issues, 30 seconds is recommended. ``` sensor: From 38b9c675441fa4daaa1315307af7291b21310363 Mon Sep 17 00:00:00 2001 From: jeremysherriff Date: Sun, 19 Dec 2021 17:48:28 +1300 Subject: [PATCH 092/119] Tweak image sizing --- info.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/info.md b/info.md index b59ba35..ca0b487 100644 --- a/info.md +++ b/info.md @@ -105,7 +105,7 @@ Setting the scan interval is optional, only needed if energy/power values are no Once you press "Submit", the connection is tested to check that everything works. -![image](https://user-images.githubusercontent.com/1082213/146663895-41e1902b-4f09-4b21-b9d7-067d9cd67069.png) +![image](https://user-images.githubusercontent.com/1082213/146664103-ac40319e-f934-4933-90cf-2beaff1e6bac.png) Then, it's time to add the entities: this step will take place several times. Select the entity type from the drop-down menu to set it up. After you have defined all the needed entities leave the "Do not add more entities" checkbox checked: this will complete the procedure. From a5245c79f7ade6a85404c4148f74b7649e77d9f3 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 21:54:57 +1100 Subject: [PATCH 093/119] Implement optional light variable to reverse color temp --- custom_components/localtuya/const.py | 1 + custom_components/localtuya/light.py | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index 12fa66c..167490f 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -16,6 +16,7 @@ CONF_COLOR = "color" CONF_COLOR_MODE = "color_mode" CONF_COLOR_TEMP_MIN_KELVIN = "color_temp_min_kelvin" CONF_COLOR_TEMP_MAX_KELVIN = "color_temp_max_kelvin" +CONF_COLOR_TEMP_REVERSE = "color_temp_reverse" CONF_MUSIC_MODE = "music_mode" # switch diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 49fade8..a910997 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -27,6 +27,7 @@ from .const import ( CONF_COLOR_MODE, CONF_COLOR_TEMP_MAX_KELVIN, CONF_COLOR_TEMP_MIN_KELVIN, + CONF_COLOR_TEMP_REVERSE, CONF_MUSIC_MODE, ) @@ -35,6 +36,7 @@ _LOGGER = logging.getLogger(__name__) MIRED_TO_KELVIN_CONST = 1000000 DEFAULT_MIN_KELVIN = 2700 # MIRED 370 DEFAULT_MAX_KELVIN = 6500 # MIRED 153 +DEFAULT_COLOR_TEMP_REVERSE = False DEFAULT_LOWER_BRIGHTNESS = 29 DEFAULT_UPPER_BRIGHTNESS = 1000 @@ -117,6 +119,9 @@ def flow_schema(dps): vol.Optional(CONF_COLOR_TEMP_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( vol.Coerce(int), vol.Range(min=1500, max=8000) ), + vol.Optional( + CONF_COLOR_TEMP_REVERSE, default=False, description={"suggested_value": False} + ): bool, vol.Optional(CONF_SCENE): vol.In(dps), vol.Optional( CONF_MUSIC_MODE, default=False, description={"suggested_value": False} @@ -154,6 +159,9 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): MIRED_TO_KELVIN_CONST / self._config.get(CONF_COLOR_TEMP_MAX_KELVIN, DEFAULT_MAX_KELVIN) ) + self._color_temp_reverse = self._config.get( + CONF_COLOR_TEMP_REVERSE, DEFAULT_COLOR_TEMP_REVERSE + ) self._hs = None self._effect = None self._effect_list = [] @@ -199,13 +207,15 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): def color_temp(self): """Return the color_temp of the light.""" if self.has_config(CONF_COLOR_TEMP) and self.is_white_mode: - return int( + color_temp_value = self._upper_color_temp - self._color_temp if self._color_temp_reverse else self._color_temp + color_temp_scaled = int( self._max_mired - ( ((self._max_mired - self._min_mired) / self._upper_color_temp) - * self._color_temp + * color_temp_value ) ) + return color_temp_scaled return None @property @@ -364,14 +374,16 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP): if brightness is None: brightness = self._brightness - color_temp = int( + + color_temp_value = (self._max_mired - self._min_mired) - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) if self._color_temp_reverse else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + color_temp_scaled = int( self._upper_color_temp - (self._upper_color_temp / (self._max_mired - self._min_mired)) - * (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + * color_temp_value ) states[self._config.get(CONF_COLOR_MODE)] = MODE_WHITE states[self._config.get(CONF_BRIGHTNESS)] = brightness - states[self._config.get(CONF_COLOR_TEMP)] = color_temp + states[self._config.get(CONF_COLOR_TEMP)] = color_temp_scaled await self._device.set_dps(states) async def async_turn_off(self, **kwargs): From a9ef48bcfa4f13f34656d1271b53ce0c8f770b5d Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 22:07:42 +1100 Subject: [PATCH 094/119] update light config info --- info.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/info.md b/info.md index 3c69026..9e4c1d5 100644 --- a/info.md +++ b/info.md @@ -66,6 +66,13 @@ localtuya: - platform: light friendly_name: Device Light + brightness: 10 # Optional + brightness_lower: 0 # Optional + brightness_upper: 100 # Optional + color_temp: 11 # Optional + color_temp_reverse: false # Optional + color_temp_min_kelvin: 2700 # Optional + color_temp_max_kelvin: 6500 # Optional id: 4 - platform: sensor From 39cbd73d66093bdcab47ccda7574decd1dd257ab Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 22:19:52 +1100 Subject: [PATCH 095/119] Formatting and variables --- custom_components/localtuya/light.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index a910997..8e76dca 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -36,6 +36,7 @@ _LOGGER = logging.getLogger(__name__) MIRED_TO_KELVIN_CONST = 1000000 DEFAULT_MIN_KELVIN = 2700 # MIRED 370 DEFAULT_MAX_KELVIN = 6500 # MIRED 153 + DEFAULT_COLOR_TEMP_REVERSE = False DEFAULT_LOWER_BRIGHTNESS = 29 @@ -120,7 +121,9 @@ def flow_schema(dps): vol.Coerce(int), vol.Range(min=1500, max=8000) ), vol.Optional( - CONF_COLOR_TEMP_REVERSE, default=False, description={"suggested_value": False} + CONF_COLOR_TEMP_REVERSE, + default=DEFAULT_COLOR_TEMP_REVERSE, + description={"suggested_value": DEFAULT_COLOR_TEMP_REVERSE}, ): bool, vol.Optional(CONF_SCENE): vol.In(dps), vol.Optional( @@ -207,7 +210,11 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): def color_temp(self): """Return the color_temp of the light.""" if self.has_config(CONF_COLOR_TEMP) and self.is_white_mode: - color_temp_value = self._upper_color_temp - self._color_temp if self._color_temp_reverse else self._color_temp + color_temp_value = ( + self._upper_color_temp - self._color_temp + if self._color_temp_reverse + else self._color_temp + ) color_temp_scaled = int( self._max_mired - ( @@ -374,8 +381,13 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP): if brightness is None: brightness = self._brightness - - color_temp_value = (self._max_mired - self._min_mired) - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) if self._color_temp_reverse else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + + color_temp_value = ( + (self._max_mired - self._min_mired) + - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + if self._color_temp_reverse + else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) + ) color_temp_scaled = int( self._upper_color_temp - (self._upper_color_temp / (self._max_mired - self._min_mired)) From d6418200ce20e0bea731b097017ca453da52a0c5 Mon Sep 17 00:00:00 2001 From: Louis Date: Sat, 11 Dec 2021 22:22:45 +1100 Subject: [PATCH 096/119] update readme and info --- README.md | 2 +- info.md | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a4ccf04..d645eaa 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ localtuya: color_mode: 21 # Optional, usually 2 or 21, default: "none" brightness: 22 # Optional, usually 3 or 22, default: "none" color_temp: 23 # Optional, usually 4 or 23, default: "none" + color_temp_reverse: false # Optional, default: false color: 24 # Optional, usually 5 (RGB_HSV) or 24 (HSV), default: "none" brightness_lower: 29 # Optional, usually 0 or 29, default: 29 brightness_upper: 1000 # Optional, usually 255 or 1000, default: 1000 @@ -79,7 +80,6 @@ localtuya: scene: 25 # Optional, usually 6 (RGB_HSV) or 25 (HSV), default: "none" music_mode: False # Optional, some use internal mic, others, phone mic. Only internal mic is supported, default: "False" - - platform: sensor friendly_name: Plug Voltage id: 20 diff --git a/info.md b/info.md index 9e4c1d5..1954f9d 100644 --- a/info.md +++ b/info.md @@ -66,14 +66,18 @@ localtuya: - platform: light friendly_name: Device Light - brightness: 10 # Optional - brightness_lower: 0 # Optional - brightness_upper: 100 # Optional - color_temp: 11 # Optional - color_temp_reverse: false # Optional - color_temp_min_kelvin: 2700 # Optional - color_temp_max_kelvin: 6500 # Optional - id: 4 + id: 4 # Usually 1 or 20 + color_mode: 21 # Optional, usually 2 or 21, default: "none" + brightness: 22 # Optional, usually 3 or 22, default: "none" + color_temp: 23 # Optional, usually 4 or 23, default: "none" + color_temp_reverse: false # Optional, default: false + color: 24 # Optional, usually 5 (RGB_HSV) or 24 (HSV), default: "none" + brightness_lower: 29 # Optional, usually 0 or 29, default: 29 + brightness_upper: 1000 # Optional, usually 255 or 1000, default: 1000 + color_temp_min_kelvin: 2700 # Optional, default: 2700 + color_temp_max_kelvin: 6500 # Optional, default: 6500 + scene: 25 # Optional, usually 6 (RGB_HSV) or 25 (HSV), default: "none" + music_mode: False # Optional, some use internal mic, others, phone mic. Only internal mic is supported, default: "False" - platform: sensor friendly_name: Plug Voltage From 6add1f7d1a7c1130e6e4cc0b3c3cd043bc7f5d71 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 13 Dec 2021 11:09:13 +1100 Subject: [PATCH 097/119] rename variables --- custom_components/localtuya/light.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 8e76dca..e99414e 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -215,14 +215,13 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if self._color_temp_reverse else self._color_temp ) - color_temp_scaled = int( + return int( self._max_mired - ( ((self._max_mired - self._min_mired) / self._upper_color_temp) * color_temp_value ) ) - return color_temp_scaled return None @property @@ -381,21 +380,20 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP): if brightness is None: brightness = self._brightness - color_temp_value = ( (self._max_mired - self._min_mired) - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) if self._color_temp_reverse else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) ) - color_temp_scaled = int( + color_temp = int( self._upper_color_temp - (self._upper_color_temp / (self._max_mired - self._min_mired)) * color_temp_value ) states[self._config.get(CONF_COLOR_MODE)] = MODE_WHITE states[self._config.get(CONF_BRIGHTNESS)] = brightness - states[self._config.get(CONF_COLOR_TEMP)] = color_temp_scaled + states[self._config.get(CONF_COLOR_TEMP)] = color_temp await self._device.set_dps(states) async def async_turn_off(self, **kwargs): From e1ac7deace562f4d1e200964b6a44bc3c852e9a4 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 22 Dec 2021 11:35:04 +1030 Subject: [PATCH 098/119] remove blank line --- custom_components/localtuya/fan.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index ca501e8..0d9aeed 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -111,7 +111,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): preset_mode: str = None, **kwargs, ) -> None: - """Turn on the entity.""" _LOGGER.debug("Fan async_turn_on") await self._device.set_dp(True, self._dp_id) From ad0e1b186c381d7480d0d38f8b384aadabc7df8b Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 22 Dec 2021 17:34:32 +1100 Subject: [PATCH 099/119] fix missing color temp label --- custom_components/localtuya/translations/en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index f12c4ab..5981c59 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -64,6 +64,7 @@ "brightness_lower": "Brightness Lower Value", "brightness_upper": "Brightness Upper Value", "color_temp": "Color Temperature", + "color_temp_reverse": "Color Temperature Reverse", "color": "Color", "color_mode": "Color Mode", "color_temp_min_kelvin": "Minimum Color Temperature in K", @@ -120,6 +121,7 @@ "brightness_lower": "Brightness Lower Value", "brightness_upper": "Brightness Upper Value", "color_temp": "Color Temperature", + "color_temp_reverse": "Color Temperature Reverse", "color": "Color", "color_mode": "Color Mode", "color_temp_min_kelvin": "Minimum Color Temperature in K", From a25bf75a4d13679961585357c9dfa62f524fe58c Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 22 Dec 2021 19:45:16 +1030 Subject: [PATCH 100/119] tox fixes --- custom_components/localtuya/fan.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 0d9aeed..e742481 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -107,6 +107,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): async def async_turn_on( self, + speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, @@ -116,9 +117,12 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): await self._device.set_dp(True, self._dp_id) if percentage is not None: await self.async_set_percentage(percentage) + elif preset_mode is not None: + _LOGGER.debug("Preset_mode not supported yet") else: self.schedule_update_ha_state() + async def async_turn_off(self, **kwargs) -> None: """Turn off the entity.""" _LOGGER.debug("Fan async_turn_off") @@ -133,7 +137,7 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if percentage is not None: if percentage == 0: return await self.async_turn_off() - elif not self.is_on: + if not self.is_on: await self.async_turn_on() if self._use_ordered_list: await self._device.set_dp( From 0e048af29b82ea547fdd92e52b1aecca2d7b9b17 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 22 Dec 2021 20:01:39 +1030 Subject: [PATCH 101/119] fix issue with last tox fix --- custom_components/localtuya/fan.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index e742481..39cd896 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -117,8 +117,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): await self._device.set_dp(True, self._dp_id) if percentage is not None: await self.async_set_percentage(percentage) - elif preset_mode is not None: - _LOGGER.debug("Preset_mode not supported yet") else: self.schedule_update_ha_state() From b748f0425cb4e7ac31bc60c0f21187dfba964eb3 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 22 Dec 2021 20:48:46 +1030 Subject: [PATCH 102/119] Remove blank line --- custom_components/localtuya/fan.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 39cd896..d2b4583 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -120,7 +120,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): else: self.schedule_update_ha_state() - async def async_turn_off(self, **kwargs) -> None: """Turn off the entity.""" _LOGGER.debug("Fan async_turn_off") From d878391d5e27b04a94b67fe20b163076c3f83a0f Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 22 Dec 2021 23:02:08 +1030 Subject: [PATCH 103/119] readme updated --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d645eaa..705fc0b 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,16 @@ localtuya: - platform: fan friendly_name: Device Fan - id: 3 + id: 3 # dps for on/off state + fan_direction: 4 # Optional, dps for fan direction + fan_direction_fwd: forward # String for the forward direction + fan_direction_rev: reverse # String for the reverse direction + fan_ordered_list: low,medium,high,auto # Optional, If this is used it will not use the min and max integers. + fan_oscilating_control: 4 # Optional, dps for fan osciallation + fan_speed_control: 3 # Optional, if ordered list not used, dps for speed control + fan_speed_min: 1 # Optional, if ordered list not used, minimum integer for speed range + fan_speed_max: 10 # Optional, if ordered list not used, maximum integer for speed range + - platform: light friendly_name: Device Light From ddee11f74123f8627a26cf5b57ef460759d75837 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 22 Dec 2021 23:05:33 +1030 Subject: [PATCH 104/119] Revert "Merge branch 'add-string/integer-and-preset' into master" This reverts commit a6ef6a4b7f07c5277beb93801f1c1f69f69f7497, reversing changes made to b748f0425cb4e7ac31bc60c0f21187dfba964eb3. --- .github/workflows/tox.yaml | 2 +- custom_components/localtuya/const.py | 3 - custom_components/localtuya/fan.py | 149 ++++++------------ .../localtuya/translations/en.json | 10 +- 4 files changed, 48 insertions(+), 116 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 81d0350..40ca8d9 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -1,6 +1,6 @@ name: Tox PR CI -on: [push, pull_request, workflow_dispatch] +on: [pull_request] jobs: build: diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index a4da5ac..159bc12 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -41,9 +41,6 @@ CONF_FAN_ORDERED_LIST = "fan_speed_ordered_list" CONF_FAN_DIRECTION = "fan_direction" CONF_FAN_DIRECTION_FWD = "fan_direction_forward" CONF_FAN_DIRECTION_REV = "fan_direction_reverse" -CONF_FAN_SPEED_DPS_TYPE = "fan_speed_dps_type" -CONF_FAN_PRESET_CONTROL = "fan_preset_control" -CONF_FAN_PRESET_LIST = "fan_preset_list" # sensor CONF_SCALING = "scaling" diff --git a/custom_components/localtuya/fan.py b/custom_components/localtuya/fan.py index 4ea98f4..d2b4583 100644 --- a/custom_components/localtuya/fan.py +++ b/custom_components/localtuya/fan.py @@ -11,7 +11,6 @@ from homeassistant.components.fan import ( DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, - SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, ) @@ -30,10 +29,7 @@ from .const import ( CONF_FAN_DIRECTION_REV, CONF_FAN_ORDERED_LIST, CONF_FAN_OSCILLATING_CONTROL, - CONF_FAN_PRESET_CONTROL, - CONF_FAN_PRESET_LIST, CONF_FAN_SPEED_CONTROL, - CONF_FAN_SPEED_DPS_TYPE, CONF_FAN_SPEED_MAX, CONF_FAN_SPEED_MIN, ) @@ -47,16 +43,11 @@ def flow_schema(dps): vol.Optional(CONF_FAN_SPEED_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_OSCILLATING_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_DIRECTION): vol.In(dps), - vol.Optional(CONF_FAN_PRESET_CONTROL): vol.In(dps), vol.Optional(CONF_FAN_DIRECTION_FWD, default="forward"): cv.string, vol.Optional(CONF_FAN_DIRECTION_REV, default="reverse"): cv.string, vol.Optional(CONF_FAN_SPEED_MIN, default=1): cv.positive_int, vol.Optional(CONF_FAN_SPEED_MAX, default=9): cv.positive_int, - vol.Optional(CONF_FAN_ORDERED_LIST): cv.string, - vol.Optional(CONF_FAN_PRESET_LIST): cv.string, - vol.Optional(CONF_FAN_SPEED_DPS_TYPE, default="string"): vol.In( - ["string", "integer", "list"] - ), + vol.Optional(CONF_FAN_ORDERED_LIST, default="disabled"): cv.string, } @@ -76,27 +67,23 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): self._oscillating = None self._direction = None self._percentage = None - self._preset = None self._speed_range = ( self._config.get(CONF_FAN_SPEED_MIN), self._config.get(CONF_FAN_SPEED_MAX), ) - self._ordered_list = ( - self._config.get(CONF_FAN_ORDERED_LIST).replace(" ", "").split(",") - ) - self._preset_list = ( - self._config.get(CONF_FAN_PRESET_LIST).replace(" ", "").split(",") - ) - self._ordered_speed_dps_type = self._config.get(CONF_FAN_SPEED_DPS_TYPE) + self._ordered_list = self._config.get(CONF_FAN_ORDERED_LIST).split(",") + self._ordered_list_mode = None - if ( - self._ordered_speed_dps_type == "list" - and isinstance(self._ordered_list, list) - and len(self._ordered_list) > 1 - ): - _LOGGER.debug("Fan _use_ordered_list: %s", self._ordered_list) + if isinstance(self._ordered_list, list) and len(self._ordered_list) > 1: + self._use_ordered_list = True + _LOGGER.debug( + "Fan _use_ordered_list: %s > %s", + self._use_ordered_list, + self._ordered_list, + ) else: - _LOGGER.debug("Fan _use_ordered_list: Not a valid list") + self._use_ordered_list = False + _LOGGER.debug("Fan _use_ordered_list: %s", self._use_ordered_list) @property def oscillating(self): @@ -147,43 +134,35 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if percentage is not None: if percentage == 0: return await self.async_turn_off() - if not self.is_on: await self.async_turn_on() - - if self._ordered_speed_dps_type == "string": - send_speed = str( - math.ceil(percentage_to_ranged_value(self._speed_range, percentage)) - ) + if self._use_ordered_list: await self._device.set_dp( - send_speed, self._config.get(CONF_FAN_SPEED_CONTROL) + str( + percentage_to_ordered_list_item(self._ordered_list, percentage) + ), + self._config.get(CONF_FAN_SPEED_CONTROL), ) _LOGGER.debug( - "Fan async_set_percentage: %s > %s", percentage, send_speed + "Fan async_set_percentage: %s > %s", + percentage, + percentage_to_ordered_list_item(self._ordered_list, percentage), ) - elif self._ordered_speed_dps_type == "integer": - send_speed = int( - math.ceil(percentage_to_ranged_value(self._speed_range, percentage)) - ) + else: await self._device.set_dp( - send_speed, self._config.get(CONF_FAN_SPEED_CONTROL) + str( + math.ceil( + percentage_to_ranged_value(self._speed_range, percentage) + ) + ), + self._config.get(CONF_FAN_SPEED_CONTROL), ) _LOGGER.debug( - "Fan async_set_percentage: %s > %s", percentage, send_speed + "Fan async_set_percentage: %s > %s", + percentage, + percentage_to_ranged_value(self._speed_range, percentage), ) - - elif self._ordered_speed_dps_type == "list": - send_speed = str( - percentage_to_ordered_list_item(self._ordered_list, percentage) - ) - await self._device.set_dp( - send_speed, self._config.get(CONF_FAN_SPEED_CONTROL) - ) - _LOGGER.debug( - "Fan async_set_percentage: %s > %s", percentage, send_speed - ) - self.schedule_update_ha_state() async def async_oscillate(self, oscillating: bool) -> None: @@ -203,18 +182,9 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if direction == DIRECTION_REVERSE: value = self._config.get(CONF_FAN_DIRECTION_REV) - await self._device.set_dp(value, self._config.get(CONF_FAN_DIRECTION)) self.schedule_update_ha_state() - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set the preset mode of the fan.""" - _LOGGER.debug("Fan set preset: %s", preset_mode) - await self._device.set_dp( - preset_mode, self._config.get(CONF_FAN_PRESET_CONTROL) - ) - self.schedule_update_ha_state() - @property def supported_features(self) -> int: """Flag supported features.""" @@ -229,9 +199,6 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): if self.has_config(CONF_FAN_DIRECTION): features |= SUPPORT_DIRECTION - if self.has_config(CONF_FAN_PRESET_CONTROL): - features |= SUPPORT_PRESET_MODE - return features @property @@ -245,56 +212,30 @@ class LocaltuyaFan(LocalTuyaEntity, FanEntity): """Get state of Tuya fan.""" self._is_on = self.dps(self._dp_id) - if self.has_config(CONF_FAN_PRESET_CONTROL): - current_preset = self.dps_conf(CONF_FAN_PRESET_CONTROL) - if current_preset is not None and current_preset in self._preset_list: - _LOGGER.debug( - "Fan current_preset in preset list: %s from %s", - current_preset, - self._preset_list, - ) - self._preset = current_preset - current_speed = self.dps_conf(CONF_FAN_SPEED_CONTROL) - if current_speed is not None: - - if ( - self.has_config(CONF_FAN_PRESET_CONTROL) - and (CONF_FAN_SPEED_CONTROL == CONF_FAN_PRESET_CONTROL) - and (current_speed in self._preset_list) - ): - _LOGGER.debug( - "Fan current_speed in preset list: %s from %s", - current_speed, - self._preset_list, - ) - self._preset = current_speed - - elif self._ordered_speed_dps_type == "list": - _LOGGER.debug( - "Fan current_speed ordered_list_item_to_percentage: %s from %s", - current_speed, - self._ordered_list, - ) + if self._use_ordered_list: + _LOGGER.debug( + "Fan current_speed ordered_list_item_to_percentage: %s from %s", + current_speed, + self._ordered_list, + ) + if current_speed is not None: self._percentage = ordered_list_item_to_percentage( self._ordered_list, current_speed ) - elif ( - self._ordered_speed_dps_type == "string" - or self._ordered_speed_dps_type == "integer" - ): - _LOGGER.debug( - "Fan current_speed ranged_value_to_percentage: %s from %s", - current_speed, - self._speed_range, - ) + else: + _LOGGER.debug( + "Fan current_speed ranged_value_to_percentage: %s from %s", + current_speed, + self._speed_range, + ) + if current_speed is not None: self._percentage = ranged_value_to_percentage( self._speed_range, int(current_speed) ) - _LOGGER.debug("Fan current_percentage: %s", self._percentage) - _LOGGER.debug("Fan current_preset: %s", self._preset) + _LOGGER.debug("Fan current_percentage: %s", self._percentage) if self.has_config(CONF_FAN_OSCILLATING_CONTROL): self._oscillating = self.dps_conf(CONF_FAN_OSCILLATING_CONTROL) diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 2014b0b..2cb903e 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -77,10 +77,7 @@ "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", - "fan_direction_reverse": "reverse dps string", - "fan_speed_dps_type": "type of speed dps control. integer/string/list", - "fan_preset_control": "Fan preset dps. Can be the same as speed", - "fan_preset_list": "Fan preset list. Comma separated" + "fan_direction_reverse": "reverse dps string" } } } @@ -135,10 +132,7 @@ "fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)", "fan_direction":"fan direction dps", "fan_direction_forward": "forward dps string", - "fan_direction_reverse": "reverse dps string", - "fan_speed_dps_type": "type of speed dps control. integer/string/list", - "fan_preset_control": "Fan preset dps. CAn be the same as speed", - "fan_preset_list": "Fan preset list. Comma separated" + "fan_direction_reverse": "reverse dps string" } }, "yaml_import": { From a8883a5d8ed1a41c25f315facc208e4885c4aab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Mon, 10 Jan 2022 17:03:21 -0300 Subject: [PATCH 105/119] add Heat/Warming HVAC actions --- custom_components/localtuya/climate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index eaf6903..3eec336 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -82,6 +82,10 @@ HVAC_ACTION_SETS = { CURRENT_HVAC_HEAT: "heating", CURRENT_HVAC_OFF: "no_heating", }, + "Heat/Warming": { + CURRENT_HVAC_HEAT: "Heat", + CURRENT_HVAC_OFF: "Warming", + }, } PRESET_SETS = { "Manual/Holiday/Program": { From b19682cfd96af407d496ef7fd0cb94a81b4c1173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Mon, 10 Jan 2022 17:18:51 -0300 Subject: [PATCH 106/119] use idle instead of off HVAC state --- custom_components/localtuya/climate.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 3eec336..84e2317 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -17,7 +17,7 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - CURRENT_HVAC_OFF, + CURRENT_HVAC_IDLE, CURRENT_HVAC_HEAT, PRESET_NONE, PRESET_ECO, @@ -72,19 +72,19 @@ HVAC_MODE_SETS = { HVAC_ACTION_SETS = { "True/False": { CURRENT_HVAC_HEAT: True, - CURRENT_HVAC_OFF: False, + CURRENT_HVAC_IDLE: False, }, "open/close": { CURRENT_HVAC_HEAT: "open", - CURRENT_HVAC_OFF: "close", + CURRENT_HVAC_IDLE: "close", }, "heating/no_heating": { CURRENT_HVAC_HEAT: "heating", - CURRENT_HVAC_OFF: "no_heating", + CURRENT_HVAC_IDLE: "no_heating", }, "Heat/Warming": { CURRENT_HVAC_HEAT: "Heat", - CURRENT_HVAC_OFF: "Warming", + CURRENT_HVAC_IDLE: "Warming", }, } PRESET_SETS = { @@ -235,12 +235,12 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): ): if self._hvac_action == CURRENT_HVAC_HEAT: self._hvac_action = CURRENT_HVAC_HEAT - if self._hvac_action == CURRENT_HVAC_OFF: - self._hvac_action = CURRENT_HVAC_OFF + if self._hvac_action == CURRENT_HVAC_IDLE: + self._hvac_action = CURRENT_HVAC_IDLE if ( self._current_temperature + self._precision ) > self._target_temperature: - self._hvac_action = CURRENT_HVAC_OFF + self._hvac_action = CURRENT_HVAC_IDLE return self._hvac_action return self._hvac_action From cf125ae616a4ad74a439cd8b0a9ae3825cf844fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Mon, 10 Jan 2022 17:19:17 -0300 Subject: [PATCH 107/119] add Manual/Program HVAC mode --- custom_components/localtuya/climate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 84e2317..2e78d4f 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -65,6 +65,10 @@ HVAC_MODE_SETS = { HVAC_MODE_HEAT: "Manual", HVAC_MODE_AUTO: "Auto", }, + "Manual/Program": { + HVAC_MODE_HEAT: "Manual", + HVAC_MODE_AUTO: "Program", + }, "True/False": { HVAC_MODE_HEAT: True, }, From c7ee5fdb78c7d31060c9304cf75eaa8317606b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Thu, 13 Jan 2022 17:15:49 -0300 Subject: [PATCH 108/119] add turn on/off method --- custom_components/localtuya/climate.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 2e78d4f..7b03f31 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -313,6 +313,14 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): self._conf_hvac_mode_set[hvac_mode], self._conf_hvac_mode_dp ) + async def async_turn_on(self) -> None: + """Turn the entity on.""" + await self._device.set_dp(True, self._dp_id) + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + await self._device.set_dp(False, self._dp_id) + async def async_set_preset_mode(self, preset_mode): """Set new target preset mode.""" if preset_mode == PRESET_ECO: From 12bea161a8632dd6d04d32eda33d5f719c9fe7ac Mon Sep 17 00:00:00 2001 From: rikman122 Date: Mon, 24 Jan 2022 10:44:38 +0100 Subject: [PATCH 109/119] lint code style fix --- custom_components/localtuya/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index a1f6c60..4148771 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -97,7 +97,7 @@ PLATFORMS = [ "select", "sensor", "switch", - "vacuum" + "vacuum", ] TUYA_DEVICE = "tuya_device" From 35ddf1564de37fe4ddea970c4925c49f50d93654 Mon Sep 17 00:00:00 2001 From: gpaesano <97922012+gpaesano@users.noreply.github.com> Date: Sat, 29 Jan 2022 10:33:58 +0100 Subject: [PATCH 110/119] Update climate.py There is a typo in target precision which prevents corrct handling of temperature --- custom_components/localtuya/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 7b03f31..9d9358f 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -196,7 +196,7 @@ class LocaltuyaClimate(LocalTuyaEntity, ClimateEntity): return self._precision @property - def target_recision(self): + def target_precision(self): """Return the precision of the target.""" return self._target_precision From b16dcfdce918d412759cf4d14e4f762da9530cfe Mon Sep 17 00:00:00 2001 From: Leandro Issa <67451572+LeandroIssa@users.noreply.github.com> Date: Wed, 9 Feb 2022 20:56:56 -0300 Subject: [PATCH 111/119] Add files via upload --- .../localtuya/translations/pt_br.json | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 custom_components/localtuya/translations/pt_br.json diff --git a/custom_components/localtuya/translations/pt_br.json b/custom_components/localtuya/translations/pt_br.json new file mode 100644 index 0000000..c27dc51 --- /dev/null +++ b/custom_components/localtuya/translations/pt_br.json @@ -0,0 +1,217 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo já foi configurado.", + "device_updated": "A configuração do dispositivo foi atualizada!" + }, + "error": { + "cannot_connect": "Não é possível se conectar ao dispositivo. Verifique se o endereço está correto e tente novamente.", + "invalid_auth": "Falha ao autenticar com o dispositivo. Verifique se o ID do dispositivo e a chave local estão corretos.", + "unknown": "Ocorreu um erro desconhecido. Consulte o registro para obter detalhes.", + "entity_already_configured": "A entidade com este ID já foi configurada.", + "address_in_use": "O endereço usado para descoberta já está em uso. Certifique-se de que nenhum outro aplicativo o esteja usando (porta TCP 6668).", + "discovery_failed": "Algo falhou ao descobrir dispositivos. Consulte o registro para obter detalhes.", + "empty_dps": "A conexão com o dispositivo foi bem-sucedida, mas nenhum ponto de dados foi encontrado. Tente novamente. Crie um novo issue e inclua os logs de depuração se o problema persistir." + }, + "step": { + "user": { + "title": "Descoberta de dispositivo", + "description": "Escolha um dos dispositivos descobertos automaticamente ou clique em `...` para adicionar um dispositivo manualmente.", + "data": { + "discovered_device": "Dispositivo descoberto" + } + }, + "basic_info": { + "title": "Adicionar dispositivo Tuya", + "description": "Preencha os detalhes básicos do dispositivo. O nome inserido aqui será usado para identificar a própria integração (como visto na página `Integrations`). Você adicionará entidades e dará nomes a elas nas etapas a seguir.", + "data": { + "friendly_name": "Nome", + "host": "Host", + "device_id": "ID do dispositivo", + "local_key": "Local key", + "protocol_version": "Versão do protocolo", + "scan_interval": "Intervalo do escaneamento (segundos, somente quando não estiver atualizando automaticamente)" + } + }, + "pick_entity_type": { + "title": "Seleção do tipo de entidade", + "description": "Escolha o tipo de entidade que deseja adicionar.", + "data": { + "platform_to_add": "Platforma", + "no_additional_platforms": "Não adicione mais entidades" + } + }, + "add_entity": { + "title": "Adicionar nova entidade", + "description": "Por favor, preencha os detalhes de uma entidade com o tipo `{platform}`. Todas as configurações, exceto `ID`, podem ser alteradas na página Opções posteriormente.", + "data": { + "id": "ID", + "friendly_name": "Name fantasia", + "current": "Atual", + "current_consumption": "Consumo atual", + "voltage": "Voltagem", + "commands_set": "Conjunto de comandos Open_Close_Stop", + "positioning_mode": "Modo de posicão", + "current_position_dp": "Posição atual (somente para o modo de posição)", + "set_position_dp": "Definir posição (somente para o modo de posição)", + "position_inverted": "Inverter posição 0-100 (somente para o modo de posição)", + "span_time": "Tempo de abertura completo, em segundos. (somente para o modo temporizado)", + "unit_of_measurement": "Unidade de medida", + "device_class": "Classe do dispositivo", + "scaling": "Fator de escala", + "state_on": "Valor On", + "state_off": "Valor Off", + "powergo_dp": "Potência DP (Geralmente 25 ou 2)", + "idle_status_value": "Status ocioso (separado por vírgula)", + "returning_status_value": "Status de retorno", + "docked_status_value": "Status docked (separado por vírgula)", + "fault_dp": "Falha DP (Geralmente 11)", + "battery_dp": "Status da bateria DP (normalmente 14)", + "mode_dp": "Modo DP (Geralmente 27)", + "modes": "Lista de modos", + "return_mode": "Modo de retorno para base", + "fan_speed_dp": "Velocidades do ventilador DP (normalmente 30)", + "fan_speeds": "Lista de velocidades do ventilador (separadas por vírgulas)", + "clean_time_dp": "Tempo de Limpeza DP (Geralmente 33)", + "clean_area_dp": "Área Limpa DP (Geralmente 32)", + "clean_record_dp": "Limpar Registro DP (Geralmente 34)", + "locate_dp": "Localize DP (Geralmente 31)", + "paused_state": "Estado de pausa (pausa, pausado, etc)", + "stop_status": "Status de parada", + "brightness": "Brilho (somente para cor branca)", + "brightness_lower": "Valor mais baixo do brilho", + "brightness_upper": "Valor mais alto do brilho", + "color_temp": "Temperatura da cor", + "color_temp_reverse": "Temperatura da cor reversa", + "color": "Cor", + "color_mode": "Modo de cor", + "color_temp_min_kelvin": "Minima temperatura de cor em K", + "color_temp_max_kelvin": "Máxima temperatura de cor em K", + "music_mode": "Modo de música disponível", + "scene": "Cena", + "fan_speed_control": "dps de controle de velocidade do ventilador", + "fan_oscillating_control": "dps de controle oscilante do ventilador", + "fan_speed_min": "velocidade mínima do ventilador inteiro", + "fan_speed_max": "velocidade máxima do ventilador inteiro", + "fan_speed_ordered_list": "Lista de modos de velocidade do ventilador (substitui a velocidade min/max)", + "fan_direction":"direção do ventilador dps", + "fan_direction_forward": "string de dps para frente", + "fan_direction_reverse": "string dps reversa", + "current_temperature_dp": "Temperatura atual", + "target_temperature_dp": "Temperatura alvo", + "temperature_step": "Etapa de temperatura (opcional)", + "max_temperature_dp": "Max Temperatura (opcional)", + "min_temperature_dp": "Min Temperatura (opcional)", + "precision": "Precisão (opcional, para valores de DPs)", + "target_precision": "Precisão do alvo (opcional, para valores de DPs)", + "temperature_unit": "Unidade de Temperatura (opcional)", + "hvac_mode_dp": "Modo HVAC DP (opcional)", + "hvac_mode_set": "Conjunto de modo HVAC (opcional)", + "hvac_action_dp": "Ação atual de HVAC DP (opcional)", + "hvac_action_set": "Conjunto de ação atual de HVAC (opcional)", + "preset_dp": "Predefinições DP (opcional)", + "preset_set": "Conjunto de predefinições (opcional)", + "eco_dp": "Eco DP (opcional)", + "eco_value": "Valor ECO (opcional)", + "heuristic_action": "Ativar ação heurística (opcional)" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Configurar dispositivo Tuya", + "description": "Configuração básica para o ID do dispositivo `{device_id}`.", + "data": { + "friendly_name": "Nome fantasia", + "host": "Host", + "local_key": "Local key", + "protocol_version": "Versão do protocolo", + "scan_interval": "Intervalo de escaneamento (segundos, somente quando não estiver atualizando automaticamente)", + "entities": "Entidades (desmarque uma entidade para removê-la)" + } + }, + "entity": { + "title": "Adicionar nova entidade", + "description": "Por favor, preencha os detalhes de uma entidade com o tipo `{platform}`. Todas as configurações, exceto `ID`, podem ser alteradas na página Opções posteriormente.", + "data": { + "id": "ID", + "friendly_name": "Name fantasia", + "current": "Atual", + "current_consumption": "Consumo atual", + "voltage": "Voltagem", + "commands_set": "Conjunto de comandos Open_Close_Stop", + "positioning_mode": "Modo de posicão", + "current_position_dp": "Posição atual (somente para o modo de posição)", + "set_position_dp": "Definir posição (somente para o modo de posição)", + "position_inverted": "Inverter posição 0-100 (somente para o modo de posição)", + "span_time": "Tempo de abertura completo, em segundos. (somente para o modo temporizado)", + "unit_of_measurement": "Unidade de medida", + "device_class": "Classe do dispositivo", + "scaling": "Fator de escala", + "state_on": "Valor On", + "state_off": "Valor Off", + "powergo_dp": "Potência DP (Geralmente 25 ou 2)", + "idle_status_value": "Status ocioso (separado por vírgula)", + "returning_status_value": "Status de retorno", + "docked_status_value": "Status docked (separado por vírgula)", + "fault_dp": "Falha DP (Geralmente 11)", + "battery_dp": "Status da bateria DP (normalmente 14)", + "mode_dp": "Modo DP (Geralmente 27)", + "modes": "Lista de modos", + "return_mode": "Modo de retorno para base", + "fan_speed_dp": "Velocidades do ventilador DP (normalmente 30)", + "fan_speeds": "Lista de velocidades do ventilador (separadas por vírgulas)", + "clean_time_dp": "Tempo de Limpeza DP (Geralmente 33)", + "clean_area_dp": "Área Limpa DP (Geralmente 32)", + "clean_record_dp": "Limpar Registro DP (Geralmente 34)", + "locate_dp": "Localize DP (Geralmente 31)", + "paused_state": "Estado de pausa (pausa, pausado, etc)", + "stop_status": "Status de parada", + "brightness": "Brilho (somente para cor branca)", + "brightness_lower": "Valor mais baixo do brilho", + "brightness_upper": "Valor mais alto do brilho", + "color_temp": "Temperatura da cor", + "color_temp_reverse": "Temperatura da cor reversa", + "color": "Cor", + "color_mode": "Modo de cor", + "color_temp_min_kelvin": "Minima temperatura de cor em K", + "color_temp_max_kelvin": "Máxima temperatura de cor em K", + "music_mode": "Modo de música disponível", + "scene": "Cena", + "fan_speed_control": "dps de controle de velocidade do ventilador", + "fan_oscillating_control": "dps de controle oscilante do ventilador", + "fan_speed_min": "velocidade mínima do ventilador inteiro", + "fan_speed_max": "velocidade máxima do ventilador inteiro", + "fan_speed_ordered_list": "Lista de modos de velocidade do ventilador (substitui a velocidade min/max)", + "fan_direction":"direção do ventilador dps", + "fan_direction_forward": "string de dps para frente", + "fan_direction_reverse": "string dps reversa", + "current_temperature_dp": "Temperatura atual", + "target_temperature_dp": "Temperatura alvo", + "temperature_step": "Etapa de temperatura (opcional)", + "max_temperature_dp": "Max Temperatura (opcional)", + "min_temperature_dp": "Min Temperatura (opcional)", + "precision": "Precisão (opcional, para valores de DPs)", + "target_precision": "Precisão do alvo (opcional, para valores de DPs)", + "temperature_unit": "Unidade de Temperatura (opcional)", + "hvac_mode_dp": "Modo HVAC DP (opcional)", + "hvac_mode_set": "Conjunto de modo HVAC (opcional)", + "hvac_action_dp": "Ação atual de HVAC DP (opcional)", + "hvac_action_set": "Conjunto de ação atual de HVAC (opcional)", + "preset_dp": "Predefinições DP (opcional)", + "preset_set": "Conjunto de predefinições (opcional)", + "eco_dp": "Eco DP (opcional)", + "eco_value": "Valor ECO (opcional)", + "heuristic_action": "Ativar ação heurística (opcional)" + } + }, + "yaml_import": { + "title": "Não suportado", + "description": "As opções não podem ser editadas quando configuradas via YAML." + } + } + }, + "title": "LocalTuya" +} From 22cabf0c1a98857151072be9c0f19a41860f3bfa Mon Sep 17 00:00:00 2001 From: Leandro Issa <67451572+LeandroIssa@users.noreply.github.com> Date: Sun, 27 Feb 2022 00:42:32 -0300 Subject: [PATCH 112/119] Rename pt_br.json to pt-BR.json --- .../localtuya/translations/{pt_br.json => pt-BR.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename custom_components/localtuya/translations/{pt_br.json => pt-BR.json} (100%) diff --git a/custom_components/localtuya/translations/pt_br.json b/custom_components/localtuya/translations/pt-BR.json similarity index 100% rename from custom_components/localtuya/translations/pt_br.json rename to custom_components/localtuya/translations/pt-BR.json From 5570a94cb959c0d6847db1c77bdda965c3c98981 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Thu, 10 Mar 2022 11:23:21 +0100 Subject: [PATCH 113/119] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 7d43da2..7404fe9 100644 --- a/README.md +++ b/README.md @@ -206,3 +206,6 @@ TradeFace, for being the only one to provide the correct code for communication sean6541, for the working (standard) Python Handler for Tuya devices. postlund, for the ideas, for coding 95% of the refactoring and boosting the quality of this repo to levels hard to imagine (by me, at least) and teaching me A LOT of how things work in Home Assistant. + +Buy Me A Coffee +PayPal Logo From 2a277a0c26497085a310306805df8ce7fb60a4d3 Mon Sep 17 00:00:00 2001 From: "Peter A. Bigot" Date: Sun, 31 Oct 2021 13:53:34 -0700 Subject: [PATCH 114/119] Fix kelvin-to-mired conversion Use the core utility rather than reimplementing it. Signed-off-by: Peter A. Bigot --- custom_components/localtuya/light.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index e99414e..5a33192 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -33,7 +33,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -MIRED_TO_KELVIN_CONST = 1000000 DEFAULT_MIN_KELVIN = 2700 # MIRED 370 DEFAULT_MAX_KELVIN = 6500 # MIRED 153 @@ -154,13 +153,11 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): CONF_BRIGHTNESS_UPPER, DEFAULT_UPPER_BRIGHTNESS ) self._upper_color_temp = self._upper_brightness - self._max_mired = round( - MIRED_TO_KELVIN_CONST - / self._config.get(CONF_COLOR_TEMP_MIN_KELVIN, DEFAULT_MIN_KELVIN) + self._max_mired = color_util.color_temperature_kelvin_to_mired( + self._config.get(CONF_COLOR_TEMP_MIN_KELVIN, DEFAULT_MIN_KELVIN) ) - self._min_mired = round( - MIRED_TO_KELVIN_CONST - / self._config.get(CONF_COLOR_TEMP_MAX_KELVIN, DEFAULT_MAX_KELVIN) + self._min_mired = color_util.color_temperature_kelvin_to_mired( + self._config.get(CONF_COLOR_TEMP_MAX_KELVIN, DEFAULT_MAX_KELVIN) ) self._color_temp_reverse = self._config.get( CONF_COLOR_TEMP_REVERSE, DEFAULT_COLOR_TEMP_REVERSE From 8a1c6e03806fbcf410e896b323e76436f581b439 Mon Sep 17 00:00:00 2001 From: "Peter A. Bigot" Date: Sun, 31 Oct 2021 13:59:53 -0700 Subject: [PATCH 115/119] Fix out-of-range color temperatures If light.turn_on is invoked with `brightness: 128, color_temp: 500` for a bulb that supports mired range 154..370 in value range 1..255 the calculated color temperature will be negative. When the bulb receives the command it may turn on, but will fail processing the color_temp operation and will not send a response. At this point `light.toggle` will have no effect because Home Assistant is unaware that the bulb is on. Fix the state by clamping the sent color temperature to the allowed range. Signed-off-by: Peter A. Bigot --- custom_components/localtuya/light.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/custom_components/localtuya/light.py b/custom_components/localtuya/light.py index 5a33192..7c74e49 100644 --- a/custom_components/localtuya/light.py +++ b/custom_components/localtuya/light.py @@ -377,16 +377,17 @@ class LocaltuyaLight(LocalTuyaEntity, LightEntity): if ATTR_COLOR_TEMP in kwargs and (features & SUPPORT_COLOR_TEMP): if brightness is None: brightness = self._brightness - color_temp_value = ( - (self._max_mired - self._min_mired) - - (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) - if self._color_temp_reverse - else (int(kwargs[ATTR_COLOR_TEMP]) - self._min_mired) - ) + mired = int(kwargs[ATTR_COLOR_TEMP]) + if self._color_temp_reverse: + mired = self._max_mired - (mired - self._min_mired) + if mired < self._min_mired: + mired = self._min_mired + elif mired > self._max_mired: + mired = self._max_mired color_temp = int( self._upper_color_temp - (self._upper_color_temp / (self._max_mired - self._min_mired)) - * color_temp_value + * (mired - self._min_mired) ) states[self._config.get(CONF_COLOR_MODE)] = MODE_WHITE states[self._config.get(CONF_BRIGHTNESS)] = brightness From c222b727e2019ba77b716bad42d4e3025555858c Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Wed, 25 May 2022 11:30:40 +0200 Subject: [PATCH 116/119] Adding images for README --- img/9-cloud_setup.png | Bin 0 -> 22177 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 img/9-cloud_setup.png diff --git a/img/9-cloud_setup.png b/img/9-cloud_setup.png new file mode 100644 index 0000000000000000000000000000000000000000..c51927d3fc52ba4b66f2c14fd8ec61eda373d08d GIT binary patch literal 22177 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 mY_yILDe$0Dl9bqG(WHx7cm5w@;K>I7 literal 0 HcmV?d00001 From 84e665afc172bf33ef6ca93c03295842182b2b12 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Wed, 25 May 2022 11:27:56 +0200 Subject: [PATCH 117/119] Added images for README --- img/6-project_date.png | Bin 0 -> 59139 bytes img/7-auth_keys.png | Bin 0 -> 66636 bytes img/8-user_id.png | Bin 0 -> 73998 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 img/6-project_date.png create mode 100644 img/7-auth_keys.png create mode 100644 img/8-user_id.png diff --git a/img/6-project_date.png b/img/6-project_date.png new file mode 100644 index 0000000000000000000000000000000000000000..de324ac34addf10066c34b62a7c75e6ecd2582a8 GIT binary patch literal 59139 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 G{Q6%Kvi_C; literal 0 HcmV?d00001 diff --git a/img/7-auth_keys.png b/img/7-auth_keys.png new file mode 100644 index 0000000000000000000000000000000000000000..b8cc331aafef085e4c73ce2a32f973ab2c4628af GIT binary patch literal 66636 zcmZU4WmHvNv^FZCA|NS^bc1wb2C{*Fet@6rwv6)T^H+f`W45 zf`Xr|ZLJJV%nhKRD8f9#c*NTHaXYlf0*Rs!9Y4zV$R*ME2eM0`2kE3$psP82sXd2} zXC|Sf3;AKz(h}N{?-NuGL4^GHeEn*5dp&cTi1d-3vcmslxrX5Akmqpml|V+TBGZ>w zx$y>>s|*YY>ge^_$bY{GV18B9%T5-fH(cguI$T-X(~LB7Nx8E-r*w&U#T(`vvg0e< z@RhsVWGC?|pskREvVE(44Oa8db=Y26pH*TIB0M*fxxT_#yRKg8bc-!qd3YL%EUGA) zQieO>URkS4P`O%O=_Jn$ypM|o>3~DLp#Qgb???y*L=X%VbqFO2L;?fQ4!XGSeM0!e z>EbjDzSz>FzDEyVgM0E>T8RIZC(cHR`KC_G{O%jt_99HY!`vfp)xqYwWiMDrMOE`G zlp&vZadl-k^2b?NI>=mn1DwVo)En74eX)@B97?qJfA6q7K#1GTFs@Vci)wf59#D+q z6Q>B))^C5KQ@SmzE$}2YKDcF1EFrZ<*mk}7`7Qxq%R6f^RXZpsI^ySlubi{^9Dz2|p50$?Q9+$rggYjXoYs#j5tGlS$gAO3`}#+VzrZGMde66FGE3!C#oHpA zJSDe1g-?1g=(&=4y-LW!W;SLejlR|Ie_)iL1)IrJeg3LE(|Y>IApW)>H}zF>N>>g- zL>v>7k>Tljg5BnCHBC*tR{(K9%cXKq{O|E|&9l4#?SCJBQ2*D7b9lOzz5lbP&*Q4LrL0tyoryH0|G5d+zGE@&`F?Cm| zM`L*v-whkZ9V+#QlhV(WN-oXJ%&g{XPS$%Ov>IJiRa7b@#$ zUrI{KFo~?&+uKIBt3zOXU0tD{qlt+LKR;CYb&C&NaB#482{sY&bBkhQs6++fnn=;%L2^ni{!m^7wdR@lar3;LX_4Ot+KL`mX?;9nmWqpg)AuN1D^ue zpv5GqTe8ftVE^vIPc9y0ZUu$0tD%1?!avN6Us*^zM46iHe$rQuP|~h*^!689ie(q1 zykN@@n;g>76K8!xc>q?ZAkEK<7-Pr2rXQWmpka7elCMkoDrCO{&GCVpg@vk{l!%Jb z*du~~$5l~TIXfrkYCNfLV8CKSGfz=XNhu3gCiPAW5)U&F1#*1 z5^kF`pK>}9?2TzHYWZ2;Z2!gPq)Ca$666AP`%5p~D9neRTi{;c2#h4`bfK4FjkB1z zF_sol?lT1fnaJ=o;0^-x5HcdRAuf*GIE-KMtJ*eIS>K-Fp-vr3AJ z?HwIi+1VIqXxrP{*6fKR8KO)c-y0hntri=^g=CYtIqWt+Fg4fLbJgn#N+39HU;re8)uC8vO-l_R&(QRPxek;Z6@$v3xW@ct%V1vR8)FeH8nL9tRkg~78VCtk})VKD7JQXEX>TVm-`&<*T=8kz*5A> zfk1YE0PsCt6H`+c)YZ8Tr+y{yI7bGxCUH5fuB}-u)G29cS?cMvZrHkTYkmFt)!cml zV6J*^cbA5icFE&P)5yq3L-F}?lYa1j-d?D`0yGgFq^GBMxKQ7PvW}DD$=|lB_$n*Wylz5@9yrdawj+1ot&Gizlei>|9*2QB_%pqLRlsx238Z}*A;VL}6iM?@eI@a}JI zUA4dwIJ-VK6nRI-Dx#VYJTB$hfpSu-rkm`d=VsyHP|5%Kb9!rQ>)q86z?jC7#A>T0 ztulS_48zgsX%)Jddd~+}Ky@~D02P#yk|If$&k#Yt=T<3I)YD7XqbHM&AFp>hN>5KO zZhpK4{wdJhbrExiQc+cA)(RIPx|7|Zn3d4f^qS3C%JGPx}zpwdJ#o7|;)t>1gym@)3$Wc+_L#aj-ZY@%Yjq=fI zUJvwmYLXkI%$uP2(&+JVW5{>9F5ac8n8--?iwQ6c42)1_R+fR0Q4C-|Q&O}+AXIE@ zQ(fJa)z#Bg7JVI^RA<^KiG;z_?Ck8cG+iU3sSHshJiMgB!a}nt$i&1%R%Yh?`IzkD z;$kmTPG;s8`VR;Qa7g%tiGKk>?P#c~tULfD5V)tlOl1}p7A~%OK=1%=p@zv(Ftf5I zb2-r=KU}Yc29Dswe@SXx>d8ake=c4&J&M8z!v+u7Njou8k_$72Tw2?zmthl@zg z#1xTNx7HOJip8`!z_!#*PV;=v74({#n%?}MKfd+6J7C-J)!Y7jb92MW%BrkfM%L#2 z2Ck_!Sl(*u6kj6;j!nOqz+g0qPyfmGiHvA890^TPNxs=XTETiEVyn(JYdN1fMMYUk zjdPth%70sg$RbG?$x8k!BA%A)Cqz`_MqN?mhh(cZ*(A;mA$p9U%5PmE7IRgA9)|9S ztAoLS7&aNvmY57J0>T*^iz&?Lz81_DPiJ6e=5~L5^5@SVbZTX2SlIW_uj%QFMHrM? zdV6~Z2M>#?7j12A6_O#v#a4hG8yT_b{QVAisTbk9KY=@&1cb23ZflUh^LoVxiB~5( zk=-V}rY6=u_6a2{Cub`FmHhH>;qIV%Nlirs4WqYHpOuMeuGy=Z|FcS|-Y-4P*WRBg zD2xGJ>8Fr$aB$e!*{SJ%MmwDKKYqNnva&+-Yg@5K)7C=|4-da~ZxSQXGh9Bq0qh#R zQQQ2&0xAkh>;zyaY}R8#g3G?hYYlFlMO90G{z!3|o|>w&*x&;AT*7qV$YdYv*m13_ zj(+_35gQwu?N$`q-Jg*QZ~>8`_+c=KD}qcKB|r%95Z@-(9|?OE^fwn8GmCIG%o1k!U5VM3KNq*3w$u3QY}^L z70sU^?k@b1H>Xsf%%GIN_;}I&yI)8sN_tRpqjMs=rLwGnH-iceyuoZ{DLCsYBm^F0 z!KA(}4l4W9xj9rl0WQ`uc(%~zJ7a$S_V)IGacGxnaJz!6{;3YL?V1J5CEy!o=H~8Z zE6w^Nk%>hr>}3l9py30D1Q_N}Jg&=!t0gA3NfdciRn-iRt$_r+Y4WL^)>GXbh7MvqB!8X6jV2Zsd9SYKb*h|Yuk{avKI zDpq7HZ0xcb^NucP-t8A={feRn+eQ{@)GCDG0>*i0>#Rf1Mo({*zWIKXd7{0ulZ>1^ zs6($*#oWXMn0sqmH8Vb50URzporsdsT$v(n(_IQ4&WVPYec_&OL@v~%k~@TS z(H{Ery@BFX1R>q41F)pjYHVitDOLppe4aGw38QN&=*58ZJUu&0sh5zDlne?ALYKcx za$3CEFCU6eVq#-+8c7#!YHHe>ESjq{14~Ku2jj459e<~&nxCK77(p}#e6haF2G(q@ z$v0a`)Mx(iM|l7$s7KoWhB#n*vPiwr^#YsO_~Til&H45SaOc6%7pJFc6~@SCuB+_< zY*vfzlhMha&o>Wu zu+Y+?+wPAnG+wOD_w5^C3<+XlV#rBIoPmkIeq%Bak(2w)favMz4QZ>YssJqd2*_zX zZA#oTc3=*5$c?VLY!5)?<>i&qjEjwRc6R=9c6Q^wnLGj55Jo;YW~x86!IOZA9~mh% zX>$WTvvE?-%9JDP8p=ck!Lo%%~9rbq)FVvrw9_n#%3N5LM+a~rYx;6>DO?v zrcIk~rlb$*FEnZIvge4V!;fiHS~6F^bXPB8Pmq+9L`O~Gb{Vzz-CAB=)-g9PEGil> zEMIcj!rQo$$C)Yp1~=B;Ygo?1%{^^?34s2e0xLndA3LI{j}Xu9@n?qKI#0gDaLoN` zrKP38zGoH&kAOttU}HBWTJ#MK8BIn-M64YiLSiAt0UpQ8-dR~2cJ6L$EG(7P)mG;V zfC{Ywp+&FHm71z5C3a)-Esy&`okIa&K5A_@`mm5{XNm>}2Q3xBL#C(eJ;WlvWBI&1 zhmv{jF3k2C;{b{*^O7$vE~u%f0AU`Wn|ReynH|6Cndt@hCjjPGcKG&Nbe{Aw6wc;Q zs^sB~vGZcea}mDTWT`2-VRgv(kp)q@I(R1~0kB^~VP|*aF_J9-_B`IOZoRNXWa+1R zMCrK~>rXr;#-sU|y{&nyAeja1i5 znE+7Fd@h@nBpRDPM5VM9Ksf$`$vkruHB_^#bJJSI+cODKQ4UX!4?s+ZEDwOEbeI~z z$6edMM3cXgdE9tG!>;$&rFwrO5dnaTkB4_~a9{zTP7{-x<(4-aIkhUPsyo};=le4e zCN*wh(d00AczAAZcYt)WVw2VcCt;w!kznBDtgou5l-}D_1!R()i;K_sIQ5|AVzJR} zz;NpR{(f{+0e~OTAv)zVN{xb4=1)|05fVcBros`tIkK#y=}F86SjZ!x+6sXB6D7P_ zuM~?`qwb?noE-RmNs~4zK$PBO6)}~5J1%i5|7&PJeQgODwN9$YxF5G{S8|k!DBYA2 zeUoR2?Aa(ueeUn;Q?D|g*%`}eYi;$s-67vxcn%hZe(x9u7VOQ}+C76#hdt%7(vaW3 zf5(0b3kxHc0X*`mkg~EewL#O}VZ9XGcL1{h<~o_(hKP#F6tE(J?IFw0nJuW^+lxH_ ziH+at2mqoel`{`uqvPXaU<;>n9|Zs^8V4KFxF@0EFplRgd3KADX1k6e&Rp z05=~dHXc11Iphtv=FH4YfOosw+nCtcj2_2X`3 zcktT#`%eG-D6+ZZH%_t|<;YP5Cr)IVpj076b-^nLGP1!^HodQWtN6@9KFdVpDK)JF z`H8_yqN<+SstDLKY@sOLF6J?sygK~;*1CQ?$ul7Jx8ynKt-(TOc|FChx*FfF-oV*1 z?_hlYUX8>Q_tSR~nqy;QK0ZFOiR>*E;wot@n?C$CrFvgb;? zy<5r*`qXA)qqEuS)1xuq0G520>1uD+2T)00480Mog{fn=Dge-!j0UaQaRE0jDcKj| z#yjvEiz#*>mf2*qGZ+UwIC^k!5U`H;Y?ip?2oMOQpA`T+fN$ID4kPYY01y@+0R02G z006*6G+O@aZDGYqlHLERy~uC!{f@#l!}a;m6gRHNlHROVfpS*Zosn0 zR1#lW5(?Y9WRp|BfH~=YuKrF|yrGX0p zaw61Vi22SekR}mSPzbN8y3CeNh&2WR;Tgb_Jpni>(8b)`+?jh32wvlqm-6%Tf%rRx z*CYO23ZPnmQkIsK)VN;c^M9tGrp95=6XNIR&p-!4ax1___4M@g7Zm}KW1BC+H_x*0 ziQ$=YWMpI|B_$w|9;q-HLxbmazqX#pS8{Q20kGJ+ckgr^Er6(d3ZgkSIf)4TnY?em zKV2&5hd41gIS2?C@W*w{vw&>30>C&uBV(#aeTr_uCJkN(*6#B1(sGuNgN$qw&_nuZ z>DymE1=V?ldsbXe-QT{VHm4>(>{#HNn4lsb+$MUyw=74n`)XHV2sJxll>n@YOXU zkTk))S+Dxe!5m?(ys|_H`E#!1dPf+ZTiwqG;bq{l8thcp%z40p9pBIF32Bhk!J#TN z_~q62sW$kfjp;@>+W8T}gvpRR^p|cZt)*oNp^PoLWv$UxibwfM%2`P_r0f!AT=V~yzg8>G&Q_k9XrpHA-@{b58Ga$FPRiOtTJHlQ$A5X?&ox9fum5|l zaI!YweCKpieBJ%!E#zg3;hP_OW>MRJbpInT^$Z4Vj}H6aeaac^=KhyxmfKklyrg)U z10`n=f6nQUDlFl(kXrENxsU(TcS!5Mm#L%7@-R@xr+89y+IIQuPvW%stE5Z^*-efO zEB54?iqG7BPDxi74&<$7q&Rxjlfx^GI6`zujSQXKS_B{u#1{`%gfAr9(`{3IfGXyH zj8My*1nV8+&@kISpZ|Yy-283=VBPPV3*AN0k&`GR^;K%Q|6~N9f8w+xx094&h+m!d zUhv!a3mo`c@CzU__`x%$*K>h542zV$+{WJ7S=0xv(@;VMc z*i!>WszQ2$b{JXDnV|F{AL0dcbny5?temeau<&;XoaQg}C^r}F<4b~}9Eo1BxmKIY z+oMlNf?4(k+9`DJZnSs023@%tgJ(0i%|fxXE@6cXRMWSoE5{g5LlWQGk9&@czE?oR zU*x8y`_^UkI=QF3QyH=(J9VGz=EwNU%%?R-^Z?oYucr%GSA%XVMvaKbj`oBR6g0Wj zKSG%}a$8D`a$GPEV>md_e7C+Gy;>U@JGJW;tt!Q0)yInQ?i!udBB}NbXwMHhlmKl~ ze_atynz?=B13>&+Nb#>;zAD+2X6inh%hA-1Nj1ed9Ix6El!;L#;E=SO3Yd3B9cA*j z)S@^QpuDOE**7^VD9ASm6Zrd1G~9H=!(nrXS-~oM%vU;O!y|ocu<+Zo zSFy*=*eT=nyz^j?v$hC4go=I&2FYXm>MszzVY&=Uors)cbkXvNEFFXljziWS zzSQPS#$8a5D4*llV9cv*CfzHsa!T>EzgC>7{Zbr63!u;~rz5Y5lRlc&w@TE*=CuZ&TN`wbe6dx{+SB|73$Gg24 zYSHU&%57wD5&hPf56d^LL?)`rod|ooDanY$`?(SQ$~n|(9OF^_n2uBsk;B&Ypr44V zTgn7jyA~A^ttk^Ya0lnVl?@)UbYL-p;Lc5%4VWHmICN`C1X zmJ_Azt-jNq8U3ATsxb?&cGw`bqY`Gvrv7oK`TU#Hn_vth${T~3pY#yf_YWK+U&;6S zcXv|U1sF=tmP~aIf1DKaNjWF+ZqLX@cfLPbqkFE^1{+pSg?GES()HKnS#pim?_lhwRLSXIJNyfsq! zYk46#=%qWEjl2cWqOqccL9Y`puU|zD%^dP{&|%_YgX3nuL^hM1OyN`WO{;`k@eEEh z5<_3T{g57}{SCkrB5o3k$kj;luqp-H7~5;DFezs*IRrLlBnh3N1H9^Z!Bd@;6z zoq?^Jqt2f3Ds>M;h!8;~o(!VGn@XH!T2S`!*WhONJ(5P$a-%Tl9rbODWD2!dbn~lb zacOfFB(lX4aMIiS^%S|iwH;n|MXl>ybd;96ab%2&T+1-GvZ_EsM8w1e{!|^~5L!@c zl9?*@cZfCmV)?*uEQ9?KIgEoeYur`d;+{C(R0}O7`;b@AB+TGK^OxP4txG0YWhtuK zKQ}%m9Zc~h4pimYB+|AKBKp~ zicvEXhKDH599LD5y^Vsh?Ai#TyOL{J<^qf2zz^01)B_57Y6^L-3~A(|5EuJaM6z7p zKqED6={&63C9d>NPT3}i=X*ILeZ(2~wAMik$Dxqw@gVig*T$(iH+|_y@s|`s{6_ke zr-4xbQJb(-$p`;HweATrqVUZ2oR2rB^JwJWX$AFgNvI1qe%&KxH(bbOK4hF7$Jy?? zTw6;TS>GvcdwtHL+>sZf7d#*Kmh4JnmiAqFPE@dJr7la9>@hI6TZsc}Tto$Wv%*M} zA_asb6WJvrvmYbC0!|8^1)1JsISm;^IeEz1I_+N17L<;*JhpdYJqzswPQkq@o3EPl z5dfk&9L6UBwh5}2;_mMc;udmsL7;AC2gGr;%3`XBTwUv;q)QYIAO*O?yt%J!%R56tcE4w@;Fu1m7xYS~kTG zQ7#CH9FasN>h5F_RLwrh!>r(gl~(mL(D$Z1bQR_QmZp0Jn$8}vf{rkE< zN(!HAJ+1E`udif9E(!JKF2}OJ-RW6Ii9K>Qn0Y1B*s<4KrFqQx&>NwPK?u;Gv)1N8 zv^*bJWcv4b?Q$2RYet9CgJ#Q(5d>rC9j!3GrbnIq08s}_SA|Rp35=k3M9JMuDIp^d zH6h6(g!xuoaDk%zwC^q8Lk~r8e}lOnCZexQbaejO(^o5l%n01@<@Gf5qTMUV>!QNN z^am=f#_MA&hAPQWGzW)S8JHFCna0V=HbThj+@S3UAY4iP6Bx-Yp>H+Rn^^V}l>iM-O@AudENT}TwL=W{1V z)DA-DuXge5YzHd*?l~_$I*pZ&x0VNliXVKc;1y2)NGxD2GaG?l(n*u>P?hghT$YSU z=l*Re8jlZ>Or1w!ajAZoVs1SA(ddioLg~fVf+-p6o+4)J>*E4J$))PgM7HM$DIP33 z1I;|sYfWlfY|Mx1L`1u6T=(^b8+-1eqNPjPWtJ^mHecEA6o)SJM0li`Sre&UR8-2H z>TIqo56=5Sd@mx4+1{ewtZ2i!wbN|5(x;qec(uPyX%KD##kR!Bn9R{%oG}vCVRN7S zNjoBOW9pUUCL$`UtoG=n?)bA+Kdyy&KG9z56zxTWlStX7-y4E-fHO4XWxDKd{n+Hf z8_vHZerXt`8nDG^-mJ1_VA6fZJbbBmJllf`!nl#yk#$pW}#a>~Z{0XpM<18yeFXr{rupVL7B5{sPUCIOiRDvVAcP z)v&kMyjQN2l(b9?o|`FR>5%9@vm#DX|F!a|Z$8FXXg4Nw92{2d zjA1kT4@OP)Bhe%x=I4X(Td_9~&7%;FQ)e~gt{VIwn%cLS21z3QbcgezUEhiDOLMG4^LYBT3(A;q}TDKE=h zF@gOZ^&tq>$vqz*)>%0`3{PEeKsIx}wWuuuH3w5Yo@LBIBUv@P5-)H&pKGcT%G}7S z9#ptP_azrdSjJ9#Ul!ns%pw3mXCQT1=%D)dm!{E+Ccp7%T=TD+jBt`Jq zD;UfAVdk1_6R3R0ngT?8z;YCO*KQhSeUE{Qwm*6AZKJs>QNrrd)(WG>wo5*9Ha1|0 zzETWHC{mtgUTbSw2zHpL9}ig=5F;7IH}30Xu>2HyYIIy4d^K^1W%08YSPyEO9nU?~ ztcb+rXb`#{hB|%+y>QBH6>R4qXFEqTu$w(i~$6if)T})WC&}Yh_r}|w`qPwabsn>=Eic>*alpEgm25T<|0|N*0f2+J5_kipxxe!{#ec9+}IRo>G;p1mZM;&HpL|S^3ka$$G+*h ze|+wQf|AQxrs6HV%(NGy#|T^Re0UWU?Gl^*_;RmM{J(xshsB*?TFHWlY{sK;$9euu zs(9tP_bf+1%qp?Bi~&aI^*a9CtSB4mpPO6wh+yJkqFT#}97eL&luRgM_}|FQJRUlV z-R{#qe3EujZi<)(gB|XXMx-DLzKiu@whh!^*bpq1;o~gk2W*}F)hQ`)lemCV!rgzA zv{a7qTMJvi}W-Hp{)Z{xb~ zDmz!Z7*#V=4b7fVn^zN;bnh_1OSdom7d!^7MhS&Pp|hm*iokg*OwbQH5>eWZ8h|Kz!q3<1@ezYLVawq-FOahW z^7y9Kv=r)Scml^(dGKzZca7L@VBNnPE;;%%#wsk*_$g`a zk%CV`)Q`49yr7zEn0kieJ*oi+|EX2 zjUfn*b0pL&Fe$dFB^vE6XL`q*-1cAPC4bBmAQIWh1kUYNpbzDlnEKQ8&D=lhw8erz z_VWfv_UYUH@UmDe0QZP_H>PlHvD+@|EG3!yp>%r z!o@?2*xP$6f0~sgB>JR&Bm7SeGM+R4-|hXMM*V-uOd#=@TJQ($@ZA7j9nD;9%E7V2 zVSdeO`YfV4*kxppw)RJXK~i;fQi zd)iD6ncWb0ffP9BCuzl3$P?q^Vp*egb)1UAIT_j6oqDCr%*=|GeT-*~f(1W!cMC*z z*vt1AizQ&1xy5@6d2aJcefYE`b~VNDAriR0{4->@?-w$wjI(2x2tK@O;+e^RhVry$PQtV0!&V z%vpGqrKzP88Xx(NqnCA#_B)-gbE#4e6)5zS!`quMfD*oT{*s;Xz*$dl?xad{rrZDd1$qwfanRpGJ zxYc;hL~cR~l<>*~?8bQ1UH7p@iX!kud(a7k<2fDBPC|2*3lFz@2T?%FUxWws+=927 zZi9QpKcZOelHk*F#E-7ZevtRxD#OnmmxYS?@y73_78+VKF?_jw7aKVCr|MOow@^?m zO268Ws9yOM^DGojpT%y5j5Pa=3(zXKK{zzZ=^&-3b(OmA01RniHqs!i2Nk>y;IJE%wMsJnJpoFI&?|YgQYO~OWL|OQDEf21MdT|8R%i^OxsG2!ljv_QClCE)6p9DiiiiPbQJ744~ zGe$CYSRGwc=CSRAedN@l6o(q3RUSy*VU(CCU!q=KQixC6OF#(y$}}Y0*RB~nJH<)< zRg4=fnLcR;-!^LI$4>Rf`E{5|;Wi8QY6QC!4MtFh=<8pSO}b5=BxG*tX-h*l*pIvhet03OGTq2A=+XJFoProCJH9UkC;3BTmQE-ZJ~G8V!b@GXhw~n4=V# z(7Oqt{-T$Q5U4Ub=_r~kJK7sVb1)uSn%+nFS*A_rKMl!nru}Iu!7=y=w#Hg%i&Iwm zgJD(I2u6&%KO34T8K*q|L#rbb&nmQ)2?z>_#1ciChy>PAIe~A;WtRx>!5p^ z`l@Pl%*=3!uj6kV&2~0$__0)rtBfe;sD)@SL`Rr697o!h8Ghu21O}6@pwEcFdJ;W( z^HZjYL*C+abtg{ye^gy2C%SZ|r<3o^(pj9$JLOEI`Yikx^qB4yIdpG3Qd1h_PvIP2 zDBbIq-VU!sUM->L^&$GIX0s0ZE&7CTCVP_xm=6Fx1FJ(<-oK%MYXjC*08!rxTqB66IH{7*R(b=ZC@R z2nOF0CKcKnofpQ3UFt`Hgzs5a5hk$am*FT(&NeVj>Jz|_I{BtWv0Enfvs@;Z4scFX z(J;0S!O@CMU zb~CWujGMGQY`M=jDlUQ)_uGR*8~ur@9*QqBdyRL>M;7iH%9tNJxGX9cd3k{?wrD;~ zdC`EC;Xp}VgPxM*jAvX7l{7E@6NI+hsVac>m*gND1Q3|94BK}K&Cy;^Q`%;x8o@ekgM)CFzMWyvmv=NbT9;4+48< zn1X=-;0N_zv$W!9C$^n@kT5E2e2e^Cr!Aqdd46`OhhKd6H?xp^)g=8oJagy1g5fLU&!6pgbt9qh z(UT2!stBSKhU#_{#}SjwkvSy3$JifMc`v5ovNsyiBQkqrzl0ZlAaXfU7Ue6Q4z<;L;JFvCXe+QF z4|m60jI>51*XYnw@3rBzXzV6U-+Z~)xFPT8;@9ZUHXr>b@YNE}Mi{KBeM6u+X9m=LK_ zYiya+!KM`t3wHj6s>MJu6U&rq**s2lbd^d-@SDCyjk-q#yd&B->6U|zXiv@gJPRyI z1KK-I?UF6rs(iXw9klx9W^Un5X_1MyzFvnjP3H9>t1zb~CfI>GU6dZhRHHiIztnD0*kp>I(-R>Ej{Q(0aq;cA{@2utsS< zB|qMJ&~yP7iOABz6OH+6cW;G8jIz=0S^g6`qDF{>1D&q^*Tm?o7Q-1xHgnwi<^#Z$ z-02DAzbD9jT$j}q^hFvl!Mt#`df!0mHSZOxQJ|m{(bP0vasC|SXIcqtO@gB)TBB;O z)E|@fID~E-nnyq{O6ByaycX?)31#ub&+n2d#nipGlOEZHH$eQ3dtzUpjNI2qk8JY6Q<9jssB=NHZxdY^Bx5f0Hk4eC?IwEP zj(MVQXoQ~X`&!#OGNQ#viWW@WXzn?g1qn-h+(*oojz1VFN!|F(YeYBoHthW=vU|VD z0~WeJp}gZ+e|X*Xmd;lS{ZB$+h7P}ujz>?EH!gvU*cxMsO>pu1x%lggMuNSB`dUvS z?`A*uy@09K44N58Mz=+PM`VqOPi}(~wW!vqGm&U>kow-ndUsaM=^d#0Pma}@4>t-`iE9u39^P5or zjt~j1#rviTTXg1I+~s#dTKY5dN~wC!mi_i6amedtmRdZue|~yuoA^E{9%6Zxq$&9? zTH$*MQj)Oi-Z$HaXWZI-s5eL@`WLv&LIYpi!wag@QaUv^D`84Sr(AEq2y0^XGNbgP z9JlBNj)pzVM*r1($Qc;hSVZlW2L6GI(mp71B)xdz+%f0KSDG`pEE0Iwd=#h;uVB)cQ=y2)@vBF8O4 z(jH~`oaWZ`zjO`j;DO>Bcy7Fi5r(a>UI_oFQSWQ&c7%PVae<>pG#V1ufBP`7TN&Cr zX_Z^gtRNaoiPv6`@dg`97${PYnV<(1!O5zTjNcgs%nm;(yFaCxie-0Ze@>FfLNOb& ze>$TNE_*jRfJD`_DCcAsGHFRDX`>_aq;R)$guW+eM(RFE7qn=<~9LabdM0J!c*Z4QXu(SVtRagaFvFR!IEhtx&O16@qnC?iI z7=N43e=|V4vkvqc?5fXA}h}+=#o$0}w zdmu>CqePxR!bB#m8ez*a_%9zOr;C}GcVIh?kBm_DSv)*Gavbr3iJinNM>Y$4N3rps z7>jN^o~7b2^bNCqv0{SC6D}!b=_UkJkugcRKvZxciydZXfi#n%Kn#y30Ro+qO&Xv6 zcsF!GIF=SIEu>gL=*5`3ZfJ+oiIxu1?rf8iwuQ8w{7X?mPeGJH;PeSe7ZyQt|0H!$ zp%iWkkEz@iTp#DkLfHu`PkwN&W2q=Ym`LcmOg_cBfo6ePaS+j+*8~pYELw50^f@dt zasfq5clO}-+@JpbruyZ1QRv78775)|CYEjCD(ucLJQ~c7sFw;{%sP1w#zS`eEAD1_mdI5q( zI!Z;WC*EEspzHI&q^p1h*+d9`I1H_{?3Wrt{SMC&mBo!WhX=o?LVKOtcLz10qm$W5 z{@GSv7TFvK&d7jb(&+ode@9R49bTQy|4GUDPKX9^#uZ$QSmZvI8|^QavzqagA4NMd zKPoF|`qj!AOdl=$b!U!A_1)KYb$ot74V_$Kgrn>mZ? zok-1YjA$@&EU#|@^AJxRmGeF3RO}W$D(4|0@F;W4q*=(Wn3%N18<1>R4A1u%!9bhP zG+scjF});NzUM2rX~>=5?iYDCX_b%l_4O$kQyjC;0F|W%WklP*lHEMIjGu zrZOU186)V|bEOlok+p}ilH%Q@+ahnGk%|y5UL>uDaAoStk4?PG|0SRJpTFY1jDGp? zXoW&x;J@DiLq#D#4D#-p1Ddpa>;CJS_yBR0VKN*Zq3!-xz6O;V-x4Jjv(|Vn$XNl3 z^n3qY2?;dx4-hFw&3@@c3{r?t(m#9s{XHx={k~Zt5YEocBxCTu?Js_q-CX4UDHwdU zh^wi{@o##rhV?-GM_2!B`g7(E(6@g>m$6J7@2LXmg~>T+wX%)PX`5;e`h4^{<`Q?* zG3v2a#s1CxTksy@+lE1Fc3eHTm1ddi@~_(%)qe3`&J!wX(uNjd(2-@ZG4#bt9xLR( z!!;Bxn~t0DkA!|*>g4TX+)rz&nW&7?NkMiyB{b?42feUt4 zvbvqc=|9y%!yH-J&Hf#(1$~!?o3@VUwyg=fUt2iFQ(6aWTYRp%st9<9uYW9oMS~Tff!$A!< z-cXkUvbDd7aS5wIkr)~-hh)a+j_Ahpuv+Ogofe^gKbv~ob z{x~~~U)q#}M6Q?wO?r#{aZFo7PPBg|!qT=fAvX2lOeCgTfkWtz|eR{CY__#^H zJ4RfWWMr*;-gSU2q6%XqQ zC@8H|u4>NKPqOfoNV}Itu^4xG8Y`z5Gc7xNK4l5ISFpLtQbzK7)s#=pelZO?CUGgM}^f7Hs3>{kh-^bjvT zia+ApozoG42R9EuQZ~uu+dJ3QXA+?84?2QQVMVH(+NOs7$=Z;n^nJQ&{7u#?p#$AQCR;;i@kSr~VhhWu|4 zTx99jt{=~;&w3?#@v{j~7tDESxf;wI?(V(MstGT_Ppj4MTT#cMRR#Fm(P8`u^VUzrMA;yIihe=H7eGJ@=e__I~!W z_dY&kv9a9h78@rfa^T8;%fNhN`mLp>vD9KB{}MHoo0f*EyPAPBMou8?05SW+8%uU* zaLcPq;qL6Z#dcY$-rYjM1eH3j5gEwYwrr0mP*1IRxniyz29eori%p82DylCa6Okw(ex7@FA z1!3;?aGrszU_2vJ~lbk4eExAA0A{z&&+v0>yOh?2Pq?_Y9?G(P3Jtm6M%pa z^@8^xS46otJCq%o9zKfSk8n!r92y*yhqR-r-!&OWUfRbeH_xKz(CKmkdNI(asH{v+ z-~1vlAfB5_QcPmHM2pV-95P)#=hQg#>?WbsCD@G?FjK0W(PAT#_@1=t2L8~l%lF^J39BWDuj`b{!ku@3rOO=$EBzirc6t56l zr8gY0P&gIKkpLn``_`C;Yq<7o$k|S(Co;F-=vS-s1%^XMm3U~r0d8spM@QmPdCh*f6)98S`wHy<)33g}1d_w!C86Uu_`b@z|A|OZJsl zzyAzr@;6GGGw)M5^~}smV|@Vmii(raPkc1-86RJTfG=r?_kGJgRq!T(%|~(q!cdtJ9&;18yPkL2eg0wZ!i0**4z+R|(bmtO6d~YsN zkZ7XVMcCXN7FFqa3elP2wAJnGsjRG=D*quIcV3>kuAb5s=pM^?eO8!4tLD^v!E)E& zMa;o9FRl;G9XDtHN%Gh!smd!4nh$X&>w_sVY%mOv6eG}=l7hlz@S79J)pNAFrDrgA zwd||IRc=8=OZ(IH_&6>gpn8z`&o!ja(@C%rF*^gys|S0D;|>d5OQkKxXkE} zHk+$yO%V*xtiqj|6(?h-aXU=vz({w%~ zK}ks^fz#>AT|BZtceP|B%oDMOIYYBKD1Id)-hS3?b-Fi)<8p%aOpvS04EuC>$-?KQ}B3jSaKCB91zR&mEzkl9Np7+p}R{NGEVEEKhIF-_$;K*ps?8 z(&s8i5m=tBd_J|jJL5`HR8(YdGKU7glR}9oLU~m*TK^1bjxk z7%vLfuu!ysM|q!xZDX(J)+8B8)|1b1g6TF(HfBTCm-%9`Sr)vctkCv}{sa^vPU8{> zkc3BEOzfsQ`yDg$Z3l?rOPeW33jLB}BV!=v>Xe+gKwH7bT4EGrtwlL65J>8uO7MC(NHW~#j*F3HHE zy(51+?tqZE#(D)+yeaw25+nNCRO1_qb#6Z+L&lv2_R z&9P1Er^_v#$vwmi_w7(eU#4IcEN?1a$A8rWR@wCQBI-ul_2tcbCdPx!Q_hj~`EvaDko}ADH1O#dv?spM|p$&!^F~`QL z@Hw#=B_1apeYCO)fx(FCnC@)h`QGY?^VeIm%@CIS(!}Kurh3y4G;V6_ERSF;vh4yy zqjpB50her?w@bTTc>Ri~F?`*hev$<%*7L4MUbxW+kHZH7jtkC z1+#5F7gf+-)!z`ZvGHd$n_?*~3GlBDbKFtB42tu~%-wE0HbDY1C_O!}tBYtPKYwL1 zJh-s*I;EhwE&t@;on6r9#(Rfyx_>r&iVh`Z4>9ZOJtLWqCp*lNrI z8Ez|!vTyc+If58jFcN_YSsve79%KrvBhU?LjUfH)WR&;My?`E0GtnNdih4Bvk zffUD*J2j`(k6Nwc9Ye(92iNUrIm&P1a4nNpP~R-glysw)-^{zG=gf7Xou>Wzy8Mwh zZrH($CqZy^cTVEi%HqnFMo-Z1Q{&J>E3wkD#H^_Si`v$*2dv34(ksrr6Bg=HzXV?ml9aIYmzkYR`rrTJv71PWHgwHAZsqe+< zQP+xiWos0@n$P)BaT2y4Qr_S-Q**SuQ4qqqle%{%2 zoJOOFkw2hiwO|Gs=Qpob5LKtc-nCMbQW^ck^REW(Y3085Os2_vJEp zBoK`fhxOK66|Y=RX8)V!Oj_Npmw==VE9L0-CpzwOY3|{xW&f^Ul2WM$?`D;xZ$8is+i*KfrKQ@1e zk5BZ(1Tr4CZvSqa7Cv11>PA@g&L;W_G5OY%EjSj&KELnq3ii$euzuv@}>{nb$T|Q zGUp-!y%|cjARsVel0GseGoUrXmX`sW!;X#py|~NMdvK7acQ7&Tagr{O{_v^&EY_>M zj3l;?1l$X(gYn;fFapfwk#2b=wFAlc99wUNR~9tfUxSE)*7&9l0I zG8HtPW*Hl?tds5bjy;1#fs{264fo4Y_?&`USDlkT$IWvZOz`_FPS@pu1yU>M-C9?# zWM`v!ug-e%~j!6C1QK)n^W~j{ymksKDQ4(~0rn?ajbTc?k5s4*q zY!1J#8(lPtT@+ne{U-s`EEVeSSD0Cb5UH2mF)`olB!C}M!Mt{_tG(%d39=C44lH!0 zLd?w1VHcC($%cB?R2-AMD7?qWlD@zEcMFBR9w^+F9pZ(nKL zA!RNV?!s=hvlDL0%S^VGH(_Ch;b(bo>&m6qgQZOCGWj8QDMElARumQc})issSz z_&hr-_K`sLltw*@Rvr&+m}Sr%`Ql-VO;27Eo5`F3l9ovk-L8T3MRA<&0VsDvmuZrX zFATMqr=iKVqPr0As8zv#?g2y~!^?FQ5X{fOLrj?AMva}f@=iaiDl;?jO2Y^jD{ijn zCOhS{ye{R!sC#Tr%#|QNonRI#TwN}VsMJk7%-*E`L@3&f-Tvf#&t$!uyS9Ri2a|Dk z1UsqK=@`}%eMz&V6CH`dHW zmRY*Um>jPgckh4q@!76smA;uvowr3VdBtv$?Bc2Cx8qH>4}DeR=DSA^B(vu@+#*#c ztze>Ltt83;^5|)QSeEG2fYrcqUq!_Jp|hXMX>M!oA3*vmJXPI%_UU`djC_bey`cP? z@JDZhQ4SIBTP4q*VPRtX{&-^HR^OVo?sC`luQ*95YJzWM|7XBSefpnKW``&1xd8Q& z)B1Co!}I^0GC%XZ;g4MhnBo5mGq1^n-}1mvL9QR;=zaKy*8@UlcX7Aiu%NJ{6CE$q z46JdJ6eq#rvAsEoL_^_L{WEUBT0wZ2XgOyOJKmz9u*ivym%UB-`MUDBoU zrHvbqroD0W8rYc%I7=r$22#?o=pCLewL1snZSjZ^xu27%RL3OL8#YekI7-t~Q7P5g zB5}Pqu>j``%YJEUX6+zBtv@VQucQ50>uplkYwbCW@3LYn%4Xqsyt}ZuJwIA4Jk*On zTI&KSm-pmRC~)ZCRbo!J+Cxk$M0-a6GM_!OUVgXxuHrg~h($F#60)KMnRW4uQ((8+ zeP5~J$`Z?d_jq3+ne!?I&6}bO2lnl z+fbKt$thgBzb^kI7o>mKnJf(sr)cTIPy~E^Hih^CgWIG){IQ(F!|Yu9%dxz?soQ{X zid+;WV$6yPYuDelh_*K5t>6Ss7glmsQqu3O4V9G~(AtaQbaLq^1-*YZN2|U0iSqL^ z+6i?(&o7R?+0t1-(%0AbCPk4ylYDQ|R+8Az8m$%=6MA<8h4}rY5hMSH*v4(iwCQ<8 zH1>Pb>gVw3+NLI+OVrjb43ZmO{i&Xy>qUH~Z3GyGZyGrHY8V0a$q!s#dzctC!733B zWMRom_h&zDYO|nCOt4Up@M^RdOKTh<-K+86U0u`gt>=c0nLhmVN#W26Zq7}#SIcLW zBr=kw;&4)28c6qi6!Y-R%g=~}$t_|%+JEy1CZIeysl0CgRT&9&YJV>)w7w^nb<9Q1ar}t_ zWpI6ub!QW6t`3<|78O_v20D+Y`yCL72`|;Ii34}t?gtMe7}S1+hnKo;{8Hm8le%%c zg6*hOMu54EP50Y{jW2;GH`b39(-#;o)K`oyd+{?Wtl|c}zgIW6!BjS<*YLm2i@P>b zWfqkPk7NUH@_38C#`%=J!hLT%U&!Kx8szedrZ<b>BHQX(qxj)u5%TGh`X0Ep% z1SNLXXGo3lGDqQek6x+04z8#5=kBR3w_#xk4*h}%_!YMN79_;7E38T^`1yPks8Pd1 zL)4IA+RpCoQTXuE)_#uZ&udd%UDy1A&w2Dx;^Hw}muq;x9BIH+OWJB79hu-l!Qj%6 zn3$BDjP|vyk=VXaTZ9)s(cb1{88@dq&Ml|AIat)2+IA90PQkFaia8u7r^?5V9V(HZ zKYMOtW3&J4!P(0P5Hs(_TQ-}5@2s3RSRD|8pItC4p6X;}+)^7z58=@`7u90^^{_#& zdKt-?g0iu)v6!-P*I2KX%^Jfq?2T|-u9#%2RA}3D=Q}1eZEsZSO?H;VOxPi{E5O+UqynFAT5kU!(d z%*aT`;@Q2SR%@?gB_jhy!?S$su!4q#O!wH&XxJSEd8)`%aNh&!UNpc+!;Dz%6^KB0 zeSf%bqeosm_ZSSCa=AVhnx~f;?mNT%107+d5#T&cdi_;Qe6y2f78 zk~rVZ^amn}myr9%3KA;riIZo&OPzIgO$`uo#e+-Hyx3X(<>n~FU*7iOF@z?US#w{~-e@aOrLJjK7G9ej$JY0Ay z<2Y?peg6AG?pf^L?=5aOIzot5o@evPtsEp`G1Au!x1^UE)lsgrl;u_TQ6G*9xHfwX zE1;Okq>h6rvCt5nLXb1FIp&)<%1HkO+Da~vDoenFc3eBJVG;3jo)AOek8OD4zzoV3BpTBs=Tns>rs>5PnN!YE= zO9?#j@)Bwz&&s0eRbii&kT}}p`9v{yU>i{fhplti!`5R!?*pbMro+G%NbhVEVSi zLl+mddAOZfOG^%KjA_x7l$B48H~Xo7M@1E)Qi4}AeZN2_^X1!z4c=`%J&QxFzo`=r z*U4Dly{84BNN=Y)_N#_)huxvxUm`^AQ8c<_;VWxvHWnkG_d8tnce6g}d$m!D(05)! zevo2pOh7JK0nW=2b*VOx8%5}H%F0|U0-?^~>WiLho6sP_p+iY+&nl1~%55vCC+gF9 z)i5g;OA+MuzRhZHzTQ36n9l%} zdkBFkHy=h#RoWS@jU{zOn|y;5YCM{rV?1|_OEBoqd)bHrrWMHO1n{e+w#$NHFt&;L z5O-2iotkg=X4@O&*BN0NEekYx_S2NTn@^dST+ZJ?)=P|#z(8d690?&Ivy3tJa$`cgGBTh~RI%iwOWsy`9rJTL zA8|UI1ORqKNTJnEB-X)=@%G|Z&;FjvDxf3*Q32L8c(n4;e?sfxXXCHEU*2rR)NS4dd91}~nI)AbO9F@Az{{ zHIct!ThhNPWVSVpW@)~S)j)s^n7vSI4_B?to^P6=w#vxr>Q*5r_-ylp_Fj!>K9Br(L#N5v zJ{rr_)|T#PhiY#N$^jh3@oHP*MJ{(gMN6|S*tjeSwPe6Q7fwxzz5-14-)?tXNJs>K zzn!O)Ngjca90d1}boouZd}kCZKoSxVhejaJD0zBv+?_TC6?W!O`R9mkyE+iF+>yJ% z<^2bcbah2wf-%XTJTP43?XN=S?;+N6p<`tYfe({f4@#jL)#`u452UTkF)u*ivqg=Odv z|Bg@BZ2jsDw(sSGQ?g%6Yj1doF_e(w>Zx_3y{TkmvL+goZl5Eooqt+u%ADWN%*aS) zbwIqAn7}O{VPdQg2>)%L!0MifHQ3JkCWl|B>@78A>8p1pE0Y7^pG~Z_nJ-P6v$>lqJu8j3Qr+8jH zN*lhYrex*{fg!7HE+yx|J`uXzB{S~n0a>TH9rs@|Ga~@vT!A#hm+jG*%Whf^3C;se0=g|8OK~k!q2+Zs*hhMvzpC}KO-SK-GiJ>>zHd< zT`DLeuu)j{)F{z6{&{3NyU~I)cP15c&@(p&B&JSse>EqU#D%%*7x5fwzQh)Rg#497 ziL&19^s}4yj+%U|2X;O1xes)oKO=RAcq4*%uS;%K;`3CI&C;Shu9@_d3Kxz@+Irg{4IG=`P^n**A(!) zj28zB!=>V;Rk`e~4XhZ-S!XMYWx$@7(~FBFn9Qi*-h3h`0K5dUHqc%GJseIcR36{H zcW<@K&@|Jz?D>BGD?CHpT>Zw|rtHGGG+{Z43~?H++#lx5rJ%tdMu$kCE{&oOXAdQj zK>taCbeO~5Ub=Z$lpN+hmOqXinx?mKvx3SyLz``$;3M*%oljONSM*k?XhhmHSxFu5 z0MVoNEvc9sxr8m(I71qR$!g2thLLCODYDZQz8oRO)yy-&r^NO7Gm=|Fzmz}<|9rpOEIjHM`Q7P zkDPQ{cT@|tueV?4rPUEb{;8CeA{ZyDPL*4M%zSz+$%f`Es)f zF}=L?czIlPX|K+eZ4dY1!^r$o!s^+kYy3H@-E=$6BwBi77AB^m@^XsgmX?;IBU=(@ zk8!McU%etI74G*xcK+8Yq^zb=d0Spt1(b4235^^-M$@>la)3t&p>(o~T1haExNeInm znB*7h4Jxou@eUt_S@SSfXO;xWaVQBOmzHL|XWnc{>)N3{dAiGK#X^?znug36p46>7 zUR3#JWAm90I;QXAG=c0~E)d2tDVH63gg*Z(v5?v#aCUa5q z{d*vZdM1f!Yg1{dKU3RxVNOt#5sCFM&|$4V!yiivDlaP1mZ5R!(1w7*4AZ~zW5W?glw@3_3M<0xfuG!Cmh6|0|}tLTR3|8{OGX=c+l@XJ0!Cg=Z@ zSE1H@vX&#NGFRfnFlUsYj1eNP!QhW|d0p@Svj5rf#iSZqSrc2=$c{-Njo^&_)P$dR z8E>Iopv4GdXO~8Wg%r-SqHhMl&*{m`{8>L4AY0`+zLTy?5uUOdd7zQE(pcXg7H+Ik z5^~jm5fP6PTa}k7r=q!c@IBaDYi7fR-Z<~@&7URU%B~Jylt7vyZwYBfGA=g$@o9!* zYE3B?Y4)jN>>s|#{LCRaj)9mD*TzCo6|!Va*t;eVK^RASjO>G zrZS|bJ?SOE>Sd(9KGDbte6?I&cXfWiF<%I0Za>~k! z?&#)Ed4zB(Ox2nsqlVq6o%e$C&v&7@_C2!^U%US@yYJ;QTlVr=?K?W)JSDNVJP`4V zShMW6jgjSD86DU8P#!aQu)>(Pv!CBBtfb=SJw+5;?e}jwg?sr7mc1du6iyW-t^__q zPJtcANEpTXa(abi^#7u9+y|R$6{{@?(#bJ2?~xiY`=VH^B?oUd@K3iVpsrjw11;ql7aowA4RtvWuLDo-chEmXJXJhk~= zP}bbLNG_%LQDFI?*%-AY@9#)9IE~tfAQ=XPQ8?VGt8~1P~dc$z^{;pqEuSd zf!&exxLZ7a*D7_kAdhb+jSb&(#4hl{V>hQx2W&u)_@pzgA=nxTk}&_Qe)TvZfodyW3F!i+=?u ztU4|sVdM0(T>s!x1a`a%e?>2nL;V{g|;?Q_=Yh!1^-Djr?zyTKZC$N`t zCMW&*h_x~DdsEd0<#PmDwhR$CoWlE`{7o$=2$p5-9)n$R@XG^fh8<$OyOV+KOSKC&6ZIDT0L3e}*=iu-3*3|QgQeRV383%Q*chZJz?4ZbeHDET66!3-O^ZuPBt)gO`2eV4-7{_h z$OD}ZC_K4mftvcz)pL@yScS0YMx-SvlHjm42O!Idut%^qqG5=9_&?k4zKOqm@|BvM zy`(1Ux1y{p3BbzGp%m_XymTyN;Gbn@{#svr6uWr<_l>ou0gH-=q$DE~>q4@EZkC?$dC{%19;6LBbZ7ZL!_s> z6OzF;P|{$6)kDqE%|$00{k`!zyZr{Y?#RbQjLjNr{e(QmjzZ2C&vhZjL7%PoimzcB zI8DF!a(bpz>uBE{b}S60c$uVU7>~KGDkl5;=Kxey0mo(|Jw+H9va&)pQ^RQ0#Ku)- zv9rAD{AQIq@~H|3#xVdP0obZKlIHBJ24lf5|oeZqB2uI`T^7FffLF>VujpRvlO6d0Ko8#3P-zP695skFZ+2wBlWOsf^55&DR1J5ex{}5aN zEN>;E}Gyy zB@{Pj78%56HOT`z@%^4kk_cv<-qY$2YkfjSvlRWuo9uR*KRzVl(@4MCLqJz*RedTX$N2B|rSPz@AGykI9P|-B%GDJLzipMzmFRKD z|6)PU%p$_VmREHdp1+tE<8;{2H#A&d9?3Q-CrV`;Qi+j_E_b9(_}0D@h&Wd-7I@1< zO-U<FwjKVPU3xGHwxc#Pp25Us529N>Ocr848EZ#Ko!<;B4hLt(PA&GB#RS7!2JR?CHtPhwv%zS`N&E;hJbB&XCeUBc$10o;}}` zZ=Ls8VF92ffHY{lU8C`$q7KeEfuItd3kmn)|ew&=s>}5lt(tJ?tmeU)cH}={j zb_N0no`W`44gA2;`Qy@}}ywQ@#VLbzZw8!OmsA3*Loss#~q8JlZ0E)J>3Ln)-=;v=G)cnKgr$~i7$ z{~nLKmf?~pWfA- zQ_$Y!-AI=z7uOYc+mj@Sg98U|3G48TSN&QM6Z&@XC0ZZ=4Z@MpWpcT-&Wjo}+~Ny4 zb)lXa#<)&(PV(_0c3>?rGcjcnjarT>jzs^8#iWteY+hnG+05)6&tIK0j6Z!07@409 z-pId7lzNUj^)n`Prc@;uy%ad9I|c@R`kQ1onZ( zYZqS;;n$fzya=6HE7d zey>l$2j9_m=z8)e{q!-eoEDXOl)Iblc^q<69OCf)2x|I%S|I>d9tsy4DFi`_H`n< zsS*%yQcNm#Qxi0;ZGltO~2-G*@sy$SCm>I}}zmKK?L_;y55q+NA{c;?qs}*H)*))4| z!f^-h0w>>7zugJeV(=ttU~^k#5*!s&0?Vml#en_)#wzem`g*POXvSU;ymyq`--Gp` zeDv^VdHS<*ruX-`d$ohPLCnjL)t3?aTAtD2@RvZy5O5a|mm^&+Vq?2H+wQbH!?k(t z|E;MBxLnxGRRJ|qONDCr;KJECe5nCoN?etdz=86DXuN;sS|{nV970QOwi`GmSRCAd zBWVlnDL1Z4iOym%`|bJp_-8G|D&2T<7qYd<=Qu9>R&>>B+nqLS1E%tV-;0+RaE^Y} zhgnSKK{QivW{W4i&0G$w#sE&qRn?FVCVT=p>zVph9d}dy-g4`N&n#vFfEZm6Nkm=V zd4!96eh?TPuNRlGGs+g*TyEff%zkyQbUG*Qo7mh!vh3ueeC#PmlGLc;!!15|x<^r_ zD7CT?y+?+Ps5zzRexTpWCjrDk!&KTG;Xnv@;uD`m(yQtE=HF@L7KC>y{a0#N#Lgqz zJKcT+O$5v96!%}&!7QB!*1zO;2UH+Y{v7|g_3u>SKgY`ox6hcezV);!DX&#l_ZYhQ z+yDOX9~EH!JtaN;xnrcjxA=5{R2j;6 zj<^T(6{59-zCHkH{gURJt4eLv!UHl2f#b8yydS-zxpkI>+}i7`gpJT&ucGP0Ig1nD zD~`2EoBkq0pi(lt1^4_=JKdEsk!8Ti5PlK$vNH|cUFm1SIOe|uhR-~y&-nP5pI-{B zdsz7qcRtx}-^fu}RPS8)>P5iUHWrLrH1X-hQ(-n0NK7#(W&;Y1V8Kpc-kWtdD_o~j?W>@)eK%5O9@8T;udflUd4KJ!Jg`)JmM zEm1z0XbZk0{yrTgk_OZXkNk;lUSE~u<8$7>$NRD#nbTJa7&P$ISu?#=fQ1E02=>cs zpphdv#*SU;rpnQW(^6^hEB=ABWV9KuzZA6!@D+9r=R7rmQD5oWn9FE8sy%z=FTnqu zt$uD%Rv_=**Ksf<<^|UcNiCs$4af4Gen+U}#Jc~G0#3-Ed9(k(gGK?%?Yc;>abctq zIr(pF&sQ7|4p_b~KQ(CjrNrWftc3C3=*187${Flpsj^5__us$%j&g67@AqA0DvKxo zqs80_r@zPldwTwNP7TfMJbZz>Cf{Kz+ny0^ko41~i+)wwy4&A7WRiO1+@y^<-x6g) zPErM4ze>Kjicx*2`rw{pjM&H~%^|xyKn>OD(R>Aj_(MWMi=PMg3$NW>>-xbXXV!`D zI&y(9b&ubX0gXE&!vQWG^zzl5C^3d^o(^;;lL$*aBFe*%l34`ax~~ zVyz;gZ6n?vc9FF>^DPB}R{o$t#fterd3oV`v<%Ifo2V%G#DlMH+-FISL4I_OSxgFvY{z3ELgFskFyqo+RV#IO9!^>0ED#ymY zI;Y-a@!dJ;GiktKzX;z}1BG#1de|5Wv_SPjb92_qiLOa~v&Mq0qMYALXkW27Otj+L=T?(yP?p~B*{sTzf2%nt^eyMeW zXo?dfTM740m!;%ApPNf*Elthwp-!qd3&s*hm37w_TN@h~gOfkyAR^IIm&lu%ICQ26Q1wR)eE)SL9tgE-=J36tTmKp!Ti zAvSM|s#h3l%F2^HeQrJDRw%@!x^&$P?RJRuLifFRw3NCUJDR|q^`4DT5LK~&B0-7S zcW@h?pF%o=>C8x81!^fp#m$_CLh;#*364}6TAvRJ9Q8sp0Bs?daI|27^b*Q+SKJAQ zLBL|_P*}_w)V;A^qo1h@bJa7Xl#r5&UOPp6^+?D*JPaZTddI{R&2D6!GX|4C5#4Tm zu5$Yi{&||il>Ls9sM5s4i8*25qfeL30u)#2*rSvz-;G~iA3so=AMOc{ksps!Ui^uidtG9 zS8U{F>#*o&24+$KqM{Cp{g5VuTFZnS1P9)>OPLJy$~TvIQ0Vn2&Fv|uSB3*GgJS@g zqvvh%RM!-6#mrQl=DsXd=$tHzf`4ty2qJE+`9mc}ePh`>+*KDG|JYFrtj^`sn7%yK z&CwkA$k0&F^!9etFpwkJu(}9^hdj`Sba@xZ_=i9Elgi3iY4(OnRT%ntgQWU^1z&AL zLCVF&MSh{Guo}mx)6P+DJkzv5ny^QXF?eocpajLh_>zC~kfz0vn^%Jp^AL}f+tyM; zTzD&I(J~eUf)G;WZM8Oqh!_jE^-Cv z9>*9<%qK2yfV2fg$~bY9NgS7Wn{!3(lMS|L#W2X zL+J6Ejcv^;plkRAI|m3cDl@w#Rm(a;Xmn67udd!e&+?x$4pwq-oS$ctOU8^@z9J*r z?BT*C=HRpn#K(`p#KD=Xc90$~c;nI$RP1(bd>f{(smX48<^xtT+%q?xTQ553!_SzQ zn2fHea)Z-4o@y49NeTRLNY(Kuhnp_ zLRQiB`fEPvFTcde+?7W#{f*_`9Up}{^H$?U`CO-JPxtP5S34jW+SQEL#u@?ZPvKW= zSc%Tm>J}VBLP8SX(1?c;`dQgI!3S{e&(;@G){JX>(^hi69Ts(PVN@BjFq%swdz;3i*aeu1p=FR!038<(Z@_D2mG*0i*w zq{@B|t;o2w)t?d2^b&%vz6rKsl~`KVoXmZB{@lTKZ>ZqS-iU9C^ei#{d&7l!9~Y1= z>fU^w3!RN>cdiho?akSFnbDPw7R^i*oX^k45`W6R9mMnftr1|wpcHVmv8c|R)?px@H? zu-sy1^3Yo@t48fc45lHSQ#S^{FJNuq)$wM?)8G(#XzHvhgTGt0;Qa8%aQKk5*>IWH zl4zBa&P=pCSb5`nz*IObhW*@Ymytz*O!+*Fj%~JOxZ0# zV1R!uD1X`vW^Q|s5HBqaDG9!c3J>%;&s`*DVoF6x2{I7|i&qCWCX0BVVBA=MvTr7| zy^rBMjSGzBy?wEo^!(zRL`0aLBQBXc=ke zW6ze#Y!4g*2483aQw%{)_u$UTi6iru!VIH-O_6i4RG@VF76S!T^I?%GcNzcVd|RNE zf6gi=MWbqZdtbQQ6yH*@ZO^(ec1C(?J333Np5ZWk9%5?)-{4tpK@WA|J; zoz!eL(fN~0Ua!(kSGe_FRTleZmiqh8)p%l$KK+SG6n0a27caSc;2s26`jluJ_^-|Y z1x#~5b0d9#nBx3sV`~LyFo0pFpfTMpP9F@=UPl0|s#TxPPadofiYJo(6KT%vx}NwU zajIP7w(*k}T)#2(SxHF=U{z47Wlh)GsDJ<|p#Z$RyqR~lW+F0!Jfx&4_rLR+B<4v9 zI6Gg&P4nG+sKoM&iGy(Qw6`Jb zd_1D^Df|k$PV-&YpY6CNROhpQiaju_3M-}Rep#IgncV**`&scVI$q8BHFhLA+I2~O zjQ)bfcd13fwO&V1nl^rhE9ll{#A3F|K?24m@pru+h>Q!nSCGKz|p zZ^$niZffIeFY#iyU22(#66VJ8NH6Y+k0sK7npe)t6*oY4LDnM;54t8R;iT_I9QcU| zKYVLS9b3DsBz_a^2ZUtTuk%Z>FlL%SSN+Z_XxfTW#K|wbXsd`vX_uE`sVsYpG&~ct zO{l^2>LW@+nh+dXgq&Y`5OoOxE z=x%-yI*=FIqZL#lr%~pp#E%2B^fKkXY}pg*G70kh9k08Ueea$fwvcL0dd2Hv2UMoK zml$4qzm89Hu20jVovWzGf(;6PRa|vz#?`)czlg|x!aXCzBu_#{;!^lqML;3t8$9$? zg=?u6$$>=HcnLq(^iT${)zg;w!gog0i{w;W|7~LrGne|s9y9pw-oEleAj(~+5H$o# zDEm)^Isk+i<e9`~ULowsu@s z`P=nOd|(M~P(9%yCFYQlcjwwdlc^oy+rmMA^KVqdS5CBw*=uo&wN+uLl)xfKgjb-mX(RzLh8fD%oj!Oe?7?_TjGOtHh$41RT^KkExB)Xdr140CRhXg z?DDLiN<$T^lC=2USi+g@`b-n;i{*B5?|!%pg8GR6&7}o^4n6L7=F;W)M(~#!tLix# zVyJX^UBsKuh|y)NL|IDr4ATZb(QN&K*x3H<^M2z~m?!IwFDpVS!&n~DoZ0%xEIT1W z`DQ;pn|F}(;ir>KbtR`7Wj_CFO~o;vO&aEIm1f6(js6GlNV5igQyH9`i|WfL^9i%J zd}O~8m^k0JE?4OXDr}mYACurjy*1dJ7H63(E~&Fr5EUmv$|E)oRaiE^c8T&5W;L8M z2+tFXn^~Wh)p5#}GOy_j5`1Oz7kxNusJ((O?8`NuaF;04u~+!G(}#C#hKy2>#l>e^ zordNq>)D*=#0ioGW=^y$Qent*lUUP)^)OvE3%g7CdneFV&FR{FQmC{L`Mw=>WoJ$@ z!?24Qb@on+oF4sN&HZm)$Gqb<3{|)Pu-%Qth~BX|4KrVCv6(J)6WMJd=Kl#A5)k0q z*-BSBn@S9=Ao)iXt)j{0i3J$I{jLo|h>VY%>ff=<9R4rB<_`XsA~Qa|aTo}^)@- zsC6g8X-u?mFHbn$IG?`TDUO9*hqaaqu&(@Y(-+Kh{h zD8a(q++1HD*45p7eTQ(~VVUu${-|cAF?QSI8`G7=svMEg-EH_voc|wNZvj=+7PSrQ zMd^}MDd`SDx|EXcPHE}xQYq;M$pg~e-5>&ohC`QhclW>W-tYatG2U+v2XZ!N@4afz zHRGAjEH{Dfu+iUws4rWk75y+@0Dv}qeg6grxl}u<*7owdXtBz&eLzf1bbuE}Pgifd z>6|)8O+A&-S-DSm1x(NRWqAUVyAQYh8FWn>YR4E+AX2Q0cbdFTrA<}2{zk$F%R`S-Dkudt;M*p#O%9X zKQ4UWaZMMJJg4tPeN5j59=1a!eZwH3jN#uk8tbXN44aa&k~0l!81YQJ9)ov$u-m(bz(SWYiI#5@J7(Pejvc3-mN3dL>`>t1PX z&1q+01&YgkWB37doy27;SPiJuAQ#Jj@&jp{c-FC+3Yu=B!^WWV?o?1$iFVCl9R0EN zya%GckZP`dmF>;>-Z*^%2klXwG|3Q5xP0DNUS9okx7O3o>oQf+ed9@ugX}M)&&KN!xBZ;WDVd{s_2^2; z)2D!S!pYvgCH@csM%amdpm348)@G4OweWdE!}|KVy|14$@3@+X%l%AlFGD1mWlDD*a~3xNi44kz!fdhG{T| z`-6|t`2C-2DxhXj0?)Uqib9Hyktz{SkGAc z!OgB;pCd8Jq8F+$nI<;i!gGVfGVp@)$WiayH*iIUl~%k}A<&vl_1PyUi>K>fIWZkd z9X()9ZDAW*aDBL^`kCkt^LPBLe!12tev(&UXl*Sk z(ksC!BP~5sW#zJ?Is?px92v%tadW0W_{jd2Q;|C~dEsek3oX8Z^-^1a2_USvI4Iu? zw8QguO~9*ZSoH7PdS+E#OKtXRj6CeoNAE;ab5p)*^vLiUUa zQe)UH3&8$%Qh$Kuo_WxmP`4&U-%p*qtZMJQu)vLJ`^N9HZ*OmJl5yXfD_5Swr29QzX7B_dyn6%1(J6v3C{KQ&KxIcM zR~82sw_NZq(IW8h^YS!?7~{X|X2yN7#1TDmE{6<|gL2j>N~Da;M+wH2>KAr#EL~%` z?><`B3*`$jd1+Td&mqXgytQu`A4W771|ubNB(Wv%4(rya8<`fhN#icT#~MF0D*CJl zjN>gL6+M|s84kj`P;$SG-)l&M;dg!mySqS;HP)Ej^tQ=0`piW!LWD&C-w7BW=GyaU zljo`(#p6{}uHzfzVs?uGGj}WDaPa5_OtngGH+U9V!n_GUpiEh+gMng-PUJTweo=mI zDs!<%xvx`C3&s>lU=i&)i@7#W&dJouaFzii6TQYWEjZ3IUteAIJQ4dDX2phQu(d+~ zFs~<P`1-kTxo1LyW78|M8xatP6@XK7QqwtJQuCR ztEChxawUc09iSBhivpt}a)#-cVjM+@FGZ8m`L%{Bdlprl;naVb zIr3hK%c-CGW9f za(}c{c!YqhA{1IT(GTlN&GISF_*GO#6MgCe`%d7}>yE}4F`Y91D+tQKT3wd2>KOK_ z%mnW|2%2m1i$@CDsfnF1Xt}FOOiCG~=2o;rlgL!ka+ixSI3~xF`cDDT-aA~61cZk0 zy&&Pe9bG`KeZEoe3U__; zoH>_N`Qk&R%c7mk+yhCQpade4{`04abQ#J<>LLm_MW4DFx-?xFXJ#O9I|Z{1Ry+H{ zrEiL>gH`i2@zt%m=&T7*`!GYE^)2Z8mqjm<1e!_oK*iX`74gWVp-|Pi?($O*yT7baakvZ||u#>+9zTHwvYd1q9d0N%=4N zEb7SK?{xIHSp^MkAlzTFK_Bhz8Pot|&VJaqFX1$vG@rI~tRM!e8h?U~mtnvOYs?tk_^I9|s#`WSl%9 z?H{1%SGr0@26&zDa_v?18HtF{gL8;_F);Xq_*z8&Bf&f5vlV?&1;-3Ohb82x`phtx z{P9+xq)N#iGW%QRe?R1X(fBWg|9^svkI0+9hb!X}5%~4XM3)gJUQF3bzxFqNn2Dh< zsCaR#xbYsn%-@@VA2wthA)lN1nu6NjXrlh?qF`54)S1;C1$+4CAr|GA>HGz;;x(2t zwxkSIjGP^ewCK)l!j#&XTQ{kju~L>{T6&*Qe1*4;Ap$mH&LX44PZE{@4yn8;TF?Z zI7+Upy>{m#Jm!=%OPu;{GkiWr*#&(XdXNh3s!C3!$T@*DCvr1R1i~ zLq*ubD683LH9vj1%`xBE8PmDOAvpbGGq0zo(P9013?lz@HE+a_$i+uHmf zFDxKXFXw*d7Oi?swwI*3?Mt)QO+B^{7B&`0Lcc!wN^-Pgak2wmEdnk7)f-U@3k#4R zQdv=fND9*Q<{O;YSXe%>y!3ll5)nH<_Cl?6b8D-Sd0B!avD&g7xD6~ zeU%6qx6Arzk`uw^kCd6sR=G4xp$3;)oArPh``p!|u+ZyshX-1k;mDx}gK1<#Z0y@> zp1YYTx?e_^R(Pa-zvM5fb7Mo0U)Rc1Mu7?9U}$r}Vygoii={Uw;iMG3&@|MgPpa_(zSdjo@eZe z%}B0PR8@of`T(r&aL3}@B6H>`Bc491xT2z>t?gkXsW6pX8gR;LD=8^yXqcIrZftFF zyB_{&YHGr!mgVB%v9H&bp;Gxd8X6L^GnyT8tR7ld$Auo~aC>Pd0I@MQmlW5MhWR~W zU1q3kyX5s#-8K9BcL5|}cj(#P+Dv21Qe$z0 zQXCdiDu0SadT60&8oyMt?NUcKiI3%{Pj6y+y&vu_j*pK)^hCSa-3f3b@VlSB@yp7| z(QR<-9kPM~SWo>SLy;Is6PQ+W|V9F*TQ+PlXfDHHK zeTWjb^2->ur&YXBQ7-fIH`qd#p5CTxmCE|aq!a=I){&E+c-C=(L_mlt%|10AAl;sN z_@u9sX;#{6xVyiG*x=kxLQb1Rc(j>FjSestRU|2(1fUzd90K_+B(+4dwW^X7WC|l0 zz6jT6c6N3Zkhfmu*0O*pr=|lBiIg}E4lH~mQfME&rGHXnkmeK=O2^M%ymYBY<`#WA z<#K9^Pwl}hck!|O?a!ZNLOuO*f|V~2xwNeh1gt@0%oRV6r!L#pOT{yNgdv0KH~IHB z2dyB6YW3;Udqmp5MFyPMb(=oqf=jJMmSk4=Wiay#;%yPIV1Lr!(^FOHNAdQ%K z+sj5imzzhrCC!soQ?Yb7vJj5xjXQn1T>RssSJxCyj)r!q6Yx<_e zX``;?lCfU1bP}eRMinCR6MiGLqN*ai4wtr7-P2RsFr@0t8TmaLRZM{wE=t^}B0=n^LY*NNl)C)VfMQ`m$xvKEgUz|ER}D&`e#t^sazES0CV+_Ch1B6D z9>3#Op>cFM07M}X+qD#a>gwvTad8S`hl)i-MID2XZ2a`F*ZhE!cm>RUzMj>yZvcNh zIMADsP!uaJn|d3zEmagLUQt<@H^s`copD?Hg~mo94Je+H_r1WpNP^PHXxKRFf*4{n(hordU<()kY;*%y5Pg@36P3# z@Tlo{9@nQ^E8P(~Ivy6$RJeQ2do%2B-zt!v4)pcW)6ubVaHy(-uKqb2mgFy4zCmcI z&)l&rxzZLN*RWelD>S_rYk&KdlYn3L(rx!wtydotShKLrx0&7HC&!M|e)7y#OzKsD z!64g0m5JO>o~YpT9~iY?7Eefn+-SX4(;$)cKmbEl`f`#dYbE^Q; zX)x??vi@ZKCY)H-OZ;q&&N6Dq=Phki*!y(ARM5sjW4rk4s3jzxVeQEEm?#(Qi+YGC z*&H+TE!{w&r1u#?4D5?rB&(jJLkU|L2B(w~ZG2O&uao!kO47xOLmjt9yCx?&Sy*hr z-Zl>a5JyK_;6K2lSzF5)*qL8a^0<@@4Gm3)(uxPhM^X}D58kkN*t}Xm7uCM)(vL!QS z*V=u??m%%l+<4%=D#kNvosT-{NLgHdkIGqBrSW<6 ztIUwO28>@g-?#mHtA>O~j=mw$XQ1U`Ew z0R;q6drf>T=j~vl%jw1Ki<2B2oFy6%Ot&a(h=)lcg-ED}HZU8+^@Z~3W z!1sS7zrPdQf*w#aQ3nn1b{&~*2RnJ)!W>TT$eV;FEW~L^Lek; z8qy&p3yX_-KYjc5H)ri%H}8}8cRz|-p7T#iK-`rSEP5-euxP<(armqWj&P)6e?*>7#)J z1Rw{JcF@<(Kom}|@5ldo`M;PL{QNJx^S_n&{|$Ei^PCaq>HArAZ);5CzxPZ>8eaJ? ze6pw6cV0)8dicP!+*Mi=Q&V)vTH@mMozBA1nZf5SqR87wuw9&>Tg2_>9kETO6WO+5JqqoSK5x zXg_Vo4^!xw5(o1I)ueNgV3ZK!TJ@&~3WTobnwbQudJ%!b_hPh+*aSytGv1B3<4%Hq znpK~;I_#b{hYNwhSRVsAaakO-DXa57Jz|hdioro7j#T_EN*OhUpuxtBUI{nziEjCj zw0BeDn9Y>HKC##FG{t zx25BEG(Rf1KOy6W&d6zC?X)`IBME&ko=o2;E#tYAM%HRv5d3A)elJ0X?;3a9!O4s! zOBJtFV?|&l;iGxSV!MywQm}XHTzc`=3Ijp@mVs>bGQSb23rnajz%XGib+~yeC@7lg z;~bf;@Lt1G|KEpyan3YuMbB-v=$~AIJueaQ`E|{&w5JXW?0UhAN9gk#J6Yy4$ybY` zR2{u%IgQeHmfzGkqhAq4nG1~P@|vHal-};xK1u%oDtRZqdUbW>)wnIH2bY3IfiCH~_jregzf=oExrQ;MfS-OO`*fmwUhRod>~UsIb-xb*QqF z6^|VLs?d>7a<1)3Ms@8h;D9IN)g6gxXaD)n*WoE?q-4{*#_Qzrd3)m9+p({+a=*s4 zg-eHOhPJy4Wn1bt{?z+9+Oa2_a@tQK&go-kW5D|mpP8DYBK_ssB8E12a=D{ zQuTFeStJDN?Pqnm-4=_S4f(ms^e&y^+$fskceF(C$a}49H39N;e$p0{au z@S^i+j%7`LZURDY6GUEHM?+S;KCddc42>u^)Rtm=97_lq(iLavtX7HyCyhiJP$B@= zkW^8>YkkMJ`@K$6h|+kQ*P$pCX$WRrbnvBt5l66&P^yV4w1&;-q`}Q?N<~l{6KWc< z$7OXBPdlIHZ>h^-qw?nrxco?wYMJq8!;)CqLI*fr{TYui5~GBnA#f6GpPBF0GbmulIjj@Rs&)nkS#86qxSinM5$fgji_oS5;Fwa&| zQPpk1iI2X`>f+SYRK;?;2+vbD`uzFNS;c=rr-eAn-;DseFSFTi;(sDxU|`(PZM*cvf~3&$s`^E zwzH#aQX3_xvN_fTfSNGKkTYcCX!As2{?%4*7LVsFwfEzp_l!EmT=B;KT)oL!7=M4E z)gzztrv0l^qYZA%Z9jOnr;(XXO$jC$r3$R$n$Nwi&X|OGEaf}G76ls*+1N~9myo~+ z%m-V7)1xr~o|5wi_GblLTwH3Q@T;LK$BsIM7{Sx|>+#>m6%{b$C2DV_2Snb*+tYFC zI7mY6u+KR-qom}|m|Q{iGn3eIx{isLw{R<)8t>rBTWzzRZ6MScSQ9oimbWH-za@kG z03lGF0`~8)q<7x^$IN&Fp0=eerz&`VNc1Zw6s1_uGMSvYv1GcJk-wydDcnx<{7e4Q;5o>GOR(kISvwPcGZFw5F&XwSi3&y6~x6 zQ5Mo)!?55;Sik48ff!;wp5TMnYb7b(pAR*2yYgqBk{hJ8?lczW+6pg1_!+GbNA4{T z3O+C!)641OT5yu=C|zJzJ;pwD=1zzrOI_Y;6i zesnY~fA@(p$^O0faqYCjhRdD_d46nG?w#r9o&?Ek2309ljjwxJD}pH^FISiS%WPb% zZ|v`=KIWzkFt@2PmfzksLCy~xc6J0Drx4d#1tcW%^7Au%h`85%^AIlbNWA$yzg(B) zNXx_|E9oA3 zp#$_29NGQ*$`p50b^pTm3Vltm{xvLg*$vxQ3D+CCU^5 z!n71>CS23ssloF6(4kS|bYpv0Ti1$AH9>!*Xc-;8y(t%T1YOxd`T-B;yXDkdcraBt zH8oVLljQ!-uk>tYudX_n;ayQZhc@o&?@>@uQD>>(##K}}ZA4CY1V2Sycqm{TgmkT; zl|(1eF);MoG$;peXs-Sx(Q znNFLJqQ(*fTX{Z+`@0PRbe-eNE$viIfQk<-Qr|s z3zVKcjun*gM&qq*<5%{4V{RhM*U4EyWO$HU-)Kk#~x!51OsGN3a zU8rd9y-EcV0b2-ofV}zbX|~(%wzth^Yj*__*=Jh<;i^&2`3V~xw_e-teg}GNTx>u_ zEWj3fDcE^ES0{;^ZgjXYXTwt9MI| z$Uc6lxsv_A#UkmXr%B2<*UVUx2Z#mrkF}g_e5%ceNLT-SedGS>s<=T;A4xz}^CQO4oPGPYZ`5Le!+%H* z>a4T=(GrDNVTcuMx*JvmJO|mHg?&g>FF~@CnM6=no9f;C10;coSkV2_7lXRt;-bmt zVZS%haC}^4>`oxxCT=u3`oQG?Go*`tvS@O05<}p$((^sd!F_Wha6?Y$G_WK}OVJkw zV(fEs@r>g$es zeD*DA8{be51^L*;>LH+Stwgs`K}(DGa(JlG#k1166Aw;UfOK_#PisDB#hc}TU2i7L7;E%^WeO9})M_!u!r?`)S} zawc24ePE(83hK&EMDigN!0je}Bk1+^ix7^cCf5CiD8`VBm*9_2##Vd0`<+YV9~T=v za?8rnTU0!QcFW?vFQaEX(>eD&m($k&Y-1rlTFRSzz7F8mxVj?0xv*ym>d~MM(4__ zmoL46(!iwCsG+VI{+g|%DoaFJIjeKUuAaxQeRynga%_y+USYuZCd#dz8`7V&H+PZ> zn!i4vCxTdy@0M_FA%~xE5g1lh{vz}>8xxoiYHBU7-jDoE4@ID zHmCakDPs1ZqN1{CzB?bw?X9k6KYHMM&HlSKr-27_g_pikRnx-IuuuTPz!)FnFl4tv zT{dmW@8#9;$+n&c9zK8}ez_AWn~Fm%#>%oP7#wzekSKp=Afu{H7Tueb^ZR$0&IoKu zohqi_wq#O_mbN|Q(X~W`;jTT4)S~Znm{#50HSTA%x%t+nuSmcW2bwIQrl$7sV{ZEU z`JR4*sJyJKy5z7guZy9dSOEI}7w+=$vAbR_VDcMYI_mG=X^D&}E-vm;Rp|EXi0zKR zesII_*jj$RjZGDk7<)R^{P0C=&p$hkeVqEY2Qie+=u76n(g|-|d@M{^4O@sy8=dHs zt~;2tJN(k8Mg7wa9%)uX!S;DE!DNYU?;uoBPck;N>Rn4zRJp`_p7LY%5OGBKAsX71 za#hjNY#D4-IyE%R%JLZ@Q|;Arb-iJE zh4r^ZBL^u{jb|OGUBM!uS<k^*b3Zh6Vq!&CUaYRg;sr3NV;3GZu9=Hqa!MQ=?8RT(E4CnsD| z5vmwyR@lyEMO9S+W(Zir`KZdOD#0TD^Nkn7>$b3>I0)pz;xOaF6YM&zf@i({Wha0x zMqtt>!#8uKRW7>5yISJTxwyeGsP5e5gEukzik+JV5NnoJ^Vwz}1CvrDjC*E2J>}r= zuyLssbX&e`?rD^!DzvDz{zq%}^3n&$JoIo`OXp^cnq3br%f&Nc%^9D?gBkD31UP!t zVwKj9^Ls=PK5sJv{fnn?)z#dbWF<{?vY*YIYF#YbB^Oqa;`4CxW!1#kRvv)`j}A$h zoX!<5E<5Ocs#XG{28-+W2(a)gMn`>!yS$I5E!|&vdBK4#9$#n(?4OMn7oIo-URx1m z3WlC&SGPdw$)<)f`=tS@8*#VmgRt*~R5>|*k=FB;{R52Qgx87NnyB2)B5q@V%JqRS z*sVJ|p}Xw^%z!nKVasmW_vXUh!QOJw{RUv*@H>S9QHh+-`}6zzQ8!$)w`X496=r2o zyd{;Dlap&rn~t!uw6uCw(@M-Of>>Tv#pkhp5qf%>2)TzOCE(KkT88`ZmA$%p!8Z^n z0WX9c>*hMXc?to=z`0!9k@S?XnMEpuGD`ej#$8)eQRrd^OUV7)ab)Sp`|7Hjc1&R1 zzP>x0*rY-o)~~`S>-DriTU6$}sHhm|7w+e$y+D^ba&3?uJb3nem(nkkK<}V%oszQ4 zNB7n+^5RnsUh+P<@!LcODmEYw4TPG?gCsTvnfcL1#>aMc_U$$%oUUVfcsX&S_jdL( z3Uen}N$I1^9I6Tn3Zc-xGlE1D4L9zn7a9&9k+IPee4POh3P^k2k|%rN4nr<23pIUR zLlj_f{>6N_&p|Oi%Iyc zy6Pah-p@C@oZgJyTZI0oAuhE%TbgyhV*B&S+X-UaEHJ;B#3F+FlG|4f!rm4ZZ(I~LZcu46&SvNB? z8#vNS?_AN))Mys;gcTETo_p3xt#P(b%{Y3;~c(y-$(ASr-a#EwW(E1wY zpO~0dLQI-f)WkqfO)dK4T2HJ2FJxc>D12t+>I_sdy>DcYw9(MeblmH=x3)5GL1$(J z<6_uAdd#yD0PmmSbB>d=$gXB*XGaP;l6$zT71LCrFOHKmW+fx{V?p%oH7fhTfEUwc zVD@-Jyb$;H>T(5oUlz-~T^1n_%}mckD0u{UJ(+nO)0@TgK{kA_=36a@bppz#5UF5# zN~~9;sPOu%KXCA3MCJOj^kL99sOSlXpFVvyFfcbW_MiR%#!44%)6ReLl{;t8=iMFGb5{CqRPM13%3-kKBtCGD%Fr*X zfN%c|J?(LRdyI%gc8ffcMLZt=2VMq%X#bx09~zeb9lhFoqj|JrTR{Jdy*h~zYOu3e zX7^uuQ#vEuWENZDx8+Md89dw&)ZdnxOI_Vd|K9P5Xfe0)l>6>a89giO%f;ol6MnE| z^(T>%K}LN!XiT!_r>#LNUP}H#?1kEhC}=YzhYfh&0T#0U7Uhic37RA>a_ek!r6QSG z)J!yhHxM51+MF{)iHl6m9ylLlp^y2O$v2$qBBG4*O{d>I1@fr0;CoW5)0L1lEWEbn z$0p4cRaaMU3%Ao7Djvy`X6(O6xAdLf9XqV%;_q=8vH=vSLn(QI-C;idr@D6 z1l?MsFt_3RFTXj+x0NhR6p`^a)uwdEU6O33t1`|CKF{txS78(-8)=%Z%(IVgooG5f zsnVI>rqQc;ClRPycxCE$n@H)CQ)^xs0V2^eFVGVjtvhq3F0%mh!ws`K4gtjR_>`OA z>-zz^OtA=1ZT!KB5mNxt>a|g;jMOyKmUBj|GEAyRkXKssZrk8t1A^bGT}yh{4Y~3h zUomQ#ZlUK8*jnhE$_vDRk+5EKWFxt7q-OvSNPg$rld+*ef1ThkIdj<%1!d*c)zvvt zbMF(aP!0k9St@E+93_W95c22bl<=_kW#ECH0(zZIy`1Ywg!#%D^4hxI2l~4JT?a}> zybxONKeLfl#Np@X-=^TfcisXeG_4Nh#bWSE0fg=PBF;fSVZv(a*OI+nL_pstlxJDh z$kKBZ(CA&&D~&-OQQg&HhG-Vr!E)wTU%x{fjQ-6Wh|Vvn7FtZG@8j_ZSk0jBT*jF87uH5 z21}`%|HSgVHX=h794s4Gl;9YX__< z!8e}{FaYdozHGlQY^No5DDF7(gZ~KYsq0JkF~`9*65RsN$o#U(Q}j{agw+roZfdA; zcG(SJVFbqTmxIlvfn+`-RwizVC9;#LRjPm&5IFch_|^v0v=xGco+u z<&0ER;piS8g*J5qry~^-9E>BWsGl3FmeGq}1Ra(4$%3uM-y1EvE-F#`WKLwGl(n?q zf(3v2L5;X2+uq54=4VO_Jia{F)A2~39ND`f)=uQBRoi+14N>CXGp`YSpgq0SxQ@+2 zxTl@;ydqMNEpE4$=VHg9qpL>MiCNp}1m$Bu&pk*Pl#P>fj!#k1_;cqs&DL5ERy{Cq2+*p~prTGm z@pm-=yl27=2ZKg~wwv(DEq>^$9fDVGwFwpnl0mByqbnG{<-P}n2ZuIEVX&cKv`S;V z01z+*3=)yK;H-TMddlcW%zmzeqvLXEn9f3vc}73zhov=A!yTPl4Z6HcjB8 z-guslt;@k8a1F*GTxru54Q`wuxS@qdL}XSGJq5{q|MCKyQ*u(90+LweO4*Iu;S66i z^dHi=$PpyGk_RtHjR1rRb`OMVv(pLu8Jo(kUu3bKjVvzmR;Dn<;!i=O8InKy3qFI| zJ_WWZDJg12+QqrK7wwlwNuhh7(!I|y1uc*|@892poQwMt z{2mN8C5?+!48QBSV3D6YFVr@!n8l@~H!V%XNE|hpCl>-Eh$FyIkRYx_D=*Oct85Z5 zT)B@=kyGliLgUVBBMp`B^F6``O-xp=F15B>`~?`|YjRpZ0n120~=SD&o6 z1GJD{tp@B0K*ki$mIbvuRpdGY$g@=x)um^aMeeK&^7YB)HMrPBxX$+sXSo3FTx+wC zF5BBi;*+B~chl*T2Dhk^ceTS|sadO!t#XeXxDJMDyRY5hPvdF+3veAE>-F``^c0oB zMIK5Ka5|l`A!OnN{#npQm>xDZRygrZ7$#PkP3i5{`z71@<3upXTU5#!I@w9XAQGD1 z<^>B^vfABdo}6@fDApD^mZ|c*e!VUt)2FRRj_H0s_oKXpIZ1{*6Zp@OgR+FkKt+@n z2?;lss4}=Ym_k+D+G|)PeFTBz5YD)dAN9wO(f%2arg=xp@z2wF8ulsTQ4J^*s>l@- zn~bu}kYj=4gkPe61CWXI^e068DVVJKqp`Sv6Q5mDKLIGM7YO^A=E~BJut_ZrTD`1I z4%G}3*)&#}rT(8T87(og8Se7z<~!l@o&v2cJQ9--Ftk85&$lm)HK2MLGhqs{o;;?KeIQvGhg;2H)(jI9*57WCL56 z==FSX=@|J5*Cz%D0C+C*T$6tdrRrECGso`pO~#hh_)3zu+hjoXj<94v(IMNt!`L;N zg-_TEa;m4FBgK{9ne1{Ee9c@bzs5^rvk`y{0aS4F%4kS~PFY38&xh?L?%{4z8Yj6= zH}OQ{o35_tS=Jwvt$V~i0p??JtROJBfEonA4*_N9mu5kEjE#vRZ7RW0>A*fpzu|+Q z%HkA2W&cr zk!U*Uj1o7$NWi?R;B!WJ$rShP+VWR-H`?)Q1|Q>}5CiCehfev)RgKMbkcjB9+K04~ z{qtfy1pid zW)YuxwkTZc`IsMbq}ofES17?wQ2gD;^T7LuPI7J;uQ|feV+Kbi&)2d=(@G66_ zAQLlV#zha|xPs#+QU!WKS53oNgsXCBahb@XKgrDBf9(hdqOEVqo}G_qHCY}&DD2$0 z;}GZlpa*rH1ceNMnDTVW4l0_(>V@X;Gsjrx`agcLy29+#6rh7K%E;_0L%aC5l0 zc#a*N=qaK@Ls1K;@|g9{K7QP4|CxjKEntr{m2wSFYmIwhx<8bTU#O={pFEG=pK1@z z?LOS%S8-28h2r)FZh;sEnuZw*kSPOx9mUhK_d^#ArpSqFS|Ft{!kxWB`V++X|PYZs; zllM1bwW`N13yYSE*~c0JXH~2!G${gB6+<_jAV)z=rsRzevo5G$bc??VK+s@F@;hR| zf^0uPQ@Cmy5_`k)mVE#Uz21O)O!9*kkkn=8_SW_(m##p^Ew;k_1IWwk@5!P*1FR`n zk0E$IALdVc1Wm2-doc)-Ij=&C)$1FK3&Vua%3mNp?i2HzgaYnO?Ikn6_m%xce#)07 z*=DWUynbC&rJOqeq>i$`pu=@6G>Z?R&hpySyBd_4H(;~=- z5nMB?XXU)dkqw;K7nbLmD0W<6&!Cx`zxgP(u(vS3Fi@-qfXD_yp6>2RNv7YEjD)W? z-_?I`Re4@`{Q8~KjAMhEwtfNuZVWw1`1K$IJGG95{M1~3B7}qV#oF)s_L%aPoI38m z`an!TUJFCD7A!pth$}stX%V#Kf>!GHnJ49*8I>M{TR)*fa@=h! zZwwXGD^4sEEe2-?^gU;z7MWvI9hF-s<#cRhY)nbXKVho}woP*K zgPh!4+Fk#qH2%eUJ8~fgJS}x~c{MKgG1xFUNEfi0_YXD)SOX9}UyJ9Vu_bA)gsrG- zzbyP3p|q?lGPm#sSP1O(hVmK=iLbU`Cz@+X!+uR8uQJU=3n%!65c#3C zFGlK2c(b)jlbo}9PGsTNB&EEB!b&{7IaQ$>1TTb^S60AAvEBC))atS0@0e)_x#f?C zj^olL`}>OmJ{s3cWB1}~S$_X-*>dmCmIxs>vB&QcH3M|jNITK?bD&oBjTadFKH=_U z4j`|fKh%-#C&=JYTr^9IiW&j$Y3CXpBp@ll_c}E!+&#}5egs}F5FXrj=2upDmu<|ZCOxiQotR; z&euK;>lITCY8X(s^A2kcpyM1&N(p83etgN_nSEE*C7MaUO*R|Uen^CP=k&_w!w+?@ zbD|S5*-~cZQ53&fY#S6xVICZ!|M32w^d)1%{idtaO&s%GW2FPjA~?A72CiwK-n*tf z{Ns<8+k&RR8Gf4T0<@aPeGUGQrl0UV#^C9(BMIBYWPiV9`%g-+cA!H!EDr0rAk6_( zc||4QSO&%cu#LYy^(aomNGv?a@k@(o>*yfkr2Z?n@biV1qW%a&_jdP^Il)__#GSDC zj{WpI<}&x;{Exgqa4Ae!d(AZdbkKUbs=cG5j2sWAYlk+Ct9UxWl>p+GyyNqM_XLoYoDQ*??*OC?HFG zt;yzVi)QGu7reu*ZiC#D%EVXfIAvvJUwDdRcPc#x)Er*Z z5x$698~zlc5BTaPjn;w&V&xF7iVOMpRicMp&i^i8FpU$1eHu`PU=M_ddMUS!jRTwS zN9UTRy87l0!RhXmZ02!}1V=2vv#T1NHL7<_(ljX|aack24jJ;9d0OXGvAAc;@8$mp z5PU7}U;^8yZxg%{MEmXrnO#|ZZJJ{7w<5dJ70`c{6fBF@sq2z>^0yp>vs2zJy)LM* zQ*~hF*sjRFn$Bps%fMG^Wu_}dm!WwOtEStw@q;ml$ra(A9X{n}(d|U3f`(X*y$+mc zqhX-4PK}L`-R*x}F*jScM0rB)OiTeOIb^jyYaXBMyj=aiJ&e{aq94Lem40~TuKa|* z9Udc)ulIN$bsGSFX7a0-)0mQg{H-CL41f6;9jqd7$(YV4=WC#zh>7>daZdYnN#}t zSjM>8JsNhh}Z!U1{lO$hahw;3l(Z^}U zuB|a8V(KJHxSb@A$w60n{=^lodQX1oTh{kvS}I}z*_`9%)|Ygs#m|NRO?2eJixc*r z-2`5xlVzq<#2}hV61HC)w3mIEzr1c?FT5CEi2Vm0htuS@E$(S@Il9chdHfrDW1jf< zfP=K|TsHP11AQflVDBW{!DE#NaJ;|MbjUjlcDVXIJ?*X=LmlSnW_jrFT;fH7td2XB zgMl1Q)gX_hitn$h6`)DfmCn0*M}%NpQe3-&kU&8y{RQmcJ)BvLe$RikBbd< za??itNbny)>_3y?PrvRY=Tu2cBkHrfBn;>Hm^@%qGxBcczv{%AEVf;fOKm%Hz`IEi z*F|Z(wj7)^Gvl151*|vYG59EP*f*p{UsHkn)Url1CZ0Pf0}k$AS`tlyf7D!kl~q&{ z;^PzILJA8l7E+qmPp)QNybG5SQ=4q@x4=Wp)|Zae-_F zOPs$5^!L1h#A>d>(lO?JOG!0;ih) z*I)P@LJ`kMk~lq7hwq;ViAy`;RPV>j5YSO#U6pr3gOnvHK$>WJd@H`2OZRzkAUg3O z5x=&vq910I_&ab1I{%K2f0QX6TlV?U>B*U-OjjJKtSU~&O=!jRv2nn}Mv<9~{0`v5 zrYmN;y)({>?9PsV_B-7OSJhZx)TWE+Xm72s84=#<=VrN=^fXK6m4w>;$li{eUx0u*bsI z(=$a}*>UsFS8p$E?UY?pX-HCXjPRMys8|G1_r?hlDZqU^CZsh;OcrU!r=^K8XQqf- z%Q{=jq7zNi$;%c9k!@L;tp9(FeFaceZPzxUD54;Of|8$cSXSGkfa5rRTQBost|~b6UhEOX8Q>jyl#{< zG~kHH(UBYY?eBk8VG_!<$Rqv07mk9Wns7*hNWpbJbIuGH9CjXgvv#0^@ZoeIly^niT|g-;t&f-4gjJe}08#A6Eqa%9Q`IR!Ng}8U7Z$oehi_hJ%X*<&^ z7<{VoQ~E6SlP*)e4Vh@SxCQ5TY}Wal);Jkt?N<=+-(%?JnF$uKF@!*97CANcvC%|4 zzqV(yJ~2`23obf^sT@JM8g*y)HI7#YeT`u2IXhr&XBfR_I-k;Z-CKCK5l78`YcFVo zicc3ipd#Zw!Y=pP%^|eVgPS~v$<3odW-Hu-(vN}*KGE)_9bJsEA6#EggGr@JwCECABt7IQM^`^-hdrsyjlmrp0 za_wH66>=5ivv}-Ei4Ni`v9l^v1bE9mHa4gD?7pk1f@nAW!8WSqAdu%VC+elKMWBH% zGFs(ut$3B<1&-A3%++A>-O%{_`Ei8O#1L+SY%o=VL(D)fyDdf~sdB{lXYx=Ybh7Ui zMgpFUJfyz(pGm;<0QZWYDl$IwdDhg}Caoj~RrkzhO*Qzz+KR4}-?yB)xXxt(y z)Pr{t+N3XfP_v`fxAreSLnc?C^z>$uQkx6t<8T$qY|tqFTjjBREfpb{|D4IB&%Xvy zt-f}Z$$CRlhNwDiK~iO_SpL$20_uVFo;-nHtT$QRfW@1K8dzkmaZH%!>?tT;2~#~2 zzco$L>_&NaAWzQ3mC0)tm0B>5cQz!ky7pwrywE9JOFI%R!@NgcA&z{XNj{UOtht5x zpEV-))SM*sE_b@}Xa5W^c!WmF^}L+%rc_o;>N$NLL~w{qjczN+x{*v{p%!D*nJ^-w zfN+AF`%?ZA0ccYF8=2|JW7ErcdBrR7n-#%d#RV1p$ge<}D#^ds{dWWhi1Qt!2Zn~A z6KsHxFy)`}BDj401JsxAuJ&%$PP|_JA^Zs$>I5S--TS@=hs1NJnbpFgcytz!g#0xU z2J~$3x<%Fs6o@BtPE79+UKP@G!d89raN9bD#R$v*%`x*2t@ns> zok;P|w~PlWl<}3Ihd!5m9Dw?UL+Hj8-_&*`qT_?dkOyIbTV+~|u9G-@erdF;eIUGu z=weqKKAq-xegeZ;Ocdq+Dr~7lZ43y7LzZ#355A-M2 zhWzpGQv797``g3dJRXO<<{!X-g7Wv8e`9hU%3_i;dr3d~5Zd|gukTnJpT8E0X7|_I zWDrJ0eo9s$FHBV^x1=m9OM*JAO`rMCB6<)Yql)^bW@g2aw6M|^_@$8IVo_FxCmK+% z{rU0vmj@ti5h9~F{$B0#S?@KNxNsl0gU0DIG%Fr42l{>F2UC~vjybzpOsW);$)+YcoagiK_<^yBiFm3U@)nRAlb8=#Y9KG3-?=X0iu?VuJo+@61>BXLa31-4 zl%70zf8eda5{+r^HDqSG{O65eL7czoGjola1HyXXeEv)11}e_#Mgp2Ucg}GUOy=ha zo#IkJIOgos0_OvwDtp+8sKs>77@HNU{Ku9=e_KBdA@EW2jvAr492_J5H+sE~*@9XdV5pw@K|9{sA-s;ci|6ZfNant`@M}UYPLia~1tY>4npitnZ z#6?kL-QnlwCC@IQtP3eB4i083cgOR^#K+TzNgTMGj#s{#a^0IqOAQYRv7T!Q$;iln z@G`XeQC3D6w1)#RUMs_cTNsF(7f?bfc#;K((8@}{?Ra0a#);|9b)uUF7~~?53sp8* z>k1jbqBF>cMxOajsLdtfwq1f7K!3_)kwD10ftn>+JPx|jblC*U@ruIg>hXyQ;BgS6 z4#UI48yXtQQm=q=+8U>w`(FA+M$L_lxf)eN{r#_g{b=MR*~H|<9vmDr>WcLb4AiJ~ zade%b)X??9hQewK3kwBp+7>&~D@u072E;o%{3)LK^@ zPu=lvN$8xGE)$m~6~$~mTmaM<`D*1|V`E2fJEKAsNT^=yKHKh>H5pnwI*9*JY)j`W1jm z7@b>Nb2<1u5C4X@wBd|>^7e9PELUjvV!tzo%l=O&ls`UpvA5srhI;v5v&{qfd3kvO z_zWA+7>!KGU?x2^HTOkvQ$xcnayA(6h>MGBxgDhX`ZgyC2dK~vb;fYmLmOg)c08cT z01rgyBU#N#n^d66fFkS%q91@(h1a(s@q_ZSXFX8)Z8csI5)n~uJu7lBQ3+oJ7C)%@ z<^(Phj^bHj*jb~5Lv4!LYIN4t9U&9$_`&bKjaMI!e~;1e4_nJhLjGL+{tEtvzHw*~ z^5^q=zb^2%1u2a0|I zLPGnMfo0e|Gcz;7!oqOY;J4Hq)Hwcm>Xm&kzT3%R4W~3`EF3ym8mv2v9^H$+-rnnQ zMtVL!fq8+Hmc#Y(DWunTvIZC0%JuhO!6M2?PJR#~CKJ!wI&bqxHeOvxsmyw|3Ao?y zvKUrWRRJ;kCTyCGjSX}5BiL&|f%4QBYu=_(Hi2KG%FY59p66Oap@-tf@bFz=@3=!i zU447!CFI8>CYHs0s>mnGG2l@E-uGFi}J;8{Sb&4_xJbVtZf~*0MPZU@lwjG9?lUe_xaHY!Y8T1rX>h)39ca44aowbGFBIy5_QjJQLHf)QEQ_&syX?E7q9P+Bqobp*;nUy3!vp22tmQg5If)e4uoYmA z$_on%BM*Q?qQZ9hqf++CR>E1wyv^3{-}LGgUuRy51QImPLw)G0)s+?4Tq`RpN_pyz z@WybI>VCJ;l$Mr)n1NmM_A+KsX=&<+imK`%P%b>8py1}>VoH4l$8}&};KIerv?>KC z&yggxycC$r^lR3+ZBs5^O`3kIP+{VQIZLvISnG{%oGCd~+neWn>U!~Dq?%u}wd3l~ z>qUiyj$54^z(rXFiUb%ND!+U=oo8^r%dC&S&Lt%#ru{7)`~_BZCuK=VNy_<}AS6IW z>7#amiURk;?kYl-77s)oJ#-LzuU#Vq%>&yLP6|JWPS9wna$qbk|M*ele&!Y@Q|`253hrwPML03__4OST zK&Ti*#HGthAaF2;>+bF@*wT~uG=f1ZJ|>38bb#_pduL~7Yip}5Ig}LADCGpV&wF}$ z+G2vt1*vb_m0x$t{qW%&oT9SgVunfRChrY$T%&$b{{!jOWS5U&Ya9whrKqpA$@Kp;sXz z2i@LMuU`EdG!haLSbdcueSeYU1JD9TJF7^!3Ela))9ba3X|~|({hqL&0-D&EhMD8N zx-(*amkPN|h3r9qwj7q@Klk?Rt*wDSNE_tP6;c;Qcu&C=AiEgKRRfWd33?f%eDr&N z+(-OCS+b^4<$AQ+6iA4MS`UW@yA<&3*V*KDROOvs3w#k;^?^i{fqCzJV@K1jK*jun z&qC8tA2dMFa=`+C*vPLvG#$)R1QDLb=ltDQ&*a}+04Stzbace2vqMB(?djoamEAhG<8OUX)j*6SDj2e`V3^$70QgYM)q3`yoA4keWmtb_$}HJFaVRFbun|+)69)S&Mlg>{SqVBFP~#zxMXh znaC!RbsBtnzs5IZz+fWDy-pJ98XCg)Z+6Rl`B`AotTDfPh!s6vrN|Bw5G<8{oZ1I{!=vV8d3ubmyWZn}4CDo^^MdKxK0Lj~BEOIyCi z=rc=8!|;rKeM!$K0ZZf4sj}KG^}vgQBW>%@e^GmrR-C0!(Iho4u1oli+}s}t=L0kf15BGWq?-FZ=PPc;>)Dh{g#6%FO9w|2HJo+EDN?Z=wovW3v#dVYDf8e zL_g;i@a=$IJvlkK&>I(aC+7jx@_kUN#|K*w>s{!GLiL;mDNEtnMXW=<1)viy-vb?u z7#P?;I_f9Xl8xgw(9`P&1sl)j4CfQbkU#){>A?2zp%-@Fz(9%uwKJ4YY-~&p;Q<#G z4h{~8K`=HNws(;>9&e+k8KJ6ap1u{IDUZ(XojKmGo2+bXptE<@Mu0T0ptAA>eS}%* z=t!{<4>xz8v-}NWZhck)_rv9MK(Z`4K5ySX)zplCeD7KIAUuWP&@Mb4AgQq0?~1uf zTGbA{2t>rBwCM(%2EdQxWGSHRA*hM{d!uR-#KkAQW@`Z;48YRy*wiUI*hqjzA|fJyH3$rhfEr;e zUtD5hS1@L7(QyF;={Me4q|HHwxD8@$m4_M`ZUJW0CT&t*%NYd>kCvQE7GI z;ilKC33uZE@AdsmEr~*j^+0_-OZ_l%A@C$x3EZf_q9MzeD}wV@^?Ml2D4xUj=KGnD*!rv zn9!vqV=UkP;r*(cw=Z77QZF?fgbfTZf26MuqR?9))`~=#ZxFCj^78W1&`4$vUPeRf zjAChSXh?>2sQ(n%91E(6o{EagX0Bzr{w;_@($B11L|q_OmdA{}1j%Z;%U}-`_vVLn z?CtEl2A;>_V!&ldJI(Fw**Q5<)M3nQNKFW$2%6O;G7v?>{eci{%#|bm8D%V-+-Nql z+Wj8^uuG;5k%cl2M8gVGz4rB{rRj)y|H-qHXVbIJF+NS06=w=F2}NfO{(-E-)Vv!f zjOBUbYDSNEA8w7vIhRi={=72I;aa|t*Pjp@e0Iafv48nTV*qI@T_y~Q@-xLan{2|B zFR!nqVyd9DLp>S6xd5Am-{JyuaVH_LkyeIS%O95xih~QPvX9y@ zjb0IhDeymKj@wK!+&&W(udU)qNOedS_Td*e0B&7Dub zrZCz#O6UqtiWT&tOXtZK!ENzoyeE9izHib zEp&oD$-N5|A5s*~_jiRULJbTJqh%4RNRq;}B9?@{kkrv{1DHS)4X_2k+o@vWv&fV- zr}%dLA`Z9w;sYzDyK!xWS9*R0+LcL>79B3{96~jgof`!tu>ADQk;|+-Sb*~O?-|c1 zN$#yKQI+I!cjPVl`}-@&JRzo?$0Es{c&o8W;+<+C)y#|yJ{q|OkqjqK1o&dAy9bc)EIJ8a`PQCwJP^d1 zpJEl#*FCl=i}%6Nef3TfUybF9vgV=TuEq`&Srx+a+S+*bf_DJ{7cXAS*QgSy19JiV zT>3Xrc;$x?YY%b6uS75t1n%tYT)42e$xOiKR1y|Nj?)T2@|BHEirg}(!~8WAsY}kU zusZD2j$AGFx5h;Y)%nUOu209Ey2qB=ucpD{T2O0sxc|IRu6F9KQSnKV;R3c`tRs7N zl16+*jl=NQ!Ppqzy}$4fWPW`;(o~amUxKM3K~h%X*m_Vf||*_ z5QRHDbQ-mlr@RH8J~FTC&^vVIoA;Wb5;)GlDgr)F0`S`0z8w}76%`R7szQ8?q8&%D zudk0@JH?R-K#>GMZWZ=~_wvS3rH?p(a1%)={ zv4jg{CfSL%osFHlVB8XmC`XZ+(YZ_o^1-$i}(bNf^H@| zD#GhN;QdjhbqdG?+;41W$4=&@bdjP|>T=OuM3BcMUTI3cUJ&(?%S;*LhAuKYs*gfj!8h<~<{O&RDH9j{C|_aB zL!a(N7U7QAifyM80Gxnw^LmY?vbn!T{$MSpil3n$av8qp3b2RMv9a;~#YAwDYz9?V$g-4q8 z>(=nPd-@{CLw8ye^yS%~-Q~`G4h}=~6P}WLE%|XZB{Xs?I&}xO#1nXAbEOQ~rq^>d z+gV@y0*XL82`RVR*VZEAr3^be7KTXb;Pi)N6G-=c{b^Im!`^Mt>*l(TKU$J`oN;=i zZ%p0HI4qk09zf9d(L~jOj*QhthG8GC`Zp6ZK2}2MqEFa1Vmc}=89oJ0Eu;sUw&K$g zsP}b)XpfA$GzDGs7Ml-<3U7`3kh=@Y2}EzvlX>lbcf}bJS%0sl!0C%6yi3dyJvp0& z6e+uyPOY}jog#y=b<8Ek59c?#QrnMrf9iNVB8g~!ZEH(QPmhOw3G97b3aOX2w)z_} zIQ~Ksr0COj#RsWGp>}R^j_6UZfgM;;vVql|SVPks@P}wUOlaRef8tmkrOO&_X zogD4;3ol`e6tBao!);0eBje*qh>1Hb)B7fGj8e;G4}u{892Lvoys!25^?i~sc=imN zxJ!a%1{`IeCDw$OoWQdyCMITW&1Au8S~}Vq`lz3WI_mm31RSaoqXiRN+S`*4pq>#t zhdq{DeM58;7#6Gj*QW~gL|t5XF*9JV(9E}aQ>F?1}GkAlSA z7>zx0co^PFNlF2)KhuHD8R+Myy#Qz&1VRU67kHW(R7+agek^#mmuS(22Z;CfV>Art z;7WC-P=$y+>|oR&cj{q_o6F zh0H$>T)vQb3)X9<_ZsuQTDZ>lrXxGM5vNckg4?Df+WcDzYKB4nem7aK!%6^(29KE7!i~g`UgkCL85-&Zmgf|qbXv8s8iT2F%dqfi(lOz& zu~KFXo!DEUGXS^gux{t&!PAJd|EG>(TIvC^Gzk~}KSjma3RM+_FR3AsE!WxCZ4!w07j zeg3R%MHOq(8e4UWfnRoN5F~YkmEz9YWibO2ZYu&=vKnkM7(Y5kQ=QrTA`a3#E*ow) zb-g0TtjD?Xr6ru@b(9r`@e~6~LHLI{z8uELv$k;piA}21S9yGH_A~sh3o)pIVndZq zp{$YVBO5c^k(&f+rkJ^`-)IpWqLx`-GR@Mgnd5Z)9{I*C*tl*iGi=D^OP z`O@6%L@GRdHAZVpiBG026>*bfU6a>*dIc{$R$qsixXpPg@`#ScWG!4j)S!Rlm#K;k zH;r!K#Ql?R$%KYTt$bgn)=>ZD~o2@DLn_C0cLn zxT9!EZr>JW7&I$Ybs~>P>Uz~&tgBabqQfq~eL{K5$Je(%A5=j**jwFY{(yIich<)u z%1plNGU4ih6RR)PI0!qTn}Z;bm9=km2krc0+XW$qn4F%mv6v?FkdTlPss~{bfHra$ z#igXK$8iCKo7`^S5NGN3n##*UyVMhxG=qVp6J)H{fnsIgyszq(f1W)kbqJouG1oNo z{x1fyIr;fo;J*ZGQTotETF$s{2@qC#roo$^AHL_Yb}zr#=9jBfx3ummTk!~^;9tLZ zE1=O0WHNaP35-|E&2!ha znevTUX+m28aEu0O5EB!lp+2#;E^OsuXJu8O2wmQBij!GfSb)kgedZTxi!&b<-9gQ> zJg3F0`}yq}>(iym0c}RmnzS#9yz;__Gy@(rl-V^OlOX*3&=;~>t#wYmTgFF*hvOwa z6MHbYnBZRf<;$oxW5(bLz`X#)H*TqC@kO;NARs(<(}tvu*drSCEN)#X`jChb}?bEwT(#}WJ?x-GV^DoSe1 zYX2Tr`f0@{HT7QB)|A0StN0qx=Goy`bxIeRpB`_`<0{I!cw^wqPP$aVpM>zrh}9T@ zj?B_{$>zl>rl`uF35Or_O{{7svfnzzJ|mDuMF7sfL0EzMSUEX2>CGEd?#CgMJ5GSw z)5wjOKXgev=ty~*=DhK7z2&7m#br0fZEA?rZke2^sXf2jDaFCT5h(I%zAX%*c6`GN zOToP#x~&-RtgNo0p?WqdMzOMzieq%nKF(Dd6P}lR;0ZJ@+uPd!y8i=CG3QN*!4r97u?trDNR`lSrD%lSQ$1_zD(c`c1o{C+N}9q zh;t{^$5d50H&@}3P8+9k?Hbc~955NU)iOic;_U1!xax)OGjUy1>CT1-8+eC%b-r5U zs%I*g!uhpmKPHlV%eqT7pdX*-5MSUp_d;F$SZv7LO`Z3#?vk*gfIuDDQy=IrK?4t| zDt(2`mhrA??(T)W&9B7^1B%J2h_&yf$Eddqg!{8^oQzW##Y<13lJRU5ol@!%ly==c zc{IYy!M(P7bHi%VJ#celu=CDLh3=cJryHMGx`%k<*GPg)8Af%%u$X45pZ&FDtTSr% zWV0F^X(@D<0h>icAS@sF+qJ}UTEF3A;^gF{p$T{q-aKnkIts3|-h9yQKex7=Ti7op zbl2I;OH^S-czAe_x*vz631up1*1A;6t0kUXWMwYFDV2j54%lNA_M7oy50jkBA!>2h z56(=Jx^NZRwAJ3#9hg-LGjuF0z}F=#E1=ZD4Q?w7AmIp7JqKOF=4|~B!Ns7Ti-Aa|KsX*KW-){AMIopI8%_1cci6Wld3vCzP zMzMfW#42FTFWh#6=$d$O+_Iu`K}uO_u8qF(WhXSRym8$Z{Y)jm@?^`Wphm8%8Nuy< zM9MDwXZycq<8o6kT)5C7XAzfw2gCc#-G#^bj!_90I=0%3S>5&~?6(V28(!qDVb$p9 z0Qd&}h96vZa9eOpl6m!z=+T_Zk*YIvW3<(GXUrDF_;C{Nh3;qO=hwYn4HtWefsW2; zGbaYN(Y{MT6~klv-U< zsM61={>_oiuUSf8)XWRpz(77EE%WV#Nl=!=vV$6W0h2v~Jm#{Lffj+Xc0We31tVp; za@?nIudU_SV_9|N$CR#i9o{*8;J?QoXm@(3elp|hOed}O!bu+CqB$srPKDrbS9Y+9 z{WK=*taB5%7D({n{kpsJ!J_qHDiH@is!$!c9fgI2dUztxS1a)s(f|-aM6`*}Dp+gN zT9-l6>QF*TN=jT@hvfCvPllCXv4<>1UHT%QN(Z@so&Yjfj3q=q5Sj2RL|(oD?*9_m z4i{^1Fnr-;5euq8=aF3s)c|*=NlSef-a;mIVqzjRi?FKx<)cDtXK&9jqW~4ZIL_7dzj2&@A%&#Ef%YJlkybWg~)?`%R6fq zlD4lht!W|SGFDS=J1dG52 zm%E+x&JCBy^&7{5_BY=P(O%;mpQ|w5tNZAv3wta%IXOFd5V#nj9oc*bq1E08s zGLkM}GKx$ERS2~COVPzYG)7Lo1lcjr7G>5ZyW4%ZV)RHf_EtPqsr{-38vR7wMAd8i zxYIeW<1;xw9z1VBCf7sT6zpe6deeARsK=0H`6r>&;f;32Kb#)CA^(+-_&3?@uS@#B zNkaZQi2q*mcbNYlC-T3!kH}le(m)c)(2&yHpH#!<(z3GA;b9nMiK48mOj=sH<($vo z1M^lqM~x%T8iU=QNYK24V)rW?RA{2&O0go{Uw2dc^- z2-q|1F?{a%{>n`V%zkQWBtb?-W|aDhWF2cLi&SqR#LC%yA0qZsthf_T871Bq{TTt}+Lvt-MD&*PmP@*dzv@*o`UXAG z;}V`a4B_)sN)(g{qKe6*?%G&QmL%8Fn0)0q{km$a?!nLh(zl4&-IVXFXa&}e@dM-WA4M!w7)JHKO0}gt>)OxYuYK(Sr%8wD3Zcep`+I6MK1qS zj_A*qmeL@Qpw@iBT2haoc*uz6KyR^T*2Z{8({4>wE4<3lzI|1^P&4{1VHsgMtv}&# z17735Q-W84Bj}gR%6=f-RXgaMe=T`|{N-NmS?N?Mwfk0Cv~u`&$1ub64b#}7hccrV z|DDP3^Dxjaij{U74y){_f`>!pwpuGp>1Tn~$A&Qrr--+)``%2o6L_WEJX}dF3ICkj zKl=za^?HA-)Px*f6zfyf_!=AEUpwS1Df1%<#B*IMT^YAMY5v*&kEczg2ct06PLCG3 ztb$%X*ikML;92>|X%9^QLD(YG?~rtm@WHN74b9Oe7m&A=358`Dj%Drq2l2${r4r4 zKkZSwqN>~l&G4m5cDnBwJIQFJ5acP}OL zMT3K$?R4;6`GyKgFvV*dy|QQc{scE-1?ARvVJ^WhSAvU{;6bgf<)y*1jrm6jPBS+G zl{;&X7k!Wm%PX6vk&Fc;imEC)v7SyNGZoGx)T3zcB0Z3+H53nB&8B_!r1_H2O9^A6 z>5==y?7ZgXYcC|$(Xj(`Tid_B;L>``t<3Q!HC%gSjV)ldMA~;e`208#Ov__iaSaC~ zBqs8^|My%o1)ae`6h{de5tOw@sE^5zUpIG~gF}RlBI=I9Hda^hB@bJJFa-=O?vzJ!!^{uP)*yG6nDD)%qNcQNF& zlN`7bD>19sbU*v-x)kaA6x%)}PNvIerMVKRZJFh9-YL zBW=Cpeeh>T9&?KD&*u;Bo&A44BaI3>Bmdd?PFl+L=QGmr*Q9%Yc5<@weE&n}u(_$}V6(OE$W~2kjtz9rZ^$A?`s%twbMy5m@iIi4B6uH=xN#FLp?>fnCa$+xzhMGHtHY$xxmip zvkG3elJAsyk*&}?21;@qY^LvljtS??ZFOU~j_mi3(f{wMjupJyTv3v%%BoN(dv}t+ zr0})>;9AX*2O8GU@BL>n{&Xn+p8Uais~gc=JvXuX_4h8`|2Tq{?$$8**`! zzQ?3!_OjvnImf8dp1?Du!3W#l8yj!Bqv?%|j2@t1lvh@ccM6o1mdYmaM5m*=vzv>lz8s46Kj(@2_{n$pnFz$E3%$<1Y9VIkym{avizT3%66Q^!q1 z6F7I({PDrs+FF=8+U4oqLX%I^fdmyL<@M#lZ9D$Ih7pR*%g;9u&Yy4aaKOBoE2^*e zJl&mxmYWVi3=CEZt9ua$L~!sENNH*5^1f@8^&A}&6BUGwt?cId5+5Ibq1GuR5Ii0- z#`B!?^z>ZeiP6zc!_u>}GkUc`ThtVv8#g&QxsO8Mv$M1JLw3LvIMZN-=Ng^P5&5~5 zkA`n;o4QfdxQ;laZ??NZAo*{=qYF25Z)MA+%-6fIY4xFCJU1P{61crO6udc0uW&g& zKK8jeH_@5PQY`rKL%Y; zOJ@iXn{n>}yobZ-zKDp3;PuhSUej%1=@sxOwLYIm>f^VES zWa!l=2fr&s?GxKE=9QP5U9l4#V-pf6F$p)3r6HstX-amhXRKys3QHuAG!#Fpl!LiC zm!6)U8Ju79H4fP@7YlQ9-~iEOWx!AsX#JDq*3;!?@a|}(ENKPZ`5UJ4J8J_=fN|@3 zpTq4|+U(~%rBcFpyQ@x zZ3C_z$YVF1K-X}6d77`y?7`jN-@n6057*q##yz6h{d)ucPp|qTFf`QK!^p$q5)4yQ z)4X(Ss6eaA88K-5SlMl!`gpTey|DZN|gL-VA=HC&Mp%p(<+3chQ}KeKNTAkccz%edS;>M=)-=(`F*uud?(&IK`AKbkyf{0W9@p zs0{DvZ3JY2+U(giwge&d#u{a7bk5T&>Y|spZa#V`J466jJl^XNvTiz$Dhx)>a}KwZIy%r!En!uCBJFZnaX;)AIp# zii(Qjb=lq6*gzwAwFr;ZBjvWwI3NY9sy~VMbUnofAj$&u&KYY1ew2 z^(P4`3JN9Gb38%NgTT#pHtg$SO=Ci zrB2)9nVFgVF(@T02JO#zoh^?xhPofQF8>Mw2DyS4z;Z9&#Bd}$m^>sG&Hop&V>a%Ik~-GK{!yy-C1s4-nywW z6N}rTXkjL=P^C^$Pi_q2bW zDAds^dG9A_z!C(7JpGH)YC5B+sHpx5IEAc?3}qUvQq~tjHvbtq4vy%$V6J&r12EBT zZEeJymer1%*i}0q9-JQz2%`G6U<#alXvn|wSUG|7&pVO5JTAN6($mRP1{Nx`_v5Xx ze6^}C-0b~RQ}Ox)Tcy%$z#sY&IP|>FDLokR0W|yEdY{I6y5O0e_H=h&Y!@}nd!5d4 zjt`BGd&8sEAmrp)?Vh5dqJ@Qp0P(}}v05{wVyzb%`&4A~^_T7pww7p9>2L@FOLc9n zJw9Ln=Z?x2MY;{`UkDEe5xrntjE}4Ob~J#&#Iu=bG6_1YeW|OvoGdZmaQz6NUs;#c zVGV={6MJTzzc&t7GhqB|*`#INClBv)z9xr&SoKk;+1Lc?c!JR2;^N}v<&`0+MHMZ$ zY_iov2$41Bxt=$XnVqY*`Jnm_cBP0n?K|k|43$TJ9DXZ|GdxTMq4OCm|72(Ix|_*! z9aY5kWTt6zZi&5!?7BFVh)7&21TTo!YZtJFvyJqy$xaEd>PK@FEPJ|umx5qghBcPH zJy*wd>L4Q{(}O#CHvaSHQnZ?Gynt6Nn4rX=Zo|^st93y-`ZDs>wvG-E#Wdj$a25iu zxzhR)xypXV$Hd^J46GZ^M@eP^*Fg+;oFJAyv5}A@z41kckqI{1EHrj?bv0d|5EFkp z-kum38Vc{5UbT0#vMLA<508w*IVC>o3@7)wz2>@U`J3Is*jQPyL)rlT&$3}V=6?G0 z30%r<;3QoX=1w<4j|)~DBzz-)%zN?h+*2UM>Y_Oq27}`Tw=GXjs>9Lg>FA&k1x-yN zEC8GaNWQ_i`LZMuC7fm*YByHuRiB@ca%UC z3nLw~)IcX<@0^+8Wnz*&S8zQ0y_7B%rb_#CH&Zedos{o2C8ZyUee#_jy6!KRm$Q?R zkqM;S9`rDm3A>b3wAy6U)5GBiEL2p~sXmo)MCF0ok{^ClhohneVzMNmufMdcjCWs^ znw_0JDmE5y3b2*#n9XQi^7N2~5+uoN{RdHR$NK+|kOpT8_14|7E z-d>&UuSCZt#>JtL33!4-ETTW`Vpa%Xq0PS5As$yq{MTU$cn zF;>Z~6cj*Ia7f5Zg~h~LUt&Q)K}hhkvZ#}t8JC4dFAz(>0`Fe9dHM2Xo%7CnYD!a6 zQ!oXPo4>Ok*Wrrm)vh(vpr9bc6G*r?e6wo1x~|RvbEX@!x0?{&Jf_$Xc+Gx0F}TV|mW)BzjHdr^re<=1WJ0UX zG~gy$?Ksb+q!7w-wy07x`l9iY~I+t%rPb%z=t-fGR50RCubxEjN#Mc^+X zBLm-eMI0pYy42Ryv7wdu+}_wUUB@uC0z^V1IYRN z+XxtAG`_o=n;m8Z1AujAW@gxeWl`hB7O-_&vI@9^v@{wQBk2THsFxccVIZD&MN-q- z2w03W5EB#M4U;)OEe%qlUT@HW;jdq5Id!lUKvwF)s+D0$NXqB>_3~A4+k5Bp5`&H} zFj&pY)yZR@>tnTO`)&V&l8Oovc2oJ8ZJMeYkJ6~RRGhxJWNeDzP=Q$mf9Z4#+>-e`R3{z zfyu$aVL8JdT2WCkjrnRq!3{XSjEea?A!t@s)@;#d>&vpgQ(w}d=?aT;$?-y+9zT35 zLATQL77!%}{PhXi+uQw<6bp#eH8o?RqLfutvobRabQ>;!yP&mZWM{9hua|`_SWd{Q zsi~dJ>UtfIRl0Oi5&^7aVq!uZ)*M9E*qgGt?9Kw3R25TPeEW2FX~_;T$-?z11;%y{&7b-jcDC_LGW2*hL5{6+d6Ffl-D8EI(V#bOc+vZoD!Ja!T-P@f=L zgJ@@G$IHtrn%Iptxd|N|1f`mWTv$LY&1(Z@TVT!2%}uD|Qp=;0t!fu)x#V6-i-L+` zoFFJTPD@MMeXp>l<{V$o^JgKS`_ab72PnD??$Lo)S67pMt^Vk2TwF~)w*thIfY3PC zG&Ef2D5L`_%LDTI6KRmSXUQgYC+oY!gaP8{>nl97l+f+VSDw|_sOwhl0vP6cvIpP? zUUq+PVC5JDbSMkL6v%9)Mn4lRTI9CN)GvaC7q=x>A5!5Zx0-Q=o&xvZ(Gs%l%j2Bt0auoYvL*^&1lBD}@CBTKa- z06hsQ0Z19gR;_o9%*=R9l^P)i03o!TviOk?*es)7<2Nr|K&}t=_w{GeOE7<1p1gr3 zCnsB>FlnbPfqZLYqq8zZ!JAbLLxxH&`CCfLfUa53=gcHR$qBO^%a!G2PcN^j($eB$ zkV`MVN9DAO<947*6wgC^B~k1f8>=wtc~JqtqlL6>e%`y~^W#~oG9WYnM=Z=8H~$Kg zjp~qfM}VLwB`G=0O_mlOKGY5)R#H?{CYs`}1(1}TnOV_vkDM6ONyoO`>k44ZL`zFc zMg~Q+zp{2|yZ0-te5Fd>O8ZOJXqOrkhr;uWkPy_wtlWZxZ{ME7@%$_%3LETJM7d(a zz@_6@45rof0rk1QIBqRUJsBJyUt31~C{+BuEm)(>7{nDRs>m)w){}Kydey%k-ghPe z)U1Myz%kTXO(%vgpj$F0me%b|@n~Inmi^)1S z^<_M|Dn`#JpJiJ|M|MC0U%gt~cLgiNYK8n6DQQ0^`^vs6DJdx*=^ikUV(t-^U^ydm zHh`4c+Jx))9qYd_<@?oYF6&Pt;_cJT5iwu3>WYHz-#_rHdV32@@P3J!P6zNdIXUUL zT;x)j6xhAc4u}R&l)6VWrl+U#j>0T>g*AZS18}L%#9zL`Br!HNTTzt+Q1BmMDws7& z4VypQ1Im=k*XQuUKsvbP`}c|fx;4>HdND-+@q6>;Ug8nZOIFv{tDHY-6Z|r(+X8?v z%{|-%vVU<3K8N>bj-QIke_*}L#en#7>ApoY z?eXL=RoagqKNz(tix0n+vWb6HEaQb6i#HDqz+e zm;jj%NFq|KX48bN*oRE?Jq=cua3I8?8Bc(B7OD^Jq7@W;l+;P&kxn@&hf#8J4t zxnA#2>Ol`MvbGj*+>m$9$@jj#;CC#p!Fb+~FIiF;0}_3~Pn|6ZmDx(mZ|EAO;Fudt z9=WAoWN+Ux_hsF*ZX~if-ca!GNw(^~fYiE&T3?Gm9hs89Wsk_;^UE5BKO0&zLHKRR){zpBYVazSqH(8vgRT2$1t-F|uN zuyF;OhGSJ;EdzUBzXFXi|Aw(pGQq-#Bs<3X=_%WU?f3DCiHS)`eX`HKCM0xM_!9Ft z(%cB$W!@iT`P@o(7n%mplV>s~Z!`o11n}_iI5|1#QhfodN=;q*aPPt7niSFIf(Zy@ zi;KV8@bz-gA{Y7#iAlQUYj-WRVpPOH(*2s3cYSrWT##3ZuuH0DLsCi#Xlq*eKS>Ev zN@B@yaM;|BOv%a9fwCh`5iy>H1z;3FFHPb*48TyVLa6(N)#R6etcXBd9Z%>v*a0ec z7d{IjxSZHrTwDYNn?M>-g^1t=cnw_M>tZt-WKNdn;j=)&jE%*w8v}VBND3!WY3b+y zGc*unMxZ?gbR1|MpK^0)6GycH#9O(vhvv0ogc|GX0|^gk3@nzDuS(Rv#l)OcXOY3* zB8Q7p#P(=4W)>CQcGDN})ijZpmzOJl5{&k78XOzrU||_r#5(u%2JR4@jrs!O{Mj8B z@FClstl zz#}VRF!YJG2x8kLeiY6L0!~YHXJ<|>a(xiIMMV4#9KAujH%_UYIX^w$7)sX)eKg*W zGvDMRh#&4aF!Fi2Oob^QpfS*7P7pHZq%;f+(o#~yuihce^akvF0gjV1Ow9robz=KV zMn;$g>2VKpldTwjNSkHR@VI#~uZYRg=s$G>!2Js? z$~5W94Cw}Nzw?dxmbxr>#abEvq@$rR9gt4aF$WC7rtv&$`UHaD&jrz%(}{zVbFDD8 zrzx;=c6PSAJ6OjXs1acT7lCt_YL9?&GmBvYFy3WX+gT9!DmyDHh+AjNAGq7H|kA7c^$?ixeWgN&Aj(~9^FQ! ze#DFy!4HeRhBCqRvb*iwt)F2H`L_GaOx1jnZ5xVu#J=cuv5f}55$SM|XJ)IlPv{Pr zarks`alv>ipkDm$XEMLXWIQAqhaT{lss-;vkFMh~D=iOI1Fb4r4-i?WmiTT#so>_c z=~l%%gqTbp2xY)*P)ERX0t5eoXx2LBCMD@uz_7vkpZK9sW<6I6R6;;ciW7_7Pj?&4 z$MVKyvnt@`n4{BccO2yRFZ~ogd?xMA{t{CQi#m%5rsyo``T6;dP!a>;3EOlHr$>b- zY29W3VQi*K{fH+lfgKUt>gwtsNjB(;phS$Kgy1nA936e*w9=H6d~#_?hKJYQ*D*#4 z;&rXv3UVxJ`5YvSg!E3u1{WstI#Li7ZO@WPoLru+wlm-wO#*I8OV2c9k(ilD>4I6T zRg!BYB-GsEhYG~4Z6JE??G;^3vKsfsnT=%4HF_l$Ffa}7|Nae%9EKkEZ076Z=j1Fn zCPl-oXR8%_mxy>1w0iuyVbRsKYY9&kS3I9t1}MRgYbe6ClxlRNRw#CLDqB~;Gy^(e zWMgyNbbDO{hXZBflul}L{f(k|(A4z;b~nDYq2W;%bxKZyy0uZoCMmCC^z?OIO-=4- zs68&9qo6rz*(DqfPXg;T$z^^iW^{ZUs4#%jpvyj#fb#J00N!U6R%~o!l$lHQ226*O zlM`aVTnxUBH$FB-JCJ@xga6v?V0C+<2xJNLnB|#2YHQtrQ6s74#w~lO8DZK;G9zZ3 z3z<)>NVW{5kVTa>p=~ZI3#=vIOW)_^D>M}p3J@kL>|V+2@wGIKJ-2Z# z0mL!I>tqUmI1B~@qK;Ofo4&dEmYm?VBfJ~8GK-Os(eLe2z1v~b3>hao`{&P}4Gj%9 zwzrEkE6jfdVin9u1APwO7J?5-J75lGG-uX;V8F-7*w@^=2q406qy(ggWZd?vK;m8- zCuBGMDo^u`!vfC(MOjU4Z>DN5R?l01MN3O-2IwK0T3RW*F0u@GKt+*D<^yFgakvnu zElh$C0}`r9{eF<3Qp<(SmyE?s5Qe|y*U`wHn530EBvgL9FvIi} zL^hx%)?Ozbse@Sr#DosrV)C_w?*sdaI|=B-3s?{l`~V~cKrE?RA4s82GwO+{si^@f zYe~WUowNw_#nn|q=L0wpxf%2twUw^9K(O=%Q549jIy-eZRMNi>4@X>YjnIuu1Y&{( z;u4JXW!MD2-y`36^m3E7u2iq@=8zQBYt7l>eVWcLqUPjQ@l{(-dgpo14$=F0&WnDEXVolKD8okXE4m!pyje>UMu+%^_ zW$r(xl}Bck{a4`sknP7A{O2!7LX7`^8V3UYXK%t>-RTq1#}F8Jt(WGXeUl*LPME%& zLYrv>Uo+04`BRSHpwR)V1pDAI^lkthobpoC zZQRuCzdnZ#{exb}Y>$4?47`J-5C7Y~@Eyl-%HMcEx%2}8T*LRj@$h78q{viOttq9j zz5rKw@uyefL%&%9{fFdu`Nu5XWyt?ciDM|!rO!s$V>y@qb!CM+`h2$yIy`&!Z1wN* zR~pUt#Lca(X;6XJ)a2C4VV(RlugwNgiHW?98-qY-NCjtstV*ZOS%!mm#oOJPry})o zz6{xm^Ph1izW_i@MMXt}vf&MWfgL)+Pn_NV)KkF?g&4oMAR~qKePqMk4vTpfHI`0O z8*jwCGf*$R9>(=n;qOr)m7{|v;Nt{mJlC9D_q*p#6MW(TbG1bziIJQxT@?`B`r!XD zpDWGL)WZ)QS>Jz<{+u#k$!a1^xcp}<zT}%Z8;CLlW5QHI}TF0P3(a`5+Ha3DFXx?03rrRz) z_7g}%+PV%`*(u&JoC@TH@*E*m+FFKequkO zyBTaP%}+V5+39jE1EWAJeQAS{3U_z~p9 zgsd9iU|5iN3%j=*khNE8h1f}Az#%P{+5vfc~Zw-eOE z-gp_IIFT}7Ky$n6?`*PTyM8o}x$g1hF4%SAQw`j%jXrqRHDcAuj5#l;d=*o+AKvCg zO8MHtaD55?X)*hYMUvDpqb?H%FX54V_Ts9E0ORX=_|61$xFvM2Rp{yOltYu2dS1fo z`6%y7&#Vm-w<_-^IJ5s&t)Cg2j`rV z4N9sX(7QxObo+Cq@A_Of4M*Eio-y1_wNMq%$F+fuqh-@j;c;rs`5K6aKqi<7?3|o* z1??jsMIA3S>Un~S%Hg4{@i0X&HZ5Khg_fhCl{cIWeO0WJ-Cm%@s%y69A>WNN>rWZV zNSgj{JwB60-H`K-5m(gJS3w(~0^|N$NXGhb@#h17eV!^gLlaDe<8`jGhO4GX-Uc#k z+zK{2_QBk|oF{DunMZBDbTnWa(XGl6B0d zTm#sKe(Ecb1h*&uI+z$5((jm7vfCV{)R~iYZiG6IXVvz2OdzTS$XS=_qRXgc;$GQ( zy6lan&(7r>@8T!Kes%5_PA*pK`e;wwfOw_TV1bz1J`rLa-SOzlbHbL!oZ51(cL%88 zjNSzT4-#5!IY>`|n^m2*eJZPE<09?k7ElE}b!NX&M*7d`VCOiT+%|cKA0717z{?`4X2CqfB zuD54bZVJ$->T%e>I&=R-(Yjo#@J@h#@TjJzPai=%BqTz6WaU*nVB$` zTEwP^PL9rx3R`}7K>5*gK|UgwuUg5xMAjwBhj>Pk;!U?-dNOE(Nlu=J_ryly>o$67 z$jJ@N&o??$5w-JLjI@0wsS^F++0#%Zo1f2j<}~&Nlc}lX`c{zy-65~C z;D~i~M`49G7lKBD8p|H-g&a?mJ)`Aga+*VNWz)`?c)uY#`bWrq!(pD#VXT{N@`)J* z>*c~(52!Pvi<+)eSZDv}2d$^aZA$~U4*rBtXcTds>-04A0oUzuIH?yZPWsz4sDOrw zSW`*?PmN48f?jWd4Bfcs2_~tyMw8_^BIWi64*@>HQ0P3O*(WeV1!I2Cv(a69!6O@E zmY)352Qf2o+B~BtPBM>GERv$$&ttq*uK2QRwbse~h z<1FhQPN1>6O-fWMm}A=w>@;jL$XpmVe;f{tI^E~JhNjb4`6R#c4ea#D+q|Jgpj;)N z&uc>7tGNc&J@WQ26K&cOA*@xj(_625gt&?3WX220n``q*nu^BV+mf83RCYGC=t`?Y zTx9u%Ml;k(m2=pHrut&;-}#uiDC=W`)NAi*iCyT>n4&tUEZaqh%OB1>T;@ekzYK@+e*U-uj0A~ zRlQ5TV`V5MAJxrAvwqQKAKUS+&uoJWpe!6kYow-J(2&s@U@SkqWpO`Ne*l4So$zUEOJWD*?AfZ}bLn!8!J^^bb|vaj zCbHAKY8nG2TO2uh62m$WddbK!4<)Z*odg4wZ&9f`yV>PkdH0(dD0Gd?WE`oNLce|q z`K2wXBCSjxMTj4CIo#&4*74*@R6{eL|ETRiB%?XgdL%H(9- zneC0uO*0D@slBSBQF%GLQpg5Y@04gtals zWR_@Qmi5=ZPz4kCylzy)3v}J5{nq`52bgQw-7!yiUWqp;34K_m{wVcS1m4{hG!gH_ zu6tu;wY@jjSaSeMvm_)W%-YqU{RPNGceM|8#FeF6Xa96d8R1(;ZmtxnBO9ye+1^O@ zrID8~MxToVe|c+(d>s}Cn`El>Fro=giU(14a{HKKtv734_tQddJM%CStxhUKkk@H? zWoF6Z#i3A)H$y{GkC35IdyR-Moq8MXa}To$cS)b6#5sj@c6l66<_x?w4h_TY*WI|? z6?`bUAQwdD_FK?=7FvCL`2kC2ujVy7r%=N;_v&xj#%_}iGE`zUe+8x8bVo!U%!?-{ z(~zg(t`Of^eGUw?p7RjWD8*4>H@`uY@UL=m-~{zST~Mzo=TlY29Uj)c)hIE2MhV|U zeqtWEi>+YgpWYZKK8IvtB*8j;9x8drq(eKiXc2YO;gN5H%`wowg>TiT^{bG|`pejl zqwC2vGnZ|%JP8AY(Hd8UVw3`>iR4O_%QmHp1!Wpvn-u*HuQKpG;*E*?{2m&*%qx3u zt4?Vp^XHcgtLSVf#GV6xN6A<~5>`Pg2I!idR$m8F80gLzon=9pbdwzO*E^V%4#sp^ z2}!_vbY4a1R*`$o7jdJb;CWu7C|8FrY4({UC8NgPoW*(a!^gJ}NOu=3d(hR}bcFY< zoxo#KJVv;ol;4A?l}^ex)@f($u&P|0h8?Hcj4KWm7a$)1!K~j=D*f{d1t}^*5TG zivpg~k(X(bEv_c~U1x7DB0dO6rHA-p;4jb>j?8{FAx)w77Ob<8*ivh>31}cO^+H@~ zIAcz|?eM<2VI$p)cu?0UP^?N+(Id#?Tp<*`HsyBa#(T7DA=v-(=bLvn`<*6gQc!lm z1L@*}CixA5I7Qk+*PLwE{cp;NTKkCSG@VWSUIb)J{1HtZO!4}z zzntTHcOsU{N^siqD@`4WSFmz1SYsj?Gd~gsB2T(KX(RKmDkF(`+eksjFqyJclB&LY zU*uLy4RQB^MwOlPg>D7e$P+l zW3I+}PnYZ^Lr!cR$=-T2J?2l+qG|FJdTx)USIjRZsFYOQ=5@pU)79eb=0V(hnO)6} zQMm7(V2X5*1%5!58rD7{2@#kPWKtvdb}Z8&2>LeQCDaMZgY0ZzQWPpJP@*C}k&Z!O zheK6*XxV@2Y{B-Lc=-i|q>Ol<)i=)%h24rlh=&+-cKicx-ibxI@{BaMj7D7!jDG0U zT@-P=uw6dx&|`MN-nCOwobN57hCtX{IxaHwwo$lGAoQ(-ZqbnVN{@VlnD`pr@h7Ji zg>MVWHz$VMmFVVg>HBOEnOB7_qDE!h>;-5h@)b0xKk`w76CsP4MY|=D$cw^%zp^lA zFkkP+&CLzc#e#yT1O#!Bk!&{eeCVW6>u|E?YcrMvmq(qQju7hD#a9cmU$z$___!j^ zoDl|5S3i`e_xaHXXribxXxi7qexs|@MZoe0*+#kEw(I!Ws258aq?Zly!|>xsH;M5O zdu`;)HL@><@FOsK=VC~^PM>l=H|&t_>4M=;)7o=sB5jn7jcQuk3JnCki=Y)N zPHbVoptE1{=8I;N)ooLiSN-WgMDp!Zf~JUIFYDp`#vK z?X8Ja+BcT|Jt3s1-kY~WVW}epL4n_{$QKByv~DrOG(3(M{)+pq?lH+5uGb)8ccbK2 zcCt~wKdNDOb6x5V+fnPVKJg#m|564KM2n7&^c~om!mCl( zI}di2P>iHhp!a-Wfc{(+*;VUlW{twFNPHthvh)mr@DS@T=gdBIG4EN;|tE_g{nmwOGW z_^{l9h7lWZ66HR8co1_$S;}UAy}dK0cUZE%hQ_Xsm`C_>RJTe1qs0l6N42hlDk1NU zW(yORp7OWDv;w!a%McDNRZCC2qteHp{+2&BWI>^E0d&BC#_bZ&ti82m4$*A%eNk3NF zfXYKCK6=&gwRdujflos}iRPV11K)RBJ}o1*aX23cp)Qg{T=q8_kkt}&Dy3KY@5+Ad z>gjR}v^^M^sPP^&DFqf54m7JrCF(tE0oWw#sl4;ZW2-$`&J=$@CDx<2c)be?y7Yy+!5nIH6`vx0Jk*xwtZyYp@XFO8glLU;#^ zc%uLM8?TEsLY3-W$<@~&N+=*AGR(*8;hCI=KrHh+#Cw(do{UwE$Msz1S|6oe)^btvCKWLMQ} zV^Q(B+@+9JSpUuj&20TGE$=~bn+C--!BNUtm07TCdnRgWuRvjhf5i27Zx~+Eojxoo5PM=TwDo$Ny3tLTcT z75I_Y8Fb5YG+bTT&QTU$PuHn{cXGV{?aAn0LTY(EXgd2tAGMf%3SK>Zb-(Rj`S|x! zoo`9ex?dho=`7crB;k{?PDjB%Q@Rdex!eazcO;)oPHMnd$fTpYOWnF!l9+>0q9Hl~?ay z2(@X_K(JKz3A}HcC`zn(RHM<)&ME4Q{QJw`#6(eHp|z>0kb)x{P~$<9UDQ8HZnLnH zT^*YhKRFKnmXY+%#=8uQcZP=VztG)t^>6$Dx@(-A6fpk2+4qACc~#Z%d^P50j~v1K zaiuYGxnWDW65Og7chzXAUf> z+(CWpf6qq}YOpqQ$r66V&HX5|y&Vc|gUi^ADo4er5j@4}awvnzu>7l;gD)Jt&^$#A zsk@r*>ix;$;c=x0|!PfTN#<)%kZ z8sRSEepe)m)PfeC{ekq6$8w~-b$VRp5E=`MR23B-`%b{zHyzjvV>lnn=NZ*xg5mQ2 zUiJKt1)7K=_4pj-S`{(_sBA{hUIr1O!JfL#ir3}*RUG#vLn4$skV&jaaOFzlm&*)! zqen{P+WS&smh(`o1uH#nv-*_`ViO{+S_Uace44$6#no|}4e)G@Go&SESMgy6OgIa6 zitmvVf8x%jzKqfXNN&52#vS&yhwG0K2g@<~a9{nJmUIfwJQjjLmU(OSnjB&StDi|H zyB=i(#HR65FW3#kJVhiqC)ZU(L^!^0I6l@A>HHqdVOv!026ck&mR&dQuq4{7iy$N`LgJ$Bl#!SRKWdYFQ|O?3H9S$ z*=ITyV#Q^lK}&m-_W2l-@?LG0&@<^GC=R8wQ8h4I<~mGN#PcuTv|>G1 zBy&wSR2`>fcm@4ujftKKp<|Put-vE+QMl~~1u}@ivNzZ`3AX=xrMmX>_;t&&$ zJ93T{E-Y`mL96j0axs;@Ck(mP=#ow95z*^nn9J*xxqX_K-Aq6{TxHJAL^`wE z-Nn!k)DKI`sQ&7g_e$_755+uDv-;ZNkOV>Fr%VTSuy^6kj6F#In0LK7Ob*Hh5)Z-q zQern*-oocY%~?z~GD&lj{i=Nz>a;FwmX%{G5y?bEtB@BTrM@b=$DoQOoIaBZTSbvr zg_i!+IP-+o@0`wM*`Urd??gy>cYneW2HfuZs;S{hOM5!|9%(13)KIv*+rW8*P@@fv zga~P{Qv&|?>Bg^jr%~w45endOI=8r*1=oHlRpQb;-1U&b{ppmiOxLQq$F5PkdIg`)x2|{<#pU*)ddBvZXw@4W)+Ov*n!1MC@ecJ`) zLSfH}7_N%qou1m*q+T*s@hOaJeIFDn^A7|BblyOrD$*@FDPec@OB~LH=(PzfEPk4K z!k9gpZZ>@Qr{`zr-B_=s(zregYu(btQ(o%#F z>Sc;K=F-gg>Bku-t$e#r;J5J(J6O>0@^RLxtb(%NBpdry&Ns{Y)+t8^V@p5n<7n?e-}Z!{4os+t`_SmoPGFojU#+2tzTLTyyc)g>TpPInU-_8m-0%`n zJC(3Aexg20MH}c5tY%q-$n8JR=Hyfkww3jJQX}7L78;j9d;SXu)T}l3knVv#H0%(5 zHiyF?i@n3waH+Iv2UBt$Gbw{n5x%`gKYy)u=PEyPs8lGEvNjOpS2PgQ{MekE#D$Heu#z1_ZM#*1ua^9x zK!*6;i)4M{xf$qdF&_o5>r@B0ukIj@qStV=IfN%g!C_u}z^nQ|J`q>qHeZHq1Xa&+ zQ=rpzkfuij=Sz?zCgrGB7}tgCry#DYLy1TnUy5SDa`FZ}I z`|iHE`^E79JJw!nt~tjX;~C#)r$B~AB&AisS#a?@tv;pAioo*wFQ*S`X{NTU-4Tfd zDW*u6)XWs;d>>x2V~i+fqh4R!l zW!uW_n+z{|(+ZeH&CTyTj`I(y5)PJV!9kDNRcsTEgF=ja9M3aQob+hrH=@stY>PzE zQZG=^ubyDyp`=1so*A#>MuVvry5s9oFee#J+M zD3vfstO~OSXNa9q4sYd=+MDhS;wb%I8^O2~#DO2BiaYr=$>l$X)2@wGng*l;(5Ve*F82a zZiR@EV%Om^AVKH=r%7aLocZ6@n`KhxK1`DGC+ z^6cj)bgW2;ZVh#f;$6jw_p?=wT)w-X2wP>?Cq4@ zY|qN3$f(dDuqw<~EEczX8)sXdsHoCBic`&SwJ&yLErhWz%04;cM>4kSLpdIvHBK26 zqxyO2M9)+VeLC)4WnEIivBY_46kAIIECPrkli3zVqYMUe*T8(Q_Wfu*Zm=8ER0P?Z4#KRWCpQ+~ zQ%ng+2Y@7LM&0DKe*W#Lx8zZ6`)qdu^!|p)eHT~kOYO_LCL5TmgQHP2=4m9+cjZSt zAAa->nCCbM-$;)|YiVNWOIrCl-}?43N1SEkCBM@l=}<%1A;wu7CDRS9&!X=zUmN2j zXUY2cC|Fna=F}Aw;Y{G2dyffO49E}5+sO^K@r~jk)lo~-Hj=CrwtHP(3_7|OeZZAX zZcOi>zCU%SK5>+zf(E`A&ZVGZ%g)H+!!)lCqd)gh ztI6~*qbKvt4jRc^xn26)*pV%QUH|}whr@qX!<2KAy*>haXop0PctGG9ffbwpQijPx4eD|Px#sL)ei1QTmI?A(xJSyH>ms%fMb*Jil
  • |MKO(zl1!F+V=v3Smm>&3v}J}Da%#P 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⪻W2N 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 literal 0 HcmV?d00001 From c89a9c3dea5ea3ef2f6acb6ccb88aa3836ce0c87 Mon Sep 17 00:00:00 2001 From: rospogrigio <49229287+rospogrigio@users.noreply.github.com> Date: Wed, 25 May 2022 14:34:36 +0200 Subject: [PATCH 118/119] Add files via upload --- img/10-integration_configure.png | Bin 0 -> 10229 bytes img/11-config_menu.png | Bin 0 -> 14953 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 img/10-integration_configure.png create mode 100644 img/11-config_menu.png diff --git a/img/10-integration_configure.png b/img/10-integration_configure.png new file mode 100644 index 0000000000000000000000000000000000000000..f023c22595dc0bc6661a457233dc74183e5415aa GIT binary patch literal 10229 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 literal 0 HcmV?d00001 diff --git a/img/11-config_menu.png b/img/11-config_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..22f29ff8e72ffca6a9e17d4a0e938879d8d4633b GIT binary patch literal 14953 zcmbVzbwJbazpnxl7$_~EG}0m#UD72Xh@^ywNTW0(1VlQ{eWi?er-3u2kCV(Gl zl1t#3n_~&@z@Lk*x+?M)N(Y$M!2=>2IZe3>7b;@NPAwteF{z8Hk?VyESK9GE7rUGb zpI^8jex{}<_t?v9bC%fmvHr>VE)QvwsFLCX38-#fSRu-os-f^pDa6zLX)#-UJ>nOx zr0JR5Q}c*`*2*vo6-WgcuPVJ_o+=$tKIWS&JXGA0A?~57Q=4MT_BiV9z1n%ndq{Dl zxQ+bOw?M*Lqa?@aQ+AUjl$W<)pPFHSXDtoYhc-10jp_9}EqlhYuZ+-lE|D{oYw*EM zTH)|F-y}jVGeM;|IJ~E4FMJ5w+^>q68SAk<)4G`NbNFYh-rfF-No}e1^XDCF5n*8) zv8k!HWc0VHd*XSf%UaH^rP(L3zz!@%N=!t*n71?sg@=W$L@>w|v%R2{3_6I-$+6CP zz`?cStUP-^7Y9eBGMMXsq1K*9EH#v{=IXHB6*xxOMDcRo@hJb%U)&yz&R~gPVRH&#v%u+3>`t*MkE>VuURxt@) zZ3tzMzxk+F1Dx2eXI6p4pOEv&44lu){w~jXwqF0D5_s)B{wOm1e_RbAOXuK=(Z-!6 zC4)ceVVqE2{Na)iTJQmFJ8!6AxA2cybQre4$noQNIZ^)iNcOXKW&APSXO_8PVXBsV zS>X4L14(cqUjn5HEf{~EP9+#W`#9(CPuf2|+!tTTcdW5QEv^Xs$z*<|cbQTIQOugmy;l|EjH_du1@ zPPvaiwV{wbEyryKoL5y?b$uU;%Xv~Pn1J1GnI}tY4JPhd%lpLYf7BW+wnQR3-uP*2 zwgDUP1J2J|Eqi|IDtUgoUt(1CLMFp~%m|I1cAoyEnfA<{E-IXc$GFU_X`^AgxoWLH z&7{WemsHk_+o-0;PAl;=F{>}7N!9bCwKV5qh0rT#Ow0K!VPxK?5Hfn71+TMXB<`Mw z{aEpa`h`9cZlc77t@^&;Xxhs$>;lch^LxE&>m&Ikua>`$ebT%R=VjAK9aC?hly6osmPkd8Wz{&@ei2H@Ur@LK<*Q@HX zW963ZjY)#_wdkz@wWzbi#E|+9p>eCc1>VNzepwv^)pf&R~9i@9y1= zUV*aeN4d(!;t4m1Xtvg+F*8=HeaSKXiH}=h?MNxJFHcG~A_PjyCAS+#S$ux82WaK< zv)E3G&Uta7`D-W{j;|nj`gCtcvp7-#;730QvR+aul(lQoRvgP#~e&1EgaB;BP zyI7PEd8@wrio|cW>$3ihgH{36+`Mm$&Qq+SE5^-Wf`en7_3kLCl8`T@^}Xo%fPjE# z3HLuo;yMei{ngLEiQupp3=ZkRq#J(|wh&0{dv5u3%Tty0Ey@>M7u;c#=-h9V&h%gj zdQf;TABkIN!VLzTEw{!Tsz!30oplz_O;VsL%+ahZEFlkTon|YOUzMbL{vKcSV)C4< zw4TJ~bRLhHI3J_-2PB2CUg2G&IrYxEF23;BQ0{=^K69xzZ=n|kU7`GWA$q%hS%-?f zTSWz#Q|tGV!>o(j>x)L0_-ziSSmQrl4EcBvLT~mv#5tZ@v++&dH*xGnl_!77ODy&2oZbfD7ngv5^8rrSxCmKmNEBbCV{FnTnQyKz67wc$1|+sVPY)x8%# zcE*X~{c$JWO8EEi7pfa%O1@%p3F;D^Z0=tt>^SwaHoIkgC0-kG*ShxzrTA!0luxXA zIs9xVRQ8AyPB>zbXL`JqYd>Ck#vnweXHL}NhnXEGeBmU=MMN`Z`4pkX#V6{JA|wI1 zs5+|C&dC#ine~MAe-Pp`cW6!!YTOAz))Qa8wt}K!soe zID+c6s3m;pwP7`!qhv-oG8i9r7TJD9@`s6hUbb>HlP#z~hZhvGxNp0jzofg&c(-Ga zlLc}oM8;fxO!DM+u48WkpNQv{sjMh;HJVxSq`r$az}Mr;4W5JTA&v#ZM$wJ<2n8+k zli5xj6ap1@9Z0=^-Rl#=R`O(ewGGC=9xg00kuFrnxvZtyOqrjb7;IyE)Dke7(07>R zL7$ugTnjCA4ddq63WsXtHZ#*?U}y&+UU?_@6bFQzuln+Jk)y^O zWr4_HE!yWy$rN2+F4sfGvqM?_AeHOixq`Zdm2J5?#P@J2;zDmS8_&=03arX^-#6x% zLulVuXFixG7L4ln$)HO2>Pn$*enc&QY)a4%3T5qA??<&H`(sE40x=c0NubrYCJcOh z>VK7)%Qgt{rAwCZ1(9^k&$`Z!Vl)xCxBE#;v_2iF8}P2Y{&N45@dG*V1rvkS8WT_n zJcc`~&x!PpMuI+y@OX!LIe8$Bjr z%4-@ceQHm`{cuN0Mv&HSXxER0RwPe}*FxMtSB<=C<0v2U+Ur$PaH;>~UxzI!9L5BkwqvQc8n$J$W$9{T(bw+1cu!1cY(*6P=hqkP z9%w;hl1^&QcTSUCt#7DQwk7-R$>rY89nwUED$J+upICNkz)X6`9ndA=I>j)V6u1+BsbCjTkv) zUv@}w1*!fmJAg~*XRPhI?>P=LnK824jl0*qm}p?Oi~96Q3mP?M))=eH`E7OV&Qpf! zyT`g&g`+Yv#9K&Gp7aIgWff-kgn4aa28f)!xAv^)q;y`NTFm}aC|_>+M)+L|&ydQC zyMuZo5xYg>e1pypc-?G`bRvn7DmEg*F#o29x3gK_Hj3}t45Y84hHc~vm_m4#`{j(u z^FDr)cn^)V%e%~~zIW*BrPdy}&!w5@MLuHYJ7JXU&mrNca8A)y7iVV4`mmb6&EK`% ze?9&JUq)vnJ=S$-`~aXKpPiiLG>eC^ONQjmtA-0I;WLtB*k#u9l-W=kVO=k#;DOEC z)G@AMtWCuHSMtQf1GtCka}=ayWw`bzPs_oMbVI!xsyshZe zN(3n}r6nGalJL5iBI5@WA%k2*E<%fQ(V7d?J;3b}Zk`Xt{p+ zMTUdn+n;$0tSBkPJv#~-TmV|}v43m7@Gnwnz1(my3W;OJ?;0v% zf|>0UvNPkWGQ$-Qvj-$*U<2MAVN^0B;Xh3uE%pt3BOQo$U71KHL{i6SISeFYk};`o zdfa!dMoulhiU%{xoc!jLy)x}8OG8Lc7o<}@bazGid7h;femiVZ2k6Dc@Az}0oO=eF z(Qi8}+B`*NaETFeAq=`jo5wKjRi!A`kvz4AhwSIta^GSt-i?k7e!H#F{Ou<>-&?}w zA#%pbWEqzaA-Dtr)9$bIKas2x?A3`c(e^`4HgPdoU^&(`KTXfaq2qx6eqrMhyPz?~Q&Y;cSjm>%6M2)%tdjPVX}@ zU7q!?BgBVGOT-qs2darXmOLra`#g*n zoD1f_gRauZ5|9@?IT1N}#px$^dSZG*=>0-3*~L&kp&oPv&rX-XK9pP4p{QJw|Iq_J z?gNSsPuMr*KfZlDvt2>x*%|~nBXhl^HH1{?OKHA*;{7|+qOd^KtT1)dQ(wmyqkCtz z(>tU^Hp$=%ClTG4b=u>c1cN)4&ly|pSpL3!u0oRAr0dUMLA9uoamxKxHcIYKj11?= zo!`ad+v%>7eZUGT*IV%LV)h@Rj6Wh?x>9w$VY_TH{eG+^d|!xW<@lZ z^)8Jm!(=xn2}@px*JJ*gm;W%GN)_)rBcHwoOaQ{v!px#_HF}s=h>9v)`GiAQOHuI# z!TP@EDYG1)NCX<@SBK4K>8L);*SeWhcPDk4;Yr?OpA1LY$vAUQocS5xdV7&uM+ZLc z_Z-h=)~Av919W^!E@9L9&+njh0ha(pXx;yMwsiX5aiYAHTl3>e3`dLp^>Jt3<~No8I3Lt(L#?pg zPhObacxlG0H!=g^Fj+Za9A{2DVUzw!ITIq<8p)>2Zcbfd)-;8OPuHb=+#Iz2kiqwl zvYNbhpMh06?XdPEwL+YuMS7i)F{%0at4}5K*V6RJ@f|Kr8ioKcvk#Ay7>#&d0oLfV z$WC2mTw{kG(GW5kFMVp|s{ZNS&P~?D%x*dk(cRA4>v!`7aq;lGj;Abz-#+E% z8%13c-Vh2njud=a|C8{c)>w&&O^nP*VeN~Q6U?@AnuueBps>kTmu!GM`>u#BT##Ie zRoFO2!ZDB49_;)X&J7i|ANvt4zQzE%*|(%L8|khCyH8fYSl~AOANLm}>dXdS4Q0t8 z00*5I+1#5lH^bb&qscI0%M@c3g3|JsKPGoE-QvW!0`OYFC9Z)^U6MN6ho2w4j>a5} zYQrQV$2hi$TbI9IcRomh3MQokm~V9o(_SF&A{p;NvZsq8+mT8)^z{e$2S*2O4ZdIb z!k8BS>?r6EH9iRDcgZ#eGaoSjJ(}6xx%v2$P=Wq_Spcr@iu8d_nuPo3rrqz?(Tkz1 zQ}pJ(ADYsG;^p@NN2uvyf&Iwz`NJplTux50b?CFcK$?_y(F6;KeWL|=igpPc>jIGYksm3Jc0S;|X%_GSe{-83DT|P6$m9J% zS?r!7tM5*r_6*HHzVGp#85?mAI(b~WzbArDz~S_07rhiIGzCa#`ayG*Ojm9e(udUW z=AdTfAnnDaOxRWhl#c)CIGW*3+dLd_&i;@0ZI>_$cpW6k3ovb*|BWRE&zV=D<^tFS zKR;xK=jlg;NyI2sfSudO-2R)Vogrr<|B! zE>zPcE1={zC{6!W`gHp2pg`6;D$)41`JYf0pKh|px;E~Ae(uUA5zx}!E%pR7))=o% zC`Dd9Q<}FgYw{ImZcc9p;BKk!x~S6z#L?+uhN%^&v$ z-)}k^^vLK{dx&9N8o@4})His#IwLj@V zcO{+X`SY_Vo?a&a>>BCI@`36S;crqlOli0zMF{D5Q}_%Cpy#|NG=S{V!Vt`mTQsc^ zV-O_OK=}T$lu>5bVE~X2Hn8Cb2X63KIBwNl+qKP?z%q8D*%i)LPU>+Ho;Gs>`w%Es z-^sjXOfJHUss<$b2w(WSXb}yABG9##5^zTPt>tttvPG*=YXxmYtwz_$q9b1*MYA4D zZ|2q75?d#Jy3;PLPld3A77>kb6BQ0*NW!8rt)*e;_l|W40|mPH28&l7~bTZP6op95Oy1&wLbtjrY1ykAHp^WE1HTO=xDzAM|z zmV;0Uy@$_)>tf{2b0sk1NEd%K}Nk_T0!^Q>nz7 zwfOs)lv@NAqy){B65|)t)^D8{S=md8F3jGWqAbV`wuGY(QF--ZEbCzK{P~2?^-CkH zL*tIUl%ZF3x|5!E* zXIKMT6FtlfmlvtWd``BGJ~J&P?#m-03y8T2BY>BVRp4BS*$GZSomcJoaF*<0 zy|Hj=O%Gt(>%=*mHtq(eoV{|ObBnE0*wpB-*rn~qbmK9dhTs1Pgb_^tg>D2Ih<{KR znm-?p)~Hm+;bj2ch5c8&_TRy~kiEI3hp2m&@p#-vt5qw(h<0hX`xg*i{vQFt|Lz0+ z8~pg64$tYhlt91cP$3&N^)hPC$i|wu>@Xbp!zvXFsf?Ol$1J64lCX=*N_1)nV&X7Y z@0e8A@4FexO>=l9QXL{||2 zq~;hrBY*}jhNq_C#O0PFt$HWtuO}m3B#xFm{&?Ksx>oVt{hJ7spMyiGOp%CI*ML2R z9NUh55J!$GzcrEcI5jU5^nfX#gq}W9R7X%#>73y<(N`~(L@#4{c934HU4kAzyRH{6 z+1ukkmSF6%U}8Xk$^DDgV7K8#=jnKD8Gv!B0K%Detet%!d%C0GFmT0NP{Tb$8p9fJ zI_!hlas-lUcs~`WCaZ+dlK3GT>iN5MmLuE^J71#E$AhVeJ6~S+;Rk!uB5kNZmp0=d zl3ob0K3Xt6QDNoS89`T37iSLiuvc=4$}O>__xu2nuBDPXL~1bPEZQ_>HYoj335L(Pbb4t!V!P*mJs`|CTP z(oTZPjzsO81QN59*UtM-HZ+b+SEo1Bd3T?PoUsk z_Mc=Sik^b0Jn(+uv6rNLDq%TYURD^pbqtOyaYYVz{LT)Y>i)plR8CY>uEOf~v)*le zNiYR3^_$%5N#go=$LbwkBsGI=3Ohq8SXw!G0C=pxH_%U78Gk~y04DT1D>kaysOVy; z!fT+II{;N+ciJf&%bL)YpuNKC!`r+8xD)Z-d){(uO~2T17znqkSdu*n&!C^cfwBMf z#h?~gNK-(OS`V}W^kXbrftcb-ah1=Zok{h}4>gX{W3^<4%74lM`fT1-Ve#(3BU@Se zpa~pi-r2z1uSPTVZYTfaLJL5spE+nysX_Qyb*`4qn>`TyT)q6b#Vk|mc1IZv;xaY@ zdK>`l0`{K~-c13<*Ps)fPWw4P`WjFJ+Li+4zIg|SH%q&Z>LTYZ#V?^*?Ym1K?yh|8Vsd@I z-2k8wOCyP$>)MaqF7)O!wpm+zmDbsk>QwQDaGvi2HEZA=A%d?+bc1m{tFa$Px5ZD% zs?LJdHB8o`2=AYp%rX`z08$|a@a>~ubUGmMl@+)=G|#RPZSGWNEqeW3976FxJFOv z4+e4%sWBNzNoyBl7vvv!gN`oUUG5yjgYjv9ce%MVY{MH||369E`(2av9^?X>k6l$x z1VndHZynp4a~^2omSbcGZWR{gQ?N))D8FY|v}K3tK$W50e@E{VNpJ?5{*KJsOljEn zq!m_(IA_I#%tJpc`fyAo_|j!5N=hbDZX&7?OFU*1(n#2-C*c4#-qgAwkwQ>Al?u}$ zju$NM!|EqFABF9G_yso74%pZ42HDNlyO*fMu&^+-`-fg({?kSK;b-jL#e9XR&A`Rw zd`VryWMO_fSW?50dmF;i{83hH!P>(X1>Oaj;}x2+z|jJo5@TUNu5cTWM4wKe@bhYjQcpe2<4+j4>00TjF)z^+`ya+iExATSmx?y6Kgt}7ik z5118koLb?#dFjI2VP~S71Mp4i(WhDYUE(fImhRXY+*wd0d)rK_8>s)~$>C}?L?JT7KU2{XHsZLE_ThQDs9XdN zZ}!f?f99#tG@wcwq#TnnON&cXKdYjqYn`7Qf9(%-`ow=W#M#2Rs1mW;qGb8BM=5$`iT~2XqhJGgl?W8~k}gYWB%te`YrYYl*1{+IQd{PDQ@H zkHt-3Pn6(ZEUXU=lO0`PeNXK>fM}BedXUfkn~ny;vn-FbP91`4CE_fZ#~{<_mnn_& zc;mjb<3<)1ArQ~}_@z6O%xjp%vH#(d>B@il=T&2Fhjs)&Bh~7UW4Hqs>!PijWS{@o z+gvXuoHub^z4T8XW<*JiSG)(A1=TxzuN0oEJ{_*pvRH36=c8a^v|XbaOzpZm#;(r| zI&p3XB;n1sJ-g)1K&$0{n(t3@@cyUj4tSC1XU_jcQu<9wmdKaqDR-?SaG9mz?gQvsfQ$x4k5$qj9p<2-zu6tVhF4%Dqg$JzQ{Jel`m z@TM!c98!1dJvM!aweel&p6+BA73apNwrow*bx~umRdMMtKmdYYk=(eAC&M7s&?n+B zVF3zpRq8#PDSR{nXfhSj0Ac=Aj$wIPY0OI$46-SS}O+no(C|ACP06^o)+&z(t&rJzk(Mbom0s{mBnjcFrembfW|++8sk=ZnRAT6#pT;YVlOu1 zTBoND>jQfdEP>HDQJ}k-sBBmP{orqAq$8ZhkJjSgs5PVtW$%w4?*)Nwn+Cq(=>XB?Yxj4$f=H&{`O(!t$gWNRCi)C_ za##g&4wGlt?QI-)_<#)70k)cG&{*!jo}XL{AW;R^^c^Cf?pSsfHlTW;kZ?V9!LsE? zM>O9`n@{YVv+?G-neVQWfZnI~HMXPQxgVx1pziQT`!oBu)5KJd1Bb-HbKb936D~6w zJRNY3HK$;zdeL9`R}%x&O}52$^wVT0t3SiEuA;v)@cr)Of8NPe8qt8z;W0ul(4#hv zmSYCDWJ&$l!GhSI@&&&IIXT-U+n6IM(->XL7%cCCG)eH3KgEuwm_^z3*dY!DJvLV7*^U zog@IUmli)9A$caXf+y*Q_mWQ4HWb#UYs(Ua%;&D&GW{Jx`z=IHzykF4%bN9N)~A!uSHgZ`JrqDTtAOaHApjzKCad!frkGXFKZcX_Rh;!VZGePpm`PDM;r7x z?DPx(7E;bAhF)VG%juFxRwBOuJkx5Ru-?bN+=EGTryv^!fTW@37es#*Y z#*Ob9q5-ahu@bikWV_3^JUogbwJie~n!#WNPy_tTq)x0RL~%SDu!(ZZIerlTh{PjA zlb0KC+;A_W`0M8Y0UkH09lxQL2rmFA6~8?+H(YKNcgWCk8`A;Egutg%N*njow=pS_ zo}~bj-a-5t5aTf>uI8XNu>=S+kC;ih=xG4%@JX4Obee2{zr*?!Z{`?l(OI{6Os7ln z-;@%GA6pj06|Vz=kCBQ>4BNyWtS_&NoIHBjKh$bo?`Er=`xNLMGJ9VDrWAQO#71$K zV-K4!R_i=p+`B8Xt)r!Xnyn(?+kL*h<@|`DQ?WN}U=1kP93lpaiaGM18#+z}%Q&OO z+E_`+>+=0xx79lYms5ba*flPWtet?g+Z%Lw`fbiUdu{j%_Uk+b-@@+V1)}r)L-EUu znp|$T?oEZUVs@uVj9J}?_NSrTBOa%fSO8S_r2Vc=lN`6sr5onD-*6E*=}d6-*h6I^blMOV1dd_1!ioT?HW# zv^%g>UM5al1^hWLiCP$+sndo3n=Z;fJ&dnWpXM){zxMxO#QbX;q%}+Q_&2C~-r?r7 zr^!{|sdzd(>A4LI7%Y&%9d?&G5ik0$!3~OZxxVNXL<`%Fbl`JOa2~*i1q7lEjg5_a z?6WCVGky2eU?9`t2reDM?ic@bz55q!DI1nP!%+Oi@JcAW+KX>#AU(E=bVrTVJSj0Y z0{FEU)NC1NKv2=3DNOw=(sNvF3y~=U_|{|QRoAgAXvLYeI{w#8UK{i7w&sC=jmKgL zy>WxbW-%ZxNtu~j|3Hn4)8!@jxdA;&^=XL3>j813*O2NvcSCvyEKDQ7`o#;MdFm2v zst-##90a{cc)(UnwZt_Mhu?$y!R`tgTpOJHctGI(Paj*MxwrvD_8t~Cof*7cQ#qF`~r6Ju0a%N$5yTsO(}qUx5xZrqbR{HcdK+c^Re92;V>fe(D@=6;8XvjOM0 z3IlP#Rxuknk+=5F5MB=Sa~hwVX6uiSL-(z{e392rk$#Q6*C+y@rRcp0c2k5omDpXwF6G#n}8nR(MHO1 zB z0LLY>>2S>CY;wtuo(~Er;86xCL%%*K{pDpj4=f+puYMz>sNP%cg9%~)p4tDn1(X`; zA>X6L(B>@wS=J!eOGqVm3=L$s6n5gbJW-k9{^M zbE4tVsun0~9AWWVxF}oZl{fh{380S7H3yZ?>j`>V;Z0M4s4T7r#&Jm6>YgCNi8TQ5 z4zViy0*p{I8M3GKZo7{A{D#OMsaDHfF|}ZgeP3I-IWvtni7;yRDM6Aqmz(kC*O@=H z)ERdE+=qP-tGT)WfC#*R2f)`fW>wqCUB+iJ?elfWW5>zeRG+On?1Y7o2tMg_7MOfv3dS92W)fYLiZug^i@$S$K%I*4jXZJpkur z_*5cBL!wwck-s9}JJ($$Hg9a8qObz)Hlk*B!Q=N(tIF6Tm+!eE%*G5oF0wBLUFN@P zgOG3Ky+Nr_z>VYcuty*c++^L>v|z>p5_lC{4NGD~d_g^+PFR5=GQ$N?*SL9*ZR)e0 zZ0JU62 zxmTbPO#uxGj_WJ~VR(!tU|``%Bqa<46d@)LBEo1HN@(_$vSXLW&wrxHU~}nYm^W4{ zH<)-B2Qt{?r*59r^QJF9y1i91ZkBl~Qqa=dO*DMS7x5yI*NRdjdYM-NUBchb=`l*Y*%935Z+06n; zylyNGe115^C5xv)v>EH+t(jA3+k%X>I^PjT+N`{SiRc3(DPvA%Nsj|}J~3MeUXwgK z>XLmi{(D_Et1N7Fw}^G?4koPm&X%gsmI*X*A=fo~4T^!G(2I0!v>W z86aCx@!j0Q;)CG7dS5;`8)HIh4Xv8J#vM4j#2ho-HjX`ZI0u<-*fm1Fwle48elh1W{1TM;Ea49Dt@` zc44WwYG6-3nINi}?nJNfSkG|A2URlnN@Ff#VD@k3ceX7{$?ccEf%ihre6YMTXqsr( z7mRg&ueVqJ6!9U)IG*__t3LW&3rJ2`@+OchubEhdYGp`FS|>kU!@N1m#yj;qTN)fe z6yw@(fxoKcIZxQvN=OkY;lH6uC@emvt0 zY$m_uOd5+x*7lD@=_|x=KjR0|J_i%|1Z>oa)VsBf4D0eyA6fZyNw%*jH{IrsAP5mc zZc_=~*gJMp9_9IVTNi31k22Sn>>emL2X#IklkU>SEX7Wo-rLPpL~W?22utnF6s}-xGYx4bNv~N6p8QU#-)5^c;|HYalF0d z3N0!kOm&#QLJ$Oo)C9Tg62J{1c{2av7FpE%b(7}Cj_3LDF{4FXrElHer&wg?|0Pe9 zy*q#RuTho#tI$aA?;R&Xel1CVA#7lE@4o?M|6QBw-*KTVzWg)l&)z0@G@e&}*Ky0P z>Y%#1dV(A;7j>75l=Oba-&-?`{|-H!u@IBuo(5F$kA<2i*4@SpzB9@uP1b2bX*Dmt z{PMHUI7}_?G3PAs=J7|gsyEe(z&`JqyEV$G^|7^Pbmxn@SbgiB1Y5G&< z3WIl=8^*0K(hcX%Vh^R5BFAeHHY8;#ZHy_H=NrTs_PT6ohu2zq=1fy;uix!=5-+coot{XQtaP0W-9tfgYC6#kX3fx_BzXm0`=KdN{y+g$dBjnJ1dw$ZjTa%xFJ{gyGMR>&VN5!gRYZ2W>oO1r=x>4FDtC5_w zXUzbVMMJyHLZIHDeA*#+|=qwvE!4z<-?HFv4DZA=E&$` zcRnqOekl2_hz%r^wTfW$eeqaV)2QflMDd#rL^z3Q*RO$c=gOcb$d8N-d7Zky!m&w} zDwPDK#=xx>7kf2N4nyb}U9Y)0I(6CHdA4O{fwT1F1zq~%*3^?4A*Y_rGtin)^-9ZnrDv=`|FPe&9s9zZFPWx35qbmI=+NK$%);N}_qJji zVZ;djCO2^Z+~1qz{#RKz;uXqo1Uo)Up6T!1ZXDV3c>6xvn@s6%Q4)|`?Tp9YwDb_v zfm05Tx9`+ESbH12_n!^o$HgP#@timTkfx$Ud;mZh+`>mR$yb)Ikvs*0W)5S;MtGMx zPT(0)MNo>|1_@tTH4ohPwaHaHao$ZyOfq z0J%%E{|zBof(1D-N3x5AyRDpOccduvmh9gihc6{}Y4~QOB?=fA188hj=Fz>4Hf-{# z(FZ;c`g7LUq-Xo337#)k5Tk=z&#?{xtd%8Lr4O2*v2jzA7)*u?4azSW=y8|BJlkB#V;z0s#+A?|WqPON6g v>iMIEZ&oRlgo-n$+d^j_NT=Zi)91t$l Date: Wed, 25 May 2022 14:44:28 +0200 Subject: [PATCH 119/119] Add files via upload --- img/9-cloud_setup.png | Bin 22177 -> 21521 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/img/9-cloud_setup.png b/img/9-cloud_setup.png index c51927d3fc52ba4b66f2c14fd8ec61eda373d08d..cc70a5183a8b8963953238ae493dbce5155afb10 100644 GIT binary patch literal 21521 zcma&OcRbd8|2N)5rKpHxWF#RwD^z4O&@dt+GfLTeon=Omgb+eegizTdTQ*6Oz4t16 z+|N(fb=~*%``zC^?(^|DALr@dIF8SFzh1BBdVd~WQIg-gn{M}(EnD_pP&j{W%a*MS z_;-ek1g}JX$UBZ7yi8?eu3V6jIci~LZft61v}FrNkZX{nLg`uha^35fWH}jro-1RP zvCUxNjEhuBCp&cUW~?e{@THJh$qF=l5UOZcLe5{9)>><<903{_qo@;%Z z&!qitPz0>bPIdK9F&q`*ynM>JVykNAV-*Q@W9#I&O=>Am$}(ExW5#2Te(RHHSGGw? zxD@!$y`LK1pkKe(W%!RgTeLy0-hOjRWphpOVb?xBt_!sRFcOhpu?-uAcSS{4#yNu4KS7lFD~$ zAx*~yEy-Hm+06SL-Ibp^CAQ`_6o-6YP(M;!dwlfI=R;!K9(EL$+ujUb{HKQDR?men z^6H>`Aus8z4rk)cziy4a`O3U*GUZcX3+YiA?UUVGNPUVGf->6j^7FDXv)mg#uB>jI zt+4vZnmvPFz!WUUQ0J^;}v_;P|@tjCoJ`C3lgT z%&TpG&YiO!b$7q!t99Z%+hfn{^^(Dhy_P4_;*-XV0*{bBEH)Gf3JN->sY%Bkd||

    1`Ip}ueY?HrDSHT|zNpsEwD|EApO65Xncd9ZEpNp7CtLPQ z`}Xc*Jj=^Z$nK0;lZtXm|F&Bl|m@Vy=JAE{mRiY-^Db{g2*i`>t;`Pq+B|Htn? zf0{Zw*Y!qzm=4~u$;Kl7s0&)!QV zGWNzOkW!jl{Y^Z_?%Y&^iLtS{E-N1VQEXaTU}|bA#p!3A_eB^3<^O)g(&}ozRn8UF zKkh!CDRGGR^+$6zid@B;(^x%er;4}c)M%0QmNquox;j*}Sc=mBn6$IeuDr$9Gd&%c zlw@OXKVQWfIMXOY!SBA|qGD^JX zv{WHd_@%9I;gtC4(*=R{HZ~vh%XdF}_N?mbSN(yiAa2c%zC{~<(z@E#hD3_0BTn6w z3d!;ljZ)gjD4}rmYLIg1uvN~FY)jTRZ{A3(jKr5!RLr#%ZG;?;b||^6uCThY@*~Tf zDN#9QVXFVwrBI8tCCiv7t>enk1B*qQlH=b~`bV1Lg-)His;*ucE92uZl{0)J&+g>j zy?bNYDH50qYVwnl{fDES_xY55`QkykYxnNHncmX1m9`>w0f9L`k0M>CKeZ-8`h2wc z#~;UWbfrg7EaQ&acrqa|wws)GxP1S9JtHrM94b@FKy zTSOh_*v_o=QtTopXXE9i@GtMvP7G`o+MCs}uIH4@DHq?gYC1LRkblkyUqJmLbv(QC z{xm}tiN}}K6(-k$2aDn}n#P%jWv}LM-V+GVPrEHdH|rq56q+W-nv~Ss(o%{Grl6>J zjEigBPqnAmZfzuYBsZXDaL%o$#86FLDu^q`L)g%U1@KddwE-=n*< z?NTUr&O`x+-@~LNlL+r7lk+V?LW+52V)(ki%6@Hm?KaYa=}T=CuQD@(X>?tJG%}6( zoqn50OH2RAwH<%OIee$`*PChkTaMeuty31JyBk7J-n7Wk^7Qc$#Hz)yVPWUb*Po`M zrna0LxDXK;sgYxK?CRC4rEbqea6?p7%3g|D^PDEJ(H<`G+Vk=AX92wuPrVd5-Xkwu z-zL3WUtOH<@t`=s#H4)d*3q1t9B!@brvU*0HTSbj?%e4cO^h-AJ zVBdt@X=RDo9YrgxY55K7vw`llyza}Fw0LN6vn4i``fsGDF**+H=j7r_%T0f&bezd$ zX6yAtrNLewrlFLquG|T(ZEDHXXU?42wrv|$CySt9@ceLtyP0jjjd**G^>JLV-e~8k z-n+W1bXbjDGuof-P`9_YzfMS)tI^$f78-iUMmsSn$#2?ybM?HOoPa?Ex$wQ8o~6CT zy%rygY7WiK&p-0>>r1a>?EjTxJ(63WZczC`$n=1sqT-g9;r=f_9&^-9q$a~JHUyXt;^y6ZSwPJ`>~vF{%o;(RI7#?z`Qg#_i8 z`idt<@VbKpV*sP%*RLU5k9>V^jW)lb7qi-t)loOwUs+I-*B@}2s6G_$?}dfAj_!?6 zkuC=hhSPR4OZ|Z@?d{cjc=L;cm8C}SS-saOhk4)#!?_2D0=C$v371>;C z*u1QvaX2a}YHdXROx%YLPib@<9@vh5x18;hHLi;oOj)3#qg(ou=C0`EBrYZ0xIQXxsK2PNFgmN-t;5-L2{(uN^yx?}1r=4J<+(wu z4IH1Xt?k6GoR;HRM_b$4GViwWbh&S?*95GOjos04=-r9B9nUQ6WFA-eZX4xK!`o*g z{^-%8St&+(`r1;8GqsNo3zGclMML_Yan`kE9kn<0(i5WN zr+b3Z{cP4Qt*E${nwpyERZNlfot>Q%T?LU3A8s?Qeevbd!Q*e=z01+x<}ATj|58lG z)bs?a+~e#!UfZwe=+tL6NY$XJYU?--QfFjlF84ESD(LDmO&6~6m>2vxWS&1u;^~cm)Y8F$!#6o#>>y_Q+ z4&J$Yx1h%V$rGcF&-Wyj2ZG;gWO}Ej@)V-WTTd4(K8uNIpm3Ynjw;Kij!Ye=mvnlr zuIn1|>2BNH_nYSXsi|e%-Ou>Nz2ew?$ls{Znf|R3mF$;Mn`gnnrC69}t*r%%Ry(I1 z)@Cam`=5x=p1MPote#eJi*JS~gA2jz=~jamr}Czy9?P+wJ9m!5PZ}SWmOr-}WtP9W zv6{a;6lwP3!-K3Yr<$8VcOoi0D5R=-OMOr&J#`}8`9ldtdV0(S1qEG&U-%6wuH3j$ zg$vyfv+?^ldP#p};9_B-L1lop9rr$;T?g4exGYlXi&+h*dSvG3zueexv$D3nprnKe^^8Jlsd&}J z<4dtYpkzh1Wq(z5b?>{x=su#uJ32Ztt-4h{Ia0}$mM?wh4ku0u?fJTnjuKWwd3pK3 z(9pB+@Cz<365SZn#FdBl`a{5h7n2n8(k4{b=-LBSSzgkg@LhiR->7QQgsx4 zUjF*ks}CysnMFluGus~*6&2ZYcx}bmO|)nF$oe1R*DoWxcK!O_E1qS(=-rLesjjLz z%EJ@z@+Cb+gr#^?871T{1?exaEV zS4`4*;fktiIk8IQ0_dx;rX1>4^%PcYNcI&+rr1%*&bq~EX_h!jFpiCl;j}!Ys?pZ& z-Mi=M=}DgCmz$SonWM$jp@rhfOZfbuV{&LRtGnWUR;Js!(-zE5+M{wA*GE7msst?hdX#@9w@?`8inEY4jLSTXkLCAgZ&==<~;qPs77UB}WAW1h%|= z`}S2{UdPQ(FD|pAjeE3=zJBw@3%z~18$eh{aehIN{1cW|ZfjVYbBwzj9YcUL3*-7rQ9oI=-b%az}wFQr^p4z99b1Qr#= zEG*cSl$MgJUYFs28dcdJTUfZ^NBt2k1WPA2D=Ta}={|=x)vM2IHjG6*ZugZvy1{+u zd#bj7>bT+U+twI;OD^@NZC|m@7@vMhCCe-<9NO4;Wje&;f$;A@x8JM_lN}->1G~t` zj*5x3clf87(mi?d1kKFxv^nWM&j-QgJ%*Q+l!n%p=ev{xlRGP}-n?0}I$flULMMxnGdnvQlYJ0)1h=oJF;+%m;fI0Ca*eJrE*M}{ zyBRw_e~`B~iTSPPVPV!wGkR-lYk&81sNKjLPCfdMC%C@$rPMkRLt*x!4wRLdsTZ}Ae85wg2M*{eDk(P`%pJf

    xHYZ}U|3rlJ~PavySy8qV706;=mGm36fRDut3T<}r;r~%ZqypjD|)TA z2FPf)RW~#o7Z;CEO;&6A^~F_?9;4TA%D!(|wj%O>+Yy|oGlBuW;Rt2$!o$|qIz?k>++`;I+W@&CDWN~;6@ES;N z#@k-!?Aa~A0uvosfdDVUW(y;^G|U>qNp&+w-KSr-RSzr=lvunJ6eL-(Fpq zZ2PvsxKw`?2n3AOBlXr!QqoNHG#$5PBQ*AtCr`fD{N$Gy;}-G$J$GM!Kk;1$k1Gve zoMN>L7}wJBXk}X%9Q)K%wKUuBxI7r9k#(N|cQGGL8VpWF^%%Mbx_-vJ4&lDOz8ghu z>#81Q-Q7%yE!ABG&IKJtJp*88s5U=;{}WtXtfHbnR_(#Oez`7o=C6z;cWrzD7@GZI z5V$v~#QgVqd0Cm3-3hH0A$G-x^QNYw)LgOqS>=`{UZ1g_qQ}f*2iPeqJC7^(PqiTn zFR%HJ(%Y{(WUo?@qw}8;rX{b9mRwDsG2^496*MMC{pN41jMIzT_~K247@-G_9PzIi z>??k-{qYfz0I&mq_N@dv0=jkSKhk{*7zNz|J^5X7vH>O&t^?k4NkM_^dctL9F|osd zlt;|-AE2%=7eQe89On$DtABKK)SyB!vDoap91|A{_PWGP;VxGPAGZb*5D*kha}ew9 zpS3^*9XN0R2Uq**pd>rH_tU48-_vxJ!AAve*AMiS`{M*6aW_C0^SfIduoiE8G}(`F zgx54RH612DwV9Zi4K&F!9b;wnn3=K2$jJEdL2oG@Z z`zst}Douo*g@nl7ycxO3*$^Knb@d%G?%|nxShpttgEXd zJ{HrfCX|MX>imTZyONTuDjDnnXz&f$T0DTdvMMT6@p4c4KDFueVkSh19Flfn z8A{r=gU6C%%I;8s62>O-fDAwG5Ey`h0B*8 zXHooVPFT0#;1sX+YMD+q?Kt-wgD^3-Wpv=t@#v?^wdEhIMCkz+oX&K1JB^>*nDIF? zGgre)q4CL;E9$g;K$wn`X(NrQt@bcZH7++dVs38k9Q7684+4XJ4`!ILqYGp>9opbJ zW!>d3o||gjSGEhM-+7`s8u8aUOpc_2Yqj>pX>L1+obcfl#bLv6V zvdqVilP0%1Y3R0muw^;2S3v9COcZe8Ou@8kAf|qgRnAhI`?@;8UxE)ds55-puFFf1 z1VI0{Nnf#A#NX<&o;uaK`e~Q0!o`a$Cr*qejlT>D8Elzk3kjHUoB_ia`t0gX;_<+7 zeu&?0;zj_|86~hRTx#^NpwQ5v^@W0>qRrb2w0vKqTBOM>4@#>(dh+DWt5?O&;SN1t z>xzpS1p15l_8pLtCU;%(Et>7R|K`@(`zPbfp4qlqt_1>e8I&b7RBI*8w6WhV$rC?> z$5T>;d@ZB>Q|g@NNoZU(M|GshW-kz`*#U8B-;OXFwmr-)XXV=@vPmoc3T<`tQ3=hi zFKvA;&0i$zQ1%JG;lL-5b@RP=HFhIbOMpgt zDX!$|BU;I*WexAx6dUh5I=?Sz-L3(+4xeRjY9)1=?=Ire3xH6i! z4E0IOWJOQ0#bGG?_KQ<@525$JPD~^XthBC?JM%s@b>qk36KQt(z=B1?1m4AlU=Mv^4&c2M{TL;pbhD?F@s;eN*}KN*MMz z2uw=-Cgm@(%$T$dAWC%L_ zqRv=Iu^;1`7$G2ZXx&?~Eqb>ltEE6|O1J2hM$dx+MD(9;-@ftbmF$4rBrvk$w}S*h z^5WuH^bHJvOe%XVEJhlcS5{UCndRikC(tF)XD(d2)L(yky39}jGrBrj(wU5$oL*%R zZDVes!+bj*R3%J!QH!1}_p>4)P+q%s?S+^%`*Ee0Jw0DMpdUSZ@xnKb1B9Wcbosme z)UOL*&u9+$6NZZp63_sGLHPmt!%y7}wXYY@ZX;Se-hC942M{O>(+AhICdYc101QC1 zppIyu>+9=4Mnm;c{ur`oylL+1$>4!YC^2zGofo((1DPuOXTjPAqVm>$kG5cjK}Tg|#|N1dGODVAaTtAleu9dM zijtbzE5VeChUWT}D`$c42=O8}H51|&x-fJo0_wKDImZO z)kWfZ5{h#0aph~;+I2D%lEk__!NZe~ob2DG>mEg@koSLyii?ZqhqM!H%eW!B?%TWT z8bxlBxKs3J++IS7nkw3K!!@eMY$pUZLgj-Zzz#%=U*5TM2di}A=O_PPzqAgW)GWK3 zRv@RW-0RXG-~9ae=TA*2L#Ia;?SEO467T;)%EZRP@{s5(fS-&`Bh2+N zXQrdZa^ zOG)QYFg5d#_U7h?n6ZOly6mS;h2-Z;fc>R8Xhn-zdykBmOck!)-@RuK?_K?^Kyg@_ z5C*FpuE)eRZYAo5pP!#-9G^c61O38>ftEo@NB0$K4eEYC^~?owlI=-JI>K+pi~F~4 z*G*3P`f%qSG75_0xIMT!PR`C^PQN+O+a@~mX!1JY^pw8xc>DUb=jrME;D7$vGHp|w zn;IM%S^>Jw7k(K_(*p{r-Q-VMC^)#v=zs53!z7&h$kIri`_U%M{ z)M=lZ!IJLon9-KRgLHHk_4Qd&Qc~1i&4J6%vaVo(5Tp)*8;HHf=c|y3q45CVWID|1 zqdV|)NRfDKjc@YBH3FL>dTVjGwH=|DYL69hUr6C3LLDYJPe9s~7qZ}N*LNoT+C}3Qxl49kxS$WD-D_%Ua`yIFA{K;_3$@nqPpi7AiOH>B z*_PJ^UFagznIxCezFCUQ%onpQ$h5xCv(dnzrr&rWV$KLT3jmcSvt453s8wEPm$E~0 z>qS6YK+Z0{BTLU?B%OsZH$YXZ4dW6Lyt($8Ok0T3o^g{vzuAKFbO?LL;b-3|HPzBG zXqA4yQ{1@zvr125@9XGnBgQ_~k*2~kKIxRbfL;<&_Bb#)*v2})5Hm6QS&e|)^bZDVJ5@glh* zZ^j~zb&R`m<)mH9&!2vk{k>rIY{!qsCvoPcwtW9Chb9m1<{>>EuWkmR&``kDY&fO$ z&M{HZ7m$V=6BQ@N#^`>(1m{6P`{Vt<+b=~e^&2mk2+>_oR0LAo=3g$YtV{ti3d(t1 zU0ojb8GiVfm9?D`ON{b9-)} zTH?>Q>kthcxqOzE90=_cC$>hC-1-&910+$#Z=Sg}0qG~MC(JZo;@yAb$ha$oab`Fg z$o%gXWz4sL)bTDZ>+RzqXiT~rb2J2p!d2r^PtBj$k!N$}7o3XxxvyOL>kA!am6h{e zye?nz(xiA?F0%(OE}~7M%epP*b?;|jPy`>ekF2by5WqkJZQD{+RYf2XLZ~?|A+eT` z)Mz4+mMSVLxBhwtgnYO*s_d>0QHK!Y&D!rU(p>&e7C-=Z z805Iei$-V0?Zv>r036?9Lauh-Ty%HN>UcA)*dhd@XDmf}tG>Se$jHbHqq%y=q3s>9 zm>F%`N1l%_cl!j;zs#a(kLk+|ppgy9tM#Ik+D#vS*n=hW z92v9AxvP(AtIA(pd;JZE-gUFvu7=Lrd*)L$BnP%?TFxlcCeqC8pecE5mQRc9||a}wm_+zIloBXl;-eO3}2zv17;3!U&86C4n&dV5k>E>;(j`Ng(@f%Yb>RtK0qR zojqwn=j8PCq8gP_;=#PmfX@4sXgM)wX6Uxh`v7K~b04gD{eUJHj!rJ-#Hv+@L^=<;}+M4N@k zUBzLOnncK{sT~Rl2>~aqSUn91KPH9=C{rJ^Q$<}pI4H=nyeH}Xd&BjWKd6qDA3v%f z&>4Zww1e@2lNI~4w0GIIX%iX&oJm2WuT+r2L@{r5*T>Xzz!nH6W^;~rvjjCsh}wtz zccOa1N-7}_5sF$cyP|$moGf(lEf$!?g`4Zs#K%DBQ@C=4GOcJs9FiG6Q zCNYpmv>6cj7v}D(9dJL1bqelz`dVBa9)*Q=ArjO9mWAwiWDs?LIg$O!ahG3TwS)l*xi6-Gcq|z*kYJ%z$emn zcEa#^2=Ki*@4i6+g^uvb5)#;{Wc8+g<@~~h469R=yz`9%6P8ff@Kyr=?Z-@KqT}TQ zGn^L*UQ+X5JE_C(Ci(Om+#o@2H{>?%DOm+pmLkp&E_{y;&NdLH076P+`CR6&!fyTFRJ(1^cJ##4nq^>GG!aC39RP;+&aMCsw?(*9Fh zdl|YP*{)q*fG}jQe$Y(u*wASa0+S7wa1?;QdH66v`alG_{$OhRmkYdI<|v!IuOO3{ zni>Gn>cF8x-fNLT-fIJ8j1JW931L=Cfms>}nFoRuQ zgR=#07A|14`@w?;9-&;ltWtN$u}IE*7kUCgwd-5{H>9G3BN6HHTx|KJpH6w;fHdZL zY-}v9mCko^;*5jHwe;1fyfC*_RNr7eL7IaU3`Lw!p9>46z$ic@j{#s{u|R&vS7cXJ zJq%M4`~d3$Hjtp;LB*C4n2CVu(CZ97X=1&x9zEKVQ4B+nnT2JWs_LW1j}6Ow_apMN zABzDBBE~=DVnn3)XvrbL2*61wDJg+%Ly&oB#*nv7@7!TIcC0+_iHwX45*j7OnR=P+ zzKyAy5$2%K5R=O=_R;1OXi z?Msn@_V@Yo=iVHrKN@Ej#tJF=rR*3XZ6PuqS2Pxv7E! zf2SNFzy0@=`#S<5s{QX@_EoHF|01#c&{SH!Z*SmrdH%Ei6kSyM_<$q%@$2KBp7oSX z4_fXqy16!2SMx-XQA?{>j96UV9=TgrNhDpO&hx?;KQo)n{qI3?6qhJYzo2Vx0=qrky5LetnK}WN?LlDq+0k(#hvgZb+TPiN8Z`|68CWuI#lK=$~ZTE zyYr>>82RPmXeM;N*Xu0@xh1DkMxUMdKc8hx66@oaB%+4wvEsd@BKVJ zEti^{JYWB6)J8l9iUMzB^>s4KNS}cA*H4+=W=uYetfds>l8gNDbE4NFFyP$Qy?h)q z>nf~n>|VKyPqs^nZ;H&Wl(JxeEzb|nx2Cy;pl3nN|NJ==ahYHDML?4ULeeO+oaZ@f zlRmI?6^C+)+!jC8?4kO-_QTwt^Q^&jPnq6lIK_nB+c7BMhX0NLlY;j^p0iTmfZj8hSevPK-%e*$ag^l#>+9JatsSl-D z3mqD&Q*m)OOXFqot`A2!iEQeO^rY(_=Y7JT$yd#xOHL)t85J2!_kM2d@}9J4yV*Vs znr>dphonL~r4@JElW<6gl$Vs850AKn1;PA`>ebx(i9?JN-$tfd8h^cdd+RgDV=}c} z0vB9~#muL$M~(T${bM+av-LLr2Uq@E`Th$q|1Z!<{4(Z11ys+MN{NrDxjkuxK?xCS zjZNaLfY?F2`sz4dMS{%T-JOt5R78>dv@@+&hzX!9N&L^J6OfknK$cDRwX6jqq?Rh9s&de*LG`Zmy6+b#_!d*gd_I?P?M8* zDE}SD2&2U(#kkCJLst9nfe&(zeP_)WWc1Y3egA+eW75Nng46_U7(#WJbG_$`Z16?Z z&Bd5B8i+9?qod3`JW~3+tUq5;jR$! zA$}{y;yDw0vpMMf_{yp7TTX%^ah!fu?cK>qez+1y5fQpQq+qe#o-;5EgHk7@b0DYjUM(i5F0&!--+^IcL>Tz2-7fkz@V zPwhOrAed}cR&%zzTu6fBbgo~!Rsu~8b!h}HP2ee*uY&edT9{;m9F!$K_bIc^7l6b?_cxx^P6s3@RS?!YoJB z&=boUWEV(@$ZSG+L1e&?7k?{g^W`AuJQN(*08GD6AI{Xkvei4r%vyHagS_vU=aQKk zVLrXAyf9zxPq;`-ji(~GC^ECyuYkDu_9l)p-br=}Kjr!!^bJ39kYr4~eA3#$4gOa` z6C1Dpsh#hw9|mSg7Ey6haV>VnMZ}LZj`1zd>5msLYBvXQV;v2d2&o=w`Zf^+dP#mp z_7tC0h0L0JCV6u{kpcg?Z1J9(C{^VPpq~G!UCZkv6}5!rMSey*ez|s6lJg4xXWQRB z!{#|}#tA738q)o&BdV(Dnkk-MUeFhfZtt>;oDp5MG!?J#>$8#@x}1HC3ryu4d<97R zi{1fo9LNK(u(F1WTC$9ej%H+eWcieW!dEmRuFjOR^&PA>j`uS=^t7TjKSy6 zpMOAj4Dwy|>kGlCR^p8e(#P4Po;82wN!9c2W=^pXzW)oUy-lNe&2}?kKz5*^A*ctp zu3qiy-?!qYRfNNd}Lmfzl5|!WcABt7X2 z7dQrNUH~Qpn}$6PZMy5uC9>JWyBIJTpc8R$a>nK5tsacj2jKX)99GZ0kdZ(OmOl0 zny4@P`8Am?HZ~6@J3~{7Q8kVa&~CZXL`|O|iG3LgSE(LK-}|_jcQwlH$(R(w30u7Q zLVSmJMYCh zoh7`B*@aqsIBK`>VRbpOUe=1=7Tc8)9~9NyBQ}8?5kJKGHkS=;M!t@#eTVe_2enH{ z+F0b-4jdtx*^U6E+mVsa82^0tMy-L*`T6<1egFP-UfvO;_&~4G$&U*NTr)PK?ECzR zlGg0joouUdY3z%+c=00P2SP7L1PHzff;&VKxHVZlI3%PTZT`ZQD^Di`nWZ|zJ?^OR z(PrDmS`Sq1N7mUDN(Fco)-v3sCZWAPrRPv0m;rAvUNBB-YAL0EvERQSKL;==26q)E z#%%=Nh>Y;?=WD9ek#!Xab2-j@AvHBM)kBU`MD^+}W~tIxw8XN(su{7m zKHS<0bql7g&SD05d3&MjX{E&L3m++CS{p|(+~MzF@EZt(OU*6=zMy`QeRk`t*$v+d z?lbKZIcN|Fy~Gt1F!;#Gc^&Hep1qTy8kR4H3W7kXqsyps==5*{oPZa`#|{3BwXXG_ z* z?CcqcY7;1_e3;lG1W%l>D%{+pzV0QzXOh{Drd_F@PT1s#Rzs99+*n&iiMFpw|ME-t zo^s=DxxOBRHG-)kKrU!CpimLOa0x6O8We;8EX6H1&>H|kGZG=Y<}CNX9(#rR1-S;J zWTc>15C$r2LOvWB`1yY(dT?~K67~m0Bsi|xtEiTn2wg1lheBK0uUQz*?U^m9aN>VZ zasM+@iCX&~(Ka#s|DS(Z_K9|`dcj6}vHsf$&!ykjsCLR;-6)iSX^nVJqmbgl3(Bl> zBUoFH<@%p`H#`+iW3%wgc7;*aG08X=TJzL9U`GIDmhQ}&w$Fc&ZQ>XRy_q;b-$;EM zQF@eY`vGZRJuH1R>9-(zE+*#tVU6h>lY}jgsw4zy$YyzVlX1uvt>4oOj*78EvRU`|rq3o)zuJZNk2Wfb9UVr@fyeVG(k~}0xnfWA0r6>|DE&8M+ zq}R$2XD8{D>qayU+y$QnXSb!RO9{gjVoE$R74Y5nF-cv%af23;HMGoV;3L3Nn3FIb zUCd-(kBdWbgl__?_(_5(dQD?Xiwx{%s91Bh+lZ6LX}}yepD(|~_w?B_D~&7I5QWzI z`t^>#vFbDEJKzi$a|hWk6G0Jl-^L$5e35K7&fHH)X;DY*SFZo%epb9`guN@g-p8Hy z!&uMzcDzB~7<+ZD2>bd5t$+l1ou`WV-a_n#77QI2nW6Z+Jm)LNtl~ZPnnnyUJZmx& z7Bp>U18m;sBeP=TNjqj;gl+?3A48GAh#VonB`D_>z#>q%P%bqZ%mM%@)VhL#!cie1 zs`w^{a$Cl1I=hs3+FUIjbP9Rcx&Ly&!9K7b5coq(+LLzMJ|B;N>23TZq@-BNt{ac0 z5P_7FBbhGRlwuFw1#Wu(K96wZy(=%sRhWqsEcgU^2}m5|A^wXm-fSE8iME`gYnU*!*6<{pBi)^WN|n8J zM2FBd3Q#is@-X(Ej`qDV8s@S@5f1N})+?+T-aIydVn;;oKX2UMC%h+$NZ1@Xa@tVw zJ@Xz0Y}t^ZkYsbdKsKBHTKQkTR`$swcSoug3E~n|X2tz6n|J`NxGnafTlU;GxEe5rPW*@VQ_c;pa#l)03ZnyXh;$9TKvk_mh=`%AX-f3=< zyN#nc5nnUbe3NE_amwdoxX{X}!>KUru3>)<@+2_UuuB1Zw;FUY=@B|{pg5a}=>w#RY z)Vpr?LH3E=1DN{Im9A@Q`n8O*LC(Q;1K>%_7ldDEJ-(cUWrD~zL6wGweaiJ?;g!II z`uX!G+1|Z`QV+We`Z?ec=5Zr;jB zV*FiEr4Fk>lnOh$`0jq^#<{{Q>VuDlDYg~%7Q`kbxDh@IgZ@SY17R}aU;tK-(?5Iu z{0^i-h%q=YV2B(R2NWvApb?9bdrbQPNj_|xfueyiggxt7Y=d637aJe^N@WrZ7d`qodcJE2^_c)@{G}lGNNm;hyw|z3j9W6~dn@qjRjM zNB7vVml{YPrU;l{RXEh&A1(-`G-h+SwqQ6^jxt#-z2OUkZ_r56^0wR@gU**9GKv~%i?2=mleO;n~NTn0|A#QNX zu>^6{G1b-AXCQQHY;4R|VUiZYTY%Nk>}kpGtJQY0PFwTttVEj1F6#3B*$#nniw@~Z z+smBgEI!JWGPcfx`?9 z5F?UY*u25>XHaw>6n?FIhai9_m+s2N^=ll|>O$P9i<;^n}h>pt2Jc9B}IH_g^bND>o^!?y=-RoZmykV*yfz;OmR< za6N?ak%8kA7r$t3&I>t^<-nt-7vFsNK1YN}MMV|34-v^fz&1i!VhMT$x)&SE48T(h z%MB@`HwbP%dbA6lc5z;uWzUm-(KpLA_=|*u1cZ@?apFkyd&4KT>M7oeyRRTbX9T|x z7xK@Z&EAyX=QA=9ZyR5g!gh(m!J>_JvxS5yCU0gvTK$xA+sUK9(BccIDpL-}_S+=~?!q_d((vzS+HU zi14`0%pSM5Ys58i;7-82uJ!jnZlW(^xK&Ledbp{%*{ZclU+T)Pqb#k-6)K~?3ZDjU zZ+GJLOtLxudbdnrW3xNw(P$f0{KB?ERriv!(Dx%=0lq5C*7QEIwmcQB=QDg&tY4fu znAE&Kh;vq^i{bO%cTxv@dMT>i^WyLCy<|A4dzH0`_kAfgtcq3Mre#1a&LXg=#-<&i z=0w|Oq9lJlsZ{fW%94<8Uz`R@#+8S0b76OqZj9_WR}858@v&#swmnhzIz9&}B@XM3 z&>A@OJv#oI^HJ5_cij8;%5`^$g- z>5xKso5JVs_0@L_SgyHRr_-6p${Z)#=F)wfT&6&iWzSIyiH6eod_NLpSs(S?^g&*w znwMg)yj`k#6Bip_ykEoNi(s0J{NGRgAHU<@2S6N-#;aKC!SLh|iQrH9X?>&1AyTrs z=kG_`=S4f<&OoCeb|s<_1qRL&kCl*Tbo9hSUM}fjM{pTzvD(k~I*4#z(Q=)+iHXV4 zD5XrOdx!-EG_S9&u0E>lx3RG?VK^G(MKAtc^Wo9#iB;*?EoY@-67et%GvQ0voJwqr z)I*x;DCn{gwwiNmWO$~h^Ru(FLtyJ6Hi^J*Pby@n?8WAvL2Ufiv$1*p?_KPW_!>Ye zu=7}1f$hL$MMX;(eEp8m3%i57#Pf4?-Vi!It@ppJepCxBE$z|Z>JLtT?jSaIn1Nxi zMfuENW30^UYwwPz$(Qd=Kp7&X@9pi)S3zv|om-X&4h}{-(BN?sBlDc?1F+TJm6jU}bnz6k;P#1tBT$OG>7QF$H-^ zhN_!$aZ;0%ewNbc>`(^?#|YFTteVFdIOrwJi0 zIfp(dP5O<&W9H!M=ScyV*=6qCJDEBzz{f}Il=uobjy;P{1A=8c@QBjCoY9efq>kF{p(D*bo4 z{NozanP=ZUcU**MHa^$NZ9^C;PwWjzZDaIi{{HjYP5v9HFa1xXj%(&Od|EjW#LIrj zzjMxqn|Q`RaP_UnIR)0@0sUI@XRHerxVHB@9V{9o-KLYqyFW)#OPgN#{E#O9?f0J? z5V%|_OzYVnBqn(JbPeX?n_g9xptnoZYv<2@m5sl8;L(l>+9)F)mew~|?f+vb7RKe9 z2`95YlQmQtv=Yl`=T05(qu#yKrMpcH9~-Q|_BJi8h)O(U;zpR`evKQPr?zqG{)yF( zJ-A~lk{T^er&#uI+n^5Q#G_Jg#8B^HFd)anDLi$K?ayHjIrg8r{!h`zFo_!&82miI za;=~-{Kbn#Q%aU=oxLFeY=?r9yp#mOzlBl;jd=mB1b_DQRpGS}`gkBH$vfb`o5u3B z*#nQx%bq4Ly+TUo$GxRIWO8yF!*ZqA?HM<#ty!MM%(6R(6$j*I9ml{@H zP_=t9ARYS(-Qno_Pdm>2GwhhfC@ws(*0Y*#C-ipWCD4V_oAZZm%&5dY{qSADt&h## zwlnRK;r@5^S5CHfeQlCqI)SE3_pZM=H^YC@J_nuU-Pf~wFTXUh?@Vk&dp)moIn1#3}oM#jh);1=<8~3hOYKu8N>T^{;bag!^shOzHMRg6api5 z@tlT%O-53wK~*P$ zl9VxwRTIzA3P6tqSoS^7@^@1(PVjY$DYi)9q$(`w8J#EiB^p@6YG#S2KA?pVKs)vd z#~-7VlPOIu1ztYa-cfBK>>CpkVuKJ+WUIRFpnA~?HCV`y&zZH@1@a&-;E4X`(j}Jk zF5SF!Y0n#$Z`=)k`Er1GumnsSHo&PCZ0zuMrCH^cqQH_Q8a4K_5Q!_K(QpM5(T}HH zr=5w6?oY1shvsHD5BX!t63Ml8vrsGyV*dy1;}`f?gwifwzWl+wTS_4Ovgw6yeJ-^j zTh*>#ml2LE*SsXbNZfdwiqzUnX|pTNo!DPQJWqm_z^$~UWo0|S)B?Uc9@6j)QNq%g z?JK7>_yJX$cxn-mf36_{xrlP}Rm@=G%po-KtYMhg8l;h~zcWyBftI+5|Awfd0YrB` zN7<9hgI!a7Z^Cp?j5GL<$l)0GXyQRZm@3zGbt%r`Om#O`+KA84qs_p>m567R5ZEtC zH5u5iHJVvKJw6hA%5Y`>9=&d*&@lM zO3lS(cDQib8V^Xqww*7;Cb4FNnwZ2j-JrDG8{FRfL!m2#xfFhx#GrPPK_{dr-M_M3 zYuUIcqd-){#i%3X46mE68#zTWv2BM)4&%HX+WEx>T{$hzAL^MIv-A3eXs{#p4B6z+ z@Ap#r^H4tIan)q}K`B`R%Ip!f(UH{XiA(GC_EkAy^S!miPzNu!_EI0nQuV5sT{q1!)#16hT`@u1CN!z_%wrm+m8pFX`&Q6zxF|j!{KGRHvu7!g z)iGM@pwV@IW~P!%S4XKep#gJ|@~WLP4b$3V)+Rsa`qCwqdzwq!_-Pmmxk^|kL!RtX z6=#{RmF=FJmY=ubwp#Yz%O4QnMK5}==Vr~96aGxw4he|W%rNl|TJhXAHM3YAud{ra zUCOn6=Ne_k&YC?fp}1^24)F|~+!Us76g+(328U;8x3b0@vzf`Mwa!pcHf^=fbQ=j= z^&%}HX_wHTYXvJF7*fe4oV2azWooPR&9&gq-e^5Y|El8b-CxZ6E%}Vj+*BK%>vE`C zK3vn3{`3*$p*;15K{;)c0P%0xSIl1>8@e@g>#KT5K$XPxaj~^QSAM#^5`VsPZ`uec z7eC=WX!KzF?d==APY!pTVY1Nm{~~(pB%7qkTg$@{WR8~E#UB1zaWV7tF*W|B*{^oC z$_};8aGEc#hi`MYGk@;hPB$$5eXd((=i~lcoek1@?EkD(-43ZQH7)HnSDaCg+P^XX zT6rokBxoz6uKrS6pexU1FSGl?S%1Xs0r!WFkap!z&IHFhZrDp0{>l=nWYFy2cXwPX zT1=y7GH6>v%$^_3`WKmn1n!DG+vQ*?ddsNf%SpHMlf{xxypuf+Ao4_@PpHFrtsmd?`5gVv+;x1< zTC$?%F@E;Rc2k;!gp*N!c-%8s{#OA10Ra9&L)5_qIuj0eHyc*hIBZyKXpXtulYov! zmx$|dPXtyp8WP^6kV-nRBI(jv>sG8HI(+yrv$M0fuB(Y;k@;wG++p#Zk*nI9;|>pB zqkV0gfi`nhdwbHQJ$b2^*zmQ_-tOtzKlWi@$MVbj+mxQ$SWsyrB2}f(5fO!sh$wVK zL@Ec0{CF;iF1NM5@Yl97mOoh|GC~qd6TBse15~LdS6& z!Z6fU5s^6$p3?B@^y$;uA|g_8V`F3Fa=Fs?E`aO096EG}GiT1I;t`RG22O8pFEcYU zrFk8%cq5a^@bb$qlSm{=!>u3)u-0O&B@DyTZQ!zSDsL}$y|#QlPb3og!oRrh`*@zG zr8neDtGM4sqtP$?nBx8C@`Il1^?j}O^2y-h?eZ_zbxZ#iZ|8D3a=F}P{dwh2kk99t zot-5N!&2xJZx@57JbbPUo9o>!ZwtflrZ;|}P{3MCEEdz=D_=^!?-K;UO^1##=BEBY zS1NqUzrXVKwZf)6aEvjfw(_49`|%vd!CH&!x`bhPRcGaaQEV?(IiD9i*Q$DQxg3#5 z| zFz+TK0NxS#i^%qizDWD2Q|t_!m1+r+=#i#)`68TlzfuXn3J26 zk(%LNowm3{Hu1%}nZv1&9{WOmQ(8h4f8)OeksNpYwb@cq*@i^oI!OE>b9^goj~6Ly zrDVk@`Zn&}%y>rJgT4qa?X?wCvK6r~Gt)D-C5c$;Y1``Q?sqV66!iFd_1UiLqK8C|ojP>#!&}d!km&kReN|&u zMUMDHKjoa^=B95{+4k1kQhfwpvh6*(Zg~y#272Uamh2^#Xb{d*{cYS;gvxnnf%rhoG>#GBNjhLdN>ckbCU)#*uXJ^YhrH@Ak2 zmR9GkrQx4HmD*kB)K@3`+%3oQ=HgCCdhXo0b2|e=MRU4|QDYM0!#&(zN=va zV9SuRk&)4Izwny&Xls_PzJA;*zB&Knp_MKZpZAoMl)Trkq!+LsmYf@HE3d5VeH)~4 zv+rAQnBc9+kobhJ_~hhk*QHrUXJ;J) z1Bc4G#Hgr)w{PEeymhN4P9a`DtL^^%`?-sQ@z@wO+otYPzf{MGn_b=AGkV6XvL{;8 zZ2B+vRfS8(Tn?Yd4M`~}(UE)JPDwct;l6q+e{qn*JbQqRP3DEAtU?1W`LuP3xuS;A$CjWMFUk-sW*S>JZ`S_)@&c~=*YL2+<;#!L9keG_ z<~nrvLbHaFazgZy51%_1Q69kFJv^*7ma=QtE>4Zy7d255-6JEeuG6eCY_#z=61z(7 z)5=5$2Dj%qtCpT=Yi*@FsZTjOJBy988LW$S^0wc7Or->YmRNkMB8a>HTky#}d4p+` z3t?wjudAtj3p-=8iHeGilk<^7higzuN{aMgphbIb@b~XmBHR{@9z1w3KbkX!t=^`x z_JzTsCx2;leP@59`$zld+;6poroNrLd+*+_u`xMYTiy=WImT0`PVot49#j2z+-=$M z&Ye4(wr=eXHi*dT+>(%)`Jz$7+1JNslZo=?&6{7yunnCss0ur9;K1$QzYUjHR_^=w zbhbHkqz?$`85o?j>^y(-=FP)gTp#lzLP8!C6bRSG$RU|NWw++dwM!ct9}QrWWjTD< zr>so!MuO7qa@`cFyvmSMTRm_md)(dKugS{~B?Nd4mo5}XAdO@~`Fxj`m$%MF<&VU_ zd#ANHJ+SWx!rB3Jnf^$3&C3_kjD+QrsvO-1D~mJTkI#;FIutB1N=Zrav6nuKO-RU1 z5U4CK*G3p|YUYRCUQaPuxSrawr;KKGzEn5`c|ByU>9%l)T5z0n)_(Yw2Rw)VXH;n%d@vhFM-i*T8g%3q$8{-%EtIpNTL%iE|n z+6teG%)cKHK>Ojt2f@YPQ9F0-Qg3iW*?H*A(2*qsXO*Ok@($|Y7o8@`t<3iG^5(GZ{N})3L_l5Ha8?|x;J$s zAC!1J-V-2)kFqYZs;;Wi|8#dVB?I3heB#u2F?B#>q%E~&9Uzr@q=&KQvBeE^W zA>hdqtD2cg{gX?xBRVy&#Yocwwf(Q#zD?z{W@Tjw&eX~0PJCeyTw9)6UKw+z&>Cq@ zM@u3CAdpk}Vkn=P(nr(1$kqhqRIgHI_prLU>qEPm2J7PyLfSvxNF}Id5Z&b5+Omy+ z-CzlRX1DoQvaX35c_A-e(D9kKmf=m@@n)IX*(cmrU2Y_*d`7t@_W4~> z65FX$yA%}_qh*3WjsGep``wbkjbuk(D`-fePhmcE=+L)FVP+3%;bs53cQ+94t>7+@ zv9jV>Tb(N~OL{caa*l$6Vl=CtX*7SyoOH+2bD=_W_1e(=a_d)96Q#^3g-3?Muf!`R z_o2cjr?&Bn+S#3=Ws}h=eawi!-*@)xS)%rU7w@a$Ulc!A|O6waM= z|5tR!k4iz;MP3Ge^9|ToaHj1$Qch3r5Czq~{Tv(*QGl~;2iAGi7_tfogkZ<^ zQBzanPJU+Du%mOdx-E?~1Uyep=Jpeu+*IsMozXBBD<5MzSn>JuJ*)07zXk`LYF>TH zo?lp?!9^8m6>1$<>bK#{IH7Tg!Klnl*T5hkFz^cy$UuGk9uk^si+PT3zUz`o|8sih zB~HaxOJsZHM^gq)Q!Ee{|^h$h@H z$0Tk8&uJ!QVYtIZ0l25g zVsBO08IhYeS)>CwwE?B@y_)t5mA&(kXE(d@1eOnEe|q_mNy=u+0~Fs*$u__QOgSFJ3eb@5!%A2@$!XD2JccdCn)3RUgspRiSuw!Zb{COkV`Qv?Otcx}@O>~zgUa0#XZ)IR$Fr?I*<&egE zT|bNI^`ZxvI+4V)JCR6w{%f)FUfIlMjd>%bw<>aJf zXJ-%PD#*)Q*{HruV8vTg1oF1BwvJ0Tmt~{fyLYc?Q_8)sUoV+-hHGk~=>ku84h$@W zeyMkl&u%WuT0%uvGYG$~pkTE;Z-xHt85*h@V5)<#08;JhEbiMyKVPPRbJtq*? zcT-2_{(}cBCr(VJM5=WT54R5c6?`VL1<{g6Tj)_a_a;jq!{KME+f4iK1KN8jEb8NS zp|AJFM7pKqE24iGM7oNjd?7VU+uBy{%rG)C0y@3S&JM{0y)K_aG`h^xEB*YI~%llE5>hFS{}#xFHcr*H}n!^ zl<3O&Ypc#-;o(FLKw(3{`XZovMM1yxCX{c}FEW~_2K@W^S)PLCN;*U@nPg5E7zNi7d?w+v~_h`#C3woqRNewCKH zO{Qod&l6QMejubfc!jS-qUh1=+xHEKz3IKSr=2E%t!dxpu&}V-tyw2GZrs?=h?aQP zVKnVb{!D!Uq8GTh!RasuNBkUJonpp$Pd|5g<1&kNoPM1?etyaPKFJMZTj>R^S&i;1 z`8?{B&E*wJBFY4+?E=9ffPR7du%-dw;gtimF@TN-*w~`0W+Wc(lLAyFdit?rz6|D> zw?(|EtUxNiJlZQ#;I`tC+C2hfrc>;-?GgQ{EvUqkQ&VPNhL8CkL2=&2apSmg-QnEa z+=Slxm^W|k&I~nn4-S4sD7zFbC90%%^@Umx6v?7sB``@nhfm7E7MvnMCH(=>tP2az zALZcSrb{&gH3eGkij9f5`RxVI26FOi>guvUqX>kC4~sMh#os&47^CBV0_91tQ$>&w z4Hg?bffz2LU?x&At)UUgcgC`ll+kLv^`Q78Qq|+D)O5!^GCJHsr2^P$vTO`4UAmNT z+v<#f!|0>DJRxS+X&Tj!rrM}f4O0C!r*wrW$=CY{ubu|7Jq*tGQZ^hlH-v)8@#WS2 ztyNWa3dz zD41RR2{-|6nd-jgHapxT0-)&Q>r1fLxVegfuC7mOD;;Y*HhnUk!!HU#Lm2>BbLYDr zHe8yTp6+Qfh}7#WyuEYxZtMARZ6@bo9yZw!+Y7f+4XX7gd&-{$2BzR++2kS!hBesc z2SoP4dCnX$b2~ZNB2m2rUrg*I=pompm@g-srVUqtWx9KNKZD89a; zB=Vdq0nY$ci7IBNF~O%ROj?{9tA2V&I&+~f6as_(+Um+9I-WbIJ1i_LA=4H0*qhy4 zYLXe|Z9j7yxY7KBG~Ea?8QLrEOaB>JX64|J!PLRu?ih7Jv9PMDN^oVm7GHhuq(Rkt ztQhfIxGB4$E2<(xA77QO8PBnDItZK+SFSv;AN7RW$$3~|@0OGN)9l48)>5Ds`l(HQ z%9nv{IuWOD#2)T;884zpv*^&+rQnSYw$gVt0Qp0|lNBFOrP&wm(Ebas3i5$wJ=49_ zh`W-sxRE|z{`U9hR^LWPd-e3_rlh70wCCki9{XYa3SeD>SJXJCW&GOLOnR^?mzox} z>anXJC5nDWWLn8@I&&Vtc|y`JghCe+rcQX<(pNd_0FI z`L?cd_Q>2$hf~)wnj0OIc8`nz-At=}_3ub+I>e+rF$Vy013 z^F#GnVEzjiF5oM=J3GH0)*~k;e;O8MDx@ql-|6`+{WoR0yWw!x@68YWPsYSdEP&#oaFeqpWO3?P*yTzfMJ=}ea z=$&?>CI~k|@%W1%(^L)CZG(+anqwCd3g^-J(m6I(JwMTHmeg6z_yE_S+mxagcFwiA z@8rV=4;*Kk3=-{v{Qb4^LLNSR*!_8rX4(;*-mlMJRQ3rB9Xfk9v;%~%;lMG3#e-cO zbON@AvA{ARy!TOF>(9ERLFq#sf}o|3_6ofCrs|cSFjO9wm6=A{vGz8ndzwhrkW(gf zkazIbWn>Rg!}v<4Z&NU7{rYqdYL`F_3Q+Khqg^9} zZLhb~FM$lv3A+gr*B&mAUR5oiC?&Ny$6<_EdICkE<|nlGGVa^=yD^!$`-`tmTeckl z=D={AeY3SH&USw+{Eg$vazM;lv07A8wxd5MhZtlz`SeMR?lM5>)ZV0#WyX#pw`nlq%_P zeX#Dm1ZlkO-AH{?Q!u1O&~g2|m-7%21{x9>2=aqwm^=PyE1|j)*RU|vKZVXy*QCF7 z>sB5*N>JOD-1fK9dsfHRR{enZUNyXT9Z7FQ-D5j)O&4pXH z&R|m-<}7kX_=c85yeMDcXK&uT;a#v_n4dp>Yf`tht!)5c7nyO0gJY{lYpWVC!Dr+i z$jxi@9Q#(tq;F8pEKgCt2QooeKtb3k9z_H3kWj`#dHQokvmauKe}FOqxJ^tA<55=np$XjJM=eSJ{h|tgyxnlTOh%8 zqDr9%`jV|D|NdHRDoMsD1= zaqY^LLI}$*P8vL}w8m;h1L>i#*FwQri_Q*bw6|ZYoW)3nC{Kol?&-KUUW{8J<1xQVUxAZnh2|v*_O z6`b7p^EU(3^FEiB5@eVd^kFUpRsI(g4wJmRR0z9|gq&ynpFJyqX7Ga7Xb0;5qHI8_ z;v&~eY|nA6LK2FlVa@A}__#N?#D-T%1*gv+k+SQ`L46cL{0dgN}ElLRRlucvzSrb_3A( zT`Ch4N9h|kc9;4wTYTr!c#LuY;6kura!<0!6% zeoIKbP!CEFAyU%P2YGmE`riCVQa?+@C?JZ8nW)*|#c6X6dJ@8zfb2vXp+>nZPm~hn zC*7?Ew|_xQY~!CJs@buQYE%w0Hq~l9UivIdP>9Aj)Ry-bD;p;TpQSx=cqWiAl}q=qRU3nh!=emm>tFOiejE(6c?|%J#(X0o~^_{dpc> z_1?XEoNAfh*`ArwKMQ9J<+tb~^fcn)Fh0TJp*(-cuuv)`x)RWc>Re7-+ym4RYinz| z<67sV(z70>b5!^Tp!@;!^959)9w7%Y(=EDB%RUcKp`2l^9v)f$(MD}-j(#I3Cq-Ure+YPw9|HBJ_F#qH{mWMH|P(lTQ-n#`GREB&8F1?(rkvBh_TKyZP*G}EDs1Q?+ zDn(7#&yc%J@@|@yok&+75XL|Q+%E#{MY&%JG%8=kTNa{RF-LOlCqmPOR7HrGL?7`| zS67Ui>Z{aQU0xtcH{|ALOQSi}0@N7fh+V(F6Ah9dwUeT%iXzN;ogcJ;-!)N~LY*}H zz8}?!j?Yxdb1?6~4AJ?Zczp9eAoAjb-WK$PXaqs}<#q;2awf-4GNA3fXRKKfLt;+V{f=4JJS3d8~y_`_Ek*j8k-{?i61i&>FrJgTW!td}rYr4A&82v$6wQ&)?J&`6*jOyN zTR$}zq!jyd5d&w_b{jjpLo6(JAUMoE?Mtm1d&{Zx-UHK~3kK~bK1HoBx_1QO>WB)X zUlkU6kfriLlj$GPjBXWV>mBIhw1cHF3tWK+PQ+l+Aa}YL*k$GSPxrRog7^r~m|JO9 z#v>lTtv^9``THM zsyIp-qwBM>%Jb0MFkDZ-lnmnpNVR6|V_12t<^jl=(a7=XXDhKNn)uU@4=gX2VCii(Rj=Q8ivQ;Mt$7INmBYqg;Ux6uY6Df#?axl!0< z{y5NaFTQ!*%iP>B&>7G7?~en;D5V+FV2V$+AM6Z5p2xkddv@&JeZz$MU6S4)M`KDP zDm^<$Appn;i*~_ixyZ{z85T;y+#z(b-LZQc!79f}zkHbliark7LZl1m8B7ADgjm|y>4C5( zXcn~hU7B@4@`Z)%L;HgWQe0H@>vg2Ox;h~-FSL!hd3$=U2RG~n;iLfiA)$d^ftK>_ zYLq1~@&5fD@7^5_r5%P+`nl}ML8G?pQ}*`uckkY{gaD1q^Fm^x_;f~xnt#G@@9!vr zHKrycIBy((zw*XRBhH2yI@--%tZ)#@{Fld!5222#XWRaQIV3(+!`jBCDbJZ76G&o~ zhZcuDufi|G8kEk(k6H=*>!45E{c9i+eswr^>@bAt7K)-Tlg&X(`T6+SugmZ>=6N z^6}x7k_5YY`s|q~^aV2w>TTQJqUX_3k{8-{En3t0iWAx}#K>|GOr=~$p5%g60W>3m ziFiP2n(TVW&G*h=ok71iIn};0h&_aT3$CI!HvLr%a};nlv{)4Q=Hxg>H7LtJH@PY= zufuqemL|)%ShlyfmzWyAl@3Y)LK$*Lwhv?ZFj3=eHeeKX3o~o^2(J`}_#-;PgtBSV zrcNv%=_OtUWFW>^p#g83Op`KN{r$HC+eu1FzEn)s0Mj6S$J-D%U(lksVsjs6DP;jL z>=Slt6?kLEW7eGZ3;dE#L?STD%f@*R3yY#4tJ~^=0kpi|Ki)p996|@cor8zg8%8SJ zxbX%06mkSZB`+8-{DXoF^61C@4l(8RI>9Q{-(Q+-Hw0ZIa>nj&L0o~+3+x&6HWXA$ zvSJbv9>K(N586a-)g(MXh#d-0_ai9Cpv-p(^n($5XMcap4Q>`iZdN{vb{R>@O_;Bw z+-k!#3M4Wg973F74kF!2NEW|945C0j9x)=8lQ`fzhHVj?D4_)b-UjUqtp*CJ4yaMwKANj?7x$k$89?wrEXMa(d3GXS zQ7%5D%2z%7!)2MYPTdP4&tD9ee`%Tj;>rB4oAZ;t&-qyvoljehZ|n~FY2PY(;d4`g zU(bl?%j#|7;`YZ+lvis=TorwDpSPtzCWAHO!JfJ6Pc${w`t0m^#xq8b?cB}6vc_?M zM`(r8V(r?-LRqs%tz#y-mZ}F=%?%_pkO;Px?Ow9-{o7$N^DGU z*3cunEa8o(E#(;wF7)aL_S_i47pln}_>eL_ykUAJ|D%wP;#@=9Nk==2=dymEQj*?s z($HjGzId}`B9(o%$ZU;a$Jj`YqhDvM9_NtCS7T1X>I0YBhK(C1en@j))zF|ij}aus zR}H-cD_K1TSFIsyAoEvcbDNFxuYPy2FWCfrNH94P&jUXe4cx3cenR5VZona2Cd=84 z&k#1;GB`V-`?3lO?vs#^z??=5ya#pxB7`K>jicMencaxSK4%^!Q;&!wZ>c1>GI*RhX=EoV0@PpqzaePVVQ zzV+IqP56=X>-hEV;|ms&N)74H>gk|7&0_)%J{W~kLtF!R{~h2nC@;jA3Y_LGY6Mz- z`eED*?>#BESzCLa_vvLNpTgGKXX#r~H!@3{KJmTQCyMcObJ2OZgZSr;mZ<{2PNf&} zpBq?c!__H$7vC2UV>cj)i*e?HvaYiDpha?UY~0>uvKXu(i5! z9v03^TOf1uz09XIt8{VJrp76J(d8%#>C-MB&S0jL0{&;H}b{V^3}6vM3QCbqM$kDdxd z9%IvnoUojnv#$PQF`$a{n{m^(+Yn9w3*_?ru+SZweADVLiRQPyQTpk%)@R zz$f$1RA)h`y?_7SDF|$bormY@g##=M~)O#RZ)2sz1)2W zZ;U2vY}j5nFy6$(XihE;3=F^~2_V+~j(Q3Bh>_sjI$s`XgY019q3oRsALn^6e<;VV z4XPsu)3DznR+pl3ut^r~oCLM39jBL+9(%103P7mUs|X}JZrvj(L~{dGe@IHni59Uz z;#B>Za6yRRQYPm?;5qi=6B;A#-ecOug{6KMwEEj0x$^pg_A6n{qeQWzd7}U2vcmqq zJ8ke^r_lcf$@KA&oYL=T6Wcqk8p6TFNB#20@ijqI!TDOk6-&p((>Sp zfQ`*IZ>TVs{|u>DwY7yq13StoD!Qt8@g`US;+m0UVPOHw)%e5&fyF(*SFllVzrK0- zlH>tA4FD2-@F0chb*`G^R;><`>}Re%y5Wd z864aI+9X6L!BL5W19+8CKK2PYT~EA(10%kblOks-%#wosjAO#bs^L1Lpy9Tl2RRC! zFI~*|5JV(mwgh{y2qt>?3|O;n;QdJqvvGG8dwY5S1tpM=0B%r_LU90Mc(|(11dhcHb` zGpxCT@}*^9Pzo2k0uO#Rd!Rh>G?IY~EZ^Vu=9(alwiK-%t#cB{z_h zuk#NMzV)9A#NS5zkhVJui|-p{p`0VAI%uow445lwzH6!@T~kb(PLRYO?~6rk&uVwt z5!f#3=y;k034O|gFnuo zy$P<-IrDFiqWw#A`0t!w`{$efhc=skoRoVLoMQ3dA`GnXUc#oL#TTkj!NMX7t2Q>l z8`eDWsEOuHn*+*46%=+MxCHLtr)8#mIwyh?_H@hA?DRd?sylGO$EWN$lm!txJG+2q zW{o=;IKb)89ZtIM|9#(G{H*bMzeZad>;F!h^u3AGJ1~lpd*HYnT#&JGaX)Bw(HM!& zR$4{D=tV)zq>K7XOcuMkE@CRQSJ3e|{G)p~-MdOQ!#}!!6AZ-6(7o0`Q9p)6-MsC3cug0mU}bef10! z30<550SftG)h&J_Nv#}VO&se|UL_7fp^oOnwFp{uiNYQ$jfBD21u)~$*|FRJ4DCq< zYpcTx?i$~9PiXVf*KZLMJy3H6^JK!Ti8c*A?@p1{OSshlD^qmKcVI|8J~vm;IVcF} z9AlOsptzfSp$qUGC>OX1;od>K!iZsfik|diCZVripK~Btso@NI`To8C1y6Dw+uv6% zhn+5hUkc-HE!^&KONQTz7cZo@gnN#4!_)^&n;0~p$BBbnVlcphqYWNd zG)tWRAr22+)6lq#!+gSP%O_vGdPVr_XQ=p3T}Ru4>16BY&!3?uZQr@G1UpVx%Lxs{ zzQwFT!^=m~rhb?$DLy`)C$w^pT3$<2Q_Tk*3G>WfUQUh4adF7ja2PXGGR%D;f8w(- z1c45|E{rka!J|hdP~!;qFuJC;wsv=Pa-I+-XpjdCROMn6VQgjT}o?gQ6O7vv{YFpM{d z3C|x3mHCXr(zD32Xe0qp9@KVtWQ1ElLd(LU8Us8g%TGrM z4`<8b>U-_tx<%19O(yomvWQ~By^gtjXrr{W^sTj(IrwY~#nyj<`h;^Pp&n<}s$NG2 zLJfs3Y1o#Xt`dw`rGE$+m2eLaH6$V}u0j}w%>Fa?)=5I-Y-w3~Jv4`*8h9S}7y)MYSOSU=24$RP1mOPu^1eOIlfF3RDk&6T>qd2l4{j?? z(5>zQyA!T$Y;$fW|BiIRv=2tzTMgbWYg`DJ*^pgo#? zzrPygcdiS6QdVD>w`N+UufDh}+M{ywIRE5nnOnEAj1@$Cjv79AvQ?-P4!r-JLin#| zegAl6k3q-Jja+J(SJDA~cYy^))l~2TQK!b;5EV6!`o;ENoiYArjsDAMiBQG<^;`b_ z>i^jddidY}-(RDB3pGH4e%OMDG~vhyCgr5lXU_aWc6n1V5;wW_@E?N?4vq?Xs(u@5 zYnIceU%>ZK3*xSy!%c2;Q1~vWrP?tN69coK?{7n8lvXi-HwNR#d4BhgaQa~l-<*j{ zNYDj90se2!9YvyH7DCt;NZ!=U`mo3M^z}7!_74qJ{AppjCaLxf!7hmr3*r`Y2#mHe zM?SWUW4^3)T&-rkrh>v}m;_N(V=OxI_n7v>GhYnB{XPmyKFH{%&_+~MVrWa>+D#Z0 zFtErPerfo?-_YJ>2c~9lwr#PuP-ck0@V1`D+zp4Dp>`pIiKBQp?eh=&a(7>!e%>cJ85!M) zuFn`O>l~*eD!U;soWn#(24P@5bZ8={V}%L>LAT`z3Ao|D?$LB9f{PzBtzVd%=T23f zC5&*egIP`XNMU^c73KoM76A?c|Kwf5DU_({jl;LcZ0lsckVreVi01;Reyrmc?{O#G zCku;1$=r@B7`1=FktRs&VbD@QPj06c%i?4S+;9j4lOMIUW({W{5WpJb=;)|6`SJAW z(`su6w2#vf);b<~Uz~&11~h?Ly}H^D4OHtm9dYyUN^HY`ri(ZkR#Wo??m6u9aXL!u zMJMd@@DsyDOPu1vCz`9}rBn5x2u=HO`@lqC+bVu;u3 zGF2HSVE2G<0mA&C!%J@jxQe>o3&RISx}HRzW@L0#rce+~GQ$aumv7$m+-V%k?50AI zUx43++c6pA1N7csJw5o=$&F(;+Xq=?OrK^)s zZMOVDhuL1=+(0e5bGsCYCS#vk*me;vgMg4XeC5~AoId;b`LWawGI|Dlp?h?KL)1={ z-!M}6R!6TX8eAf^g)*M}>%j{Y{71J-rS}l%n4(o{uM| zpiq#PkAe*_E<0NVSZcXrUFX?_kE%sFM$Jw#ZgMvdXwqxhpx`fIqev@9~gS@ z`0;AIa2fnTM-CjgfQkB~qoA?5xqjT{7akzVZ()Pgm~*tTNrh+pxxfF*!NI`;)i_Oj zcszt5F+kF#WqYZN~}+;C?W{ z36Vx2hp2(njazqbSaq|DB*_RuQvdqx+jV8-CjV05g&ta@?q!Hr^)ME`gt~l7Frv`c zWFVBGeN9teKbml_m-rGVvjm)`-@#&ezpyl`)jR{UVGNuFf}>+GQ9s4YYlwWlO>^5Q zB&EsJI!i6vy7$52(o(Ojz#*x&wl>_z55%YUHc@N|mqsp~Pf0z^hREyd#?>Wg)6>&M z#L0oyj~+XwU~m7C_>!T(y`woh0Dzd~qc#~)wg>n;ejKPxhh31jut;8ZD;OLavP1IY zCgr$-q;cmc#MfcFlc^;l1HM_I^o$G>m}r2m>HrveW}ND4YoTMZoZ{mnzhiwYsNyYD zD71#y{CtgQqqwA`%ZQ@Ek&)-czplWUmz|xB_ni8q$w6kxV42efwCuuW&O!WL6sTyQNaSABDH9FChHOpFG8 zyu}iwa7yEisFj%C_w(XJ3mzfhdRtC-W_C92%^Ok7`C{P-pN?W)k%XVw`t&|LK0^_4 zgfaE{mQ+q@j4)s>^~buV0f+bY^%)~npT6<(A|GkXQ3Qr`!XX&(-mM%Yb86w9M(gEt z?lxEEWfdwC9R-(Wegb?9uB!|Eh`-|eFTK{F>I(2e9h}{|>a&6L=p;-}*ARyIO-`wh z6M7Gz`<^>ma5PMkL^3yGoevrykwn*#&a?jezxMASkyyw`fA^Qi{ZgJE9IfymNbDl5 zk^}$pb`9}zR-~LxBYFHf-nQXy-}#?DmGp!}+P<0eZ~wwAc@S5AM~C;u;M`L0)PinE zG-pNE;q`@wk5h{9h00QSi`eJ5x`=)no2wX_`N}Knc87HQ0cXY2jxp-oz0AH>9?jd< zB4SCM4aa5W&qZ2XZ!lx<4c|y=UA*$>p`<1CI+A%_P4?{M0NcO6lz&|9Kd$Jne|_0p zs?_qTy)_wVnZWH6ka6M_nf zWPQq-+{oeG&Fx*6!l@j1DAbpXfy81ZeM^w(`Lk!gfn1)xL5;;=`I32t>eK6*n#ril zz#2y7PY*eD-lo9(Z5LfEY^s{X<52KW5P*XGDSI=%&{4Drfgg`37@T7vC3CHPD z;nl0ijmZTUV-u5FFqAZx1#3Nhed}m4x}Yy3s;vOt?{acf!QN_MI;%t8B3_jP-vT_i z!tx03O)Y~(E>ZY*?cN<16C?4ouCXx&!M_6cnBt{NU#XkjBqe#S)! zgn@&fUvXt+Wy`j03T|%s#9;tsau3N?V^>$#5FQ;bSa^2r*zp?174W=rUfJ+7HsbI> zp=bxTnd1m+SlH#5=H_Nrsmn3)u+-E6mYo?Ex;OT<*E%*iSE`P_0OL?d3F0X zS$bMp*>TFM6Qu5j%y4r&DB%g%vhA-=8!-era_*c4d}NbOd4t2l*R{1@p-!LF4Ngc% zuvm>^d>s0q*w@yG`6&yy}8sm;kXmD-3L!&xC=MXx+Q+Rx9T{lyZJ?^`3Xvq_3PJXyDUul zmo6Ct(qaRs4aVpn=69C_ht0=YIqB%(M58ik9{Zz`Y55Aue*KRhJLcb9fo2YcV;2(>mz03(Vj2!Zv-p)BOm2)v(5fJo z>~qxrtlX5MQHhDNIQ$UN+|pv;8|04b_Aw zZ2xire|_;kE{DH}hn{0*X$5(ALRLOd&*~YO)=oOF1EG~{-o*!U+RmM0`Gx8@V@Ku((V1z~H8JBukSQ(MKYy`Hv2L^!-wWs^@z6ix_Jr1sN%$ z-B8_m!It*_%N6~P#T!452MC{d$8)jySP}CGuRV#u?q(`hir4dju2mOH2e!z+dn@DEi3|l!ukY#)2D8PT5Ru^V%aF1)4-sw!)bE4ly#cx-`~^ z<2~!E)#Km4zXJRDBAmH^;4z;*U9_{eAH<-O;Y11spmoyRd2d!%-OM`j)h+TD^tD_Q z6BAVm-15-Qd*_zYL5qY|=esY-$u+`L6Ti6VRCSspdXqI=*j)pmoedNagZYaP6k`bWXoI1U(+u@ zOBOw$Q&8NQXr%KZ>Rq^y^9{u$wI{6VxsF$%B0ac&A2L%Z9*UHJ#}vuJD6a?v)$X=r zHZwP8(?GU=$H7w%FweY-S22Oo%VnR7mv50;N`hojD1Rx1v z4{|h`0QY+}dHDdGlc=k&Cw>4q|LEbvl1L=p%CU56#SG_zfZtEw;3&ByWGI}R4`Kx# z9Ryxgu(h?-+Bt1Dw2s6wA=D9@jCM zN3)r=Wh)>=t#kfB+7SEdTUz4b0ylwn#^0)e;=hl|Tj1T8=R>!@C4Y45*R03GV#KnypKXr&afdd@dC=PR@G-99u=eUu0 z?0%&uhx2a3sfK4~4Jj5ZK8F?MP)18)*-x!v|46I?6hL z=g8H7PRx`Y^dr8Spn9-8{+!y8F#>ALF(%HA2l))GsO6ue*LRISw zWcl}K_CGM`e+fn((LR2n@e3Zr({ZG6{`WY(N2A&Av&PU!@@S!`mHoFc`QLuBs~Oc#S=E2@>{_`R86k9v&T+-kFY(NY{iW^gg^_!i zX9t9bTyFQDB|r9->32}hOT{sbsZ3VaMPWCw(INS13D@lJI#fBno|SH5F5l1UGAOR8 z{tHYVDzi*3g3=}A6%-+G)l*UnfC`NWM zsMgbI_1nv9>@ClD7r4htT?5l%YGT|U*_CC|$BGvuIUd{C;>(b|b;y8U=j_kIOAJOysk3D{GWOY|IXz`djQ(E1Wd({20qh)NL z@6tc8rT-e)|6MqI-}Nbvw{J`|XqnCB#bRRPF3U>=m>JI~3H#^Ca>3S$j_zz+CbML6R7f*Ett=e7n*O0P0`fZ|^tMHukQ8S+2 z<8KB=Ukzo}njJ3lKW8)}{q$>#tR1CF$X?S&<=rBzBpJhrUwk9#qYb(-)4vVYhs?%k z2st<%x0LHzdiR_6)QFs`^`7a$aFyf;U+QaJJ+FeAw&y(Kt53og_VT=?4slW0ZZ5Aj z|1)n)(GDoBbDXRi@ku+l85cj z*+*hO+?C@ed-1*DTiVcwZIH%0y1F0h&oyl`>J7Kg`%t%c-e=Igb)@b~HO0)gekR1l z@OF`wF)QN*MiB{8y`s!p#^E4V*)1p1ZfX0wSvQUcHAS}SHI6z(GQBVg;_>qH(^d}r mY_yILDe$0Dl9bqG(WHx7cm5w@;K>I7