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:
parent
6d624ecb46
commit
09729e8c46
18 changed files with 303 additions and 139 deletions
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue