From bd8741cfdffea1544d81037730f1b3ebe6ee55e1 Mon Sep 17 00:00:00 2001 From: Ian Brown Date: Thu, 28 Nov 2024 17:55:26 -0800 Subject: [PATCH 1/3] Use timestamp device class Signed-off-by: Ian Brown --- .../sensus_analytics/manifest.json | 2 +- custom_components/sensus_analytics/sensor.py | 35 ++++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/custom_components/sensus_analytics/manifest.json b/custom_components/sensus_analytics/manifest.json index 8425a50..9745a05 100644 --- a/custom_components/sensus_analytics/manifest.json +++ b/custom_components/sensus_analytics/manifest.json @@ -1,7 +1,7 @@ { "domain": "sensus_analytics", "name": "Sensus Analytics Integration", - "version": "1.4.3", + "version": "1.4.4", "documentation": "https://github.com/zestysoft/sensus_analytics_integration", "dependencies": [], "codeowners": ["@zestysoft"], diff --git a/custom_components/sensus_analytics/sensor.py b/custom_components/sensus_analytics/sensor.py index 026c9bc..ac36817 100644 --- a/custom_components/sensus_analytics/sensor.py +++ b/custom_components/sensus_analytics/sensor.py @@ -2,6 +2,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -138,15 +139,24 @@ def native_value(self): return self.coordinator.data.get("meterAddress1") -class SensusAnalyticsLastReadSensor(StaticUnitSensorBase): +class SensusAnalyticsLastReadSensor(CoordinatorEntity, SensorEntity): """Representation of the last read timestamp sensor.""" def __init__(self, coordinator, entry): """Initialize the last read sensor.""" - super().__init__(coordinator, entry, unit="UTC") + super().__init__(coordinator) + self.coordinator = coordinator + self.entry = entry + self._unique_id = f"{DOMAIN}_{entry.entry_id}_last_read" self._attr_name = f"{DEFAULT_NAME} Last Read" - self._attr_unique_id = f"{self._unique_id}_last_read" self._attr_icon = "mdi:clock-time-nine" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + name=DEFAULT_NAME, + manufacturer="Unknown", + model="Water Meter", + ) + self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property def native_value(self): @@ -154,7 +164,7 @@ def native_value(self): last_read_ts = self.coordinator.data.get("lastRead") if last_read_ts: # Convert milliseconds to seconds for timestamp - return dt_util.utc_from_timestamp(last_read_ts / 1000).strftime("%Y-%m-%d %H:%M:%S") + return dt_util.utc_from_timestamp(last_read_ts / 1000) return None @@ -223,15 +233,24 @@ def native_value(self): return self._convert_usage(latest_read_usage) -class SensusAnalyticsLatestReadTimeSensor(StaticUnitSensorBase): +class SensusAnalyticsLatestReadTimeSensor(CoordinatorEntity, SensorEntity): """Representation of the latest read time sensor.""" def __init__(self, coordinator, entry): """Initialize the latest read time sensor.""" - super().__init__(coordinator, entry, unit="UTC") + super().__init__(coordinator) + self.coordinator = coordinator + self.entry = entry + self._unique_id = f"{DOMAIN}_{entry.entry_id}_latest_read_time" self._attr_name = f"{DEFAULT_NAME} Latest Read Time" - self._attr_unique_id = f"{self._unique_id}_latest_read_time" self._attr_icon = "mdi:clock-time-nine" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + name=DEFAULT_NAME, + manufacturer="Unknown", + model="Water Meter", + ) + self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property def native_value(self): @@ -239,7 +258,7 @@ def native_value(self): latest_read_time_ts = self.coordinator.data.get("latestReadTime") if latest_read_time_ts: # Convert milliseconds to seconds for timestamp - return dt_util.utc_from_timestamp(latest_read_time_ts / 1000).strftime("%Y-%m-%d %H:%M:%S") + return dt_util.utc_from_timestamp(latest_read_time_ts / 1000) return None From 5810e04f52c5f7a4d624896c42181d5bce1f7fd0 Mon Sep 17 00:00:00 2001 From: Ian Brown Date: Thu, 28 Nov 2024 18:53:31 -0800 Subject: [PATCH 2/3] Use mixin class for dynamic unit change Signed-off-by: Ian Brown --- .../sensus_analytics/manifest.json | 2 +- custom_components/sensus_analytics/sensor.py | 139 ++++++++---------- 2 files changed, 61 insertions(+), 80 deletions(-) diff --git a/custom_components/sensus_analytics/manifest.json b/custom_components/sensus_analytics/manifest.json index 9745a05..44af326 100644 --- a/custom_components/sensus_analytics/manifest.json +++ b/custom_components/sensus_analytics/manifest.json @@ -1,7 +1,7 @@ { "domain": "sensus_analytics", "name": "Sensus Analytics Integration", - "version": "1.4.4", + "version": "1.4.5", "documentation": "https://github.com/zestysoft/sensus_analytics_integration", "dependencies": [], "codeowners": ["@zestysoft"], diff --git a/custom_components/sensus_analytics/sensor.py b/custom_components/sensus_analytics/sensor.py index ac36817..906dcfe 100644 --- a/custom_components/sensus_analytics/sensor.py +++ b/custom_components/sensus_analytics/sensor.py @@ -1,8 +1,7 @@ """Sensor platform for the Sensus Analytics Integration.""" -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -35,7 +34,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e ) -class DynamicUnitSensorBase(CoordinatorEntity, SensorEntity): +class UsageConversionMixin: + """Mixin to provide usage conversion.""" + # pylint: disable=too-few-public-methods + def _convert_usage(self, usage): + """Convert usage based on configuration and native unit.""" + if usage is None: + return None + usage_unit = self.coordinator.data.get("usageUnit") + if usage_unit == "CF" and self.coordinator.config_entry.data.get("unit_type") == "G": + try: + return round(float(usage) * CF_TO_GALLON) + except (ValueError, TypeError): + return None + return usage + + +class DynamicUnitSensorBase(UsageConversionMixin, CoordinatorEntity, SensorEntity): """Base class for sensors with dynamic units.""" def __init__(self, coordinator, entry): @@ -51,15 +66,6 @@ def __init__(self, coordinator, entry): model="Water Meter", ) - def _convert_usage(self, usage): - """Convert usage based on configuration and native unit.""" - if usage is None: - return None - usage_unit = self.coordinator.data.get("usageUnit") - if usage_unit == "CF" and self.coordinator.config_entry.data.get("unit_type") == "G": - return round(float(usage) * CF_TO_GALLON) - return usage - def _get_usage_unit(self): """Determine the unit of measurement for usage sensors.""" usage_unit = self.coordinator.data.get("usageUnit") @@ -73,22 +79,25 @@ def native_unit_of_measurement(self): return self._get_usage_unit() -class StaticUnitSensorBase(CoordinatorEntity, SensorEntity): +class StaticUnitSensorBase(UsageConversionMixin, CoordinatorEntity, SensorEntity): """Base class for sensors with static units.""" - def __init__(self, coordinator, entry, unit): + def __init__(self, coordinator, entry, unit=None, device_class=None): """Initialize the static unit sensor base.""" super().__init__(coordinator) self.coordinator = coordinator self.entry = entry self._unique_id = f"{DOMAIN}_{entry.entry_id}" - self._attr_native_unit_of_measurement = unit self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.entry_id)}, name=DEFAULT_NAME, manufacturer="Unknown", model="Water Meter", ) + if unit: + self._attr_native_unit_of_measurement = unit + if device_class: + self._attr_device_class = device_class class SensusAnalyticsDailyUsageSensor(DynamicUnitSensorBase): @@ -139,24 +148,15 @@ def native_value(self): return self.coordinator.data.get("meterAddress1") -class SensusAnalyticsLastReadSensor(CoordinatorEntity, SensorEntity): +class SensusAnalyticsLastReadSensor(StaticUnitSensorBase): """Representation of the last read timestamp sensor.""" def __init__(self, coordinator, entry): """Initialize the last read sensor.""" - super().__init__(coordinator) - self.coordinator = coordinator - self.entry = entry - self._unique_id = f"{DOMAIN}_{entry.entry_id}_last_read" - self._attr_name = f"{DEFAULT_NAME} Last Read" + super().__init__(coordinator, entry, unit=None, device_class=SensorDeviceClass.TIMESTAMP) + self._attr_name = f"{DEFAULT_NAME} Last Read (UTC)" + self._attr_unique_id = f"{self._unique_id}_last_read" self._attr_icon = "mdi:clock-time-nine" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, entry.entry_id)}, - name=DEFAULT_NAME, - manufacturer="Unknown", - model="Water Meter", - ) - self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property def native_value(self): @@ -164,7 +164,10 @@ def native_value(self): last_read_ts = self.coordinator.data.get("lastRead") if last_read_ts: # Convert milliseconds to seconds for timestamp - return dt_util.utc_from_timestamp(last_read_ts / 1000) + try: + return dt_util.utc_from_timestamp(last_read_ts / 1000) + except (ValueError, TypeError): + return None return None @@ -233,24 +236,15 @@ def native_value(self): return self._convert_usage(latest_read_usage) -class SensusAnalyticsLatestReadTimeSensor(CoordinatorEntity, SensorEntity): +class SensusAnalyticsLatestReadTimeSensor(StaticUnitSensorBase): """Representation of the latest read time sensor.""" def __init__(self, coordinator, entry): """Initialize the latest read time sensor.""" - super().__init__(coordinator) - self.coordinator = coordinator - self.entry = entry - self._unique_id = f"{DOMAIN}_{entry.entry_id}_latest_read_time" - self._attr_name = f"{DEFAULT_NAME} Latest Read Time" + super().__init__(coordinator, entry, unit=None, device_class=SensorDeviceClass.TIMESTAMP) + self._attr_name = f"{DEFAULT_NAME} Latest Read Time (UTC)" + self._attr_unique_id = f"{self._unique_id}_latest_read_time" self._attr_icon = "mdi:clock-time-nine" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, entry.entry_id)}, - name=DEFAULT_NAME, - manufacturer="Unknown", - model="Water Meter", - ) - self._attr_device_class = DEVICE_CLASS_TIMESTAMP @property def native_value(self): @@ -258,7 +252,10 @@ def native_value(self): latest_read_time_ts = self.coordinator.data.get("latestReadTime") if latest_read_time_ts: # Convert milliseconds to seconds for timestamp - return dt_util.utc_from_timestamp(latest_read_time_ts / 1000) + try: + return dt_util.utc_from_timestamp(latest_read_time_ts / 1000) + except (ValueError, TypeError): + return None return None @@ -298,15 +295,6 @@ def native_value(self): usage_gallons = self._convert_usage(usage) return self._calculate_cost(usage_gallons) - def _convert_usage(self, usage): - """Convert usage based on the configuration and native unit.""" - if usage is None: - return None - usage_unit = self.coordinator.data.get("usageUnit") - if usage_unit == "CF" and self.coordinator.config_entry.data.get("unit_type") == "G": - return round(float(usage) * CF_TO_GALLON) - return usage - def _calculate_cost(self, usage_gallons): """Calculate the billing cost based on tiers and service fee.""" tier1_gallons = self.coordinator.config_entry.data.get("tier1_gallons") @@ -317,15 +305,16 @@ def _calculate_cost(self, usage_gallons): service_fee = self.coordinator.config_entry.data.get("service_fee") cost = service_fee - if usage_gallons <= tier1_gallons: - cost += usage_gallons * tier1_price - elif usage_gallons <= tier1_gallons + tier2_gallons: - cost += tier1_gallons * tier1_price - cost += (usage_gallons - tier1_gallons) * tier2_price - else: - cost += tier1_gallons * tier1_price - cost += tier2_gallons * tier2_price - cost += (usage_gallons - tier1_gallons - tier2_gallons) * tier3_price + if usage_gallons is not None: + if usage_gallons <= tier1_gallons: + cost += usage_gallons * tier1_price + elif usage_gallons <= tier1_gallons + tier2_gallons: + cost += tier1_gallons * tier1_price + cost += (usage_gallons - tier1_gallons) * tier2_price + else: + cost += tier1_gallons * tier1_price + cost += tier2_gallons * tier2_price + cost += (usage_gallons - tier1_gallons - tier2_gallons) * tier3_price return round(cost, 2) @@ -349,15 +338,6 @@ def native_value(self): usage_gallons = self._convert_usage(usage) return self._calculate_daily_fee(usage_gallons) - def _convert_usage(self, usage): - """Convert usage based on the configuration and native unit.""" - if usage is None: - return None - usage_unit = self.coordinator.data.get("usageUnit") - if usage_unit == "CF" and self.coordinator.config_entry.data.get("unit_type") == "G": - return round(float(usage) * CF_TO_GALLON) - return usage - def _calculate_daily_fee(self, usage_gallons): """Calculate the daily fee based on tiers.""" tier1_gallons = self.coordinator.config_entry.data.get("tier1_gallons") @@ -367,14 +347,15 @@ def _calculate_daily_fee(self, usage_gallons): tier3_price = self.coordinator.config_entry.data.get("tier3_price") cost = 0 - if usage_gallons <= tier1_gallons: - cost += usage_gallons * tier1_price - elif usage_gallons <= tier1_gallons + tier2_gallons: - cost += tier1_gallons * tier1_price - cost += (usage_gallons - tier1_gallons) * tier2_price - else: - cost += tier1_gallons * tier1_price - cost += tier2_gallons * tier2_price - cost += (usage_gallons - tier1_gallons - tier2_gallons) * tier3_price + if usage_gallons is not None: + if usage_gallons <= tier1_gallons: + cost += usage_gallons * tier1_price + elif usage_gallons <= tier1_gallons + tier2_gallons: + cost += tier1_gallons * tier1_price + cost += (usage_gallons - tier1_gallons) * tier2_price + else: + cost += tier1_gallons * tier1_price + cost += tier2_gallons * tier2_price + cost += (usage_gallons - tier1_gallons - tier2_gallons) * tier3_price return round(cost, 2) From cb986b9966c6b7411341ffdc3903897d5acfa4fa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 29 Nov 2024 02:53:55 +0000 Subject: [PATCH 3/3] Auto-format code with Black and isort --- custom_components/sensus_analytics/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/sensus_analytics/sensor.py b/custom_components/sensus_analytics/sensor.py index 906dcfe..e401156 100644 --- a/custom_components/sensus_analytics/sensor.py +++ b/custom_components/sensus_analytics/sensor.py @@ -36,6 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e class UsageConversionMixin: """Mixin to provide usage conversion.""" + # pylint: disable=too-few-public-methods def _convert_usage(self, usage): """Convert usage based on configuration and native unit."""