diff --git a/homeassistant/components/pglab/__init__.py b/homeassistant/components/pglab/__init__.py index 7307ac2f801..8bce7be26e8 100644 --- a/homeassistant/components/pglab/__init__.py +++ b/homeassistant/components/pglab/__init__.py @@ -23,12 +23,14 @@ from homeassistant.helpers import config_validation as cv from .const import DOMAIN, LOGGER from .discovery import PGLabDiscovery -type PGLABConfigEntry = ConfigEntry[PGLabDiscovery] +type PGLabConfigEntry = ConfigEntry[PGLabDiscovery] CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) -async def async_setup_entry(hass: HomeAssistant, entry: PGLABConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, config_entry: PGLabConfigEntry +) -> bool: """Set up PG LAB Electronics integration from a config entry.""" async def mqtt_publish(topic: str, payload: str, qos: int, retain: bool) -> None: @@ -67,19 +69,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: PGLABConfigEntry) -> boo pglab_mqtt = PyPGLabMqttClient(mqtt_publish, mqtt_subscribe, mqtt_unsubscribe) # Setup PGLab device discovery. - entry.runtime_data = PGLabDiscovery() + config_entry.runtime_data = PGLabDiscovery() # Start to discovery PG Lab devices. - await entry.runtime_data.start(hass, pglab_mqtt, entry) + await config_entry.runtime_data.start(hass, pglab_mqtt, config_entry) return True -async def async_unload_entry(hass: HomeAssistant, entry: PGLABConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, config_entry: PGLabConfigEntry +) -> bool: """Unload a config entry.""" # Stop PGLab device discovery. - pglab_discovery = entry.runtime_data - await pglab_discovery.stop(hass, entry) + pglab_discovery = config_entry.runtime_data + await pglab_discovery.stop(hass, config_entry) return True diff --git a/homeassistant/components/pglab/coordinator.py b/homeassistant/components/pglab/coordinator.py new file mode 100644 index 00000000000..53c5dbc3b58 --- /dev/null +++ b/homeassistant/components/pglab/coordinator.py @@ -0,0 +1,78 @@ +"""Coordinator for PG LAB Electronics.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, Any + +from pypglab.const import SENSOR_REBOOT_TIME, SENSOR_TEMPERATURE, SENSOR_VOLTAGE +from pypglab.device import Device as PyPGLabDevice +from pypglab.sensor import Sensor as PyPGLabSensors + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util.dt import utcnow + +from .const import DOMAIN, LOGGER + +if TYPE_CHECKING: + from . import PGLabConfigEntry + + +class PGLabSensorsCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to update Sensor Entities when receiving new data.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: PGLabConfigEntry, + pglab_device: PyPGLabDevice, + ) -> None: + """Initialize.""" + + # get a reference of PG Lab device internal sensors state + self._sensors: PyPGLabSensors = pglab_device.sensors + + super().__init__( + hass, + LOGGER, + config_entry=config_entry, + name=DOMAIN, + ) + + @callback + def _new_sensors_data(self, payload: str) -> None: + """Handle new sensor data.""" + + # notify all listeners that new sensor values are available + self.async_set_updated_data(self._sensors.state) + + async def subscribe_topics(self) -> None: + """Subscribe the sensors state to be notifty from MQTT update messages.""" + + # subscribe to the pypglab sensors to receive updates from the mqtt broker + # when a new sensor values are available + await self._sensors.subscribe_topics() + + # set the callback to be called when a new sensor values are available + self._sensors.set_on_state_callback(self._new_sensors_data) + + def get_sensor_value(self, sensor_key: str) -> float | datetime | None: + """Return the value of a sensor.""" + + if self.data: + value = self.data[sensor_key] + + if (sensor_key == SENSOR_REBOOT_TIME) and value: + # convert the reboot time to a datetime object + return utcnow() - timedelta(seconds=value) + + if (sensor_key == SENSOR_TEMPERATURE) and value: + # convert the temperature value to a float + return float(value) + + if (sensor_key == SENSOR_VOLTAGE) and value: + # convert the voltage value to a float + return float(value) + + return None diff --git a/homeassistant/components/pglab/device_sensor.py b/homeassistant/components/pglab/device_sensor.py deleted file mode 100644 index d202d11d6e7..00000000000 --- a/homeassistant/components/pglab/device_sensor.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Device Sensor for PG LAB Electronics.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from pypglab.device import Device as PyPGLabDevice -from pypglab.sensor import Sensor as PyPGLabSensors - -from homeassistant.core import callback - -if TYPE_CHECKING: - from .entity import PGLabEntity - - -class PGLabDeviceSensor: - """Keeps PGLab device sensor update.""" - - def __init__(self, pglab_device: PyPGLabDevice) -> None: - """Initialize the device sensor.""" - - # get a reference of PG Lab device internal sensors state - self._sensors: PyPGLabSensors = pglab_device.sensors - - self._ha_sensors: list[PGLabEntity] = [] # list of HA entity sensors - - async def subscribe_topics(self): - """Subscribe to the device sensors topics.""" - self._sensors.set_on_state_callback(self.state_updated) - await self._sensors.subscribe_topics() - - def add_ha_sensor(self, entity: PGLabEntity) -> None: - """Add a new HA sensor to the list.""" - self._ha_sensors.append(entity) - - def remove_ha_sensor(self, entity: PGLabEntity) -> None: - """Remove a HA sensor from the list.""" - self._ha_sensors.remove(entity) - - @callback - def state_updated(self, payload: str) -> None: - """Handle state updates.""" - - # notify all HA sensors that PG LAB device sensor fields have been updated - for s in self._ha_sensors: - s.state_updated(payload) - - @property - def state(self) -> dict: - """Return the device sensors state.""" - return self._sensors.state - - @property - def sensors(self) -> PyPGLabSensors: - """Return the pypglab device sensors.""" - return self._sensors diff --git a/homeassistant/components/pglab/discovery.py b/homeassistant/components/pglab/discovery.py index fec6f5ce40d..e34f80a2e2d 100644 --- a/homeassistant/components/pglab/discovery.py +++ b/homeassistant/components/pglab/discovery.py @@ -25,13 +25,12 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity from .const import DISCOVERY_TOPIC, DOMAIN, LOGGER -from .device_sensor import PGLabDeviceSensor +from .coordinator import PGLabSensorsCoordinator if TYPE_CHECKING: - from . import PGLABConfigEntry + from . import PGLabConfigEntry # Supported platforms. PLATFORMS = [ @@ -69,7 +68,12 @@ def get_device_id_from_discovery_topic(topic: str) -> str | None: class DiscoverDeviceInfo: """Keeps information of the PGLab discovered device.""" - def __init__(self, pglab_device: PyPGLabDevice) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: PGLabConfigEntry, + pglab_device: PyPGLabDevice, + ) -> None: """Initialize the device discovery info.""" # Hash string represents the devices actual configuration, @@ -77,15 +81,15 @@ class DiscoverDeviceInfo: # When the hash string changes the devices entities must be rebuilt. self._hash = pglab_device.hash self._entities: list[tuple[str, str]] = [] - self._sensors = PGLabDeviceSensor(pglab_device) + self.coordinator = PGLabSensorsCoordinator(hass, config_entry, pglab_device) - def add_entity(self, entity: Entity) -> None: + def add_entity(self, platform_domain: str, entity_unique_id: str | None) -> None: """Add an entity.""" # PGLabEntity always have unique IDs if TYPE_CHECKING: - assert entity.unique_id is not None - self._entities.append((entity.platform.domain, entity.unique_id)) + assert entity_unique_id is not None + self._entities.append((platform_domain, entity_unique_id)) @property def hash(self) -> int: @@ -97,18 +101,15 @@ class DiscoverDeviceInfo: """Return array of entities available.""" return self._entities - @property - def sensors(self) -> PGLabDeviceSensor: - """Return the PGLab device sensor.""" - return self._sensors - -async def createDiscoverDeviceInfo(pglab_device: PyPGLabDevice) -> DiscoverDeviceInfo: +async def create_discover_device_info( + hass: HomeAssistant, config_entry: PGLabConfigEntry, pglab_device: PyPGLabDevice +) -> DiscoverDeviceInfo: """Create a new DiscoverDeviceInfo instance.""" - discovery_info = DiscoverDeviceInfo(pglab_device) + discovery_info = DiscoverDeviceInfo(hass, config_entry, pglab_device) # Subscribe to sensor state changes. - await discovery_info.sensors.subscribe_topics() + await discovery_info.coordinator.subscribe_topics() return discovery_info @@ -184,7 +185,10 @@ class PGLabDiscovery: del self._discovered[device_id] async def start( - self, hass: HomeAssistant, mqtt: PyPGLabMqttClient, entry: PGLABConfigEntry + self, + hass: HomeAssistant, + mqtt: PyPGLabMqttClient, + config_entry: PGLabConfigEntry, ) -> None: """Start discovering a PGLab devices.""" @@ -210,7 +214,7 @@ class PGLabDiscovery: # Create a new device. device_registry = dr.async_get(hass) device_registry.async_get_or_create( - config_entry_id=entry.entry_id, + config_entry_id=config_entry.entry_id, configuration_url=f"http://{pglab_device.ip}/", connections={(CONNECTION_NETWORK_MAC, pglab_device.mac)}, identifiers={(DOMAIN, pglab_device.id)}, @@ -241,7 +245,9 @@ class PGLabDiscovery: self.__clean_discovered_device(hass, pglab_device.id) # Add a new device. - discovery_info = await createDiscoverDeviceInfo(pglab_device) + discovery_info = await create_discover_device_info( + hass, config_entry, pglab_device + ) self._discovered[pglab_device.id] = discovery_info # Create all new relay entities. @@ -256,7 +262,7 @@ class PGLabDiscovery: hass, CREATE_NEW_ENTITY[Platform.SENSOR], pglab_device, - discovery_info.sensors, + discovery_info.coordinator, ) topics = { @@ -267,7 +273,7 @@ class PGLabDiscovery: } # Forward setup all HA supported platforms. - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) self._mqtt_client = mqtt self._substate = async_prepare_subscribe_topics(hass, self._substate, topics) @@ -282,9 +288,9 @@ class PGLabDiscovery: ) self._disconnect_platform.append(disconnect_callback) - async def stop(self, hass: HomeAssistant, entry: PGLABConfigEntry) -> None: + async def stop(self, hass: HomeAssistant, config_entry: PGLabConfigEntry) -> None: """Stop to discovery PG LAB devices.""" - await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) # Disconnect all registered platforms. for disconnect_callback in self._disconnect_platform: @@ -292,7 +298,9 @@ class PGLabDiscovery: async_unsubscribe_topics(hass, self._substate) - async def add_entity(self, entity: Entity, device_id: str): + async def add_entity( + self, platform_domain: str, entity_unique_id: str | None, device_id: str + ): """Save a new PG LAB device entity.""" # Be sure that the device is been discovered. @@ -300,4 +308,4 @@ class PGLabDiscovery: raise PGLabDiscoveryError("Unknown device, device_id not discovered") discovery_info = self._discovered[device_id] - discovery_info.add_entity(entity) + discovery_info.add_entity(platform_domain, entity_unique_id) diff --git a/homeassistant/components/pglab/entity.py b/homeassistant/components/pglab/entity.py index 175b4c1eb0f..59a4e28de89 100644 --- a/homeassistant/components/pglab/entity.py +++ b/homeassistant/components/pglab/entity.py @@ -8,69 +8,105 @@ from pypglab.entity import Entity as PyPGLabEntity from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN +from .coordinator import PGLabSensorsCoordinator from .discovery import PGLabDiscovery -class PGLabEntity(Entity): - """Representation of a PGLab entity in Home Assistant.""" +class PGLabBaseEntity(Entity): + """Base class of a PGLab entity in Home Assistant.""" _attr_has_entity_name = True def __init__( self, - discovery: PGLabDiscovery, - device: PyPGLabDevice, - entity: PyPGLabEntity, + pglab_discovery: PGLabDiscovery, + pglab_device: PyPGLabDevice, ) -> None: """Initialize the class.""" - self._id = entity.id - self._device_id = device.id - self._entity = entity - self._discovery = discovery + self._device_id = pglab_device.id + self._discovery = pglab_discovery # Information about the device that is partially visible in the UI. self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device.id)}, - name=device.name, - sw_version=device.firmware_version, - hw_version=device.hardware_version, - model=device.type, - manufacturer=device.manufactor, - configuration_url=f"http://{device.ip}/", - connections={(CONNECTION_NETWORK_MAC, device.mac)}, + identifiers={(DOMAIN, pglab_device.id)}, + name=pglab_device.name, + sw_version=pglab_device.firmware_version, + hw_version=pglab_device.hardware_version, + model=pglab_device.type, + manufacturer=pglab_device.manufactor, + configuration_url=f"http://{pglab_device.ip}/", + connections={(CONNECTION_NETWORK_MAC, pglab_device.mac)}, ) - async def subscribe_to_update(self): - """Subscribe to the entity updates.""" - self._entity.set_on_state_callback(self.state_updated) - await self._entity.subscribe_topics() - - async def unsubscribe_to_update(self): - """Unsubscribe to the entity updates.""" - await self._entity.unsubscribe_topics() - self._entity.set_on_state_callback(None) - async def async_added_to_hass(self) -> None: """Update the device discovery info.""" - await self.subscribe_to_update() - await super().async_added_to_hass() - # Inform PGLab discovery instance that a new entity is available. # This is important to know in case the device needs to be reconfigured # and the entity can be potentially destroyed. - await self._discovery.add_entity(self, self._device_id) + await self._discovery.add_entity( + self.platform.domain, + self.unique_id, + self._device_id, + ) + + # propagate the async_added_to_hass to the super class + await super().async_added_to_hass() + + +class PGLabEntity(PGLabBaseEntity): + """Representation of a PGLab entity in Home Assistant.""" + + def __init__( + self, + pglab_discovery: PGLabDiscovery, + pglab_device: PyPGLabDevice, + pglab_entity: PyPGLabEntity, + ) -> None: + """Initialize the class.""" + + super().__init__(pglab_discovery, pglab_device) + + self._id = pglab_entity.id + self._entity: PyPGLabEntity = pglab_entity + + async def async_added_to_hass(self) -> None: + """Subscribe pypglab entity to be updated from mqtt when pypglab entity internal state change.""" + + # set the callback to be called when pypglab entity state is changed + self._entity.set_on_state_callback(self.state_updated) + + # subscribe to the pypglab entity to receive updates from the mqtt broker + await self._entity.subscribe_topics() + await super().async_added_to_hass() async def async_will_remove_from_hass(self) -> None: """Unsubscribe when removed.""" await super().async_will_remove_from_hass() - await self.unsubscribe_to_update() + await self._entity.unsubscribe_topics() + self._entity.set_on_state_callback(None) @callback def state_updated(self, payload: str) -> None: """Handle state updates.""" self.async_write_ha_state() + + +class PGLabSensorEntity(PGLabBaseEntity, CoordinatorEntity[PGLabSensorsCoordinator]): + """Representation of a PGLab sensor entity in Home Assistant.""" + + def __init__( + self, + pglab_discovery: PGLabDiscovery, + pglab_device: PyPGLabDevice, + pglab_coordinator: PGLabSensorsCoordinator, + ) -> None: + """Initialize the class.""" + + PGLabBaseEntity.__init__(self, pglab_discovery, pglab_device) + CoordinatorEntity.__init__(self, pglab_coordinator) diff --git a/homeassistant/components/pglab/sensor.py b/homeassistant/components/pglab/sensor.py index f868e7ae101..ce19ec3a21a 100644 --- a/homeassistant/components/pglab/sensor.py +++ b/homeassistant/components/pglab/sensor.py @@ -2,8 +2,6 @@ from __future__ import annotations -from datetime import timedelta - from pypglab.const import SENSOR_REBOOT_TIME, SENSOR_TEMPERATURE, SENSOR_VOLTAGE from pypglab.device import Device as PyPGLabDevice @@ -16,12 +14,11 @@ from homeassistant.components.sensor import ( from homeassistant.const import Platform, UnitOfElectricPotential, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.util.dt import utcnow -from . import PGLABConfigEntry -from .device_sensor import PGLabDeviceSensor +from . import PGLabConfigEntry +from .coordinator import PGLabSensorsCoordinator from .discovery import PGLabDiscovery -from .entity import PGLabEntity +from .entity import PGLabSensorEntity PARALLEL_UPDATES = 0 @@ -50,7 +47,7 @@ SENSOR_INFO: list[SensorEntityDescription] = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: PGLABConfigEntry, + config_entry: PGLabConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up sensor for device.""" @@ -58,62 +55,55 @@ async def async_setup_entry( @callback def async_discover( pglab_device: PyPGLabDevice, - pglab_device_sensor: PGLabDeviceSensor, + pglab_coordinator: PGLabSensorsCoordinator, ) -> None: """Discover and add a PG LAB Sensor.""" pglab_discovery = config_entry.runtime_data - for description in SENSOR_INFO: - pglab_sensor = PGLabSensor( - pglab_discovery, pglab_device, pglab_device_sensor, description + + sensors: list[PGLabSensor] = [ + PGLabSensor( + description, + pglab_discovery, + pglab_device, + pglab_coordinator, ) - async_add_entities([pglab_sensor]) + for description in SENSOR_INFO + ] + + async_add_entities(sensors) # Register the callback to create the sensor entity when discovered. pglab_discovery = config_entry.runtime_data await pglab_discovery.register_platform(hass, Platform.SENSOR, async_discover) -class PGLabSensor(PGLabEntity, SensorEntity): +class PGLabSensor(PGLabSensorEntity, SensorEntity): """A PGLab sensor.""" def __init__( self, + description: SensorEntityDescription, pglab_discovery: PGLabDiscovery, pglab_device: PyPGLabDevice, - pglab_device_sensor: PGLabDeviceSensor, - description: SensorEntityDescription, + pglab_coordinator: PGLabSensorsCoordinator, ) -> None: """Initialize the Sensor class.""" - super().__init__( - discovery=pglab_discovery, - device=pglab_device, - entity=pglab_device_sensor.sensors, - ) + super().__init__(pglab_discovery, pglab_device, pglab_coordinator) - self._type = description.key - self._pglab_device_sensor = pglab_device_sensor self._attr_unique_id = f"{pglab_device.id}_{description.key}" self.entity_description = description @callback - def state_updated(self, payload: str) -> None: - """Handle state updates.""" + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" - # get the sensor value from pglab multi fields sensor - value = self._pglab_device_sensor.state[self._type] + self._attr_native_value = self.coordinator.get_sensor_value( + self.entity_description.key + ) + super()._handle_coordinator_update() - if self.entity_description.device_class == SensorDeviceClass.TIMESTAMP: - self._attr_native_value = utcnow() - timedelta(seconds=value) - else: - self._attr_native_value = value - - super().state_updated(payload) - - async def subscribe_to_update(self): - """Register the HA sensor to be notify when the sensor status is changed.""" - self._pglab_device_sensor.add_ha_sensor(self) - - async def unsubscribe_to_update(self): - """Unregister the HA sensor from sensor tatus updates.""" - self._pglab_device_sensor.remove_ha_sensor(self) + @property + def available(self) -> bool: + """Return PG LAB sensor availability.""" + return super().available and self.native_value is not None diff --git a/homeassistant/components/pglab/switch.py b/homeassistant/components/pglab/switch.py index 554b5cf80ca..76b177e84c4 100644 --- a/homeassistant/components/pglab/switch.py +++ b/homeassistant/components/pglab/switch.py @@ -12,7 +12,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import PGLABConfigEntry +from . import PGLabConfigEntry from .discovery import PGLabDiscovery from .entity import PGLabEntity @@ -21,7 +21,7 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: PGLABConfigEntry, + config_entry: PGLabConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up switches for device.""" @@ -52,9 +52,9 @@ class PGLabSwitch(PGLabEntity, SwitchEntity): """Initialize the Switch class.""" super().__init__( - discovery=pglab_discovery, - device=pglab_device, - entity=pglab_relay, + pglab_discovery, + pglab_device, + pglab_relay, ) self._attr_unique_id = f"{pglab_device.id}_relay{pglab_relay.id}" diff --git a/tests/components/pglab/snapshots/test_sensor.ambr b/tests/components/pglab/snapshots/test_sensor.ambr index f25f459bb70..71889b65183 100644 --- a/tests/components/pglab/snapshots/test_sensor.ambr +++ b/tests/components/pglab/snapshots/test_sensor.ambr @@ -12,7 +12,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'unavailable', }) # --- # name: test_sensors[mpu_voltage][updated_sensor_mpu_voltage] @@ -43,7 +43,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'unavailable', }) # --- # name: test_sensors[run_time][updated_sensor_run_time] @@ -74,7 +74,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'unavailable', }) # --- # name: test_sensors[temperature][updated_sensor_temperature]