MySensors: Stop adding attributes to existing objects

This commit removes the extra attributes that were being
added to the gateway objects from pymysensors.

Most attributes were easy to remove, except for the gateway id.
The MySensorsDevice class needs the gateway id as it is part of its DevId
as well as the unique_id and device_info.
Most MySensorsDevices actually end up being Entities.
Entities have access to their ConfigEntry via self.platform.config_entry.

However, the device_tracker platform does not become an Entity.
For this reason, the gateway id is not fetched from self.plaform but
given as an argument.

Additionally, MySensorsDevices expose the address of the gateway
(CONF_DEVICE). Entities can easily fetch this information via self.platform,
but the device_tracker cannot. This commit chooses to remove the gateway
address from device_tracker. While this could in theory break some automations,
the simplicity of this solution was deemed worth it.
The alternative of adding the entire ConfigEntry as an argument to MySensorsDevices
is not viable, because device_tracker is initialized by the async_setup_scanner function
that isn't supplied a ConfigEntry. It only gets discovery_info.
Adding the entire ConfigEntry doesn't seem appropriate for this edge case.
This commit is contained in:
functionpointer
2021-01-27 21:00:23 +01:00
parent 6d2abbfe31
commit 0fd7c41f94
12 changed files with 80 additions and 63 deletions

View File

