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
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ homeassistant.components.skybell.*
homeassistant.components.slack.*
homeassistant.components.sleep_as_android.*
homeassistant.components.sleepiq.*
homeassistant.components.sma.*
homeassistant.components.smhi.*
homeassistant.components.smlight.*
homeassistant.components.smtp.*
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class GoogleGenerativeAITextToSpeechEntity(
"en-US",
"es-US",
"fr-FR",
"he-IL",
"hi-IN",
"id-ID",
"it-IT",
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/huawei_lte/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ rules:
reauthentication-flow: done
test-coverage:
status: todo
comment: Get percentage up there, add missing actual action press invocations in button tests' suspended state tests, rename test_switch.py to test_switch.py + make its functions receive hass as first parameter where applicable.
comment: Get percentage up there, add missing actual action press invocations in button tests' suspended state tests.

# Gold
devices: done
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/litterrobot/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"quality_scale": "bronze",
"requirements": ["pylitterbot==2024.2.6"]
"requirements": ["pylitterbot==2024.2.7"]
}
72 changes: 52 additions & 20 deletions homeassistant/components/matter/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@

LOGGER = logging.getLogger(__name__)

# Due to variances in labeling implementations, labels are vendor and product specific.
# This dictionary defines which labels to use for specific vendor/product combinations.
# The keys are vendor IDs, the values are dictionaries with product IDs as keys
# and lists of label names to use as values. If the value is None, no labels are used
VENDOR_LABELING_LIST: dict[int, dict[int, list[str] | None]] = {
4488: {259: ["position"]}, # TP-Link Dual Outdoor Plug US
4874: {105: ["orientation"]}, # Eve Energy dual Outlet US
4961: {
1: ["inovelliname", "label", "name", "button"], # Inovelli VTM31
2: ["label", "devicetype", "button"], # Inovelli VTM35
4: None, # Inovelli VTM36
16: ["label", "name", "button"], # Inovelli VTM30
},
}


