Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions homeassistant/components/matter/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,10 @@ def _update_from_device(self) -> None:
self.current_cover_position,
)

if self._entity_info.endpoint.has_attribute(
has_tilt_position_attribute = self._entity_info.endpoint.has_attribute(
None, clusters.WindowCovering.Attributes.CurrentPositionTiltPercent100ths
):
)
if has_tilt_position_attribute:
# current tilt position is inverted in matter (100 is closed, 0 is open)
current_cover_tilt_position = self.get_matter_attribute_value(
clusters.WindowCovering.Attributes.CurrentPositionTiltPercent100ths
Expand Down Expand Up @@ -206,9 +207,17 @@ def _update_from_device(self) -> None:
commands = self.get_matter_attribute_value(
clusters.WindowCovering.Attributes.AcceptedCommandList
)
feature_map = self.get_matter_attribute_value(
clusters.WindowCovering.Attributes.FeatureMap
)
if clusters.WindowCovering.Commands.GoToLiftPercentage.command_id in commands:
supported_features |= CoverEntityFeature.SET_POSITION
if clusters.WindowCovering.Commands.GoToTiltPercentage.command_id in commands:
# Some devices report GoToTiltPercentage in AcceptedCommandList even
# without tilt support, so also require the FeatureMap Tilt bit.
if (
clusters.WindowCovering.Commands.GoToTiltPercentage.command_id in commands
and feature_map & clusters.WindowCovering.Bitmaps.Feature.kTilt
):
supported_features |= CoverEntityFeature.SET_TILT_POSITION
self._attr_supported_features = supported_features

Expand Down Expand Up @@ -271,6 +280,11 @@ def _update_from_device(self) -> None:
clusters.WindowCovering.Attributes.OperationalStatus,
clusters.WindowCovering.Attributes.Type,
clusters.WindowCovering.Attributes.CurrentPositionLiftPercent100ths,
),
# tilt is optional, not required: some devices (e.g. Shelly 2PM Gen4)
# report it as present but null instead of omitting it when tilt is
# disabled, which would otherwise fail schema matching
optional_attributes=(
clusters.WindowCovering.Attributes.CurrentPositionTiltPercent100ths,
),
),
Expand Down
18 changes: 16 additions & 2 deletions homeassistant/components/matter/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import LOGGER
from .entity import MatterEntity, MatterEntityDescription
from .helpers import MatterConfigEntry
from .models import MatterDiscoverySchema
Expand Down Expand Up @@ -117,12 +118,25 @@ def _on_matter_node_event(
"""Call on NodeEvent."""
if data.endpoint_id != self._endpoint.endpoint_id:
return

# event ids are only unique within a cluster, and an endpoint can host
# more clusters than the switch this entity was made for
if data.cluster_id != clusters.Switch.id:
return

event_type: str | None = EVENT_TYPES_MAP.get(data.event_id)
if data.event_id == clusters.Switch.Events.MultiPressComplete.event_id:
# multi press event
presses = (data.data or {}).get("totalNumberOfPressesCounted", 1)
event_type = f"multi_press_{presses}"
else:
event_type = EVENT_TYPES_MAP[data.event_id]

if event_type is None:
LOGGER.debug(
"Ignoring unknown switch event id %s for %s",
data.event_id,
self.entity_id,
)
return

if event_type not in self.event_types:
# this should not happen, but guard for bad things
Expand Down
38 changes: 25 additions & 13 deletions homeassistant/components/matter/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,14 +239,16 @@ def _get_hs_color(self) -> tuple[float, float]:

return hs_color

def _get_color_temperature(self) -> int:
def _get_color_temperature(self) -> int | None:
"""Get color temperature from matter."""

color_temp = self.get_matter_attribute_value(
clusters.ColorControl.Attributes.ColorTemperatureMireds
)

assert color_temp is not None
if color_temp is None:
LOGGER.debug("Got no color temperature for %s", self.entity_id)
return None

LOGGER.debug(
"Got color temperature %s for %s",
Expand All @@ -261,8 +263,10 @@ def _get_brightness(self) -> int | None:

level_control = self._endpoint.get_cluster(clusters.LevelControl)

# We should not get here if brightness is not supported.
assert level_control is not None
if level_control is None:
# we should not get here if brightness is not supported
LOGGER.debug("Got no level control cluster for %s", self.entity_id)
return None

LOGGER.debug(
"Got brightness %s for %s",
Expand All @@ -289,9 +293,15 @@ def _get_color_mode(self) -> ColorMode:
clusters.ColorControl.Attributes.ColorMode
)

assert color_mode is not None

ha_color_mode = COLOR_MODE_MAP[color_mode]
if (ha_color_mode := COLOR_MODE_MAP.get(color_mode)) is None:
# ColorMode is nullable and a device is free to report a value
# outside of the enum, neither of which we can map to a color
LOGGER.debug(
"Got unexpected color mode (%s) for %s",
color_mode,
self.entity_id,
)
return ColorMode.UNKNOWN

LOGGER.debug(
"Got color mode (%s) for %s",
Expand Down Expand Up @@ -419,12 +429,14 @@ def _update_from_device(self) -> None:
if self._supports_brightness:
self._attr_brightness = self._get_brightness()

if (
self._supports_color_temperature
and (color_temperature := self._get_color_temperature()) > 0
):
self._attr_color_temp_kelvin = color_util.color_temperature_mired_to_kelvin(
color_temperature
if self._supports_color_temperature:
# a device without a usable value has no color temperature to
# report, rather than the one it gave us last time
color_temperature = self._get_color_temperature()
self._attr_color_temp_kelvin = (
color_util.color_temperature_mired_to_kelvin(color_temperature)
if color_temperature
else None
)

if self._supports_color:
Expand Down
9 changes: 2 additions & 7 deletions homeassistant/components/modbus/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_SLAVE,
CONF_STRUCTURE,
CONF_UNIQUE_ID,
STATE_OFF,
Expand All @@ -40,7 +39,6 @@
CALL_TYPE_X_COILS,
CALL_TYPE_X_REGISTER_HOLDINGS,
CONF_DATA_TYPE,
CONF_DEVICE_ADDRESS,
CONF_INPUT_TYPE,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
Expand All @@ -63,7 +61,7 @@
SIGNAL_STOP_ENTITY,
DataType,
)
from .modbus import ModbusHub
from .modbus import ModbusHub, entity_unit_id


class ModbusBaseEntity(Entity):
Expand All @@ -80,10 +78,7 @@ def __init__(
"""Initialize the Modbus binary sensor."""

self._hub = hub
if (conf_slave := entry.get(CONF_SLAVE)) is not None:
self._device_address = conf_slave
else:
self._device_address = entry.get(CONF_DEVICE_ADDRESS, 1)
self._device_address = entity_unit_id(entry)
self._address = int(entry[CONF_ADDRESS])
self._input_type = entry[CONF_INPUT_TYPE]
self._scan_interval = int(entry[CONF_SCAN_INTERVAL])
Expand Down
31 changes: 31 additions & 0 deletions homeassistant/components/modbus/modbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
CONF_METHOD,
CONF_NAME,
CONF_PORT,
CONF_SLAVE,
CONF_TIMEOUT,
CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
Expand All @@ -27,6 +28,7 @@
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.typing import ConfigType

from .connection import ModbusEndpoint
from .const import (
CALL_TYPE_COIL,
CALL_TYPE_DISCRETE,
Expand All @@ -38,6 +40,7 @@
CALL_TYPE_WRITE_REGISTERS,
CONF_BAUDRATE,
CONF_BYTESIZE,
CONF_DEVICE_ADDRESS,
CONF_MSG_WAIT,
CONF_PARITY,
CONF_STOPBITS,
Expand Down Expand Up @@ -109,6 +112,13 @@
]


def entity_unit_id(entity_config: dict[str, Any]) -> int:
"""Return the unit an entity config addresses, defaulting to 1."""
if (conf_slave := entity_config.get(CONF_SLAVE)) is not None:
return int(conf_slave)
return int(entity_config.get(CONF_DEVICE_ADDRESS, 1))


async def async_modbus_setup(
hass: HomeAssistant,
config: ConfigType,
Expand Down Expand Up @@ -198,8 +208,11 @@ def __init__(self, hass: HomeAssistant, client_config: dict[str, Any]) -> None:
"timeout": client_config[CONF_TIMEOUT],
"retries": 3,
}
# The endpoint is keyed like `ModbusParams.endpoint`, so that a hub and
# a shared connection to one device can be told apart from two devices
if self._config_type == SERIAL:
# serial configuration
self.endpoint: ModbusEndpoint = ("serial", client_config[CONF_PORT])
if client_config[CONF_METHOD] == "ascii":
self._pb_params["framer"] = FramerType.ASCII
else:
Expand All @@ -215,6 +228,11 @@ def __init__(self, hass: HomeAssistant, client_config: dict[str, Any]) -> None:
else:
# network configuration
self._pb_params["host"] = client_config[CONF_HOST]
self.endpoint = (
"udp" if self._config_type == UDP else "tcp",
client_config[CONF_HOST].lower(),
client_config[CONF_PORT],
)
if self._config_type == RTUOVERTCP:
self._pb_params["framer"] = FramerType.RTU
else:
Expand All @@ -227,6 +245,19 @@ def __init__(self, hass: HomeAssistant, client_config: dict[str, Any]) -> None:
else:
self._msg_wait = 0

self.units = sorted(
{
entity_unit_id(entity_config)
for _, conf_key in PLATFORMS
for entity_config in client_config.get(conf_key, [])
}
)

@property
def connected(self) -> bool:
"""Return whether the client currently holds a link to the device."""
return self._client is not None and self._client.connected

def _log_error(self, text: str) -> None:
if text == self._last_log_error:
return
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/modbus/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,12 @@ async def _async_reload_config(call: ServiceCall) -> None:
reload_config = await async_integration_yaml_config(hass, DOMAIN)
if not reload_config:
LOGGER.debug("Modbus not present anymore")
hubs.clear()
return
LOGGER.debug("Modbus reloading")
await async_modbus_setup(hass, reload_config)
# Setup replaces the hubs only once it has new ones to replace them with
if not await async_modbus_setup(hass, reload_config):
hubs.clear()


@callback
Expand Down
22 changes: 21 additions & 1 deletion homeassistant/components/modbus/websocket_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
from homeassistant.core import HomeAssistant, callback

from .connection import async_get_connection_info
from .const import DATA_MODBUS_HUBS

TYPE_LIST_CONNECTIONS: Final = "modbus/connections/list"

SOURCE_CONFIG_ENTRY: Final = "config_entry"
SOURCE_YAML: Final = "yaml"


@callback
def async_setup(hass: HomeAssistant) -> None:
Expand All @@ -26,17 +30,33 @@ def websocket_list_connections(
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""List the connections, and which config entries hold units on each."""
"""List the connections held over config entries, then those from YAML.

The unit ids of a connection are keyed by config entry id, or by hub name
for one from YAML. A YAML hub is a link of its own rather than a hold on a
shared connection, so it is listed separately even when it addresses a
device a config entry also talks to.
"""
connection.send_result(
msg["id"],
{
"connections": [
{
"endpoint": list(info.endpoint),
"connected": info.connected,
"source": SOURCE_CONFIG_ENTRY,
"units": info.units,
}
for info in async_get_connection_info(hass)
]
+ [
{
"endpoint": list(hub.endpoint),
"connected": hub.connected,
"source": SOURCE_YAML,
"units": {name: hub.units},
}
for name, hub in hass.data.get(DATA_MODBUS_HUBS, {}).items()
]
},
)
2 changes: 1 addition & 1 deletion homeassistant/components/neopool/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["neopool_modbus"],
"quality_scale": "platinum",
"requirements": ["neopool-modbus==4.6.2"]
"requirements": ["neopool-modbus==4.7.0"]
}
1 change: 1 addition & 0 deletions homeassistant/components/script/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ async def async_will_remove_from_hass(self) -> None:


@websocket_api.websocket_command({"type": "script/config", "entity_id": str})
@websocket_api.require_admin
def websocket_config(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
Expand Down
Loading
Loading