Add Sensor to PG LAB Integration (#138802)

This commit is contained in:
pglab-electronics 2025-02-28 12:07:01 +01:00 committed by GitHub
parent 5cf56ec113
commit 12cb349160
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 391 additions and 7 deletions

View File

@ -0,0 +1,56 @@
"""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

View File

@ -28,17 +28,20 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.entity import Entity
from .const import DISCOVERY_TOPIC, DOMAIN, LOGGER
from .device_sensor import PGLabDeviceSensor
if TYPE_CHECKING:
from . import PGLABConfigEntry
# Supported platforms.
PLATFORMS = [
Platform.SENSOR,
Platform.SWITCH,
]
# Used to create a new component entity.
CREATE_NEW_ENTITY = {
Platform.SENSOR: "pglab_create_new_entity_sensor",
Platform.SWITCH: "pglab_create_new_entity_switch",
}
@ -74,6 +77,7 @@ 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)
def add_entity(self, entity: Entity) -> None:
"""Add an entity."""
@ -93,6 +97,20 @@ 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:
"""Create a new DiscoverDeviceInfo instance."""
discovery_info = DiscoverDeviceInfo(pglab_device)
# Subscribe to sensor state changes.
await discovery_info.sensors.subscribe_topics()
return discovery_info
@dataclass
class PGLabDiscovery:
@ -223,7 +241,7 @@ class PGLabDiscovery:
self.__clean_discovered_device(hass, pglab_device.id)
# Add a new device.
discovery_info = DiscoverDeviceInfo(pglab_device)
discovery_info = await createDiscoverDeviceInfo(pglab_device)
self._discovered[pglab_device.id] = discovery_info
# Create all new relay entities.
@ -233,6 +251,14 @@ class PGLabDiscovery:
hass, CREATE_NEW_ENTITY[Platform.SWITCH], pglab_device, r
)
# Create all new sensor entities.
async_dispatcher_send(
hass,
CREATE_NEW_ENTITY[Platform.SENSOR],
pglab_device,
discovery_info.sensors,
)
topics = {
"discovery_topic": {
"topic": f"{self._discovery_topic}/#",

View File

@ -43,12 +43,20 @@ class PGLabEntity(Entity):
connections={(CONNECTION_NETWORK_MAC, device.mac)},
)
async def async_added_to_hass(self) -> None:
"""Update the device discovery info."""
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.
@ -60,9 +68,7 @@ class PGLabEntity(Entity):
"""Unsubscribe when removed."""
await super().async_will_remove_from_hass()
await self._entity.unsubscribe_topics()
self._entity.set_on_state_callback(None)
await self.unsubscribe_to_update()
@callback
def state_updated(self, payload: str) -> None:

View File

@ -0,0 +1,119 @@
"""Sensor for PG LAB Electronics."""
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
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
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 .discovery import PGLabDiscovery
from .entity import PGLabEntity
PARALLEL_UPDATES = 0
SENSOR_INFO: list[SensorEntityDescription] = [
SensorEntityDescription(
key=SENSOR_TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=SENSOR_VOLTAGE,
translation_key="mpu_voltage",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=SENSOR_REBOOT_TIME,
translation_key="runtime",
device_class=SensorDeviceClass.TIMESTAMP,
icon="mdi:progress-clock",
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: PGLABConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up sensor for device."""
@callback
def async_discover(
pglab_device: PyPGLabDevice,
pglab_device_sensor: PGLabDeviceSensor,
) -> 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
)
async_add_entities([pglab_sensor])
# 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):
"""A PGLab sensor."""
def __init__(
self,
pglab_discovery: PGLabDiscovery,
pglab_device: PyPGLabDevice,
pglab_device_sensor: PGLabDeviceSensor,
description: SensorEntityDescription,
) -> None:
"""Initialize the Sensor class."""
super().__init__(
discovery=pglab_discovery,
device=pglab_device,
entity=pglab_device_sensor.sensors,
)
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."""
# get the sensor value from pglab multi fields sensor
value = self._pglab_device_sensor.state[self._type]
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)

View File

@ -19,6 +19,17 @@
"relay": {
"name": "Relay {relay_id}"
}
},
"sensor": {
"temperature": {
"name": "Temperature"
},
"runtime": {
"name": "Run time"
},
"mpu_voltage": {
"name": "MPU voltage"
}
}
}
}

View File

@ -0,0 +1,95 @@
# serializer version: 1
# name: test_sensors[mpu_voltage][initial_sensor_mpu_voltage]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'test MPU voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_mpu_voltage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[mpu_voltage][updated_sensor_mpu_voltage]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'test MPU voltage',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_mpu_voltage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3.31',
})
# ---
# name: test_sensors[run_time][initial_sensor_run_time]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'test Run time',
'icon': 'mdi:progress-clock',
}),
'context': <ANY>,
'entity_id': 'sensor.test_run_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[run_time][updated_sensor_run_time]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'test Run time',
'icon': 'mdi:progress-clock',
}),
'context': <ANY>,
'entity_id': 'sensor.test_run_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2024-02-26T01:04:54+00:00',
})
# ---
# name: test_sensors[temperature][initial_sensor_temperature]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'test Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_sensors[temperature][updated_sensor_temperature]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'test Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '33.4',
})
# ---

View File

@ -0,0 +1,71 @@
"""The tests for the PG LAB Electronics sensor."""
import json
from freezegun import freeze_time
import pytest
from syrupy import SnapshotAssertion
from homeassistant.core import HomeAssistant
from tests.common import async_fire_mqtt_message
from tests.typing import MqttMockHAClient
async def send_discovery_message(hass: HomeAssistant) -> None:
"""Send mqtt discovery message."""
topic = "pglab/discovery/E-Board-DD53AC85/config"
payload = {
"ip": "192.168.1.16",
"mac": "80:34:28:1B:18:5A",
"name": "test",
"hw": "1.0.7",
"fw": "1.0.0",
"type": "E-Board",
"id": "E-Board-DD53AC85",
"manufacturer": "PG LAB Electronics",
"params": {"shutters": 0, "boards": "00000000"},
}
async_fire_mqtt_message(
hass,
topic,
json.dumps(payload),
)
await hass.async_block_till_done()
@freeze_time("2024-02-26 01:21:34")
@pytest.mark.parametrize(
"sensor_suffix",
[
"temperature",
"mpu_voltage",
"run_time",
],
)
async def test_sensors(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mqtt_mock: MqttMockHAClient,
setup_pglab,
sensor_suffix: str,
) -> None:
"""Check if sensors are properly created and updated."""
# send the discovery message to make E-BOARD device discoverable
await send_discovery_message(hass)
# check initial sensors state
state = hass.states.get(f"sensor.test_{sensor_suffix}")
assert state == snapshot(name=f"initial_sensor_{sensor_suffix}")
# update sensors value via mqtt
update_payload = {"temp": 33.4, "volt": 3.31, "rtime": 1000}
async_fire_mqtt_message(hass, "pglab/test/sensor/value", json.dumps(update_payload))
await hass.async_block_till_done()
# check updated sensors state
state = hass.states.get(f"sensor.test_{sensor_suffix}")
assert state == snapshot(name=f"updated_sensor_{sensor_suffix}")