Add Mysensors battery sensor (#100749)

* Move child related stuff to MySensorsChildEntity

* Dispatch signal for newly discovered MySensors node

* Create battery entity for each MySensors node

* Removed ATTR_BATTERY_LEVEL attribute from each node sensor

Attribute is redundant with newly introduced battery sensor entity

* Apply suggestions from code review

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Daniel Trnka 2023-09-24 22:50:13 +02:00 committed by GitHub
parent 6d624ecb46
commit 09729e8c46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 303 additions and 139 deletions

View File

@ -15,6 +15,7 @@ from homeassistant.helpers.device_registry import DeviceEntry
from .const import ( from .const import (
ATTR_DEVICES, ATTR_DEVICES,
DOMAIN, DOMAIN,
MYSENSORS_DISCOVERED_NODES,
MYSENSORS_GATEWAYS, MYSENSORS_GATEWAYS,
MYSENSORS_ON_UNLOAD, MYSENSORS_ON_UNLOAD,
PLATFORMS, PLATFORMS,
@ -22,7 +23,7 @@ from .const import (
DiscoveryInfo, DiscoveryInfo,
SensorType, SensorType,
) )
from .device import MySensorsEntity, get_mysensors_devices from .device import MySensorsChildEntity, get_mysensors_devices
from .gateway import finish_setup, gw_stop, setup_gateway from .gateway import finish_setup, gw_stop, setup_gateway
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -72,6 +73,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(key) hass.data[DOMAIN].pop(key)
del hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] del hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id]
hass.data[DOMAIN].pop(MYSENSORS_DISCOVERED_NODES.format(entry.entry_id), None)
await gw_stop(hass, entry, gateway) await gw_stop(hass, entry, gateway)
return True return True
@ -91,6 +93,11 @@ async def async_remove_config_entry_device(
gateway.sensors.pop(node_id, None) gateway.sensors.pop(node_id, None)
gateway.tasks.persistence.need_save = True gateway.tasks.persistence.need_save = True
# remove node from discovered nodes
hass.data[DOMAIN].setdefault(
MYSENSORS_DISCOVERED_NODES.format(config_entry.entry_id), set()
).remove(node_id)
return True return True
@ -99,12 +106,13 @@ def setup_mysensors_platform(
hass: HomeAssistant, hass: HomeAssistant,
domain: Platform, # hass platform name domain: Platform, # hass platform name
discovery_info: DiscoveryInfo, discovery_info: DiscoveryInfo,
device_class: type[MySensorsEntity] | Mapping[SensorType, type[MySensorsEntity]], device_class: type[MySensorsChildEntity]
| Mapping[SensorType, type[MySensorsChildEntity]],
device_args: ( device_args: (
None | tuple None | tuple
) = None, # extra arguments that will be given to the entity constructor ) = None, # extra arguments that will be given to the entity constructor
async_add_entities: Callable | None = None, async_add_entities: Callable | None = None,
) -> list[MySensorsEntity] | None: ) -> list[MySensorsChildEntity] | None:
"""Set up a MySensors platform. """Set up a MySensors platform.
Sets up a bunch of instances of a single platform that is supported by this Sets up a bunch of instances of a single platform that is supported by this
@ -118,10 +126,10 @@ def setup_mysensors_platform(
""" """
if device_args is None: if device_args is None:
device_args = () device_args = ()
new_devices: list[MySensorsEntity] = [] new_devices: list[MySensorsChildEntity] = []
new_dev_ids: list[DevId] = discovery_info[ATTR_DEVICES] new_dev_ids: list[DevId] = discovery_info[ATTR_DEVICES]
for dev_id in new_dev_ids: for dev_id in new_dev_ids:
devices: dict[DevId, MySensorsEntity] = get_mysensors_devices(hass, domain) devices: dict[DevId, MySensorsChildEntity] = get_mysensors_devices(hass, domain)
if dev_id in devices: if dev_id in devices:
_LOGGER.debug( _LOGGER.debug(
"Skipping setup of %s for platform %s as it already exists", "Skipping setup of %s for platform %s as it already exists",

View File

@ -95,7 +95,7 @@ async def async_setup_entry(
) )
class MySensorsBinarySensor(mysensors.device.MySensorsEntity, BinarySensorEntity): class MySensorsBinarySensor(mysensors.device.MySensorsChildEntity, BinarySensorEntity):
"""Representation of a MySensors binary sensor child node.""" """Representation of a MySensors binary sensor child node."""
entity_description: MySensorsBinarySensorDescription entity_description: MySensorsBinarySensorDescription

View File

@ -66,7 +66,7 @@ async def async_setup_entry(
) )
class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity): class MySensorsHVAC(mysensors.device.MySensorsChildEntity, ClimateEntity):
"""Representation of a MySensors HVAC.""" """Representation of a MySensors HVAC."""
_attr_hvac_modes = OPERATION_LIST _attr_hvac_modes = OPERATION_LIST

View File

@ -8,6 +8,7 @@ from homeassistant.const import Platform
ATTR_DEVICES: Final = "devices" ATTR_DEVICES: Final = "devices"
ATTR_GATEWAY_ID: Final = "gateway_id" ATTR_GATEWAY_ID: Final = "gateway_id"
ATTR_NODE_ID: Final = "node_id"
CONF_BAUD_RATE: Final = "baud_rate" CONF_BAUD_RATE: Final = "baud_rate"
CONF_DEVICE: Final = "device" CONF_DEVICE: Final = "device"
@ -26,11 +27,13 @@ CONF_GATEWAY_TYPE_MQTT: ConfGatewayType = "MQTT"
DOMAIN: Final = "mysensors" DOMAIN: Final = "mysensors"
MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}" MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}"
MYSENSORS_GATEWAYS: Final = "mysensors_gateways" MYSENSORS_GATEWAYS: Final = "mysensors_gateways"
MYSENSORS_DISCOVERED_NODES: Final = "mysensors_discovered_nodes_{}"
PLATFORM: Final = "platform" PLATFORM: Final = "platform"
SCHEMA: Final = "schema" SCHEMA: Final = "schema"
CHILD_CALLBACK: str = "mysensors_child_callback_{}_{}_{}_{}" CHILD_CALLBACK: str = "mysensors_child_callback_{}_{}_{}_{}"
NODE_CALLBACK: str = "mysensors_node_callback_{}_{}" NODE_CALLBACK: str = "mysensors_node_callback_{}_{}"
MYSENSORS_DISCOVERY: str = "mysensors_discovery_{}_{}" MYSENSORS_DISCOVERY: str = "mysensors_discovery_{}_{}"
MYSENSORS_NODE_DISCOVERY: str = "mysensors_node_discovery"
MYSENSORS_ON_UNLOAD: str = "mysensors_on_unload_{}" MYSENSORS_ON_UNLOAD: str = "mysensors_on_unload_{}"
TYPE: Final = "type" TYPE: Final = "type"
UPDATE_DELAY: float = 0.1 UPDATE_DELAY: float = 0.1
@ -43,6 +46,13 @@ class DiscoveryInfo(TypedDict):
gateway_id: GatewayId gateway_id: GatewayId
class NodeDiscoveryInfo(TypedDict):
"""Represent discovered mysensors node."""
gateway_id: GatewayId
node_id: int
SERVICE_SEND_IR_CODE: Final = "send_ir_code" SERVICE_SEND_IR_CODE: Final = "send_ir_code"
SensorType = str SensorType = str

