Skip to content

Commit

Permalink
Add sensors for Firmware Update, Leak Flow Test Running.
Browse files Browse the repository at this point in the history
Add switch to turn off Scheduled Leak Flow Test
  • Loading branch information
jordanruthe committed Jan 17, 2024
1 parent 3564374 commit 1c0ee15
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 44 deletions.
6 changes: 4 additions & 2 deletions custom_components/phyn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.VALVE]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH, Platform.VALVE]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up flo from a config entry."""
session = async_get_clientsession(hass)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {}
client_id = "homeassistant-%s" % (hass.data['core.uuid'])
client_id = f"homeassistant-{hass.data['core.uuid']}"
try:
hass.data[DOMAIN][entry.entry_id][CLIENT] = client = await async_get_api(
entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD],
Expand Down Expand Up @@ -64,6 +64,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
client = hass.data[DOMAIN][entry.entry_id][CLIENT]
await client.mqtt.disconnect_and_wait()
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
Expand Down
30 changes: 30 additions & 0 deletions custom_components/phyn/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Switch representing the shutoff valve for the Phyn integration."""
from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.binary_sensor import BinarySensorEntity

from .const import DOMAIN as PHYN_DOMAIN
from .device import PhynDeviceDataUpdateCoordinator

async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Phyn switches from config entry."""
devices: list[PhynDeviceDataUpdateCoordinator] = hass.data[PHYN_DOMAIN][
config_entry.entry_id
]["devices"]
entities = []
for device in devices:
entities.extend(
[
entity
for entity in device.entities
if isinstance(entity, BinarySensorEntity)
]
)
async_add_entities(entities)
98 changes: 85 additions & 13 deletions custom_components/phyn/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
PhynDailyUsageSensor,
PhynConsumptionSensor,
PhynCurrentFlowRateSensor,
PhynFirmwareUpdateAvailableSensor,
PhynLeakTestSensor,
PhynScheduledLeakTestEnabledSwitch,
PhynValve,
PhynTemperatureSensor,
PhynPressureSensor
Expand All @@ -47,20 +50,24 @@ def __init__(
"ts": 0,
}
}
self._device_preferences: dict[dict[str, Any]] = {}
self._firmware_info: dict[str, Any] = {}
self._rt_device_state: dict[str, Any] = {}
self._away_mode: dict[str, Any] = {}
self._water_usage: dict[str, Any] = {}
self._last_known_valve_state: bool = True

if product_code in ['PP1','PP2']:
# Entities for Phyn Plus 1 and Phyn Plus 2

self.entities = [
PhynAwayModeSwitch(self),
PhynFlowState(self),
PhynDailyUsageSensor(self),
PhynCurrentFlowRateSensor(self),
PhynConsumptionSensor(self),
PhynFirmwareUpdateAvailableSensor(self),
PhynLeakTestSensor(self),
PhynScheduledLeakTestEnabledSwitch(self),
PhynTemperatureSensor(self),
PhynPressureSensor(self),
PhynValve(self),
Expand All @@ -78,8 +85,9 @@ async def _async_update_data(self):
try:
async with timeout(20):
await self._update_device()
await self._update_device_preferences()
await self._update_firmware_information()
await self._update_consumption_data()
await self._update_away_mode()
except (RequestError) as error:
raise UpdateFailed(error) from error

Expand Down Expand Up @@ -157,6 +165,20 @@ def consumption_today(self) -> float:
def firmware_version(self) -> str:
"""Return the firmware version for the device."""
return self._device_state["fw_version"]

@property
def firmware_has_update(self) -> bool:
"""Return if the firmware has an update"""
if "fw_version" not in self._firmware_info:
return None
return int(self._firmware_info["fw_version"]) > int(self._device_state["fw_version"])

@property
def scheduled_leak_test_enabled(self) -> bool:
"""Return if the scheduled leak test is enabled"""
if "scheduler_enable" not in self._device_preferences:
return None
return self._device_preferences["scheduler_enable"]["value"] == "true"

@property
def serial_number(self) -> str:
Expand All @@ -176,6 +198,11 @@ def valve_changing(self) -> bool:
"""Return the valve changing status"""
return self._device_state["sov_status"]["v"] == "Partial"

@property
def leak_test_running(self) -> bool:
"""Check if a leak test is running"""
return self._device_state["sov_status"]["v"] == "LeakExp"

async def async_setup(self):
"""Setup a new device coordinator"""
LOGGER.debug("Setting up coordinator")
Expand All @@ -187,25 +214,65 @@ async def async_setup(self):
@property
def away_mode(self) -> bool:
"""Return True if device is in away mode."""
if "value" not in self._away_mode:
if "leak_sensitivity_away_mode" not in self._device_preferences:
return None
return self._device_preferences["leak_sensitivity_away_mode"]["value"] == "true"

async def set_device_preference(self, name: str, val: bool) -> None:
"""Set Device Preference"""
if name not in ["leak_sensitivity_away_mode", "scheduler_enable"]:
LOGGER.debug("Tried setting preference for %s but not avialable", name)
return None
return self._away_mode["value"] == "true"
if val not in ["true", "false"]:
return None
params = [{
"device_id": self._phyn_device_id,
"name": name,
"value": val
}]
LOGGER.debug("Setting preference '%s' to '%s'", name, val)
await self.api_client.device.set_device_preferences(self._phyn_device_id, params)
if name not in self._device_preferences:
self._device_preferences[name] = {}
self._device_preferences[name]["value"] = val

async def set_away_mode(self, state: bool) -> None:
"""Manually set away mode value"""
if state:
await self.api_client.device.enable_away_mode(self.id)
self._away_mode["value"] = "true"
else:
await self.api_client.device.disable_away_mode(self.id)
self._away_mode["value"] = "false"
key = "leak_sensitivity_away_mode"
val = "true" if state else "false"
params = [{
"device_id": self._phyn_device_id,
"name": key,
"value": val
}]
await self.api_client.device.set_device_preferences(self._phyn_device_id, params)
self._device_preferences[key]["value"] = val

async def set_scheduler_enabled(self, state: bool) -> None:
"""Manually set the scheduler enabled mode"""
key = "scheduler_enable"
val = "true" if state else "false"
params = [{
"device_id": self._phyn_device_id,
"name": key,
"value": val
}]
await self.api_client.set_device_preferences(self._phyn_device_id, params)
self._device_preferences[key]["value"] = val

async def _update_device(self, *_) -> None:
"""Update the device state from the API."""
self._device_state.update(await self.api_client.device.get_state(
self._phyn_device_id
))
LOGGER.debug("Phyn device state: %s", self._device_state)
#LOGGER.debug("Phyn device state: %s", self._device_state)

async def _update_device_preferences(self, *_) -> None:
"""Update the device preferences from the API"""
data = await self.api_client.device.get_device_preferences(self._phyn_device_id)
for item in data:
self._device_preferences.update({item['name']: item})
LOGGER.debug("Device Preferences: %s", self._device_preferences)

async def _update_consumption_data(self, *_) -> None:
"""Update water consumption data from the API."""
Expand All @@ -216,6 +283,10 @@ async def _update_consumption_data(self, *_) -> None:
)
LOGGER.debug("Updated Phyn consumption data: %s", self._water_usage)

async def _update_firmware_information(self, *_) -> None:
self._firmware_info.update((await self.api_client.device.get_latest_firmware_info(self._phyn_device_id))[0])
#LOGGER.debug("Firmware Info: %s", self._firmware_info)

async def on_device_update(self, device_id, data):
if device_id == self._phyn_device_id:
self._rt_device_state = data
Expand All @@ -225,14 +296,15 @@ async def on_device_update(self, device_id, data):
update_data.update({"flow": data["flow"]})
if "flow_state" in data:
update_data.update({"flow_state": data["flow_state"]})
if "sov_status" in data:
update_data.update({"sov_status":{"v": data["sov_status"]}})
if "sov_state" in data:
update_data.update({"sov_status":{"v": data["sov_state"]}})
if "sensor_data" in data:
if "pressure" in data["sensor_data"]:
update_data.update({"pressure": data["sensor_data"]["pressure"]})
if "temperature" in data["sensor_data"]:
update_data.update({"temperature": data["sensor_data"]["temperature"]})
self._device_state.update(update_data)
LOGGER.debug("Device State: %s", self._device_state)

for entity in self.entities:
entity.async_write_ha_state()
Expand Down
73 changes: 49 additions & 24 deletions custom_components/phyn/devices/pp.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"""Support for Phyn Plus Water Monitor sensors."""
from __future__ import annotations
from typing import Any

from homeassistant.components.switch import SwitchEntity
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
Expand All @@ -19,7 +24,7 @@
)
from homeassistant.core import callback

from ..entity import PhynEntity
from ..entity import PhynEntity, PhynSwitchEntity

WATER_ICON = "mdi:water"
GAUGE_ICON = "mdi:gauge"
Expand All @@ -28,50 +33,39 @@
NAME_WATER_TEMPERATURE = "Current water temperature"
NAME_WATER_PRESSURE = "Current water pressure"

class PhynAwayModeSwitch(PhynEntity, SwitchEntity):
class PhynAwayModeSwitch(PhynSwitchEntity):
"""Switch class for the Phyn Away Mode."""

def __init__(self, device) -> None:
"""Initialize the Phyn Away Mode switch."""
super().__init__("away_mode", "Away Mode", device)
self._preference_name = "leak_sensitivity_away_mode"

@property
def _state(self) -> bool:
return self._device.away_mode

@property
def is_on(self) -> bool:
"""Return True if away mode is on."""
return self._state

@property
def icon(self):
"""Return the icon to use for the away mode."""
if self.is_on:
return "mdi:bag-suitcase"
return "mdi:home-circle"

async def async_turn_on(self, **kwargs: Any) -> None:
"""Open the valve."""
self._device.set_away_mode(True)
self.async_write_ha_state()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Close the valve."""
self._device.set_away_mode(False)
self.async_write_ha_state()
class PhynFirmwareUpdateAvailableSensor(PhynEntity, BinarySensorEntity):
"""Firmware Update Available Sensor"""
_attr_device_class = BinarySensorDeviceClass.UPDATE

