From 1503a22217ff6ee9f8f9d5e2932f5f46dfd76c18 Mon Sep 17 00:00:00 2001 From: Anko Hanse Date: Wed, 15 Jan 2025 17:17:51 +1300 Subject: [PATCH] Keep data in api --- custom_components/dabpumps/api.py | 4 +- custom_components/dabpumps/binary_sensor.py | 94 +++++++------ custom_components/dabpumps/const.py | 6 - custom_components/dabpumps/coordinator.py | 138 ++++++-------------- custom_components/dabpumps/entity_base.py | 5 +- custom_components/dabpumps/manifest.json | 2 +- custom_components/dabpumps/number.py | 124 ++++++++++-------- custom_components/dabpumps/select.py | 96 ++++++++------ custom_components/dabpumps/sensor.py | 86 ++++++------ custom_components/dabpumps/switch.py | 91 +++++++------ 10 files changed, 328 insertions(+), 318 deletions(-) diff --git a/custom_components/dabpumps/api.py b/custom_components/dabpumps/api.py index d8f3d14..6cfe449 100644 --- a/custom_components/dabpumps/api.py +++ b/custom_components/dabpumps/api.py @@ -42,7 +42,7 @@ def create(hass: HomeAssistant, username, password): client = async_create_clientsession(hass) # Create a new DabPumpsApi instance - api = DabPumpsApi(hass, username, password, client=client) + api = DabPumpsApi(username, password, client=client) # Remember this DabPumpsApi instance hass.data[DOMAIN][API][key] = api @@ -60,7 +60,7 @@ def create_temp(hass: HomeAssistant, username, password): client = async_create_clientsession(hass) # Create a new DabPumpsApi instance - api = DabPumpsApi(hass, username, password, client=client) + api = DabPumpsApi(username, password, client=client) return api diff --git a/custom_components/dabpumps/binary_sensor.py b/custom_components/dabpumps/binary_sensor.py index a33b405..b51d107 100644 --- a/custom_components/dabpumps/binary_sensor.py +++ b/custom_components/dabpumps/binary_sensor.py @@ -33,6 +33,11 @@ from collections import defaultdict from collections import namedtuple +from aiodabpumps import ( + DabPumpsDevice, + DabPumpsParams, + DabPumpsStatus +) from .const import ( DOMAIN, @@ -45,6 +50,10 @@ BINARY_SENSOR_VALUES_ALL, ) +from .coordinator import ( + DabPumpsCoordinator, +) + from .entity_base import ( DabPumpsEntityHelperFactory, DabPumpsEntityHelper, @@ -77,22 +86,47 @@ class DabPumpsBinarySensor(CoordinatorEntity, BinarySensorEntity, DabPumpsEntity Could be a sensor that is part of a pump like ESybox, Esybox.mini Or could be part of a communication module like DConnect Box/Box2 """ - def __init__(self, coordinator, install_id, object_id, device, params, status) -> None: - """ Initialize the sensor. """ + def __init__(self, coordinator: DabPumpsCoordinator, install_id: str, object_id: str, unique_id: str, device: DabPumpsDevice, params: DabPumpsParams, status: DabPumpsStatus) -> None: + """ + Initialize the sensor. + """ + CoordinatorEntity.__init__(self, coordinator) DabPumpsEntity.__init__(self, coordinator, params) - # The unique identifier for this sensor within Home Assistant - self.object_id = object_id - self.entity_id = ENTITY_ID_FORMAT.format(status.unique_id) + # Sanity check + if params.type != 'enum': + _LOGGER.error(f"Unexpected parameter type ({self._params.type}) for a binary sensor") + + if len(params.values or []) != 2: + _LOGGER.error(f"Unexpected parameter values ({self._params.values}) for a binary sensor") + + # The unique identifiers for this sensor within Home Assistant + self.object_id = object_id # Device.serial + status.key + self.entity_id = ENTITY_ID_FORMAT.format(unique_id) # Device.name + status.key self.install_id = install_id self._coordinator = coordinator self._device = device self._params = params - # Create all attributes - self._update_attributes(status, True) + # update creation-time only attributes + _LOGGER.debug(f"Create entity '{self.entity_id}'") + + self._attr_unique_id = unique_id + + self._attr_has_entity_name = True + self._attr_name = self._get_string(status.key) + self._name = status.key + + self._attr_device_class = self._get_device_class() + + self._attr_device_info = DeviceInfo( + identifiers = {(DOMAIN, self._device.serial)}, + ) + + # Create all value related attributes + self._update_attributes(status, force=True) @property @@ -120,22 +154,16 @@ def _handle_coordinator_update(self) -> None: # find the correct device and status corresponding to this sensor (_, _, status_map) = self._coordinator.data status = status_map.get(self.object_id) + if not status: + return # Update any attributes - if status: - if self._update_attributes(status, False): - self.async_write_ha_state() + if self._update_attributes(status): + self.async_write_ha_state() - def _update_attributes(self, status, is_create): + def _update_attributes(self, status: DabPumpsStatus, force: bool = False): - # Sanity check - if self._params.type != 'enum': - _LOGGER.error(f"Unexpected parameter type ({self._params.type}) for a binary sensor") - - if len(self._params.values or []) != 2: - _LOGGER.error(f"Unexpected parameter values ({self._params.values}) for a binary sensor") - # Lookup the dict string for the value and otherwise return the value itself val = self._params.values.get(status.val, status.val) if val in BINARY_SENSOR_VALUES_ON: @@ -145,34 +173,14 @@ def _update_attributes(self, status, is_create): else: is_on = None - # Process any changes - changed = False - - # update creation-time only attributes - if is_create: - _LOGGER.debug(f"Create binary_sensor '{status.key}' ({status.unique_id})") - - self._attr_unique_id = status.unique_id - - self._attr_has_entity_name = True - self._attr_name = self._get_string(status.key) - self._name = status.key - - self._attr_device_class = self._get_device_class() - - self._attr_device_info = DeviceInfo( - identifiers = {(DOMAIN, self._device.serial)}, - ) - changed = True - # update value if it has changed - if is_create \ - or (self._attr_is_on != is_on): + if (self._attr_is_on != is_on) or force: self._attr_is_on = is_on - changed = True - - return changed + return True + + # No changes + return False def _get_device_class(self): diff --git a/custom_components/dabpumps/const.py b/custom_components/dabpumps/const.py index 575e606..ef6cc85 100644 --- a/custom_components/dabpumps/const.py +++ b/custom_components/dabpumps/const.py @@ -77,12 +77,6 @@ # "": "Thai" } -# Extra device attributes that are not in install info, but retrieved from statusses -DEVICE_ATTR_EXTRA = { - "mac_address": ['MacWlan'], - "sw_version": ['LvFwVersion', 'ucVersion'] -} - LANGUAGE_TEXT_AUTO ="Auto (use system setting: {0})" LANGUAGE_TEXT_FALLBACK ="Auto (use default: {0})" diff --git a/custom_components/dabpumps/coordinator.py b/custom_components/dabpumps/coordinator.py index ecbb6d5..290eb20 100644 --- a/custom_components/dabpumps/coordinator.py +++ b/custom_components/dabpumps/coordinator.py @@ -68,10 +68,6 @@ COORDINATOR_RETRY_DELAY, COORDINATOR_TIMEOUT, COORDINATOR_CACHE_WRITE_PERIOD, - DEVICE_ATTR_EXTRA, - SIMULATE_MULTI_INSTALL, - SIMULATE_SUFFIX_ID, - SIMULATE_SUFFIX_NAME, ) @@ -154,21 +150,6 @@ def __init__(self, hass: HomeAssistant, api: DabPumpsApi, install_id: str, optio self._install_id: str = install_id self._options: dict = options - self._install_map_ts: datetime = datetime.min - self._install_map: dict[str, DabPumpsInstall] = {} - self._device_map_ts1: datetime = datetime.min - self._device_map_ts2: datetime = datetime.min - self._device_map: dict[str, DabPumpsDevice] = {} - self._config_map_ts: datetime = datetime.min - self._config_map: dict[str, DabPumpsConfig] = {} - self._status_map_ts: datetime = datetime.min - self._status_map: dict[str, DabPumpsStatus] = {} - self._string_map_ts: datetime = datetime.min - self._string_map_lang: str|None = None - self._string_map: dict[str, str] = {} - self._user_role_ts: datetime = datetime.min - self._user_role: str = 'CUSTOMER' - # counters for diagnostics self._diag_retries: dict[int, int] = { n: 0 for n in range(COORDINATOR_RETRY_ATTEMPTS) } self._diag_durations: dict[int, int] = { n: 0 for n in range(10) } @@ -188,17 +169,17 @@ def __init__(self, hass: HomeAssistant, api: DabPumpsApi, install_id: str, optio @property - def string_map(self): - return self._string_map + def string_map(self) -> dict[str, str]: + return self._api.string_map @property - def user_role(self): - return self._user_role[0] # only use the first character + def user_role(self) -> str: + return self._api.user_role[0] # only use the first character @property - def language(self): + def language(self) -> str: lang = self._options.get(CONF_LANGUAGE, DEFAULT_LANGUAGE) if lang == LANGUAGE_AUTO: system_lang = self.system_language @@ -208,7 +189,7 @@ def language(self): @property - def system_language(self): + def system_language(self) -> str: """ Get HASS system language as set under Settings->System->General. Unless that language is not allowed in DConnect DAB LANGUAGE_MAP, in that case fallback to DEFAULT_LANGUAGE @@ -223,8 +204,8 @@ async def async_config_flow_data(self): _LOGGER.debug(f"Config flow data") await self._async_detect_install_list() - #_LOGGER.debug(f"install_map: {self._install_map}") - return (self._install_map) + #_LOGGER.debug(f"install_map: {self._api.install_map}") + return (self._api.install_map) async def async_create_devices(self, config_entry: ConfigEntry): @@ -238,7 +219,7 @@ async def async_create_devices(self, config_entry: ConfigEntry): _LOGGER.info(f"Create devices for installation '{install_name}' ({install_id})") dr: DeviceRegistry = device_registry.async_get(self.hass) - for device in self._device_map.values(): + for device in self._api.device_map.values(): _LOGGER.debug(f"Create device {device.serial} ({DabPumpsCoordinator.create_id(device.name)})") dr.async_get_or_create( @@ -289,17 +270,17 @@ async def _async_update_data(self): store["cache"] = self._cache await self._store.async_set_data(store) - #_LOGGER.debug(f"device_map: {self._device_map}") - #_LOGGER.debug(f"config_map: {self._config_map}") - #_LOGGER.debug(f"status_map: {self._status_map}") - return (self._device_map, self._config_map, self._status_map) + #_LOGGER.debug(f"device_map: {self._api.device_map}") + #_LOGGER.debug(f"config_map: {self._api.config_map}") + #_LOGGER.debug(f"status_map: {self._api.status_map}") + return (self._api.device_map, self._api.config_map, self._api.status_map) - async def async_modify_data(self, object_id: str, value: Any): + async def async_modify_data(self, object_id: str, entity_id: str, value: Any): """ Set an entity param via the API. """ - status = self._status_map.get(object_id) + status = self._api.status_map.get(object_id) if not status: # Not found return False @@ -308,12 +289,6 @@ async def async_modify_data(self, object_id: str, value: Any): # Not changed return False - _LOGGER.debug(f"Set {status.unique_id} from {status.val} to {value}") - - # update the cached value in status_map - status = status._replace(val=value) - self._status_map[object_id] = status - # update the remote value return await self._async_change_device_status(status, value) @@ -343,7 +318,7 @@ async def _async_detect_install_list(self): if retry < 2: _LOGGER.info(f"Retry {retry+1} in {COORDINATOR_RETRY_DELAY} seconds. {error}") else: - _LOGGER.warn(f"Retry {retry+1} in {COORDINATOR_RETRY_DELAY} seconds. {error}") + _LOGGER.warning(f"Retry {retry+1} in {COORDINATOR_RETRY_DELAY} seconds. {error}") await asyncio.sleep(COORDINATOR_RETRY_DELAY) if error: @@ -364,7 +339,7 @@ async def _async_detect_data(self): try: await self._api.async_login() except: - if len(self._device_map) > 0: + if len(self._api.device_map) > 0: # Force retry in loop by raising original exception raise else: @@ -389,9 +364,6 @@ async def _async_detect_data(self): # Attempt to refresh the list of installations (once a day, just for diagnostocs) await self._async_detect_installations(ignore_exception=True) - # Update device parameters that are derived from statusses instead of install_details - self._update_devices() - # Keep track of how many retries were needed and duration self._update_statistics(retries = retry, duration = datetime.now()-ts_start) return True @@ -406,7 +378,7 @@ async def _async_detect_data(self): if retry < 2: _LOGGER.info(f"Retry {retry+1} in {COORDINATOR_RETRY_DELAY} seconds. {error}") else: - _LOGGER.warn(f"Retry {retry+1} in {COORDINATOR_RETRY_DELAY} seconds. {error}") + _LOGGER.warning(f"Retry {retry+1} in {COORDINATOR_RETRY_DELAY} seconds. {error}") await asyncio.sleep(COORDINATOR_RETRY_DELAY) if error: @@ -442,7 +414,7 @@ async def _async_change_device_status(self, status: DabPumpsStatus, value: Any): if retry < 2: _LOGGER.info(f"Retry {retry+1} in {COORDINATOR_RETRY_DELAY} seconds. {error}") else: - _LOGGER.warn(f"Retry {retry+1} in {COORDINATOR_RETRY_DELAY} seconds. {error}") + _LOGGER.warning(f"Retry {retry+1} in {COORDINATOR_RETRY_DELAY} seconds. {error}") await asyncio.sleep(COORDINATOR_RETRY_DELAY) if error: @@ -457,7 +429,7 @@ async def _async_detect_install_details(self): """ Attempt to refresh installation details and devices when the cached one expires (once a day) """ - if (datetime.now() - self._device_map_ts1).total_seconds() < 86400: + if (datetime.now() - self._api.device_map_ts).total_seconds() < 86400: # Not yet expired return @@ -492,20 +464,16 @@ async def _async_detect_install_details(self): # Force retry in calling function by raising original exception raise ex - # If we reach this point, then all devices have been fetched/refreshed - self._device_map = self._api.device_map - self._device_map_ts1 = datetime.now() - async def _async_detect_device_details(self): """ Attempt to refresh device details (once a day) """ - if (datetime.now() - self._device_map_ts2).total_seconds() < 86400: + if (datetime.now() - self._api.device_detail_ts).total_seconds() < 86400: # Not yet expired return - for device in self._device_map.values(): + for device in self._api.device_map.values(): # First try to retrieve from API context = f"device {device.serial}" @@ -537,21 +505,17 @@ async def _async_detect_device_details(self): if ex: # Force retry in calling function by raising original exception raise ex - - # If we reach this point, then all device configs have been fetched/refreshed - self._device_map = self._api.config_map - self._device_map_ts2 = datetime.now() async def _async_detect_device_configs(self): """ Attempt to refresh device configurations (once a day) """ - if (datetime.now() - self._config_map_ts).total_seconds() < 86400: + if (datetime.now() - self._api.config_map_ts).total_seconds() < 86400: # Not yet expired return - for device in self._device_map.values(): + for device in self._api.device_map.values(): # First try to retrieve from API context = f"configuration {device.config_id}" @@ -583,21 +547,17 @@ async def _async_detect_device_configs(self): if ex: # Force retry in calling function by raising original exception raise ex - - # If we reach this point, then all device configs have been fetched/refreshed - self._config_map = self._api.config_map - self._config_map_ts = datetime.now() async def _async_detect_device_statusses(self): """ Fetch device statusses (always) """ - if (datetime.now() - self._status_map_ts).total_seconds() < 0: + if (datetime.now() - self._api.status_map_ts).total_seconds() < 0: # Not yet expired return - for device in self._device_map.values(): + for device in self._api.device_map.values(): # First try to retrieve from API context = f"statusses {device.serial}" @@ -631,16 +591,12 @@ async def _async_detect_device_statusses(self): # Force retry in calling function by raising original exception raise ex - # If we reach this point, then all device statusses have been fetched/refreshed - self._status_map = self._api.status_map - self._status_map_ts = datetime.now() - async def _async_detect_strings(self): """ Attempt to refresh the list of translations (once a day) """ - if (datetime.now() - self._string_map_ts).total_seconds() < 86400: + if (datetime.now() - self._api.string_map_ts).total_seconds() < 86400: # Not yet expired return @@ -674,16 +630,12 @@ async def _async_detect_strings(self): # Force retry in calling function by raising original exception raise ex - # If we reach this point, then all strings have been fetched/refreshed - self._string_map = self._api.string_map - self._string_map_ts = datetime.now() - async def _async_detect_installations(self, ignore_exception=False): """ Attempt to refresh the list of installations (once a day, just for diagnostocs) """ - if (datetime.now() - self._install_map_ts).total_seconds() < 86400: + if (datetime.now() - self._api.install_map_ts).total_seconds() < 86400: # Not yet expired return @@ -708,10 +660,6 @@ async def _async_detect_installations(self, ignore_exception=False): # Force retry in calling function by raising original exception raise ex - # If we reach this point, then installation list been fetched/refreshed/ignored - self._install_map = self._api.install_map - self._install_map_ts = datetime.now() - async def _async_update_cache(self, context, data): """ @@ -735,12 +683,12 @@ async def _async_fetch_from_cache(self, context): async def async_get_diagnostics(self) -> dict[str, Any]: - install_map = { k: v._asdict() for k,v in self._install_map.items() } - device_map = { k: v._asdict() for k,v in self._device_map.items() } - config_map = { k: v._asdict() for k,v in self._config_map.items() } - status_map = { k: v._asdict() for k,v in self._status_map.items() } + install_map = { k: v._asdict() for k,v in self._api.install_map.items() } + device_map = { k: v._asdict() for k,v in self._api.device_map.items() } + config_map = { k: v._asdict() for k,v in self._api.config_map.items() } + status_map = { k: v._asdict() for k,v in self._api.status_map.items() } - for cmk,cmv in self._config_map.items(): + for cmk,cmv in self._api.config_map.items(): config_map[cmk]['meta_params'] = { k: v._asdict() for k,v in cmv.meta_params.items() } retries_total = sum(self._diag_retries.values()) or 1 @@ -769,20 +717,20 @@ async def async_get_diagnostics(self) -> dict[str, Any]: }, "data": { "install_id": self._install_id, - "install_map_ts": self._install_map_ts, + "install_map_ts": self._api.install_map_ts, "install_map": install_map, - "device_map_ts1": self._device_map_ts1, - "device_map_ts2": self._device_map_ts2, + "device_map_ts": self._api.device_map_ts, + "device_detail_ts": self._api.device_detail_ts, "device_map": device_map, - "config_map_ts": self._config_map_ts, + "config_map_ts": self._api.config_map_ts, "config_map": config_map, - "status_map_ts": self._status_map_ts, + "status_map_ts": self._api.status_map_ts, "status_map": status_map, - "string_map_ts": self._string_map_ts, - "string_map_lang": self._string_map_lang, - "string_map": self._string_map, - "user_role_ts": self._user_role_ts, - "user_role": self._user_role + "string_map_ts": self._api.string_map_ts, + "string_map_lang": self._api.string_map_lang, + "string_map": self._api.string_map, + "user_role_ts": self._api.user_role_ts, + "user_role": self._api.user_role }, "cache": self._cache, "api": { diff --git a/custom_components/dabpumps/entity_base.py b/custom_components/dabpumps/entity_base.py index f1dbd8c..c7b8a2d 100644 --- a/custom_components/dabpumps/entity_base.py +++ b/custom_components/dabpumps/entity_base.py @@ -161,9 +161,10 @@ async def async_setup_entry(self, target_platform, target_class, async_add_entit continue # Create a Sensor, Binary_Sensor, Number, Select, Switch or other entity for this status + unique_id = DabPumpsCoordinator.create_id(device.name, status.key) entity = None try: - entity = target_class(self.coordinator, self.install_id, object_id, device, params, status) + entity = target_class(self.coordinator, self.install_id, object_id, unique_id, device, params, status) entities.append(entity) except Exception as ex: _LOGGER.warning(f"Could not instantiate {platform} entity class for {object_id}. Details: {ex}") @@ -333,7 +334,7 @@ def _convert_to_unit(self): case 'None' | None: return None case _: - _LOGGER.warn(f"DAB Pumps encountered a unit or measurement '{self._params.unit}' for '{self._params.key}' that may not be supported by Home Assistant. Please contact the integration developer to have this resolved.") + _LOGGER.warning(f"DAB Pumps encountered a unit or measurement '{self._params.unit}' for '{self._params.key}' that may not be supported by Home Assistant. Please contact the integration developer to have this resolved.") return self._params.unit diff --git a/custom_components/dabpumps/manifest.json b/custom_components/dabpumps/manifest.json index a746cfd..d5bf825 100644 --- a/custom_components/dabpumps/manifest.json +++ b/custom_components/dabpumps/manifest.json @@ -9,6 +9,6 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/ankohanse/hass-dab-pumps/issues", "loggers": ["custom_components.dabpumps"], - "requirements": ["aiodabpumps>=0.0.1"], + "requirements": ["aiodabpumps>=0.1.3"], "version": "2025.01.2" } \ No newline at end of file diff --git a/custom_components/dabpumps/number.py b/custom_components/dabpumps/number.py index 088d571..d02782e 100644 --- a/custom_components/dabpumps/number.py +++ b/custom_components/dabpumps/number.py @@ -27,6 +27,11 @@ from collections import defaultdict from collections import namedtuple +from aiodabpumps import ( + DabPumpsDevice, + DabPumpsParams, + DabPumpsStatus +) from .const import ( DOMAIN, @@ -36,6 +41,10 @@ CONF_OPTIONS, ) +from .coordinator import ( + DabPumpsCoordinator, +) + from .entity_base import ( DabPumpsEntityHelperFactory, DabPumpsEntityHelper, @@ -63,22 +72,65 @@ class DabPumpsNumber(CoordinatorEntity, NumberEntity, DabPumpsEntity): Or could be part of a communication module like DConnect Box/Box2 """ - def __init__(self, coordinator, install_id, object_id, device, params, status) -> None: - """ Initialize the sensor. """ + def __init__(self, coordinator: DabPumpsCoordinator, install_id: str, object_id: str, unique_id: str, device: DabPumpsDevice, params: DabPumpsParams, status: DabPumpsStatus) -> None: + """ + Initialize the sensor. + """ + CoordinatorEntity.__init__(self, coordinator) DabPumpsEntity.__init__(self, coordinator, params) - # The unique identifier for this sensor within Home Assistant - self.object_id = object_id - self.entity_id = ENTITY_ID_FORMAT.format(status.unique_id) + # Sanity check + if params.type != 'measure': + _LOGGER.error(f"Unexpected parameter type ({params.type}) for a number entity") + + # The unique identifiers for this sensor within Home Assistant + self.object_id = object_id # Device.serial + status.key + self.entity_id = ENTITY_ID_FORMAT.format(unique_id) # Device.name + status.key self.install_id = install_id self._coordinator = coordinator self._device = device self._params = params - # Create all attributes - self._update_attributes(status, True) + # Prepare attributes + if self._params.weight and self._params.weight != 1 and self._params.weight != 0: + # Convert to float + attr_precision = int(math.floor(math.log10(1.0 / self._params.weight))) + attr_min = float(self._params.min) if self._params.min is not None else None + attr_max = float(self._params.max) if self._params.max is not None else None + attr_step = self._params.weight + else: + # Convert to int + attr_precision = 0 + attr_min = int(self._params.min) if self._params.min is not None else None + attr_max = int(self._params.max) if self._params.max is not None else None + attr_step = self.get_number_step() + + # update creation-time only attributes + _LOGGER.debug(f"Create entity '{self.entity_id}'") + + self._attr_unique_id = unique_id + + self._attr_has_entity_name = True + self._attr_name = self._get_string(status.key) + self._name = status.key + + self._attr_mode = NumberMode.BOX + self._attr_device_class = self.get_number_device_class() + self._attr_entity_category = self.get_entity_category() + if attr_min: + self._attr_native_min_value = attr_min + if attr_max: + self._attr_native_max_value = attr_max + self._attr_native_step = attr_step + + self._attr_device_info = DeviceInfo( + identifiers = {(DOMAIN, self._device.serial)}, + ) + + # Create all value related attributes + self._update_attributes(status, force=True) @property @@ -106,68 +158,38 @@ def _handle_coordinator_update(self) -> None: # find the correct device and status corresponding to this sensor (_, _, status_map) = self._coordinator.data status = status_map.get(self.object_id) + if not status: + return # Update any attributes - if status: - if self._update_attributes(status, False): - self.async_write_ha_state() + if self._update_attributes(status): + self.async_write_ha_state() - def _update_attributes(self, status, is_create): + def _update_attributes(self, status: DabPumpsStatus, force: bool = False): - if self._params.type != 'measure': - _LOGGER.error(f"Unexpected parameter type ({self._params.type}) for a number entity") - - # Process any changes - changed = False + # Prepare attributes if self._params.weight and self._params.weight != 1 and self._params.weight != 0: # Convert to float attr_precision = int(math.floor(math.log10(1.0 / self._params.weight))) - attr_min = float(self._params.min) if self._params.min is not None else None - attr_max = float(self._params.max) if self._params.max is not None else None attr_val = round(float(status.val) * self._params.weight, attr_precision) if status.val is not None else None - attr_step = self._params.weight else: # Convert to int attr_precision = 0 - attr_min = int(self._params.min) if self._params.min is not None else None - attr_max = int(self._params.max) if self._params.max is not None else None attr_val = int(status.val) if status.val is not None else None - attr_step = self.get_number_step() - - # update creation-time only attributes - if is_create: - _LOGGER.debug(f"Create number entity '{status.key}' ({status.unique_id})") - - self._attr_unique_id = status.unique_id - - self._attr_has_entity_name = True - self._attr_name = self._get_string(status.key) - self._name = status.key - - self._attr_mode = NumberMode.BOX - self._attr_device_class = self.get_number_device_class() - self._attr_entity_category = self.get_entity_category() - if attr_min: - self._attr_native_min_value = attr_min - if attr_max: - self._attr_native_max_value = attr_max - self._attr_native_step = attr_step - - self._attr_device_info = DeviceInfo( - identifiers = {(DOMAIN, self._device.serial)}, - ) - changed = True # update value if it has changed - if is_create or self._attr_native_value != attr_val: + if self._attr_native_value != attr_val or force: + self._attr_native_value = attr_val self._attr_native_unit_of_measurement = self.get_unit() self._attr_icon = self.get_icon() - changed = True + + return True - return changed + # No changes + return False async def async_set_native_value(self, value: float) -> None: @@ -181,9 +203,9 @@ async def async_set_native_value(self, value: float) -> None: data_val = int(value) value = int(value) - _LOGGER.debug(f"Set {self.entity_id} to {value} ({data_val})") + _LOGGER.info(f"Set {self.entity_id} to {value} ({data_val})") - success = await self._coordinator.async_modify_data(self.object_id, data_val) + success = await self._coordinator.async_modify_data(self.object_id, self.entity_id, data_val) if success: self._attr_native_value = value self.async_write_ha_state() diff --git a/custom_components/dabpumps/select.py b/custom_components/dabpumps/select.py index b1c2eb4..f8fd8fe 100644 --- a/custom_components/dabpumps/select.py +++ b/custom_components/dabpumps/select.py @@ -26,6 +26,11 @@ from collections import defaultdict from collections import namedtuple +from aiodabpumps import ( + DabPumpsDevice, + DabPumpsParams, + DabPumpsStatus +) from .const import ( DOMAIN, @@ -35,6 +40,10 @@ CONF_OPTIONS, ) +from .coordinator import ( + DabPumpsCoordinator, +) + from .entity_base import ( DabPumpsEntityHelperFactory, DabPumpsEntityHelper, @@ -62,14 +71,21 @@ class DabPumpsSelect(CoordinatorEntity, SelectEntity, DabPumpsEntity): Or could be part of a communication module like DConnect Box/Box2 """ - def __init__(self, coordinator, install_id, object_id, device, params, status) -> None: - """ Initialize the sensor. """ + def __init__(self, coordinator: DabPumpsCoordinator, install_id: str, object_id: str, unique_id: str, device: DabPumpsDevice, params: DabPumpsParams, status: DabPumpsStatus) -> None: + """ + Initialize the sensor. + """ + CoordinatorEntity.__init__(self, coordinator) DabPumpsEntity.__init__(self, coordinator, params) - # The unique identifier for this sensor within Home Assistant - self.object_id = object_id - self.entity_id = ENTITY_ID_FORMAT.format(status.unique_id) + # Sanity check + if params.type != 'enum': + _LOGGER.error(f"Unexpected parameter type ({params.type}) for a select entity") + + # The unique identifiers for this sensor within Home Assistant + self.object_id = object_id # Device.serial + status.key + self.entity_id = ENTITY_ID_FORMAT.format(unique_id) # Device.name + status.key self.install_id = install_id self._coordinator = coordinator @@ -78,8 +94,27 @@ def __init__(self, coordinator, install_id, object_id, device, params, status) - self._key = params.key self._dict = { k: self._get_string(v) for k,v in params.values.items() } - # Create all attributes - self._update_attributes(status, True) + # update creation-time only attributes + _LOGGER.debug(f"Create entity '{self.entity_id}'") + + self._attr_unique_id = unique_id + + self._attr_has_entity_name = True + self._attr_name = self._get_string(status.key) + self._name = status.key + + self._attr_options = list(self._dict.values()) + self._attr_current_option = None + + self._attr_entity_category = self.get_entity_category() + + self._attr_device_class = None + self._attr_device_info = DeviceInfo( + identifiers = {(DOMAIN, self._device.serial)}, + ) + + # Create all value related attributes + self._update_attributes(status, force=True) @property @@ -107,52 +142,29 @@ def _handle_coordinator_update(self) -> None: # find the correct device and status corresponding to this sensor (_, _, status_map) = self._coordinator.data status = status_map.get(self.object_id) + if not status: + return # Update any attributes - if status: - if self._update_attributes(status, False): - self.async_write_ha_state() + if self._update_attributes(status): + self.async_write_ha_state() - def _update_attributes(self, status, is_create): + def _update_attributes(self, status: DabPumpsStatus, force: bool = False) -> bool: - if self._params.type != 'enum': - _LOGGER.error(f"Unexpected parameter type ({self._params.type}) for a select entity") - - # Process any changes - changed = False + # update value if it has changed attr_val = self._dict.get(status.val, status.val) if status.val!=None else None - # update creation-time only attributes - if is_create: - _LOGGER.debug(f"Create select entity '{status.key}' ({status.unique_id})") - - self._attr_unique_id = status.unique_id - - self._attr_has_entity_name = True - self._attr_name = self._get_string(status.key) - self._name = status.key - - self._attr_options = list(self._dict.values()) - self._attr_current_option = None - - self._attr_entity_category = self.get_entity_category() - - self._attr_device_class = None - self._attr_device_info = DeviceInfo( - identifiers = {(DOMAIN, self._device.serial)}, - ) - changed = True - - # update value if it has changed - if is_create or self._attr_current_option != attr_val: + if self._attr_current_option != attr_val or force: + self._attr_current_option = attr_val self._attr_unit_of_measurement = self.get_unit() self._attr_icon = self.get_icon() - changed = True + return True - return changed + # No changes + return False async def async_select_option(self, option: str) -> None: @@ -161,7 +173,7 @@ async def async_select_option(self, option: str) -> None: if data_val: _LOGGER.info(f"Set {self.entity_id} to {option} ({data_val})") - success = await self._coordinator.async_modify_data(self.object_id, data_val) + success = await self._coordinator.async_modify_data(self.object_id, self.entity_id, data_val) if success: self._attr_current_option = option self.async_write_ha_state() diff --git a/custom_components/dabpumps/sensor.py b/custom_components/dabpumps/sensor.py index c516061..b67ee0b 100644 --- a/custom_components/dabpumps/sensor.py +++ b/custom_components/dabpumps/sensor.py @@ -29,6 +29,11 @@ from collections import defaultdict from collections import namedtuple +from aiodabpumps import ( + DabPumpsDevice, + DabPumpsParams, + DabPumpsStatus +) from .const import ( DOMAIN, @@ -38,6 +43,10 @@ CONF_OPTIONS, ) +from .coordinator import ( + DabPumpsCoordinator, +) + from .entity_base import ( DabPumpsEntityHelperFactory, DabPumpsEntityHelper, @@ -65,22 +74,42 @@ class DabPumpsSensor(CoordinatorEntity, SensorEntity, DabPumpsEntity): Or could be part of a communication module like DConnect Box/Box2 """ - def __init__(self, coordinator, install_id, object_id, device, params, status) -> None: - """ Initialize the sensor. """ + def __init__(self, coordinator: DabPumpsCoordinator, install_id: str, object_id: str, unique_id: str, device: DabPumpsDevice, params: DabPumpsParams, status: DabPumpsStatus) -> None: + """ + Initialize the sensor. + """ + CoordinatorEntity.__init__(self, coordinator) DabPumpsEntity.__init__(self, coordinator, params) - # The unique identifier for this sensor within Home Assistant - self.object_id = object_id - self.entity_id = ENTITY_ID_FORMAT.format(status.unique_id) + # The unique identifiers for this sensor within Home Assistant + self.object_id = object_id # Device.serial + status.key + self.entity_id = ENTITY_ID_FORMAT.format(unique_id) # Device.name + status.key self.install_id = install_id self._coordinator = coordinator self._device = device self._params = params - # Create all attributes - self._update_attributes(status, True) + # update creation-time only attributes + _LOGGER.debug(f"Create entity '{self.entity_id}'") + + self._attr_unique_id = unique_id + + self._attr_has_entity_name = True + self._attr_name = self._get_string(status.key) + self._name = status.key + + self._attr_state_class = self.get_sensor_state_class() + self._attr_entity_category = self.get_entity_category() + + self._attr_device_class = self.get_sensor_device_class() + self._attr_device_info = DeviceInfo( + identifiers = {(DOMAIN, self._device.serial)}, + ) + + # Create all value related attributes + self._update_attributes(status, force=True) @property @@ -108,14 +137,15 @@ def _handle_coordinator_update(self) -> None: # find the correct device and status corresponding to this sensor (_, _, status_map) = self._coordinator.data status = status_map.get(self.object_id) + if not status: + return # Update any attributes - if status: - if self._update_attributes(status, False): - self.async_write_ha_state() + if self._update_attributes(status): + self.async_write_ha_state() - def _update_attributes(self, status, is_create): + def _update_attributes(self, status: DabPumpsStatus, force:bool=False): # Transform values according to the metadata params for this status/sensor match self._params.type: @@ -139,35 +169,13 @@ def _update_attributes(self, status, is_create): case 'label' | _: if self._params.type != 'label': - _LOGGER.warn(f"DAB Pumps encountered an unknown sensor type '{self._params.type}' for '{self._params.key}'. Please contact the integration developer to have this resolved.") + _LOGGER.warning(f"DAB Pumps encountered an unknown sensor type '{self._params.type}' for '{self._params.key}'. Please contact the integration developer to have this resolved.") # Convert to string attr_precision = None attr_val = self._get_string(str(status.val)) if status.val!=None else None attr_unit = None - # Process any changes - changed = False - - # update creation-time only attributes - if is_create: - _LOGGER.debug(f"Create sensor '{status.key}' ({status.unique_id})") - - self._attr_unique_id = status.unique_id - - self._attr_has_entity_name = True - self._attr_name = self._get_string(status.key) - self._name = status.key - - self._attr_state_class = self.get_sensor_state_class() - self._attr_entity_category = self.get_entity_category() - - self._attr_device_class = self.get_sensor_device_class() - self._attr_device_info = DeviceInfo( - identifiers = {(DOMAIN, self._device.serial)}, - ) - changed = True - # additional check for TOTAL and TOTAL_INCREASING values: # ignore decreases that are not significant (less than 50% change) if self._attr_state_class in [SensorStateClass.TOTAL, SensorStateClass.TOTAL_INCREASING] and \ @@ -176,17 +184,19 @@ def _update_attributes(self, status, is_create): attr_val < self._attr_native_value and \ not check_percentage_change(self._attr_native_value, attr_val, 50): - _LOGGER.debug(f"Ignore non-significant decrease in sensor '{status.key}' ({status.unique_id}) from {self._attr_native_value} to {attr_val}") + _LOGGER.debug(f"Ignore non-significant decrease in sensor '{status.key}' ({self.unique_id}) from {self._attr_native_value} to {attr_val}") attr_val = self._attr_native_value # update value if it has changed - if is_create or self._attr_native_value != attr_val: + if self._attr_native_value != attr_val or force: + self._attr_native_value = attr_val self._attr_native_unit_of_measurement = attr_unit self._attr_suggested_display_precision = attr_precision self._attr_icon = self.get_icon() - changed = True + return True - return changed + # No changes + return False diff --git a/custom_components/dabpumps/switch.py b/custom_components/dabpumps/switch.py index 1efcc09..03e8703 100644 --- a/custom_components/dabpumps/switch.py +++ b/custom_components/dabpumps/switch.py @@ -32,6 +32,15 @@ from collections import defaultdict from collections import namedtuple +from aiodabpumps import ( + DabPumpsDevice, + DabPumpsParams, + DabPumpsStatus +) + +from .coordinator import ( + DabPumpsCoordinator, +) from .const import ( DOMAIN, @@ -70,14 +79,21 @@ class DabPumpsSwitch(CoordinatorEntity, SwitchEntity, DabPumpsEntity): Or could be part of a communication module like DConnect Box/Box2 """ - def __init__(self, coordinator, install_id, object_id, device, params, status) -> None: - """ Initialize the sensor. """ + def __init__(self, coordinator: DabPumpsCoordinator, install_id: str, object_id: str, unique_id: str, device: DabPumpsDevice, params: DabPumpsParams, status: DabPumpsStatus) -> None: + """ + Initialize the sensor. + """ + CoordinatorEntity.__init__(self, coordinator) DabPumpsEntity.__init__(self, coordinator, params) - # The unique identifier for this sensor within Home Assistant - self.object_id = object_id - self.entity_id = ENTITY_ID_FORMAT.format(status.unique_id) + # Sanity check + if params.type != 'enum': + _LOGGER.error(f"Unexpected parameter type ({params.type}) for a select entity") + + # The unique identifiers for this sensor within Home Assistant + self.object_id = object_id # Device.serial + status.key + self.entity_id = ENTITY_ID_FORMAT.format(unique_id) # Device.name + status.key self.install_id = install_id self._coordinator = coordinator @@ -86,8 +102,24 @@ def __init__(self, coordinator, install_id, object_id, device, params, status) - self._key = params.key self._dict = { k: self._get_string(v) for k,v in params.values.items() } - # Create all attributes - self._update_attributes(status, True) + # update creation-time only attributes + _LOGGER.debug(f"Create entity '{self.entity_id}'") + + self._attr_unique_id = unique_id + + self._attr_has_entity_name = True + self._attr_name = self._get_string(status.key) + self._name = status.key + + self._attr_entity_category = self.get_entity_category() + self._attr_device_class = SwitchDeviceClass.SWITCH + + self._attr_device_info = DeviceInfo( + identifiers = {(DOMAIN, self._device.serial)}, + ) + + # Create all value related attributes + self._update_attributes(status, force=True) @property @@ -115,20 +147,17 @@ def _handle_coordinator_update(self) -> None: # find the correct device and status corresponding to this sensor (_, _, status_map) = self._coordinator.data status = status_map.get(self.object_id) + if not status: + return # Update any attributes - if status: - if self._update_attributes(status, False): - self.async_write_ha_state() + if self._update_attributes(status): + self.async_write_ha_state() - def _update_attributes(self, status, is_create): + def _update_attributes(self, status: DabPumpsStatus, force:bool=False): - if self._params.type != 'enum': - _LOGGER.error(f"Unexpected parameter type ({self._params.type}) for a select entity") - # Process any changes - changed = False val = self._params.values.get(status.val, status.val) if val in SWITCH_VALUES_ON: attr_is_on = True @@ -137,38 +166,24 @@ def _update_attributes(self, status, is_create): elif val in SWITCH_VALUES_OFF: attr_is_on = False attr_state = STATE_OFF + else: attr_is_on = None attr_state = None - # update creation-time only attributes - if is_create: - _LOGGER.debug(f"Create switch entity '{status.key}' ({status.unique_id})") - - self._attr_unique_id = status.unique_id - - self._attr_has_entity_name = True - self._attr_name = self._get_string(status.key) - self._name = status.key - - self._attr_entity_category = self.get_entity_category() - self._attr_device_class = SwitchDeviceClass.SWITCH - - self._attr_device_info = DeviceInfo( - identifiers = {(DOMAIN, self._device.serial)}, - ) - changed = True - # update value if it has changed - if is_create or self._attr_is_on != attr_is_on: + if self._attr_is_on != attr_is_on or force: + self._attr_is_on = attr_is_on self._attr_state = attr_state self._attr_unit_of_measurement = self.get_unit() self._attr_icon = self.get_icon() - changed = True - return changed + return True + + # No changes + return False async def async_turn_on(self, **kwargs) -> None: @@ -177,7 +192,7 @@ async def async_turn_on(self, **kwargs) -> None: if data_val: _LOGGER.info(f"Set {self.entity_id} to ON ({data_val})") - success = await self._coordinator.async_modify_data(self.object_id, data_val) + success = await self._coordinator.async_modify_data(self.object_id, self.entity_id, data_val) if success: self._attr_is_on = True self._attr_state = STATE_ON @@ -190,7 +205,7 @@ async def async_turn_off(self, **kwargs) -> None: if data_val: _LOGGER.info(f"Set {self.entity_id} to OFF ({data_val})") - success = await self._coordinator.async_modify_data(self.object_id, data_val) + success = await self._coordinator.async_modify_data(self.object_id, self.entity_id, data_val) if success: self._attr_is_on = False self._attr_state = STATE_OFF