View File

@ -54,7 +54,7 @@ async def async_setup_entry(
) )
class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity): class MySensorsCover(mysensors.device.MySensorsChildEntity, CoverEntity):
"""Representation of the value of a MySensors Cover child node.""" """Representation of the value of a MySensors Cover child node."""
def get_cover_state(self) -> CoverState: def get_cover_state(self) -> CoverState:

View File

@ -1,14 +1,14 @@
"""Handle MySensors devices.""" """Handle MySensors devices."""
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod from abc import abstractmethod
import logging import logging
from typing import Any from typing import Any
from mysensors import BaseAsyncGateway, Sensor from mysensors import BaseAsyncGateway, Sensor
from mysensors.sensor import ChildSensor from mysensors.sensor import ChildSensor
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON, Platform from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@ -36,56 +36,24 @@ ATTR_HEARTBEAT = "heartbeat"
MYSENSORS_PLATFORM_DEVICES = "mysensors_devices_{}" MYSENSORS_PLATFORM_DEVICES = "mysensors_devices_{}"
class MySensorsDevice(ABC): class MySensorNodeEntity(Entity):
"""Representation of a MySensors device.""" """Representation of a MySensors device."""
hass: HomeAssistant hass: HomeAssistant
def __init__( def __init__(
self, self, gateway_id: GatewayId, gateway: BaseAsyncGateway, node_id: int
gateway_id: GatewayId,
gateway: BaseAsyncGateway,
node_id: int,
child_id: int,
value_type: int,
) -> None: ) -> None:
"""Set up the MySensors device.""" """Set up the MySensors node entity."""
self.gateway_id: GatewayId = gateway_id self.gateway_id: GatewayId = gateway_id
self.gateway: BaseAsyncGateway = gateway self.gateway: BaseAsyncGateway = gateway
self.node_id: int = node_id self.node_id: int = node_id
self.child_id: int = child_id
# value_type as int. string variant can be looked up in gateway consts
self.value_type: int = value_type
self.child_type = self._child.type
self._values: dict[int, Any] = {}
self._debouncer: Debouncer | None = None self._debouncer: Debouncer | None = None
@property
def dev_id(self) -> DevId:
"""Return the DevId of this device.
It is used to route incoming MySensors messages to the correct device/entity.
"""
return self.gateway_id, self.node_id, self.child_id, self.value_type
async def async_will_remove_from_hass(self) -> None:
"""Remove this entity from home assistant."""
for platform in PLATFORM_TYPES:
platform_str = MYSENSORS_PLATFORM_DEVICES.format(platform)
if platform_str in self.hass.data[DOMAIN]:
platform_dict = self.hass.data[DOMAIN][platform_str]
if self.dev_id in platform_dict:
del platform_dict[self.dev_id]
_LOGGER.debug("Deleted %s from platform %s", self.dev_id, platform)
@property @property
def _node(self) -> Sensor: def _node(self) -> Sensor:
return self.gateway.sensors[self.node_id] return self.gateway.sensors[self.node_id]
@property
def _child(self) -> ChildSensor:
return self._node.children[self.child_id]
@property @property
def sketch_name(self) -> str: def sketch_name(self) -> str:
"""Return the name of the sketch running on the whole node. """Return the name of the sketch running on the whole node.
@ -110,11 +78,6 @@ class MySensorsDevice(ABC):
""" """
return f"{self.sketch_name} {self.node_id}" return f"{self.sketch_name} {self.node_id}"
@property
def unique_id(self) -> str:
"""Return a unique ID for use in home assistant."""
return f"{self.gateway_id}-{self.node_id}-{self.child_id}-{self.value_type}"
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device info.""" """Return the device info."""
@ -125,6 +88,96 @@ class MySensorsDevice(ABC):
sw_version=self.sketch_version, sw_version=self.sketch_version,
) )
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device specific attributes."""
node = self.gateway.sensors[self.node_id]
return {
ATTR_HEARTBEAT: node.heartbeat,
ATTR_NODE_ID: self.node_id,
}
@callback
@abstractmethod
def _async_update_callback(self) -> None:
"""Update the device."""
async def async_update_callback(self) -> None:
"""Update the device after delay."""
if not self._debouncer:
self._debouncer = Debouncer(
self.hass,
_LOGGER,
cooldown=UPDATE_DELAY,
immediate=False,
function=self._async_update_callback,
)
await self._debouncer.async_call()
async def async_added_to_hass(self) -> None:
"""Register update callback."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
NODE_CALLBACK.format(self.gateway_id, self.node_id),
self.async_update_callback,
)
)
self._async_update_callback()
def get_mysensors_devices(
hass: HomeAssistant, domain: Platform
) -> dict[DevId, MySensorsChildEntity]:
"""Return MySensors devices for a hass platform name."""
if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data[DOMAIN]:
hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] = {}
devices: dict[DevId, MySensorsChildEntity] = hass.data[DOMAIN][
MYSENSORS_PLATFORM_DEVICES.format(domain)
]
return devices
class MySensorsChildEntity(MySensorNodeEntity):
"""Representation of a MySensors entity."""
_attr_should_poll = False
def __init__(
self,
gateway_id: GatewayId,
gateway: BaseAsyncGateway,
node_id: int,
child_id: int,
value_type: int,
) -> None:
"""Set up the MySensors child entity."""
super().__init__(gateway_id, gateway, node_id)
self.child_id: int = child_id
# value_type as int. string variant can be looked up in gateway consts
self.value_type: int = value_type
self.child_type = self._child.type
self._values: dict[int, Any] = {}
@property
def dev_id(self) -> DevId:
"""Return the DevId of this device.
It is used to route incoming MySensors messages to the correct device/entity.
"""
return self.gateway_id, self.node_id, self.child_id, self.value_type
@property
def _child(self) -> ChildSensor:
return self._node.children[self.child_id]
@property
def unique_id(self) -> str:
"""Return a unique ID for use in home assistant."""
return f"{self.gateway_id}-{self.node_id}-{self.child_id}-{self.value_type}"
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of this entity.""" """Return the name of this entity."""
@ -134,21 +187,33 @@ class MySensorsDevice(ABC):
return str(child.description) return str(child.description)
return f"{self.node_name} {self.child_id}" return f"{self.node_name} {self.child_id}"
async def async_will_remove_from_hass(self) -> None:
"""Remove this entity from home assistant."""
for platform in PLATFORM_TYPES:
platform_str = MYSENSORS_PLATFORM_DEVICES.format(platform)
if platform_str in self.hass.data[DOMAIN]:
platform_dict = self.hass.data[DOMAIN][platform_str]
if self.dev_id in platform_dict:
del platform_dict[self.dev_id]
_LOGGER.debug("Deleted %s from platform %s", self.dev_id, platform)
@property @property
def _extra_attributes(self) -> dict[str, Any]: def available(self) -> bool:
"""Return device specific attributes.""" """Return true if entity is available."""
node = self.gateway.sensors[self.node_id] return self.value_type in self._values
child = node.children[self.child_id]
attr = { @property
ATTR_BATTERY_LEVEL: node.battery_level, def extra_state_attributes(self) -> dict[str, Any]:
ATTR_HEARTBEAT: node.heartbeat, """Return entity and device specific state attributes."""
ATTR_CHILD_ID: self.child_id, attr = super().extra_state_attributes
ATTR_DESCRIPTION: child.description,
ATTR_NODE_ID: self.node_id, assert self.platform.config_entry
} attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE]
attr[ATTR_CHILD_ID] = self.child_id
attr[ATTR_DESCRIPTION] = self._child.description
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
for value_type, value in self._values.items(): for value_type, value in self._values.items():
attr[set_req(value_type).name] = value attr[set_req(value_type).name] = value
@ -157,10 +222,8 @@ class MySensorsDevice(ABC):
@callback @callback
def _async_update(self) -> None: def _async_update(self) -> None:
"""Update the controller with the latest value from a sensor.""" """Update the controller with the latest value from a sensor."""
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
set_req = self.gateway.const.SetReq set_req = self.gateway.const.SetReq
for value_type, value in child.values.items(): for value_type, value in self._child.values.items():
_LOGGER.debug( _LOGGER.debug(
"Entity update: %s: value_type %s, value = %s", "Entity update: %s: value_type %s, value = %s",
self.name, self.name,
@ -182,57 +245,6 @@ class MySensorsDevice(ABC):
else: else:
self._values[value_type] = value self._values[value_type] = value
@callback
@abstractmethod
def _async_update_callback(self) -> None:
"""Update the device."""
async def async_update_callback(self) -> None:
"""Update the device after delay."""
if not self._debouncer:
self._debouncer = Debouncer(
self.hass,
_LOGGER,
cooldown=UPDATE_DELAY,
immediate=False,
function=self._async_update_callback,
)
await self._debouncer.async_call()
def get_mysensors_devices(
hass: HomeAssistant, domain: Platform
) -> dict[DevId, MySensorsEntity]:
"""Return MySensors devices for a hass platform name."""
if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data[DOMAIN]:
hass.data[DOMAIN][MYSENSORS_PLATFORM_DEVICES.format(domain)] = {}
devices: dict[DevId, MySensorsEntity] = hass.data[DOMAIN][
MYSENSORS_PLATFORM_DEVICES.format(domain)
]
return devices
class MySensorsEntity(MySensorsDevice, Entity):
"""Representation of a MySensors entity."""
_attr_should_poll = False
@property
def available(self) -> bool:
"""Return true if entity is available."""
return self.value_type in self._values
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return entity specific state attributes."""
attr = self._extra_attributes
assert self.platform.config_entry
attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE]
return attr
@callback @callback
def _async_update_callback(self) -> None: def _async_update_callback(self) -> None:
"""Update the entity.""" """Update the entity."""
@ -241,6 +253,7 @@ class MySensorsEntity(MySensorsDevice, Entity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register update callback.""" """Register update callback."""
await super().async_added_to_hass()
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, self.hass,
@ -248,11 +261,3 @@ class MySensorsEntity(MySensorsDevice, Entity):
self.async_update_callback, self.async_update_callback,
) )
) )
self.async_on_remove(
async_dispatcher_connect(
self.hass,
NODE_CALLBACK.format(self.gateway_id, self.node_id),
self.async_update_callback,
)
)
self._async_update()