@@ -168,7 +168,6 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
Every instance has a connection to exactly one Gateway.
"""
_LOGGER.debug("async_setup_entry: %s (id: %s)", entry.title, entry.entry_id)
gateway = await setup_gateway(hass, entry)
if not gateway:
@@ -283,7 +282,7 @@ def setup_mysensors_platform(
s_type = gateway.const.Presentation(child.type).name
device_class_copy = device_class[s_type]
args_copy = (*device_args, gateway, node_id, child_id, value_type)
args_copy = (*device_args, gateway_id, gateway, node_id, child_id, value_type)
devices[dev_id] = device_class_copy(*args_copy)
new_devices.append(devices[dev_id])
if new_devices:

View File

@@ -87,12 +87,12 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
@property
def assumed_state(self):
"""Return True if unable to access real state of entity."""
return self.gateway.optimistic
return False
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT
return TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT
@property
def current_temperature(self):
@@ -181,7 +181,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
self.gateway.set_child_value(
self.node_id, self.child_id, value_type, value, ack=1
)
if self.gateway.optimistic:
if self.om:
# Optimistically assume that device has changed state
self._values[value_type] = value
self.async_write_ha_state()
@@ -192,7 +192,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode, ack=1
)
if self.gateway.optimistic:
if self.assumed_state:
# Optimistically assume that device has changed state
self._values[set_req.V_HVAC_SPEED] = fan_mode
self.async_write_ha_state()
@@ -206,7 +206,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateEntity):
DICT_HA_TO_MYS[hvac_mode],
ack=1,
)
if self.gateway.optimistic:
if self.assumed_state:
# Optimistically assume that device has changed state
self._values[self.value_type] = hvac_mode
self.async_write_ha_state()

View File

@@ -3,6 +3,7 @@ from collections import defaultdict
from typing import Dict, List, Literal, Set, Tuple
ATTR_DEVICES: str = "devices"
ATTR_GATEWAY_ID: str = "gateway_id"
CONF_BAUD_RATE: str = "baud_rate"
CONF_DEVICE: str = "device"

View File

@@ -31,7 +31,7 @@ async def async_setup_entry(
await on_unload(
hass,
config_entry,
config_entry.entry_id,
async_dispatcher_connect(
hass,
MYSENSORS_DISCOVERY.format(config_entry.entry_id, DOMAIN),
@@ -46,7 +46,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
@property
def assumed_state(self):
"""Return True if unable to access real state of entity."""
return self.gateway.optimistic
return False
@property
def is_closed(self):
@@ -71,7 +71,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_UP, 1, ack=1
)
if self.gateway.optimistic:
if self.assumed_state:
# Optimistically assume that cover has changed state.
if set_req.V_DIMMER in self._values:
self._values[set_req.V_DIMMER] = 100
@@ -85,7 +85,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_DOWN, 1, ack=1
)
if self.gateway.optimistic:
if self.assumed_state:
# Optimistically assume that cover has changed state.
if set_req.V_DIMMER in self._values:
self._values[set_req.V_DIMMER] = 0
@@ -100,7 +100,7 @@ class MySensorsCover(mysensors.device.MySensorsEntity, CoverEntity):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_DIMMER, position, ack=1
)
if self.gateway.optimistic:
if self.assumed_state:
# Optimistically assume that cover has changed state.
self._values[set_req.V_DIMMER] = position
self.async_write_ha_state()

View File

@@ -13,11 +13,13 @@ from homeassistant.helpers.entity import Entity
from .const import (
CHILD_CALLBACK,
CONF_DEVICE,
DOMAIN,
NODE_CALLBACK,
PLATFORM_TYPES,
UPDATE_DELAY,
DevId,
GatewayId,
)
_LOGGER = logging.getLogger(__name__)
@@ -33,8 +35,16 @@ MYSENSORS_PLATFORM_DEVICES = "mysensors_devices_{}"
class MySensorsDevice:
"""Representation of a MySensors device."""
def __init__(self, gateway, node_id, child_id, value_type):
def __init__(
self,
gateway_id: GatewayId,
gateway: BaseAsyncGateway,
node_id: int,
child_id: int,
value_type: int,
):
"""Set up the MySensors device."""
self.gateway_id: GatewayId = gateway_id
self.gateway: BaseAsyncGateway = gateway
self.node_id: int = node_id
self.child_id: int = child_id
@@ -72,11 +82,6 @@ class MySensorsDevice:
"deleted %s from platform %s", self.dev_id, platform
)
@property
def gateway_id(self) -> str:
"""Return the id of the gateway that this device belongs to."""
return self.gateway.entry_id
@property
def _mysensors_sensor(self) -> Sensor:
return self.gateway.sensors[self.node_id]
@@ -131,9 +136,10 @@ class MySensorsDevice:
ATTR_HEARTBEAT: node.heartbeat,
ATTR_CHILD_ID: self.child_id,
ATTR_DESCRIPTION: child.description,
ATTR_DEVICE: self.gateway.device,
ATTR_NODE_ID: self.node_id,
}
if hasattr(self, "platform"):
attr[ATTR_DEVICE] = self.platform.config_entry.data[CONF_DEVICE]
set_req = self.gateway.const.SetReq

View File

@@ -2,7 +2,7 @@
from homeassistant.components import mysensors
from homeassistant.components.device_tracker import DOMAIN
from homeassistant.components.mysensors import DevId, on_unload
from homeassistant.components.mysensors.const import GatewayId
from homeassistant.components.mysensors.const import ATTR_GATEWAY_ID, GatewayId
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import slugify
@@ -14,6 +14,7 @@ async def async_setup_scanner(
"""Set up the MySensors device scanner."""
new_devices = mysensors.setup_mysensors_platform(
hass,
discovery_info[ATTR_GATEWAY_ID],
DOMAIN,
discovery_info,
MySensorsDeviceScanner,
@@ -23,7 +24,7 @@ async def async_setup_scanner(
return False
for device in new_devices:
gateway_id: GatewayId = device.gateway.entry_id
gateway_id: GatewayId = discovery_info[ATTR_GATEWAY_ID]
dev_id: DevId = (gateway_id, device.node_id, device.child_id, device.value_type)
await on_unload(
hass,

View File

@@ -211,11 +211,6 @@ async def _get_gateway(
except vol.Invalid:
# invalid ip address
return None
# this adds extra properties to the pymysensors objects
gateway.metric = hass.config.units.is_metric
gateway.optimistic = False # old optimistic option has been deprecated, we use echos to hopefully not need it
gateway.device = device
gateway.entry_id = unique_id
gateway.event_callback = _gw_callback_factory(hass, entry)
if persistence:
await gateway.start_persistence()
@@ -230,7 +225,7 @@ async def finish_setup(
discover_tasks = []
start_tasks = []
discover_tasks.append(_discover_persistent_devices(hass, hass_config, gateway))
start_tasks.append(_gw_start(hass, gateway))
start_tasks.append(_gw_start(hass, hass_config, gateway))
if discover_tasks:
# Make sure all devices and platforms are loaded before gateway start.
await asyncio.wait(discover_tasks)
@@ -249,7 +244,7 @@ async def _discover_persistent_devices(
continue
node: Sensor = gateway.sensors[node_id]
for child in node.children.values(): # child is of type ChildSensor
validated = validate_child(gateway, node_id, child)
validated = validate_child(hass_config, gateway, node_id, child)
for platform, dev_ids in validated.items():
new_devices[platform].extend(dev_ids)
_LOGGER.debug("discovering persistent devices: %s", new_devices)
@@ -259,35 +254,36 @@ async def _discover_persistent_devices(
await asyncio.wait(tasks)
async def gw_stop(hass, gateway: BaseAsyncGateway):
async def gw_stop(hass, hass_config: ConfigEntry, gateway: BaseAsyncGateway):
"""Stop the gateway."""
_LOGGER.info("stopping gateway %s", gateway.entry_id)
connect_task = hass.data[DOMAIN].get(
MYSENSORS_GATEWAY_START_TASK.format(gateway.entry_id), None
MYSENSORS_GATEWAY_START_TASK.format(hass_config.entry_id), None
)
if connect_task is not None and not connect_task.done():
connect_task.cancel()
await gateway.stop()
async def _gw_start(hass: HomeAssistantType, gateway: BaseAsyncGateway):
async def _gw_start(
hass: HomeAssistantType, hass_config: ConfigEntry, gateway: BaseAsyncGateway
):
"""Start the gateway."""
# Don't use hass.async_create_task to avoid holding up setup indefinitely.
hass.data[DOMAIN][
MYSENSORS_GATEWAY_START_TASK.format(gateway.entry_id)
MYSENSORS_GATEWAY_START_TASK.format(hass_config.entry_id)
] = asyncio.create_task(
gateway.start()
) # store the connect task so it can be cancelled in gw_stop
async def stop_this_gw(_: Event):
await gw_stop(hass, gateway)
await gw_stop(hass, hass_config, gateway)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_this_gw)
if gateway.device == "mqtt":
if hass_config.data[CONF_DEVICE] == MQTT_COMPONENT:
# Gatways connected via mqtt doesn't send gateway ready message.
return
gateway_ready = asyncio.Future()
gateway_ready_key = MYSENSORS_GATEWAY_READY.format(gateway.entry_id)
gateway_ready_key = MYSENSORS_GATEWAY_READY.format(hass_config.entry_id)
hass.data[DOMAIN][gateway_ready_key] = gateway_ready
try:
@@ -296,7 +292,7 @@ async def _gw_start(hass: HomeAssistantType, gateway: BaseAsyncGateway):
except asyncio.TimeoutError:
_LOGGER.warning(
"Gateway %s not ready after %s secs so continuing with setup",
gateway.device,
hass_config.data[CONF_DEVICE],
GATEWAY_READY_TIMEOUT,
)
finally:

View File

@@ -3,11 +3,12 @@ from typing import Dict, List
from mysensors import Message
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import decorator
from ...config_entries import ConfigEntry
from .const import CHILD_CALLBACK, MYSENSORS_GATEWAY_READY, NODE_CALLBACK, DevId
from .device import get_mysensors_devices
from .helpers import discover_mysensors_platform, validate_set_msg
@@ -18,7 +19,7 @@ HANDLERS = decorator.Registry()
@HANDLERS.register("set")
async def handle_set(hass, hass_config: ConfigEntry, msg: Message) -> None:
"""Handle a mysensors set message."""
validated = validate_set_msg(msg)
validated = validate_set_msg(hass_config, msg)
_handle_child_update(hass, hass_config, validated)
@@ -41,19 +42,19 @@ async def handle_battery_level(hass, hass_config: ConfigEntry, msg: Message) ->
@HANDLERS.register("I_HEARTBEAT_RESPONSE")
async def handle_heartbeat(hass, hass_config: ConfigEntry, msg: Message) -> None:
"""Handle an heartbeat."""
_handle_node_update(hass, msg)
_handle_node_update(hass, hass_config, msg)
@HANDLERS.register("I_SKETCH_NAME")
async def handle_sketch_name(hass, hass_config: ConfigEntry, msg: Message) -> None:
"""Handle an internal sketch name message."""
_handle_node_update(hass, msg)
_handle_node_update(hass, hass_config, msg)
@HANDLERS.register("I_SKETCH_VERSION")
async def handle_sketch_version(hass, hass_config: ConfigEntry, msg: Message) -> None:
"""Handle an internal sketch version message."""
_handle_node_update(hass, msg)
_handle_node_update(hass, hass_config, msg)
@HANDLERS.register("I_GATEWAY_READY")
@@ -62,7 +63,7 @@ async def handle_gateway_ready(hass, hass_config: ConfigEntry, msg: Message) ->
Set asyncio future result if gateway is ready.
"""
gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format(msg.gateway.entry_id))
gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format(hass_config.entry_id))
if gateway_ready is None or gateway_ready.cancelled():
return
gateway_ready.set_result(True)
@@ -94,7 +95,9 @@ def _handle_child_update(
@callback
def _handle_node_update(hass, msg):
def _handle_node_update(
hass: HomeAssistantType, hass_config: ConfigEntry, msg: Message
):
"""Handle a node update."""
signal = NODE_CALLBACK.format(msg.gateway.entry_id, msg.node_id)
signal = NODE_CALLBACK.format(hass_config.entry_id, msg.node_id)
async_dispatcher_send(hass, signal)

View File

@@ -17,6 +17,7 @@ from ...config_entries import ConfigEntry
from ...helpers.dispatcher import async_dispatcher_send
from .const import (
ATTR_DEVICES,
ATTR_GATEWAY_ID,
DOMAIN,
FLAT_PLATFORM_TYPES,
MYSENSORS_DISCOVERY,
@@ -39,7 +40,11 @@ def discover_mysensors_platform(
async_dispatcher_send(
hass,
MYSENSORS_DISCOVERY.format(hass_config.entry_id, platform),
{ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN},
{
ATTR_DEVICES: new_devices,
CONF_NAME: DOMAIN,
ATTR_GATEWAY_ID: hass_config.entry_id,
},
)
@@ -125,12 +130,12 @@ def invalid_msg(
)
def validate_set_msg(msg: Message) -> Dict[str, List[DevId]]:
def validate_set_msg(hass_config: ConfigEntry, msg: Message) -> Dict[str, List[DevId]]:
"""Validate a set message."""
if not validate_node(msg.gateway, msg.node_id):
return {}
child = msg.gateway.sensors[msg.node_id].children[msg.child_id]
return validate_child(msg.gateway, msg.node_id, child, msg.sub_type)
return validate_child(hass_config, msg.gateway, msg.node_id, child, msg.sub_type)
def validate_node(gateway: BaseAsyncGateway, node_id: int) -> bool:
@@ -142,6 +147,7 @@ def validate_node(gateway: BaseAsyncGateway, node_id: int) -> bool:
def validate_child(
hass_config: ConfigEntry,
gateway: BaseAsyncGateway,
node_id: int,
child: ChildSensor,
@@ -188,7 +194,12 @@ def validate_child(
exc,
)
continue
dev_id: DevId = (gateway.entry_id, node_id, child.id, set_req[v_name].value)
dev_id: DevId = (
hass_config.entry_id,
node_id,
child.id,
set_req[v_name].value,
)
validated[platform].append(dev_id)
return validated

View File

@@ -85,7 +85,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
@property
def assumed_state(self):
"""Return true if unable to access real state of entity."""
return self.gateway.optimistic
return False
@property
def is_on(self):
@@ -102,7 +102,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1
)
if self.gateway.optimistic:
if self.assumed_state:
# optimistically assume that light has changed state
self._state = True
self._values[set_req.V_LIGHT] = STATE_ON
@@ -124,7 +124,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
self.node_id, self.child_id, set_req.V_DIMMER, percent, ack=1
)
if self.gateway.optimistic:
if self.assumed_state:
# optimistically assume that light has changed state
self._brightness = brightness
self._values[set_req.V_DIMMER] = percent
@@ -157,7 +157,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
self.node_id, self.child_id, self.value_type, hex_color, ack=1
)
if self.gateway.optimistic:
if self.assumed_state:
# optimistically assume that light has changed state
self._hs = color_util.color_RGB_to_hs(*rgb)
self._white = white
@@ -167,7 +167,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, LightEntity):
"""Turn the device off."""
value_type = self.gateway.const.SetReq.V_LIGHT
self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0, ack=1)
if self.gateway.optimistic:
if self.assumed_state:
# optimistically assume that light has changed state
self._state = False
self._values[value_type] = STATE_OFF
@@ -210,7 +210,7 @@ class MySensorsLightDimmer(MySensorsLight):
"""Turn the device on."""
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
if self.gateway.optimistic:
if self.assumed_state:
self.async_write_ha_state()
async def async_update(self):
@@ -236,7 +236,7 @@ class MySensorsLightRGB(MySensorsLight):
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
self._turn_on_rgb_and_w("%02x%02x%02x", **kwargs)
if self.gateway.optimistic:
if self.assumed_state:
self.async_write_ha_state()
async def async_update(self):
@@ -263,5 +263,5 @@ class MySensorsLightRGBW(MySensorsLightRGB):
self._turn_on_light()
self._turn_on_dimmer(**kwargs)
self._turn_on_rgb_and_w("%02x%02x%02x%02x", **kwargs)
if self.gateway.optimistic:
if self.assumed_state:
self.async_write_ha_state()