def catch_matter_error[_R, **P](
func: Callable[Concatenate[MatterEntity, P], Coroutine[Any, Any, _R]],
Expand Down Expand Up @@ -112,30 +127,47 @@ def __init__(
if self._platform_translation_key and not self.translation_key:
self._attr_translation_key = self._platform_translation_key

# prefer the label attribute for the entity name
# Matter has a way for users and/or vendors to specify a name for an endpoint
# which is always preferred over a standard HA (generated) name
for attr in (
clusters.FixedLabel.Attributes.LabelList,
clusters.UserLabel.Attributes.LabelList,
):
if not (labels := self.get_matter_attribute_value(attr)):
continue
for label in labels:
if label.label not in ["Label", "Button"]:
continue
# fixed or user label found: use it
label_value: str = label.value
# in the case the label is only the label id, use it as postfix only
if label_value.isnumeric():
self._name_postfix = label_value
else:
self._attr_name = label_value
break
# Matter labels can be used to modify the entity name
# by appending the text.
if name_modifier := self._get_name_modifier():
self._name_postfix = name_modifier

# make sure to update the attributes once
self._update_from_device()

def _find_matching_labels(self) -> list[str]:
"""Find all labels for a Matter entity."""

device_info = self._endpoint.device_info
labeling_list = VENDOR_LABELING_LIST.get(device_info.vendorID, {}).get(
device_info.productID
)

# get the labels from the UserLabel and FixedLabel clusters
user_label_list: list[clusters.UserLabel.Structs.LabelStruct] = (
self.get_matter_attribute_value(clusters.UserLabel.Attributes.LabelList)
or []
)
fixed_label_list: list[clusters.FixedLabel.Structs.LabelStruct] = (
self.get_matter_attribute_value(clusters.FixedLabel.Attributes.LabelList)
or []
)

found_labels: list[str] = [
lbl.value
for label in labeling_list or []
for lbl in (*user_label_list, *fixed_label_list)
if lbl.label.lower() == label
]
return found_labels

def _get_name_modifier(self) -> str | None:
"""Get the name modifier for the entity."""

if found_labels := self._find_matching_labels():
return found_labels[0]
return None

async def async_added_to_hass(self) -> None:
"""Handle being added to Home Assistant."""
await super().async_added_to_hass()
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/matter/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ async def async_will_remove_from_hass(self) -> None:
MatterDiscoverySchema(
platform=Platform.UPDATE,
entity_description=MatterUpdateEntityDescription(
key="MatterUpdate", device_class=UpdateDeviceClass.FIRMWARE
key="MatterUpdate",
device_class=UpdateDeviceClass.FIRMWARE,
),
entity_class=MatterUpdate,
required_attributes=(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/openrgb/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["openrgb-python==0.3.5"]
"requirements": ["openrgb-python==0.3.6"]
}
12 changes: 10 additions & 2 deletions homeassistant/components/ps4/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
DEFAULT_ALIAS,
DEFAULT_NAME,
DOMAIN,
PS4_DOCS_URL,
)

CONF_MODE = "Config Mode"
Expand Down Expand Up @@ -66,7 +67,10 @@ async def async_step_user(
failed = await self.hass.async_add_executor_job(self.helper.port_bind, ports)
if failed in ports:
reason = PORT_MSG[failed]
return self.async_abort(reason=reason)
return self.async_abort(
reason=reason,
description_placeholders={"ps4_docs_url": PS4_DOCS_URL},
)
return await self.async_step_creds()

async def async_step_creds(
Expand All @@ -85,7 +89,11 @@ async def async_step_creds(
except CredentialTimeout:
errors["base"] = "credential_timeout"

return self.async_show_form(step_id="creds", errors=errors)
return self.async_show_form(
step_id="creds",
errors=errors,
description_placeholders={"ps4_docs_url": PS4_DOCS_URL},
)

async def async_step_mode(
self, user_input: dict[str, Any] | None = None
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/ps4/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
if TYPE_CHECKING:
from . import PS4Data

PS4_DOCS_URL = "https://www.home-assistant.io/components/ps4/"

ATTR_MEDIA_IMAGE_URL = "media_image_url"
CONFIG_ENTRY_VERSION = 3
DEFAULT_NAME = "PlayStation 4"
Expand Down
12 changes: 9 additions & 3 deletions homeassistant/components/ps4/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
"config": {
"step": {
"creds": {
"description": "Credentials needed. Select **Submit** and then in the PS4 2nd Screen App, refresh devices and select the **Home-Assistant** device to continue."
"description": "Credentials needed. Select **Submit** and then in the PS4 2nd Screen App, refresh devices and select the **Home-Assistant** device to continue.",
"data": {
"token": "PSN Token"
},
"data_description": {
"token": "To get your PSN token, please follow these [instructions]({ps4_docs_url})."
}
},
"mode": {
"data": {
Expand Down Expand Up @@ -35,8 +41,8 @@
"credential_error": "Error fetching credentials.",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.",
"port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info."
"port_987_bind_error": "Could not bind to port 987. Refer to the [documentation]({ps4_docs_url}) for additional info.",
"port_997_bind_error": "Could not bind to port 997. Refer to the [documentation]({ps4_docs_url}) for additional info."
}
},
"services": {
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/sma/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import logging

from pysma import SMA
from pysma import SMAWebConnect

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
Expand Down Expand Up @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SMAConfigEntry) -> bool:
protocol = "https" if entry.data[CONF_SSL] else "http"
url = f"{protocol}://{entry.data[CONF_HOST]}"

sma = SMA(
sma = SMAWebConnect(
session=async_get_clientsession(
hass=hass, verify_ssl=entry.data[CONF_VERIFY_SSL]
),
Expand Down
18 changes: 12 additions & 6 deletions homeassistant/components/sma/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
import logging
from typing import Any

import pysma
import attrs
from pysma import (
SmaAuthenticationException,
SmaConnectionException,
SmaReadException,
SMAWebConnect,
)
import voluptuous as vol
from yarl import URL

Expand Down Expand Up @@ -42,7 +48,7 @@ async def validate_input(
host = data[CONF_HOST] if data is not None else user_input[CONF_HOST]
url = URL.build(scheme=protocol, host=host)

sma = pysma.SMA(
sma = SMAWebConnect(
session, str(url), user_input[CONF_PASSWORD], group=user_input[CONF_GROUP]
)

Expand All @@ -51,7 +57,7 @@ async def validate_input(
device_info = await sma.device_info()
await sma.close_session()

return device_info
return attrs.asdict(device_info)


class SmaConfigFlow(ConfigFlow, domain=DOMAIN):
Expand Down Expand Up @@ -90,11 +96,11 @@ async def _handle_user_input(
device_info = await validate_input(
self.hass, user_input=user_input, data=self._data
)
except pysma.exceptions.SmaConnectionException:
except SmaConnectionException:
errors["base"] = "cannot_connect"
except pysma.exceptions.SmaAuthenticationException:
except SmaAuthenticationException:
errors["base"] = "invalid_auth"
except pysma.exceptions.SmaReadException:
except SmaReadException:
errors["base"] = "cannot_retrieve_device_info"
except Exception:
_LOGGER.exception("Unexpected exception")
Expand Down
17 changes: 9 additions & 8 deletions homeassistant/components/sma/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
from datetime import timedelta
import logging

from pysma import SMA
from pysma.exceptions import (
from pysma import (
SmaAuthenticationException,
SmaConnectionException,
SmaReadException,
SMAWebConnect,
)
from pysma.sensor import Sensor
from pysma.helpers import DeviceInfo
from pysma.sensor import Sensors

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL
Expand All @@ -29,8 +30,8 @@
class SMACoordinatorData:
"""Data class for SMA sensors."""

sma_device_info: dict[str, str]
sensors: list[Sensor]
sma_device_info: DeviceInfo
sensors: Sensors


class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]):
Expand All @@ -42,7 +43,7 @@ def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
sma: SMA,
sma: SMAWebConnect,
) -> None:
"""Initialize the SMA Data Update Coordinator."""
super().__init__(
Expand All @@ -57,8 +58,8 @@ def __init__(
),
)
self.sma = sma
self._sma_device_info: dict[str, str] = {}
self._sensors: list[Sensor] = []
self._sma_device_info = DeviceInfo()
self._sensors = Sensors()

async def _async_setup(self) -> None:
"""Setup the SMA Data Update Coordinator."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/sma/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/sma",
"iot_class": "local_polling",
"loggers": ["pysma"],
"requirements": ["pysma==0.7.5"]
"requirements": ["pysma==1.0.2"]
}
18 changes: 9 additions & 9 deletions homeassistant/components/sma/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

import pysma
from pysma.sensor import Sensor

from homeassistant.components.sensor import (
SensorDeviceClass,
Expand Down Expand Up @@ -859,7 +859,7 @@ def __init__(
self,
coordinator: SMADataUpdateCoordinator,
description: SensorEntityDescription | None,
pysma_sensor: pysma.sensor.Sensor,
pysma_sensor: Sensor,
entry: SMAConfigEntry,
) -> None:
"""Initialize the sensor."""
Expand All @@ -873,17 +873,17 @@ def __init__(
url = f"{protocol}://{entry.data[CONF_HOST]}"

self._sensor = pysma_sensor
self._serial = coordinator.data.sma_device_info["serial"]
self._serial = coordinator.data.sma_device_info.serial
assert entry.unique_id

self._attr_device_info = DeviceInfo(
configuration_url=url,
identifiers={(DOMAIN, entry.unique_id)},
manufacturer=coordinator.data.sma_device_info["manufacturer"],
model=coordinator.data.sma_device_info["type"],
name=coordinator.data.sma_device_info["name"],
sw_version=coordinator.data.sma_device_info["sw_version"],
serial_number=coordinator.data.sma_device_info["serial"],
manufacturer=coordinator.data.sma_device_info.manufacturer,
model=coordinator.data.sma_device_info.type,
name=coordinator.data.sma_device_info.name,
sw_version=coordinator.data.sma_device_info.sw_version,
serial_number=coordinator.data.sma_device_info.serial,
)
self._attr_unique_id = (
f"{entry.unique_id}-{pysma_sensor.key}_{pysma_sensor.key_idx}"
Expand All @@ -908,7 +908,7 @@ def available(self) -> bool:
"""Return if the device is available."""
return (
super().available
and self._serial == self.coordinator.data.sma_device_info["serial"]
and self._serial == self.coordinator.data.sma_device_info.serial
)

@property
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/package_constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ standard-telnetlib==3.13.0
typing-extensions>=4.15.0,<5.0
ulid-transform==1.5.2
urllib3>=2.0
uv==0.8.9
uv==0.9.5
voluptuous-openapi==0.1.0
voluptuous-serialize==2.7.0
voluptuous==0.15.2
Expand Down
Loading
Loading