@callback
def async_update_state(self) -> None:
"""Retrieve the latest valve state and update the state machine."""
self.async_write_ha_state()
def __init__(self, device):
"""Intialize Firmware Update Sensor."""
super().__init__("firmware_update_available", "Firmware Update Available", device)

async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
self.async_on_remove(self._device.async_add_listener(self.async_update_state))
@property
def is_on(self) -> bool:
return self._device.firmware_has_update

class PhynFlowState(PhynEntity, SensorEntity):

"""Flow State for Water Sensor"""
_attr_icon = WATER_ICON
#_attr_native_unit_of_measurement = UnitOfVolume.GALLONS
#_attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING
Expand All @@ -88,6 +82,37 @@ def native_value(self) -> str | None:
return self._device._rt_device_state['flow_state']['v']
return None

class PhynLeakTestSensor(PhynEntity, BinarySensorEntity):
"""Leak Test Sensor"""
_attr_device_class = BinarySensorDeviceClass.RUNNING

def __init__(self, device):
"""Initialize the leak test sensor."""
super().__init__("leak_test_running", "Leak Test Running", device)

@property
def is_on(self) -> bool:
return self._device.leak_test_running

class PhynScheduledLeakTestEnabledSwitch(PhynSwitchEntity):
"""Switch class for the Phyn Away Mode."""

def __init__(self, device) -> None:
"""Initialize the Phyn Away Mode switch."""
super().__init__("scheduled_leak_test_enabled", "Scheduled Leak Test Enabled", device)
self._preference_name = "scheduler_enable"

@property
def _state(self) -> bool:
return self._device.scheduled_leak_test_enabled

@property
def icon(self):
"""Return the icon to use for the away mode."""
if self.is_on:
return "mdi:bag-suitcase"
return "mdi:home-circle"

class PhynDailyUsageSensor(PhynEntity, SensorEntity):
"""Monitors the daily water usage."""

Expand Down
Loading

0 comments on commit 1c0ee15

Please sign in to comment.