View File

@ -10,7 +10,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import setup_mysensors_platform from . import setup_mysensors_platform
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .const import MYSENSORS_DISCOVERY, DiscoveryInfo
from .device import MySensorsEntity from .device import MySensorsChildEntity
from .helpers import on_unload from .helpers import on_unload
@ -43,7 +43,7 @@ async def async_setup_entry(
) )
class MySensorsDeviceTracker(MySensorsEntity, TrackerEntity): class MySensorsDeviceTracker(MySensorsChildEntity, TrackerEntity):
"""Represent a MySensors device tracker.""" """Represent a MySensors device tracker."""
_latitude: float | None = None _latitude: float | None = None

View File

@ -42,6 +42,7 @@ from .const import (
) )
from .handler import HANDLERS from .handler import HANDLERS
from .helpers import ( from .helpers import (
discover_mysensors_node,
discover_mysensors_platform, discover_mysensors_platform,
on_unload, on_unload,
validate_child, validate_child,
@ -244,6 +245,7 @@ async def _discover_persistent_devices(
for node_id in gateway.sensors: for node_id in gateway.sensors:
if not validate_node(gateway, node_id): if not validate_node(gateway, node_id):
continue continue
discover_mysensors_node(hass, entry.entry_id, node_id)
node: Sensor = gateway.sensors[node_id] node: Sensor = gateway.sensors[node_id]
for child in node.children.values(): # child is of type ChildSensor for child in node.children.values(): # child is of type ChildSensor
validated = validate_child(entry.entry_id, gateway, node_id, child) validated = validate_child(entry.entry_id, gateway, node_id, child)

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from mysensors import Message from mysensors import Message
from mysensors.const import SYSTEM_CHILD_ID
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -12,7 +13,11 @@ from homeassistant.util import decorator
from .const import CHILD_CALLBACK, NODE_CALLBACK, DevId, GatewayId from .const import CHILD_CALLBACK, NODE_CALLBACK, DevId, GatewayId
from .device import get_mysensors_devices from .device import get_mysensors_devices
from .helpers import discover_mysensors_platform, validate_set_msg from .helpers import (
discover_mysensors_node,
discover_mysensors_platform,
validate_set_msg,
)
HANDLERS: decorator.Registry[ HANDLERS: decorator.Registry[
str, Callable[[HomeAssistant, GatewayId, Message], None] str, Callable[[HomeAssistant, GatewayId, Message], None]
@ -71,6 +76,16 @@ def handle_sketch_version(
_handle_node_update(hass, gateway_id, msg) _handle_node_update(hass, gateway_id, msg)
@HANDLERS.register("presentation")
@callback
def handle_presentation(
hass: HomeAssistant, gateway_id: GatewayId, msg: Message
) -> None:
"""Handle an internal presentation message."""
if msg.child_id == SYSTEM_CHILD_ID:
discover_mysensors_node(hass, gateway_id, msg.node_id)
@callback @callback
def _handle_child_update( def _handle_child_update(
hass: HomeAssistant, gateway_id: GatewayId, validated: dict[Platform, list[DevId]] hass: HomeAssistant, gateway_id: GatewayId, validated: dict[Platform, list[DevId]]

View File

@ -19,9 +19,12 @@ from homeassistant.util.decorator import Registry
from .const import ( from .const import (
ATTR_DEVICES, ATTR_DEVICES,
ATTR_GATEWAY_ID, ATTR_GATEWAY_ID,
ATTR_NODE_ID,
DOMAIN, DOMAIN,
FLAT_PLATFORM_TYPES, FLAT_PLATFORM_TYPES,
MYSENSORS_DISCOVERED_NODES,
MYSENSORS_DISCOVERY, MYSENSORS_DISCOVERY,
MYSENSORS_NODE_DISCOVERY,
MYSENSORS_ON_UNLOAD, MYSENSORS_ON_UNLOAD,
TYPE_TO_PLATFORMS, TYPE_TO_PLATFORMS,
DevId, DevId,
@ -65,6 +68,27 @@ def discover_mysensors_platform(
) )
@callback
def discover_mysensors_node(
hass: HomeAssistant, gateway_id: GatewayId, node_id: int
) -> None:
"""Discover a MySensors node."""
discovered_nodes = hass.data[DOMAIN].setdefault(
MYSENSORS_DISCOVERED_NODES.format(gateway_id), set()
)
if node_id not in discovered_nodes:
discovered_nodes.add(node_id)
async_dispatcher_send(
hass,
MYSENSORS_NODE_DISCOVERY,
{
ATTR_GATEWAY_ID: gateway_id,
ATTR_NODE_ID: node_id,
},
)
def default_schema( def default_schema(
gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
) -> vol.Schema: ) -> vol.Schema:

View File

@ -19,7 +19,7 @@ from homeassistant.util.color import rgb_hex_to_rgb_list
from .. import mysensors from .. import mysensors
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo, SensorType from .const import MYSENSORS_DISCOVERY, DiscoveryInfo, SensorType
from .device import MySensorsEntity from .device import MySensorsChildEntity
from .helpers import on_unload from .helpers import on_unload
@ -29,7 +29,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up this platform for a specific ConfigEntry(==Gateway).""" """Set up this platform for a specific ConfigEntry(==Gateway)."""
device_class_map: dict[SensorType, type[MySensorsEntity]] = { device_class_map: dict[SensorType, type[MySensorsChildEntity]] = {
"S_DIMMER": MySensorsLightDimmer, "S_DIMMER": MySensorsLightDimmer,
"S_RGB_LIGHT": MySensorsLightRGB, "S_RGB_LIGHT": MySensorsLightRGB,
"S_RGBW_LIGHT": MySensorsLightRGBW, "S_RGBW_LIGHT": MySensorsLightRGBW,
@ -56,7 +56,7 @@ async def async_setup_entry(
) )
class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity): class MySensorsLight(mysensors.device.MySensorsChildEntity, LightEntity):
"""Representation of a MySensors Light child node.""" """Representation of a MySensors Light child node."""
def __init__(self, *args: Any) -> None: def __init__(self, *args: Any) -> None:

View File

@ -17,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import setup_mysensors_platform from . import setup_mysensors_platform
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .const import MYSENSORS_DISCOVERY, DiscoveryInfo
from .device import MySensorsEntity from .device import MySensorsChildEntity
from .helpers import on_unload from .helpers import on_unload
@ -50,7 +50,7 @@ async def async_setup_entry(
) )
class MySensorsRemote(MySensorsEntity, RemoteEntity): class MySensorsRemote(MySensorsChildEntity, RemoteEntity):
"""Representation of a MySensors IR transceiver.""" """Representation of a MySensors IR transceiver."""
_current_command: str | None = None _current_command: str | None = None

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from mysensors import BaseAsyncGateway
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -30,13 +31,22 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
UnitOfVolume, UnitOfVolume,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.unit_system import METRIC_SYSTEM
from .. import mysensors from .. import mysensors
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .const import (
ATTR_GATEWAY_ID,
ATTR_NODE_ID,
DOMAIN,
MYSENSORS_DISCOVERY,
MYSENSORS_GATEWAYS,
MYSENSORS_NODE_DISCOVERY,
DiscoveryInfo,
NodeDiscoveryInfo,
)
from .helpers import on_unload from .helpers import on_unload
SENSORS: dict[str, SensorEntityDescription] = { SENSORS: dict[str, SensorEntityDescription] = {
@ -211,6 +221,14 @@ async def async_setup_entry(
async_add_entities=async_add_entities, async_add_entities=async_add_entities,
) )
@callback
def async_node_discover(discovery_info: NodeDiscoveryInfo) -> None:
"""Add battery sensor for each MySensors node."""
gateway_id = discovery_info[ATTR_GATEWAY_ID]
node_id = discovery_info[ATTR_NODE_ID]
gateway: BaseAsyncGateway = hass.data[DOMAIN][MYSENSORS_GATEWAYS][gateway_id]
async_add_entities([MyBatterySensor(gateway_id, gateway, node_id)])
on_unload( on_unload(
hass, hass,
config_entry.entry_id, config_entry.entry_id,
@ -221,8 +239,43 @@ async def async_setup_entry(
), ),
) )
on_unload(
hass,
config_entry.entry_id,
async_dispatcher_connect(
hass,
MYSENSORS_NODE_DISCOVERY,
async_node_discover,
),
)
class MySensorsSensor(mysensors.device.MySensorsEntity, SensorEntity):
class MyBatterySensor(mysensors.device.MySensorNodeEntity, SensorEntity):
"""Battery sensor of MySensors node."""
_attr_device_class = SensorDeviceClass.BATTERY
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = PERCENTAGE
_attr_force_update = True
@property
def unique_id(self) -> str:
"""Return a unique ID for use in home assistant."""
return f"{self.gateway_id}-{self.node_id}-battery"
@property
def name(self) -> str:
"""Return the name of this entity."""
return f"{self.node_name} Battery"
@callback
def _async_update_callback(self) -> None:
"""Update the controller with the latest battery level."""
self._attr_native_value = self._node.battery_level
self.async_write_ha_state()
class MySensorsSensor(mysensors.device.MySensorsChildEntity, SensorEntity):
"""Representation of a MySensors Sensor child node.""" """Representation of a MySensors Sensor child node."""
_attr_force_update = True _attr_force_update = True

View File

@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import setup_mysensors_platform from . import setup_mysensors_platform
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo, SensorType from .const import MYSENSORS_DISCOVERY, DiscoveryInfo, SensorType
from .device import MySensorsEntity from .device import MySensorsChildEntity
from .helpers import on_unload from .helpers import on_unload
@ -58,7 +58,7 @@ async def async_setup_entry(
) )
class MySensorsSwitch(MySensorsEntity, SwitchEntity): class MySensorsSwitch(MySensorsChildEntity, SwitchEntity):
"""Representation of the value of a MySensors Switch child node.""" """Representation of the value of a MySensors Switch child node."""
@property @property

View File

@ -10,7 +10,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .. import mysensors from .. import mysensors
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .const import MYSENSORS_DISCOVERY, DiscoveryInfo
from .device import MySensorsEntity from .device import MySensorsChildEntity
from .helpers import on_unload from .helpers import on_unload
@ -43,7 +43,7 @@ async def async_setup_entry(
) )
class MySensorsText(MySensorsEntity, TextEntity): class MySensorsText(MySensorsChildEntity, TextEntity):
"""Representation of the value of a MySensors Text child node.""" """Representation of the value of a MySensors Text child node."""
_attr_native_max = 25 _attr_native_max = 25

View File

@ -470,3 +470,19 @@ def text_node(gateway_nodes: dict[int, Sensor], text_node_state: dict) -> Sensor
nodes = update_gateway_nodes(gateway_nodes, text_node_state) nodes = update_gateway_nodes(gateway_nodes, text_node_state)
node = nodes[1] node = nodes[1]
return node return node
@pytest.fixture(name="battery_sensor_state", scope="session")
def battery_sensor_state_fixture() -> dict:
"""Load the battery sensor state."""
return load_nodes_state("battery_sensor_state.json")
@pytest.fixture
def battery_sensor(
gateway_nodes: dict[int, Sensor], battery_sensor_state: dict
) -> Sensor:
"""Load the battery sensor."""
nodes = update_gateway_nodes(gateway_nodes, deepcopy(battery_sensor_state))
node = nodes[1]
return node

View File

@ -0,0 +1,12 @@
{
"1": {
"sensor_id": 1,
"children": {},
"type": 17,
"sketch_name": "Battery Sensor",
"sketch_version": "1.0",
"battery_level": 42,
"protocol_version": "2.3.2",
"heartbeat": 0
}
}

View File

@ -77,6 +77,25 @@ async def test_ir_transceiver(
assert state.state == "new_code" assert state.state == "new_code"
async def test_battery_entity(
hass: HomeAssistant,
battery_sensor: Sensor,
receive_message: Callable[[str], None],
) -> None:
"""Test sensor with battery level reporting."""
battery_entity_id = "sensor.battery_sensor_1_battery"
state = hass.states.get(battery_entity_id)
assert state
assert state.state == "42"
receive_message("1;255;3;0;0;84\n")
await hass.async_block_till_done()
state = hass.states.get(battery_entity_id)
assert state
assert state.state == "84"
async def test_power_sensor( async def test_power_sensor(
hass: HomeAssistant, hass: HomeAssistant,
power_sensor: Sensor, power_sensor: Sensor,