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 (
ATTR_DEVICES,
DOMAIN,
MYSENSORS_DISCOVERED_NODES,
MYSENSORS_GATEWAYS,
MYSENSORS_ON_UNLOAD,
PLATFORMS,
@ -22,7 +23,7 @@ from .const import (
DiscoveryInfo,
SensorType,
)
from .device import MySensorsEntity, get_mysensors_devices
from .device import MySensorsChildEntity, get_mysensors_devices
from .gateway import finish_setup, gw_stop, setup_gateway
_LOGGER = logging.getLogger(__name__)
@ -72,6 +73,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(key)
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)
return True
@ -91,6 +93,11 @@ async def async_remove_config_entry_device(
gateway.sensors.pop(node_id, None)
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
@ -99,12 +106,13 @@ def setup_mysensors_platform(
hass: HomeAssistant,
domain: Platform, # hass platform name
discovery_info: DiscoveryInfo,
device_class: type[MySensorsEntity] | Mapping[SensorType, type[MySensorsEntity]],
device_class: type[MySensorsChildEntity]
| Mapping[SensorType, type[MySensorsChildEntity]],
device_args: (
None | tuple
) = None, # extra arguments that will be given to the entity constructor
async_add_entities: Callable | None = None,
) -> list[MySensorsEntity] | None:
) -> list[MySensorsChildEntity] | None:
"""Set up a MySensors platform.
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:
device_args = ()
new_devices: list[MySensorsEntity] = []
new_devices: list[MySensorsChildEntity] = []
new_dev_ids: list[DevId] = discovery_info[ATTR_DEVICES]
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:
_LOGGER.debug(
"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."""
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."""
_attr_hvac_modes = OPERATION_LIST

View file

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

View file

@ -1,14 +1,14 @@
"""Handle MySensors devices."""
from __future__ import annotations
from abc import ABC, abstractmethod
from abc import abstractmethod
import logging
from typing import Any
from mysensors import BaseAsyncGateway, Sensor
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.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
@ -36,56 +36,24 @@ ATTR_HEARTBEAT = "heartbeat"
MYSENSORS_PLATFORM_DEVICES = "mysensors_devices_{}"
class MySensorsDevice(ABC):
class MySensorNodeEntity(Entity):
"""Representation of a MySensors device."""
hass: HomeAssistant
def __init__(
self,
gateway_id: GatewayId,
gateway: BaseAsyncGateway,
node_id: int,
child_id: int,
value_type: int,
self, gateway_id: GatewayId, gateway: BaseAsyncGateway, node_id: int
) -> None:
"""Set up the MySensors device."""
"""Set up the MySensors node entity."""
self.gateway_id: GatewayId = gateway_id
self.gateway: BaseAsyncGateway = gateway
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
@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
def _node(self) -> Sensor:
return self.gateway.sensors[self.node_id]
@property
def _child(self) -> ChildSensor:
return self._node.children[self.child_id]
@property
def sketch_name(self) -> str:
"""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}"
@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
def device_info(self) -> DeviceInfo:
"""Return the device info."""
@ -125,6 +88,96 @@ class MySensorsDevice(ABC):
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
def name(self) -> str:
"""Return the name of this entity."""
@ -134,21 +187,33 @@ class MySensorsDevice(ABC):
return str(child.description)
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
def _extra_attributes(self) -> dict[str, Any]:
"""Return device specific attributes."""
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
attr = {
ATTR_BATTERY_LEVEL: node.battery_level,
ATTR_HEARTBEAT: node.heartbeat,
ATTR_CHILD_ID: self.child_id,
ATTR_DESCRIPTION: child.description,
ATTR_NODE_ID: self.node_id,
}
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 and device specific state attributes."""
attr = super().extra_state_attributes
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
for value_type, value in self._values.items():
attr[set_req(value_type).name] = value
@ -157,10 +222,8 @@ class MySensorsDevice(ABC):
@callback
def _async_update(self) -> None:
"""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
for value_type, value in child.values.items():
for value_type, value in self._child.values.items():
_LOGGER.debug(
"Entity update: %s: value_type %s, value = %s",
self.name,
@ -182,57 +245,6 @@ class MySensorsDevice(ABC):
else:
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
def _async_update_callback(self) -> None:
"""Update the entity."""
@ -241,6 +253,7 @@ class MySensorsEntity(MySensorsDevice, Entity):
async def async_added_to_hass(self) -> None:
"""Register update callback."""
await super().async_added_to_hass()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
@ -248,11 +261,3 @@ class MySensorsEntity(MySensorsDevice, Entity):
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 .const import MYSENSORS_DISCOVERY, DiscoveryInfo
from .device import MySensorsEntity
from .device import MySensorsChildEntity
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."""
_latitude: float | None = None

View file

@ -42,6 +42,7 @@ from .const import (
)
from .handler import HANDLERS
from .helpers import (
discover_mysensors_node,
discover_mysensors_platform,
on_unload,
validate_child,
@ -244,6 +245,7 @@ async def _discover_persistent_devices(
for node_id in gateway.sensors:
if not validate_node(gateway, node_id):
continue
discover_mysensors_node(hass, entry.entry_id, node_id)
node: Sensor = gateway.sensors[node_id]
for child in node.children.values(): # child is of type ChildSensor
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 mysensors import Message
from mysensors.const import SYSTEM_CHILD_ID
from homeassistant.const import Platform
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 .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[
str, Callable[[HomeAssistant, GatewayId, Message], None]
@ -71,6 +76,16 @@ def handle_sketch_version(
_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
def _handle_child_update(
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 (
ATTR_DEVICES,
ATTR_GATEWAY_ID,
ATTR_NODE_ID,
DOMAIN,
FLAT_PLATFORM_TYPES,
MYSENSORS_DISCOVERED_NODES,
MYSENSORS_DISCOVERY,
MYSENSORS_NODE_DISCOVERY,
MYSENSORS_ON_UNLOAD,
TYPE_TO_PLATFORMS,
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(
gateway: BaseAsyncGateway, child: ChildSensor, value_type_name: ValueType
) -> vol.Schema:

View file

@ -19,7 +19,7 @@ from homeassistant.util.color import rgb_hex_to_rgb_list
from .. import mysensors
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo, SensorType
from .device import MySensorsEntity
from .device import MySensorsChildEntity
from .helpers import on_unload
@ -29,7 +29,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""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_RGB_LIGHT": MySensorsLightRGB,
"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."""
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 .const import MYSENSORS_DISCOVERY, DiscoveryInfo
from .device import MySensorsEntity
from .device import MySensorsChildEntity
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."""
_current_command: str | None = None

View file

@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any
from awesomeversion import AwesomeVersion
from mysensors import BaseAsyncGateway
from homeassistant.components.sensor import (
SensorDeviceClass,
@ -30,13 +31,22 @@ from homeassistant.const import (
UnitOfTemperature,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_system import METRIC_SYSTEM
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
SENSORS: dict[str, SensorEntityDescription] = {
@ -211,6 +221,14 @@ async def async_setup_entry(
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(
hass,
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."""
_attr_force_update = True

View file

@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import setup_mysensors_platform
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo, SensorType
from .device import MySensorsEntity
from .device import MySensorsChildEntity
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."""
@property

View file

@ -10,7 +10,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .. import mysensors
from .const import MYSENSORS_DISCOVERY, DiscoveryInfo
from .device import MySensorsEntity
from .device import MySensorsChildEntity
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."""
_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)
node = nodes[1]
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"
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(
hass: HomeAssistant,
power_sensor: Sensor,