View File

@@ -127,7 +127,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity):
pres = self.gateway.const.Presentation
set_req = self.gateway.const.SetReq
SENSORS[set_req.V_TEMP.name][0] = (
TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT
TEMP_CELSIUS if self.hass.config.units.is_metric else TEMP_FAHRENHEIT
)
sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None])
if isinstance(sensor_type, dict):

View File

@@ -99,7 +99,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
@property
def assumed_state(self):
"""Return True if unable to access real state of entity."""
return self.gateway.optimistic
return False
@property
def current_power_w(self):
@@ -117,7 +117,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 1, ack=1
)
if self.gateway.optimistic:
if self.assumed_state:
# Optimistically assume that switch has changed state
self._values[self.value_type] = STATE_ON
self.async_write_ha_state()
@@ -127,7 +127,7 @@ class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchEntity):
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 0, ack=1
)
if self.gateway.optimistic:
if self.assumed_state:
# Optimistically assume that switch has changed state
self._values[self.value_type] = STATE_OFF
self.async_write_ha_state()
@@ -158,7 +158,7 @@ class MySensorsIRSwitch(MySensorsSwitch):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1
)
if self.gateway.optimistic:
if self.assumed_state:
# Optimistically assume that switch has changed state
self._values[self.value_type] = self._ir_code
self._values[set_req.V_LIGHT] = STATE_ON
@@ -172,7 +172,7 @@ class MySensorsIRSwitch(MySensorsSwitch):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1
)
if self.gateway.optimistic:
if self.assumed_state:
# Optimistically assume that switch has changed state
self._values[set_req.V_LIGHT] = STATE_OFF
self.async_write_ha_state()