From e538fc295a35b63fd2d4b48607453709ef3141d9 Mon Sep 17 00:00:00 2001 From: Martin Kase Date: Mon, 23 Oct 2023 23:20:19 +0200 Subject: [PATCH 1/3] Phyn Smart Water Assistant Support --- custom_components/phyn/__init__.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/custom_components/phyn/__init__.py b/custom_components/phyn/__init__.py index d2c5ce7..2a484d4 100644 --- a/custom_components/phyn/__init__.py +++ b/custom_components/phyn/__init__.py @@ -60,20 +60,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Phyn homes: %s", homes) - #try: - await client.mqtt.connect() - #except: - # raise HaCannotConnect("Unknown MQTT connection failure") - - coordinator = PhynDataUpdateCoordinator(hass, client) - for home in homes: - for device in home["devices"]: - if device["product_code"] in ["PW1","PP1","PP2"]: - coordinator.add_device(home["id"], device["device_id"], device["product_code"]) - hass.data[DOMAIN][entry.entry_id]["coordinator"] = coordinator - - await coordinator.async_refresh() - await coordinator.async_setup() + hass.data[DOMAIN][entry.entry_id]["devices"] = devices = [ + PhynDeviceDataUpdateCoordinator(hass, client, home["id"], device["device_id"]) + for home in homes + for device in home["devices"] + ] + + tasks = [device.async_refresh() for device in devices] + await asyncio.gather(*tasks) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) From 8b4b107bfdd67fd0589b605b539205cc89f1a887 Mon Sep 17 00:00:00 2001 From: Jordan Date: Sat, 27 Jan 2024 09:23:05 -0500 Subject: [PATCH 2/3] Add PhynClassic device --- custom_components/phyn/__init__.py | 21 ++- custom_components/phyn/devices/pc.py | 145 +++++++++++++++++++ custom_components/phyn/devices/pp.py | 44 +----- custom_components/phyn/entities/base.py | 44 ++++++ custom_components/phyn/update_coordinator.py | 5 + 5 files changed, 210 insertions(+), 49 deletions(-) create mode 100644 custom_components/phyn/devices/pc.py diff --git a/custom_components/phyn/__init__.py b/custom_components/phyn/__init__.py index 2a484d4..a6b1d2b 100644 --- a/custom_components/phyn/__init__.py +++ b/custom_components/phyn/__init__.py @@ -60,14 +60,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Phyn homes: %s", homes) - hass.data[DOMAIN][entry.entry_id]["devices"] = devices = [ - PhynDeviceDataUpdateCoordinator(hass, client, home["id"], device["device_id"]) - for home in homes - for device in home["devices"] - ] - - tasks = [device.async_refresh() for device in devices] - await asyncio.gather(*tasks) + #try: + await client.mqtt.connect() + #except: + # raise HaCannotConnect("Unknown MQTT connection failure") + + coordinator = PhynDataUpdateCoordinator(hass, client) + for home in homes: + for device in home["devices"]: + coordinator.add_device(home["id"], device["device_id"], device["product_code"]) + hass.data[DOMAIN][entry.entry_id]["coordinator"] = coordinator + + await coordinator.async_refresh() + await coordinator.async_setup() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/custom_components/phyn/devices/pc.py b/custom_components/phyn/devices/pc.py new file mode 100644 index 0000000..1100f36 --- /dev/null +++ b/custom_components/phyn/devices/pc.py @@ -0,0 +1,145 @@ +"""Support for Phyn Classic Water Monitor sensors.""" +from __future__ import annotations +from typing import Any + +from aiophyn.errors import RequestError +from async_timeout import timeout + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.components.valve import ( + ValveDeviceClass, + ValveEntity, + ValveEntityFeature +) +from homeassistant.const import ( + UnitOfPressure, + UnitOfTemperature, + UnitOfVolume, +) + +from homeassistant.helpers.update_coordinator import UpdateFailed +import homeassistant.util.dt as dt_util + +from ..const import LOGGER +from ..entities.base import ( + PhynDailyUsageSensor, + PhynFirmwareUpdateAvailableSensor, + PhynFirwmwareUpdateEntity, + PhynTemperatureSensor, + PhynPressureSensor, +) +from .base import PhynDevice + +class PhynClassicDevice(PhynDevice): + """Phyn device object.""" + + def __init__( + self, coordinator, home_id: str, device_id: str, product_code: str + ) -> None: + """Initialize the device.""" + super().__init__ (coordinator, home_id, device_id, product_code) + self._device_state: dict[str, Any] = { + "cold_line_num": None, + "hot_line_num": None, + } + self._away_mode: dict[str, Any] = {} + self._water_usage: dict[str, Any] = {} + self._last_known_valve_state: bool = True + + self.entities = [ + PhynDailyUsageSensor(self), + PhynFirmwareUpdateAvailableSensor(self), + PhynFirwmwareUpdateEntity(self), + # TODO: Ensure cold and hot lines are using the right number + PhynTemperatureSensor(self, "temperature1", "Average hot water temperature", "temperature1"), + PhynTemperatureSensor(self, "temperature2", "Average cold water temperature", "temperature2"), + PhynPressureSensor(self, "pressure1", "Average hot water pressure", "current_ps1"), + PhynPressureSensor(self, "pressure2", "Average cold water pressure", "current_ps2"), + ] + + async def async_update_data(self): + """Update data via library.""" + try: + async with timeout(20): + await self._update_device_state() + await self._update_consumption_data() + + #Update every hour + if self._update_count % 60 == 0: + await self._update_firmware_information() + + self._update_count += 1 + except (RequestError) as error: + raise UpdateFailed(error) from error + + @property + def cold_line_num(self) -> int | None: + """Return cold line number""" + return self._device_state['cold_line_num'] + + @property + def consumption_today(self) -> float: + """Return the current consumption for today in gallons.""" + return self._water_usage["water_consumption"] + + @property + def current_flow_rate(self) -> float: + """Return current flow rate in gpm.""" + if "v" not in self._device_state["flow"]: + return None + return round(self._device_state["flow"]["v"], 3) + + @property + def current_psi1(self) -> float: + """Return the current pressure in psi.""" + if "v" in self._device_state["pressure1"]: + return round(self._device_state["pressure1"]["v"], 2) + return round(self._device_state["pressure1"]["mean"], 2) + + @property + def current_psi2(self) -> float: + """Return the current pressure in psi.""" + if "v" in self._device_state["pressure2"]: + return round(self._device_state["pressure2"]["v"], 2) + return round(self._device_state["pressure2"]["mean"], 2) + + @property + def hot_line_num(self) -> int | None: + """Return hot line number""" + return self._device_state['hot_line_num'] + + @property + def leak_test_running(self) -> bool: + """Check if a leak test is running""" + return self._device_state["sov_status"]["v"] == "LeakExp" + + @property + def temperature1(self) -> float: + """Return the current temperature in degrees F.""" + if "v" in self._device_state["temperature1"]: + return round(self._device_state["temperature1"]["v"], 2) + return round(self._device_state["temperature1"]["mean"], 2) + + @property + def temperature2(self) -> float: + """Return the current temperature in degrees F.""" + if "v" in self._device_state["temperature2"]: + return round(self._device_state["temperature2"]["v"], 2) + return round(self._device_state["temperature2"]["mean"], 2) + + async def _update_consumption_data(self, *_) -> None: + """Update water consumption data from the API.""" + today = dt_util.now().date() + duration = today.strftime("%Y/%m/%d") + self._water_usage = await self._coordinator.api_client.device.get_consumption( + self._phyn_device_id, duration + ) + LOGGER.debug("Updated Phyn consumption data: %s", self._water_usage) diff --git a/custom_components/phyn/devices/pp.py b/custom_components/phyn/devices/pp.py index 640ae85..87124c2 100644 --- a/custom_components/phyn/devices/pp.py +++ b/custom_components/phyn/devices/pp.py @@ -32,8 +32,10 @@ from ..const import GPM_TO_LPM, LOGGER, UnitOfVolumeFlow from ..entities.base import ( PhynEntity, + PhynDailyUsageSensor, PhynFirmwareUpdateAvailableSensor, PhynFirwmwareUpdateEntity, + PhynPressureSensor, PhynTemperatureSensor, PhynSwitchEntity ) @@ -76,7 +78,7 @@ def __init__( PhynLeakTestSensor(self), PhynScheduledLeakTestEnabledSwitch(self), PhynTemperatureSensor(self, "temperature", NAME_WATER_TEMPERATURE), - PhynPressureSensor(self), + PhynPressureSensor(self, "pressure", NAME_WATER_PRESSURE), PhynValve(self), ] @@ -325,26 +327,6 @@ def icon(self): return "mdi:bag-suitcase" return "mdi:home-circle" -class PhynDailyUsageSensor(PhynEntity, SensorEntity): - """Monitors the daily water usage.""" - - _attr_icon = WATER_ICON - _attr_native_unit_of_measurement = UnitOfVolume.GALLONS - _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING - _attr_device_class = SensorDeviceClass.WATER - - def __init__(self, device): - """Initialize the daily water usage sensor.""" - super().__init__("daily_consumption", NAME_DAILY_USAGE, device) - self._state: float = None - - @property - def native_value(self) -> float | None: - """Return the current daily usage.""" - if self._device.consumption_today is None: - return None - return round(self._device.consumption_today, 1) - class PhynConsumptionSensor(PhynEntity, SensorEntity): """Monitors the amount of water usage.""" @@ -442,23 +424,3 @@ def _attr_is_closing(self) -> bool: if self._device.valve_changing and self._device._last_known_valve_state is True: return True return False - - -class PhynPressureSensor(PhynEntity, SensorEntity): - """Monitors the water pressure.""" - - _attr_device_class = SensorDeviceClass.PRESSURE - _attr_native_unit_of_measurement = UnitOfPressure.PSI - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - - def __init__(self, device): - """Initialize the pressure sensor.""" - super().__init__("water_pressure", NAME_WATER_PRESSURE, device) - self._state: float = None - - @property - def native_value(self) -> float | None: - """Return the current water pressure.""" - if self._device.current_psi is None: - return None - return round(self._device.current_psi, 1) diff --git a/custom_components/phyn/entities/base.py b/custom_components/phyn/entities/base.py index 347dee5..9e98ce6 100644 --- a/custom_components/phyn/entities/base.py +++ b/custom_components/phyn/entities/base.py @@ -29,6 +29,9 @@ from ..const import DOMAIN as PHYN_DOMAIN +WATER_ICON = "mdi:water" +NAME_DAILY_USAGE = "Daily water usage" + class PhynEntity(Entity): """A base class for Phyn entities.""" @@ -88,6 +91,26 @@ def is_on(self) -> bool | None: return getattr(self._device, self._device_property) return None +class PhynDailyUsageSensor(PhynEntity, SensorEntity): + """Monitors the daily water usage.""" + + _attr_icon = WATER_ICON + _attr_native_unit_of_measurement = UnitOfVolume.GALLONS + _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING + _attr_device_class = SensorDeviceClass.WATER + + def __init__(self, device): + """Initialize the daily water usage sensor.""" + super().__init__("daily_consumption", NAME_DAILY_USAGE, device) + self._state: float = None + + @property + def native_value(self) -> float | None: + """Return the current daily usage.""" + if self._device.consumption_today is None: + return None + return round(self._device.consumption_today, 1) + class PhynFirmwareUpdateAvailableSensor(PhynEntity, BinarySensorEntity): """Firmware Update Available Sensor""" _attr_device_class = BinarySensorDeviceClass.UPDATE @@ -183,6 +206,27 @@ def native_value(self) -> float | None: return None return round(self._device.humidity, 1) +class PhynPressureSensor(PhynEntity, SensorEntity): + """Monitors the water pressure.""" + + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + + def __init__(self, device, name, readable_name, device_property = None): + """Initialize the pressure sensor.""" + super().__init__(name, readable_name, device) + self._state: float = None + self._device_property = device_property + + @property + def native_value(self) -> float | None: + """Return the current water pressure.""" + if self._device_property is not None and hasattr(self._device, self._device_property): + return getattr(self._device, self._device_property) + if not hasattr(self._device, "current_psi") or self._device.current_psi is None: + return None + return round(self._device.current_psi, 1) class PhynTemperatureSensor(PhynEntity, SensorEntity): """Monitors the temperature.""" diff --git a/custom_components/phyn/update_coordinator.py b/custom_components/phyn/update_coordinator.py index ed9ec9f..91c3f65 100644 --- a/custom_components/phyn/update_coordinator.py +++ b/custom_components/phyn/update_coordinator.py @@ -14,6 +14,7 @@ from .const import DOMAIN as PHYN_DOMAIN, LOGGER +from .devices.pc import PhynClassicDevice from .devices.pp import PhynPlusDevice from .devices.pw import PhynWaterSensorDevice @@ -40,6 +41,10 @@ def add_device(self, home_id, device_id, product_code): self._devices.append( PhynPlusDevice(self, home_id, device_id, product_code) ) + elif product_code in ["PC1"]: + self._devices.append( + PhynClassicDevice(self, home_id, device_id, product_code) + ) elif product_code in ["PW1"]: self._devices.append( PhynWaterSensorDevice(self, home_id, device_id, product_code) From ff59c374f8bbbcb87651e2fff208b060bdfa4398 Mon Sep 17 00:00:00 2001 From: Martin Kase Date: Sat, 27 Jan 2024 18:31:09 +0100 Subject: [PATCH 3/3] Fixed some bugs --- custom_components/phyn/devices/pc.py | 9 ++++++--- custom_components/phyn/entities/base.py | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/custom_components/phyn/devices/pc.py b/custom_components/phyn/devices/pc.py index 1100f36..0839350 100644 --- a/custom_components/phyn/devices/pc.py +++ b/custom_components/phyn/devices/pc.py @@ -58,11 +58,10 @@ def __init__( PhynDailyUsageSensor(self), PhynFirmwareUpdateAvailableSensor(self), PhynFirwmwareUpdateEntity(self), - # TODO: Ensure cold and hot lines are using the right number PhynTemperatureSensor(self, "temperature1", "Average hot water temperature", "temperature1"), PhynTemperatureSensor(self, "temperature2", "Average cold water temperature", "temperature2"), - PhynPressureSensor(self, "pressure1", "Average hot water pressure", "current_ps1"), - PhynPressureSensor(self, "pressure2", "Average cold water pressure", "current_ps2"), + PhynPressureSensor(self, "pressure1", "Average hot water pressure", "current_psi1"), + PhynPressureSensor(self, "pressure2", "Average cold water pressure", "current_psi2"), ] async def async_update_data(self): @@ -143,3 +142,7 @@ async def _update_consumption_data(self, *_) -> None: self._phyn_device_id, duration ) LOGGER.debug("Updated Phyn consumption data: %s", self._water_usage) + + async def async_setup(self): + """Async setup not needed""" + return None diff --git a/custom_components/phyn/entities/base.py b/custom_components/phyn/entities/base.py index 9e98ce6..2e0be7d 100644 --- a/custom_components/phyn/entities/base.py +++ b/custom_components/phyn/entities/base.py @@ -209,8 +209,8 @@ def native_value(self) -> float | None: class PhynPressureSensor(PhynEntity, SensorEntity): """Monitors the water pressure.""" - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT + _attr_device_class = SensorDeviceClass.PRESSURE + _attr_native_unit_of_measurement = UnitOfPressure.PSI _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT def __init__(self, device, name, readable_name, device_property = None):