mirror of
https://github.com/home-assistant/core.git
synced 2026-02-23 02:30:49 +01:00
Compare commits
17 Commits
Apollon77-
...
matter_cle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d97424a22 | ||
|
|
76114c9ded | ||
|
|
0334dad2f8 | ||
|
|
2dd65172b0 | ||
|
|
4532fd379e | ||
|
|
578b2b3d43 | ||
|
|
9bb2f56fbe | ||
|
|
a7d209f1f5 | ||
|
|
83d73dce5c | ||
|
|
d84f81daf2 | ||
|
|
79d4f5c8cf | ||
|
|
e9e1abb604 | ||
|
|
9a97541253 | ||
|
|
fd39f3c431 | ||
|
|
2cc4a77746 | ||
|
|
d7ef65e562 | ||
|
|
e765c1652c |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 3
|
||||
CACHE_VERSION: 2
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.3"
|
||||
|
||||
@@ -64,6 +64,6 @@ class AtagSensor(AtagEntity, SensorEntity):
|
||||
return self.coordinator.atag.report[self._id].state
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
def icon(self):
|
||||
"""Return icon."""
|
||||
return self.coordinator.atag.report[self._id].icon
|
||||
|
||||
@@ -58,12 +58,11 @@ async def async_setup_entry(
|
||||
class GeonetnzVolcanoSensor(SensorEntity):
|
||||
"""Represents an external event with GeoNet NZ Volcano feed data."""
|
||||
|
||||
_attr_icon = DEFAULT_ICON
|
||||
_attr_native_unit_of_measurement = "alert level"
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, config_entry_id, feed_manager, external_id, unit_system):
|
||||
"""Initialize entity with data from feed entry."""
|
||||
self._config_entry_id = config_entry_id
|
||||
self._feed_manager = feed_manager
|
||||
self._external_id = external_id
|
||||
self._attr_unique_id = f"{config_entry_id}_{external_id}"
|
||||
@@ -72,6 +71,8 @@ class GeonetnzVolcanoSensor(SensorEntity):
|
||||
self._distance = None
|
||||
self._latitude = None
|
||||
self._longitude = None
|
||||
self._attribution = None
|
||||
self._alert_level = None
|
||||
self._activity = None
|
||||
self._hazards = None
|
||||
self._feed_last_update = None
|
||||
@@ -123,7 +124,7 @@ class GeonetnzVolcanoSensor(SensorEntity):
|
||||
self._latitude = round(feed_entry.coordinates[0], 5)
|
||||
self._longitude = round(feed_entry.coordinates[1], 5)
|
||||
self._attr_attribution = feed_entry.attribution
|
||||
self._attr_native_value = feed_entry.alert_level
|
||||
self._alert_level = feed_entry.alert_level
|
||||
self._activity = feed_entry.activity
|
||||
self._hazards = feed_entry.hazards
|
||||
self._feed_last_update = dt_util.as_utc(last_update) if last_update else None
|
||||
@@ -132,10 +133,25 @@ class GeonetnzVolcanoSensor(SensorEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._alert_level
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return DEFAULT_ICON
|
||||
|
||||
@property
|
||||
def name(self) -> str | None:
|
||||
"""Return the name of the entity."""
|
||||
return f"Volcano {self._title}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return "alert level"
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the device state attributes."""
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
"abort": {
|
||||
"fw_download_failed": "{firmware_name} firmware for your {model} failed to download. Make sure Home Assistant has internet access and try again.",
|
||||
"fw_install_failed": "{firmware_name} firmware failed to install, check Home Assistant logs for more information.",
|
||||
"not_hassio_thread": "The OpenThread Border Router app can only be installed with Home Assistant OS. If you would like to use the {model} as a Thread border router, please manually set up OpenThread Border Router to communicate with it.",
|
||||
"otbr_addon_already_running": "The OpenThread Border Router app is already running, it cannot be installed again.",
|
||||
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router app. If you use the Thread network, make sure you have alternative border routers. Uninstall the app and try again.",
|
||||
"unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or app is currently trying to communicate with the device. If you are running Home Assistant OS in a virtual machine or in Docker, please make sure that permissions are set correctly for the device.",
|
||||
"not_hassio_thread": "The OpenThread Border Router add-on can only be installed with Home Assistant OS. If you would like to use the {model} as a Thread border router, please manually set up OpenThread Border Router to communicate with it.",
|
||||
"otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again.",
|
||||
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.",
|
||||
"unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or add-on is currently trying to communicate with the device. If you are running Home Assistant OS in a virtual machine or in Docker, please make sure that permissions are set correctly for the device.",
|
||||
"zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again."
|
||||
},
|
||||
"progress": {
|
||||
"install_firmware": "Installing {firmware_name} firmware.\n\nDo not make any changes to your hardware or software until this finishes.",
|
||||
"install_otbr_addon": "Installing app",
|
||||
"start_otbr_addon": "Starting app"
|
||||
"install_otbr_addon": "Installing add-on",
|
||||
"start_otbr_addon": "Starting add-on"
|
||||
},
|
||||
"step": {
|
||||
"confirm_otbr": {
|
||||
@@ -34,7 +34,7 @@
|
||||
"title": "Updating adapter"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"description": "The OpenThread Border Router app installation was unsuccessful. Ensure no other software is trying to communicate with the {model}, you have access to the Internet and can install other apps, and try again. Check the Supervisor logs if the problem persists.",
|
||||
"description": "The OpenThread Border Router add-on installation was unsuccessful. Ensure no other software is trying to communicate with the {model}, you have access to the Internet and can install other add-ons, and try again. Check the Supervisor logs if the problem persists.",
|
||||
"title": "Failed to set up OpenThread Border Router"
|
||||
},
|
||||
"pick_firmware": {
|
||||
@@ -89,11 +89,11 @@
|
||||
"silabs_multiprotocol_hardware": {
|
||||
"options": {
|
||||
"abort": {
|
||||
"addon_already_running": "Failed to start the {addon_name} app because it is already running.",
|
||||
"addon_info_failed": "Failed to get {addon_name} app info.",
|
||||
"addon_install_failed": "Failed to install the {addon_name} app.",
|
||||
"addon_already_running": "Failed to start the {addon_name} add-on because it is already running.",
|
||||
"addon_info_failed": "Failed to get {addon_name} add-on info.",
|
||||
"addon_install_failed": "Failed to install the {addon_name} add-on.",
|
||||
"addon_set_config_failed": "Failed to set {addon_name} configuration.",
|
||||
"addon_start_failed": "Failed to start the {addon_name} app.",
|
||||
"addon_start_failed": "Failed to start the {addon_name} add-on.",
|
||||
"not_hassio": "The hardware options can only be configured on Home Assistant OS installations.",
|
||||
"zha_migration_failed": "The ZHA migration did not succeed."
|
||||
},
|
||||
@@ -101,8 +101,8 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "Please wait while the {addon_name} app installation finishes. This can take several minutes.",
|
||||
"start_addon": "Please wait while the {addon_name} app start completes. This may take some seconds."
|
||||
"install_addon": "Please wait while the {addon_name} add-on installation finishes. This can take several minutes.",
|
||||
"start_addon": "Please wait while the {addon_name} add-on start completes. This may take some seconds."
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
@@ -129,7 +129,7 @@
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::reconfigure_addon::title%]"
|
||||
},
|
||||
"install_addon": {
|
||||
"title": "The Silicon Labs Multiprotocol app installation has started"
|
||||
"title": "The Silicon Labs Multiprotocol add-on installation has started"
|
||||
},
|
||||
"notify_channel_change": {
|
||||
"description": "A Zigbee and Thread channel change has been initiated and will finish in {delay_minutes} minutes.",
|
||||
@@ -143,7 +143,7 @@
|
||||
"title": "Reconfigure IEEE 802.15.4 radio multiprotocol support"
|
||||
},
|
||||
"start_addon": {
|
||||
"title": "The Silicon Labs Multiprotocol app is starting."
|
||||
"title": "The Silicon Labs Multiprotocol add-on is starting."
|
||||
},
|
||||
"uninstall_addon": {
|
||||
"data": {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyhomematic import HMConnection
|
||||
import voluptuous as vol
|
||||
@@ -216,11 +215,8 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.data[DATA_CONF] = remotes = {}
|
||||
hass.data[DATA_STORE] = set()
|
||||
|
||||
interfaces: dict[str, dict[str, Any]] = conf[CONF_INTERFACES]
|
||||
hosts: dict[str, dict[str, Any]] = conf[CONF_HOSTS]
|
||||
|
||||
# Create hosts-dictionary for pyhomematic
|
||||
for rname, rconfig in interfaces.items():
|
||||
for rname, rconfig in conf[CONF_INTERFACES].items():
|
||||
remotes[rname] = {
|
||||
"ip": rconfig.get(CONF_HOST),
|
||||
"port": rconfig.get(CONF_PORT),
|
||||
@@ -236,7 +232,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"connect": True,
|
||||
}
|
||||
|
||||
for sname, sconfig in hosts.items():
|
||||
for sname, sconfig in conf[CONF_HOSTS].items():
|
||||
remotes[sname] = {
|
||||
"ip": sconfig.get(CONF_HOST),
|
||||
"port": sconfig[CONF_PORT],
|
||||
@@ -262,7 +258,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop)
|
||||
|
||||
# Init homematic hubs
|
||||
entity_hubs = [HMHub(hass, homematic, hub_name) for hub_name in hosts]
|
||||
entity_hubs = [HMHub(hass, homematic, hub_name) for hub_name in conf[CONF_HOSTS]]
|
||||
|
||||
def _hm_service_virtualkey(service: ServiceCall) -> None:
|
||||
"""Service to handle virtualkey servicecalls."""
|
||||
@@ -298,7 +294,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
def _service_handle_value(service: ServiceCall) -> None:
|
||||
"""Service to call setValue method for HomeMatic system variable."""
|
||||
entity_ids: list[str] | None = service.data.get(ATTR_ENTITY_ID)
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
name = service.data[ATTR_NAME]
|
||||
value = service.data[ATTR_VALUE]
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from pyhomematic import HMConnection
|
||||
from pyhomematic.devicetypes.generic import HMGeneric
|
||||
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
@@ -46,16 +45,15 @@ class HMDevice(Entity):
|
||||
entity_description: EntityDescription | None = None,
|
||||
) -> None:
|
||||
"""Initialize a generic HomeMatic device."""
|
||||
self._attr_name = config.get(ATTR_NAME)
|
||||
self._name = config.get(ATTR_NAME)
|
||||
self._address = config.get(ATTR_ADDRESS)
|
||||
self._interface = config.get(ATTR_INTERFACE)
|
||||
self._channel = config.get(ATTR_CHANNEL)
|
||||
self._state = config.get(ATTR_PARAM)
|
||||
if unique_id := config.get(ATTR_UNIQUE_ID):
|
||||
self._attr_unique_id = unique_id.replace(" ", "_")
|
||||
self._unique_id = config.get(ATTR_UNIQUE_ID)
|
||||
self._data: dict[str, Any] = {}
|
||||
self._connected = False
|
||||
self._attr_available = False
|
||||
self._available = False
|
||||
self._channel_map: dict[str, str] = {}
|
||||
|
||||
if entity_description is not None:
|
||||
@@ -69,6 +67,21 @@ class HMDevice(Entity):
|
||||
"""Load data init callbacks."""
|
||||
self._subscribe_homematic_events()
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID. HomeMatic entity IDs are unique by default."""
|
||||
return self._unique_id.replace(" ", "_")
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return true if device is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return device specific state attributes."""
|
||||
@@ -103,7 +116,7 @@ class HMDevice(Entity):
|
||||
self._load_data_from_hm()
|
||||
|
||||
# Link events from pyhomematic
|
||||
self._attr_available = not self._hmdevice.UNREACH
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
except Exception as err: # noqa: BLE001
|
||||
self._connected = False
|
||||
_LOGGER.error("Exception while linking %s: %s", self._address, str(err))
|
||||
@@ -119,7 +132,7 @@ class HMDevice(Entity):
|
||||
|
||||
# Availability has changed
|
||||
if self.available != (not self._hmdevice.UNREACH):
|
||||
self._attr_available = not self._hmdevice.UNREACH
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
has_changed = True
|
||||
|
||||
# If it has changed data point, update Home Assistant
|
||||
@@ -200,14 +213,14 @@ class HMHub(Entity):
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, hass: HomeAssistant, homematic: HMConnection, name: str) -> None:
|
||||
def __init__(self, hass, homematic, name):
|
||||
"""Initialize HomeMatic hub."""
|
||||
self.hass = hass
|
||||
self.entity_id = f"{DOMAIN}.{name.lower()}"
|
||||
self._homematic = homematic
|
||||
self._variables: dict[str, Any] = {}
|
||||
self._variables = {}
|
||||
self._name = name
|
||||
self._state: int | None = None
|
||||
self._state = None
|
||||
|
||||
# Load data
|
||||
track_time_interval(self.hass, self._update_hub, SCAN_INTERVAL_HUB)
|
||||
@@ -217,12 +230,12 @@ class HMHub(Entity):
|
||||
self.hass.add_job(self._update_variables, None)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self) -> int | None:
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state
|
||||
|
||||
@@ -232,7 +245,7 @@ class HMHub(Entity):
|
||||
return self._variables.copy()
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return "mdi:gradient-vertical"
|
||||
|
||||
|
||||
@@ -344,4 +344,4 @@ class HMSensor(HMDevice, SensorEntity):
|
||||
if self._state:
|
||||
self._data.update({self._state: None})
|
||||
else:
|
||||
_LOGGER.critical("Unable to initialize sensor: %s", self.name)
|
||||
_LOGGER.critical("Unable to initialize sensor: %s", self._name)
|
||||
|
||||
@@ -329,14 +329,14 @@ class IDriveE2BackupAgent(BackupAgent):
|
||||
return self._backup_cache
|
||||
|
||||
backups = {}
|
||||
paginator = self._client.get_paginator("list_objects_v2")
|
||||
metadata_files: list[dict[str, Any]] = []
|
||||
async for page in paginator.paginate(Bucket=self._bucket):
|
||||
metadata_files.extend(
|
||||
obj
|
||||
for obj in page.get("Contents", [])
|
||||
if obj["Key"].endswith(".metadata.json")
|
||||
)
|
||||
response = await cast(Any, self._client).list_objects_v2(Bucket=self._bucket)
|
||||
|
||||
# Filter for metadata files only
|
||||
metadata_files = [
|
||||
obj
|
||||
for obj in response.get("Contents", [])
|
||||
if obj["Key"].endswith(".metadata.json")
|
||||
]
|
||||
|
||||
for metadata_file in metadata_files:
|
||||
try:
|
||||
|
||||
@@ -310,7 +310,7 @@ class InputDatetime(collection.CollectionEntity, RestoreEntity):
|
||||
return self._config[CONF_HAS_TIME]
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
def icon(self):
|
||||
"""Return the icon to be used for this entity."""
|
||||
return self._config.get(CONF_ICON)
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ class InputNumber(collection.CollectionEntity, RestoreEntity):
|
||||
return self._config.get(CONF_NAME)
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
def icon(self):
|
||||
"""Return the icon to be used for this entity."""
|
||||
return self._config.get(CONF_ICON)
|
||||
|
||||
|
||||
@@ -451,7 +451,7 @@ class AirPlayDevice(MediaPlayerEntity):
|
||||
return self.device_name
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
if self.selected is True:
|
||||
return "mdi:volume-high"
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pylitterbot==2025.1.0"]
|
||||
"requirements": ["pylitterbot==2025.0.0"]
|
||||
}
|
||||
|
||||
@@ -107,20 +107,36 @@ class APIData:
|
||||
class AirSensor(SensorEntity):
|
||||
"""Single authority air sensor."""
|
||||
|
||||
_attr_icon = "mdi:cloud-outline"
|
||||
ICON = "mdi:cloud-outline"
|
||||
|
||||
def __init__(self, name, api_data):
|
||||
"""Initialize the sensor."""
|
||||
self._attr_name = self._key = name
|
||||
self._name = name
|
||||
self._api_data = api_data
|
||||
self._site_data = None
|
||||
self._state = None
|
||||
self._updated = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def site_data(self):
|
||||
"""Return the dict of sites data."""
|
||||
return self._site_data
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self.ICON
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return other details about the sensor state."""
|
||||
@@ -135,7 +151,7 @@ class AirSensor(SensorEntity):
|
||||
sites_status: list = []
|
||||
self._api_data.update()
|
||||
if self._api_data.data:
|
||||
self._site_data = self._api_data.data[self._key]
|
||||
self._site_data = self._api_data.data[self._name]
|
||||
self._updated = self._site_data[0]["updated"]
|
||||
sites_status.extend(
|
||||
site["pollutants_status"]
|
||||
@@ -144,9 +160,9 @@ class AirSensor(SensorEntity):
|
||||
)
|
||||
|
||||
if sites_status:
|
||||
self._attr_native_value = max(set(sites_status), key=sites_status.count)
|
||||
self._state = max(set(sites_status), key=sites_status.count)
|
||||
else:
|
||||
self._attr_native_value = None
|
||||
self._state = None
|
||||
|
||||
|
||||
def parse_species(species_data):
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["matter-python-client==0.4.1"],
|
||||
"requirements": ["python-matter-server==8.1.2"],
|
||||
"zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."]
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"addon_get_discovery_info_failed": "Failed to get Matter Server app discovery info.",
|
||||
"addon_info_failed": "Failed to get Matter Server app info.",
|
||||
"addon_install_failed": "Failed to install the Matter Server app.",
|
||||
"addon_start_failed": "Failed to start the Matter Server app.",
|
||||
"addon_get_discovery_info_failed": "Failed to get Matter Server add-on discovery info.",
|
||||
"addon_info_failed": "Failed to get Matter Server add-on info.",
|
||||
"addon_install_failed": "Failed to install the Matter Server add-on.",
|
||||
"addon_start_failed": "Failed to start the Matter Server add-on.",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"not_matter_addon": "Discovered app is not the official Matter Server app.",
|
||||
"not_matter_addon": "Discovered add-on is not the official Matter Server add-on.",
|
||||
"reconfiguration_successful": "Successfully reconfigured the Matter integration."
|
||||
},
|
||||
"error": {
|
||||
@@ -18,15 +18,15 @@
|
||||
},
|
||||
"flow_title": "{name}",
|
||||
"progress": {
|
||||
"install_addon": "Please wait while the Matter Server app installation finishes. This can take several minutes.",
|
||||
"start_addon": "Please wait while the Matter Server app starts. This app is what powers Matter in Home Assistant. This may take some seconds."
|
||||
"install_addon": "Please wait while the Matter Server add-on installation finishes. This can take several minutes.",
|
||||
"start_addon": "Please wait while the Matter Server add-on starts. This add-on is what powers Matter in Home Assistant. This may take some seconds."
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"title": "Set up the Matter integration with the Matter Server app"
|
||||
"title": "Set up the Matter integration with the Matter Server add-on"
|
||||
},
|
||||
"install_addon": {
|
||||
"title": "The app installation has started"
|
||||
"title": "The add-on installation has started"
|
||||
},
|
||||
"manual": {
|
||||
"data": {
|
||||
@@ -35,13 +35,13 @@
|
||||
},
|
||||
"on_supervisor": {
|
||||
"data": {
|
||||
"use_addon": "Use the official Matter Server Supervisor app"
|
||||
"use_addon": "Use the official Matter Server Supervisor add-on"
|
||||
},
|
||||
"description": "Do you want to use the official Matter Server Supervisor app?\n\nIf you are already running the Matter Server in another app, in a custom container, natively etc., then do not select this option.",
|
||||
"description": "Do you want to use the official Matter Server Supervisor add-on?\n\nIf you are already running the Matter Server in another add-on, in a custom container, natively etc., then do not select this option.",
|
||||
"title": "Select connection method"
|
||||
},
|
||||
"start_addon": {
|
||||
"title": "Starting app."
|
||||
"title": "Starting add-on."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ from chip.clusters import Objects as clusters
|
||||
from matter_server.client.models import device_types
|
||||
|
||||
from homeassistant.components.vacuum import (
|
||||
Segment,
|
||||
StateVacuumEntity,
|
||||
StateVacuumEntityDescription,
|
||||
VacuumActivity,
|
||||
@@ -70,6 +71,7 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
"""Representation of a Matter Vacuum cleaner entity."""
|
||||
|
||||
_last_accepted_commands: list[int] | None = None
|
||||
_last_service_area_feature_map: int | None = None
|
||||
_supported_run_modes: (
|
||||
dict[int, clusters.RvcRunMode.Structs.ModeOptionStruct] | None
|
||||
) = None
|
||||
@@ -136,6 +138,16 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
"No supported run mode found to start the vacuum cleaner."
|
||||
)
|
||||
|
||||
# Reset selected areas to an unconstrained selection to ensure start
|
||||
# performs a full clean and does not reuse a previous area-targeted
|
||||
# selection.
|
||||
if VacuumEntityFeature.CLEAN_AREA in self.supported_features:
|
||||
# Matter ServiceArea: an empty NewAreas list means unconstrained
|
||||
# operation (full clean).
|
||||
await self.send_device_command(
|
||||
clusters.ServiceArea.Commands.SelectAreas(newAreas=[])
|
||||
)
|
||||
|
||||
await self.send_device_command(
|
||||
clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode)
|
||||
)
|
||||
@@ -144,6 +156,66 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
"""Pause the cleaning task."""
|
||||
await self.send_device_command(clusters.RvcOperationalState.Commands.Pause())
|
||||
|
||||
@property
|
||||
def _current_segments(self) -> list[Segment]:
|
||||
"""Return the current cleanable segments reported by the device."""
|
||||
supported_areas: list[clusters.ServiceArea.Structs.AreaStruct] = (
|
||||
self.get_matter_attribute_value(
|
||||
clusters.ServiceArea.Attributes.SupportedAreas
|
||||
)
|
||||
)
|
||||
|
||||
segments: list[Segment] = []
|
||||
for area in supported_areas:
|
||||
area_name = None
|
||||
if area.areaInfo and area.areaInfo.locationInfo:
|
||||
area_name = area.areaInfo.locationInfo.locationName
|
||||
|
||||
if area_name:
|
||||
segments.append(
|
||||
Segment(
|
||||
id=str(area.areaID),
|
||||
name=area_name,
|
||||
)
|
||||
)
|
||||
|
||||
return segments
|
||||
|
||||
async def async_get_segments(self) -> list[Segment]:
|
||||
"""Get the segments that can be cleaned.
|
||||
|
||||
Returns a list of segments containing their ids and names.
|
||||
"""
|
||||
return self._current_segments
|
||||
|
||||
async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
|
||||
"""Clean the specified segments.
|
||||
|
||||
Args:
|
||||
segment_ids: List of segment IDs to clean.
|
||||
**kwargs: Additional arguments (unused).
|
||||
|
||||
"""
|
||||
# Convert string IDs to integers
|
||||
area_ids = [int(segment_id) for segment_id in segment_ids]
|
||||
|
||||
# Ensure a CLEANING run mode is available before changing device state
|
||||
mode = self._get_run_mode_by_tag(ModeTag.CLEANING)
|
||||
if mode is None:
|
||||
raise HomeAssistantError(
|
||||
"No supported run mode found to start the vacuum cleaner."
|
||||
)
|
||||
|
||||
# Send the SelectAreas command to the vacuum
|
||||
await self.send_device_command(
|
||||
clusters.ServiceArea.Commands.SelectAreas(newAreas=area_ids)
|
||||
)
|
||||
|
||||
# Start cleaning using ChangeToMode with CLEANING tag
|
||||
await self.send_device_command(
|
||||
clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
@@ -176,16 +248,34 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
state = VacuumActivity.CLEANING
|
||||
self._attr_activity = state
|
||||
|
||||
if (
|
||||
VacuumEntityFeature.CLEAN_AREA in self.supported_features
|
||||
and self.registry_entry is not None
|
||||
and (last_seen_segments := self.last_seen_segments) is not None
|
||||
and self._current_segments != last_seen_segments
|
||||
):
|
||||
self.async_create_segments_issue()
|
||||
|
||||
@callback
|
||||
def _calculate_features(self) -> None:
|
||||
"""Calculate features for HA Vacuum platform."""
|
||||
accepted_operational_commands: list[int] = self.get_matter_attribute_value(
|
||||
clusters.RvcOperationalState.Attributes.AcceptedCommandList
|
||||
)
|
||||
# in principle the feature set should not change, except for the accepted commands
|
||||
if self._last_accepted_commands == accepted_operational_commands:
|
||||
service_area_feature_map: int | None = self.get_matter_attribute_value(
|
||||
clusters.ServiceArea.Attributes.FeatureMap
|
||||
)
|
||||
|
||||
# In principle the feature set should not change, except for accepted
|
||||
# commands and service area feature map.
|
||||
if (
|
||||
self._last_accepted_commands == accepted_operational_commands
|
||||
and self._last_service_area_feature_map == service_area_feature_map
|
||||
):
|
||||
return
|
||||
|
||||
self._last_accepted_commands = accepted_operational_commands
|
||||
self._last_service_area_feature_map = service_area_feature_map
|
||||
supported_features: VacuumEntityFeature = VacuumEntityFeature(0)
|
||||
supported_features |= VacuumEntityFeature.START
|
||||
supported_features |= VacuumEntityFeature.STATE
|
||||
@@ -212,6 +302,12 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
in accepted_operational_commands
|
||||
):
|
||||
supported_features |= VacuumEntityFeature.RETURN_HOME
|
||||
# Check if Map feature is enabled for clean area support
|
||||
if (
|
||||
service_area_feature_map is not None
|
||||
and service_area_feature_map & clusters.ServiceArea.Bitmaps.Feature.kMaps
|
||||
):
|
||||
supported_features |= VacuumEntityFeature.CLEAN_AREA
|
||||
|
||||
self._attr_supported_features = supported_features
|
||||
|
||||
@@ -228,6 +324,10 @@ DISCOVERY_SCHEMAS = [
|
||||
clusters.RvcRunMode.Attributes.CurrentMode,
|
||||
clusters.RvcOperationalState.Attributes.OperationalState,
|
||||
),
|
||||
optional_attributes=(
|
||||
clusters.ServiceArea.Attributes.FeatureMap,
|
||||
clusters.ServiceArea.Attributes.SupportedAreas,
|
||||
),
|
||||
device_type=(device_types.RoboticVacuumCleaner,),
|
||||
allow_none_value=True,
|
||||
),
|
||||
|
||||
@@ -113,15 +113,35 @@ class NetdataSensor(SensorEntity):
|
||||
def __init__(self, netdata, name, sensor, sensor_name, element, icon, unit, invert):
|
||||
"""Initialize the Netdata sensor."""
|
||||
self.netdata = netdata
|
||||
self._state = None
|
||||
self._sensor = sensor
|
||||
self._element = element
|
||||
if sensor_name is None:
|
||||
sensor_name = self._sensor
|
||||
self._attr_name = f"{name} {sensor_name}"
|
||||
self._attr_icon = icon
|
||||
self._attr_native_unit_of_measurement = unit
|
||||
self._sensor_name = self._sensor if sensor_name is None else sensor_name
|
||||
self._name = name
|
||||
self._icon = icon
|
||||
self._unit_of_measurement = unit
|
||||
self._invert = invert
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._name} {self._sensor_name}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the resources."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Could the resource be accessed during the last update call."""
|
||||
@@ -131,9 +151,9 @@ class NetdataSensor(SensorEntity):
|
||||
"""Get the latest data from Netdata REST API."""
|
||||
await self.netdata.async_update()
|
||||
resource_data = self.netdata.api.metrics.get(self._sensor)
|
||||
self._attr_native_value = round(
|
||||
resource_data["dimensions"][self._element]["value"], 2
|
||||
) * (-1 if self._invert else 1)
|
||||
self._state = round(resource_data["dimensions"][self._element]["value"], 2) * (
|
||||
-1 if self._invert else 1
|
||||
)
|
||||
|
||||
|
||||
class NetdataAlarms(SensorEntity):
|
||||
@@ -142,18 +162,29 @@ class NetdataAlarms(SensorEntity):
|
||||
def __init__(self, netdata, name, host, port):
|
||||
"""Initialize the Netdata alarm sensor."""
|
||||
self.netdata = netdata
|
||||
self._attr_name = f"{name} Alarms"
|
||||
self._state = None
|
||||
self._name = name
|
||||
self._host = host
|
||||
self._port = port
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._name} Alarms"
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the resources."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Status symbol if type is symbol."""
|
||||
if self._attr_native_value == "ok":
|
||||
if self._state == "ok":
|
||||
return "mdi:check"
|
||||
if self._attr_native_value == "warning":
|
||||
if self._state == "warning":
|
||||
return "mdi:alert-outline"
|
||||
if self._attr_native_value == "critical":
|
||||
if self._state == "critical":
|
||||
return "mdi:alert"
|
||||
return "mdi:crosshairs-question"
|
||||
|
||||
@@ -166,7 +197,7 @@ class NetdataAlarms(SensorEntity):
|
||||
"""Get the latest alarms from Netdata REST API."""
|
||||
await self.netdata.async_update()
|
||||
alarms = self.netdata.api.alarms["alarms"]
|
||||
self._attr_native_value = None
|
||||
self._state = None
|
||||
number_of_alarms = len(alarms)
|
||||
number_of_relevant_alarms = number_of_alarms
|
||||
|
||||
@@ -180,9 +211,9 @@ class NetdataAlarms(SensorEntity):
|
||||
):
|
||||
number_of_relevant_alarms = number_of_relevant_alarms - 1
|
||||
elif alarms[alarm]["status"] == "CRITICAL":
|
||||
self._attr_native_value = "critical"
|
||||
self._state = "critical"
|
||||
return
|
||||
self._attr_native_value = "ok" if number_of_relevant_alarms == 0 else "warning"
|
||||
self._state = "ok" if number_of_relevant_alarms == 0 else "warning"
|
||||
|
||||
|
||||
class NetdataData:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ntfy",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiontfy"],
|
||||
"loggers": ["aionfty"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiontfy==0.8.0"]
|
||||
"requirements": ["aiontfy==0.7.0"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -62,7 +61,6 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
||||
value_fn=(
|
||||
lambda psn: psn.trophy_summary.trophy_level if psn.trophy_summary else None
|
||||
),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
PlaystationNetworkSensorEntityDescription(
|
||||
key=PlaystationNetworkSensor.TROPHY_LEVEL_PROGRESS,
|
||||
@@ -71,7 +69,6 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
||||
lambda psn: psn.trophy_summary.progress if psn.trophy_summary else None
|
||||
),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
PlaystationNetworkSensorEntityDescription(
|
||||
key=PlaystationNetworkSensor.EARNED_TROPHIES_PLATINUM,
|
||||
@@ -83,7 +80,6 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
||||
else None
|
||||
)
|
||||
),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
PlaystationNetworkSensorEntityDescription(
|
||||
key=PlaystationNetworkSensor.EARNED_TROPHIES_GOLD,
|
||||
@@ -93,7 +89,6 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
||||
psn.trophy_summary.earned_trophies.gold if psn.trophy_summary else None
|
||||
)
|
||||
),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
PlaystationNetworkSensorEntityDescription(
|
||||
key=PlaystationNetworkSensor.EARNED_TROPHIES_SILVER,
|
||||
@@ -105,7 +100,6 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
||||
else None
|
||||
)
|
||||
),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
PlaystationNetworkSensorEntityDescription(
|
||||
key=PlaystationNetworkSensor.EARNED_TROPHIES_BRONZE,
|
||||
@@ -117,7 +111,6 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
||||
else None
|
||||
)
|
||||
),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
PlaystationNetworkSensorEntityDescription(
|
||||
key=PlaystationNetworkSensor.ONLINE_ID,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyportainer==1.0.27"]
|
||||
"requirements": ["pyportainer==1.0.23"]
|
||||
}
|
||||
|
||||
@@ -99,12 +99,8 @@ def setup_platform(
|
||||
class RedditSensor(SensorEntity):
|
||||
"""Representation of a Reddit sensor."""
|
||||
|
||||
_attr_icon = "mdi:reddit"
|
||||
|
||||
def __init__(self, reddit, subreddit: str, limit: int, sort_by: str) -> None:
|
||||
"""Initialize the Reddit sensor."""
|
||||
self._attr_name = f"reddit_{subreddit}"
|
||||
self._attr_native_value = 0
|
||||
self._reddit = reddit
|
||||
self._subreddit = subreddit
|
||||
self._limit = limit
|
||||
@@ -112,6 +108,16 @@ class RedditSensor(SensorEntity):
|
||||
|
||||
self._subreddit_data: list = []
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"reddit_{self._subreddit}"
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
return len(self._subreddit_data)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
@@ -121,6 +127,11 @@ class RedditSensor(SensorEntity):
|
||||
CONF_SORT_BY: self._sort_by,
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend."""
|
||||
return "mdi:reddit"
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update data from Reddit API."""
|
||||
self._subreddit_data = []
|
||||
@@ -145,5 +156,3 @@ class RedditSensor(SensorEntity):
|
||||
|
||||
except praw.exceptions.PRAWException as err:
|
||||
_LOGGER.error("Reddit error %s", err)
|
||||
|
||||
self._attr_native_value = len(self._subreddit_data)
|
||||
|
||||
@@ -122,7 +122,6 @@ class RMVDepartureSensor(SensorEntity):
|
||||
"""Implementation of an RMV departure sensor."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -138,7 +137,7 @@ class RMVDepartureSensor(SensorEntity):
|
||||
):
|
||||
"""Initialize the sensor."""
|
||||
self._station = station
|
||||
self._attr_name = name
|
||||
self._name = name
|
||||
self._state = None
|
||||
self.data = RMVDepartureData(
|
||||
station,
|
||||
@@ -150,7 +149,12 @@ class RMVDepartureSensor(SensorEntity):
|
||||
max_journeys,
|
||||
timeout,
|
||||
)
|
||||
self._attr_icon = ICONS[None]
|
||||
self._icon = ICONS[None]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@@ -177,22 +181,32 @@ class RMVDepartureSensor(SensorEntity):
|
||||
except IndexError:
|
||||
return {}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
"""Return the unit this state is expressed in."""
|
||||
return UnitOfTime.MINUTES
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data and update the state."""
|
||||
await self.data.async_update()
|
||||
|
||||
if self._attr_name == DEFAULT_NAME:
|
||||
self._attr_name = self.data.station
|
||||
if self._name == DEFAULT_NAME:
|
||||
self._name = self.data.station
|
||||
|
||||
self._station = self.data.station
|
||||
|
||||
if not self.data.departures:
|
||||
self._state = None
|
||||
self._attr_icon = ICONS[None]
|
||||
self._icon = ICONS[None]
|
||||
return
|
||||
|
||||
self._state = self.data.departures[0].get("minutes")
|
||||
self._attr_icon = ICONS[self.data.departures[0].get("product")]
|
||||
self._icon = ICONS[self.data.departures[0].get("product")]
|
||||
|
||||
|
||||
class RMVDepartureData:
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@Lash-L"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/snoo",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["snoo"],
|
||||
"quality_scale": "bronze",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/snooz",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["pysnooz==0.8.6"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@squishykid", "@Darsstar"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/solax",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["solax"],
|
||||
"requirements": ["solax==3.2.3"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@ratsept"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/soma",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["api"],
|
||||
"requirements": ["pysoma==0.0.12"]
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/somfy_mylink",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "assumed_state",
|
||||
"loggers": ["somfy_mylink_synergy"],
|
||||
"requirements": ["somfy-mylink-synergy==1.0.6"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@ctalkington"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonarr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiopyarr"],
|
||||
"requirements": ["aiopyarr==23.4.0"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@rytilahti", "@shenxn"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/songpal",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["songpal"],
|
||||
"requirements": ["python-songpal==0.16.2"],
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@kroimon"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/soundtouch",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["libsoundtouch"],
|
||||
"requirements": ["libsoundtouch==0.8"],
|
||||
|
||||
@@ -179,42 +179,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
except ClientConnectionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
translation_placeholders={
|
||||
"host": host,
|
||||
"port": str(port),
|
||||
"error": str(err),
|
||||
},
|
||||
f"Connection error connecting to Splunk at {host}:{port}: {err}"
|
||||
) from err
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout_connect",
|
||||
translation_placeholders={"host": host, "port": str(port)},
|
||||
f"Timeout connecting to Splunk at {host}:{port}"
|
||||
) from err
|
||||
except Exception as err:
|
||||
_LOGGER.exception("Unexpected error setting up Splunk")
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unexpected_error",
|
||||
translation_placeholders={
|
||||
"host": host,
|
||||
"port": str(port),
|
||||
"error": str(err),
|
||||
},
|
||||
f"Unexpected error connecting to Splunk: {err}"
|
||||
) from err
|
||||
|
||||
if not connectivity_ok:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={"host": host, "port": str(port)},
|
||||
f"Unable to connect to Splunk instance at {host}:{port}"
|
||||
)
|
||||
if not token_ok:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
)
|
||||
raise ConfigEntryAuthFailed("Invalid Splunk token - please reauthenticate")
|
||||
|
||||
# Send startup event
|
||||
payload: dict[str, Any] = {
|
||||
|
||||
@@ -85,40 +85,6 @@ class SplunkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data=import_config,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of the Splunk integration."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
errors = await self._async_validate_input(user_input)
|
||||
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates=user_input,
|
||||
title=f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}",
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TOKEN): str,
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
vol.Optional(CONF_SSL, default=False): bool,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
}
|
||||
),
|
||||
self._get_reconfigure_entry().data,
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@Bre77"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/splunk",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["hass_splunk"],
|
||||
"quality_scale": "legacy",
|
||||
|
||||
@@ -107,12 +107,19 @@ rules:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration does not create entities.
|
||||
exception-translations: done
|
||||
exception-translations:
|
||||
status: todo
|
||||
comment: |
|
||||
Consider adding exception translations for user-facing errors beyond the current strings.json error section to provide more detailed translated error messages.
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration does not create entities.
|
||||
reconfiguration-flow: done
|
||||
reconfiguration-flow:
|
||||
status: todo
|
||||
comment: |
|
||||
Consider adding reconfiguration flow to allow users to update host, port, entity filter, and SSL settings without deleting and re-adding the config entry.
|
||||
|
||||
# Platinum
|
||||
async-dependency:
|
||||
status: todo
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_config": "The YAML configuration is invalid and cannot be imported. Please check your configuration.yaml file.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
@@ -27,25 +26,6 @@
|
||||
"description": "The Splunk token is no longer valid. Please enter a new HTTP Event Collector token.",
|
||||
"title": "Reauthenticate Splunk"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"token": "HTTP Event Collector token",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::splunk::config::step::user::data_description::host%]",
|
||||
"name": "[%key:component::splunk::config::step::user::data_description::name%]",
|
||||
"port": "[%key:component::splunk::config::step::user::data_description::port%]",
|
||||
"ssl": "[%key:component::splunk::config::step::user::data_description::ssl%]",
|
||||
"token": "[%key:component::splunk::config::step::user::data_description::token%]",
|
||||
"verify_ssl": "[%key:component::splunk::config::step::user::data_description::verify_ssl%]"
|
||||
},
|
||||
"description": "Update your Splunk HTTP Event Collector connection settings."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
@@ -68,23 +48,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "Unable to connect to Splunk at {host}:{port}."
|
||||
},
|
||||
"connection_error": {
|
||||
"message": "Unable to connect to Splunk at {host}:{port}: {error}."
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"timeout_connect": {
|
||||
"message": "Connection to Splunk at {host}:{port} timed out."
|
||||
},
|
||||
"unexpected_error": {
|
||||
"message": "Unexpected error while connecting to Splunk at {host}:{port}: {error}."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release.\n\nWhile importing your configuration, a connection error occurred. Please correct your YAML configuration and restart Home Assistant, or remove the connection settings from your `{domain}:` configuration and configure the integration via the UI.\n\nNote: Entity filtering via YAML (`filter:`) will continue to work.",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@briglx"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/srp_energy",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["srpenergy"],
|
||||
"requirements": ["srpenergy==1.3.6"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@anonym-tsk"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/starline",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["starline"],
|
||||
"requirements": ["starline==0.1.5"]
|
||||
|
||||
@@ -120,7 +120,7 @@ class StarlineSensor(StarlineEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
if self._key == "battery":
|
||||
return icon_for_battery_level(
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@boswelja"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/starlink",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["starlink-grpc-core==1.2.3"]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/steamist",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiosteamist", "discovery30303"],
|
||||
"requirements": ["aiosteamist==1.0.1", "discovery30303==0.3.3"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@fucm", "@ThyMYthOS"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/stiebel_eltron",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pymodbus", "pystiebeleltron"],
|
||||
"requirements": ["pystiebeleltron==0.2.5"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/streamlabswater",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["streamlabswater"],
|
||||
"requirements": ["streamlabswater==1.0.1"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@G-Two"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/subaru",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["stdiomask", "subarulink"],
|
||||
"requirements": ["subarulink==0.7.15"]
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"codeowners": ["@ooii", "@jb101010-2"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/suez_water",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pysuez", "regex"],
|
||||
"quality_scale": "bronze",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/sunricher_dali",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["PySrDaliGateway==0.19.3"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@benleb", "@danielhiversen"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/surepetcare",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["rich", "surepy"],
|
||||
"requirements": ["surepy==0.9.0"]
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from swisshydrodata import SwissHydroData
|
||||
import voluptuous as vol
|
||||
@@ -67,8 +67,8 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Swiss hydrological sensor."""
|
||||
station: int = config[CONF_STATION]
|
||||
monitored_conditions: list[str] = config[CONF_MONITORED_CONDITIONS]
|
||||
station = config[CONF_STATION]
|
||||
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
|
||||
|
||||
hydro_data = HydrologicalData(station)
|
||||
hydro_data.update()
|
||||
@@ -93,24 +93,38 @@ class SwissHydrologicalDataSensor(SensorEntity):
|
||||
"Data provided by the Swiss Federal Office for the Environment FOEN"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, hydro_data: HydrologicalData, station: int, condition: str
|
||||
) -> None:
|
||||
def __init__(self, hydro_data, station, condition):
|
||||
"""Initialize the Swiss hydrological sensor."""
|
||||
self.hydro_data = hydro_data
|
||||
data = hydro_data.data
|
||||
if TYPE_CHECKING:
|
||||
# Setup will fail in setup_platform if the data is None.
|
||||
assert data is not None
|
||||
|
||||
self._condition = condition
|
||||
self._data: dict[str, Any] | None = data
|
||||
self._attr_icon = CONDITIONS[condition]
|
||||
self._attr_name = f"{data['water-body-name']} {condition}"
|
||||
self._attr_native_unit_of_measurement = data["parameters"][condition]["unit"]
|
||||
self._attr_unique_id = f"{station}_{condition}"
|
||||
self._data = self._state = self._unit_of_measurement = None
|
||||
self._icon = CONDITIONS[condition]
|
||||
self._station = station
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._data['water-body-name']} {self._condition}"
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, friendly identifier for this entity."""
|
||||
return f"{self._station}_{self._condition}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
if self._state is not None:
|
||||
return self.hydro_data.data["parameters"][self._condition]["unit"]
|
||||
return None
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
if isinstance(self._state, (int, float)):
|
||||
return round(self._state, 2)
|
||||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the device state attributes."""
|
||||
@@ -132,28 +146,32 @@ class SwissHydrologicalDataSensor(SensorEntity):
|
||||
|
||||
return attrs
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend."""
|
||||
return self._icon
|
||||
|
||||
def update(self) -> None:
|
||||
"""Get the latest data and update the state."""
|
||||
self.hydro_data.update()
|
||||
self._data = self.hydro_data.data
|
||||
|
||||
self._attr_native_value = None
|
||||
if self._data is not None:
|
||||
state = self._data["parameters"][self._condition]["value"]
|
||||
if isinstance(state, (int, float)):
|
||||
self._attr_native_value = round(state, 2)
|
||||
if self._data is None:
|
||||
self._state = None
|
||||
else:
|
||||
self._state = self._data["parameters"][self._condition]["value"]
|
||||
|
||||
|
||||
class HydrologicalData:
|
||||
"""The Class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, station: int) -> None:
|
||||
def __init__(self, station):
|
||||
"""Initialize the data object."""
|
||||
self.station = station
|
||||
self.data: dict[str, Any] | None = None
|
||||
self.data = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self) -> None:
|
||||
def update(self):
|
||||
"""Get the latest data."""
|
||||
|
||||
shd = SwissHydroData()
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@fabaff", "@miaucl"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/swiss_public_transport",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opendata_transport"],
|
||||
"requirements": ["python-opendata-transport==0.5.0"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@jafar-atili"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbee",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["pyswitchbee==1.8.3"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@thecode", "@YogevBokobza"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/switcher_kis",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioswitcher"],
|
||||
"quality_scale": "silver",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@zhulik"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/syncthing",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiosyncthing"],
|
||||
"requirements": ["aiosyncthing==0.7.1"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@nielstron"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/syncthru",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pysyncthru"],
|
||||
"requirements": ["PySyncThru==0.8.0", "url-normalize==2.2.1"],
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@Guy293"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tami4",
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["Tami4EdgeAPI==3.0"]
|
||||
}
|
||||
|
||||
@@ -468,7 +468,7 @@ async def _async_send_telegram_message(service: ServiceCall) -> ServiceResponse:
|
||||
targets = _build_targets(service)
|
||||
|
||||
service_responses: JsonValueType = []
|
||||
errors: list[tuple[Exception, str]] = []
|
||||
errors: list[tuple[HomeAssistantError, str]] = []
|
||||
|
||||
# invoke the service for each target
|
||||
for target_config_entry, target_chat_id, target_notify_entity_id in targets:
|
||||
@@ -495,7 +495,7 @@ async def _async_send_telegram_message(service: ServiceCall) -> ServiceResponse:
|
||||
|
||||
assert isinstance(service_responses, list)
|
||||
service_responses.extend(formatted_responses)
|
||||
except (HomeAssistantError, TelegramError) as ex:
|
||||
except HomeAssistantError as ex:
|
||||
target = target_notify_entity_id or str(target_chat_id)
|
||||
errors.append((ex, target))
|
||||
|
||||
|
||||
@@ -433,6 +433,7 @@ class TelegramNotificationService:
|
||||
async def _send_msgs(
|
||||
self,
|
||||
func_send: Callable,
|
||||
msg_error: str,
|
||||
message_tag: str | None,
|
||||
*args_msg: Any,
|
||||
context: Context | None = None,
|
||||
@@ -458,10 +459,12 @@ class TelegramNotificationService:
|
||||
|
||||
response: Message = await self._send_msg(
|
||||
func_send,
|
||||
msg_error,
|
||||
message_tag,
|
||||
chat_id,
|
||||
*args_msg,
|
||||
context=context,
|
||||
suppress_error=len(chat_ids) > 1,
|
||||
**kwargs_msg,
|
||||
)
|
||||
if response:
|
||||
@@ -472,39 +475,58 @@ class TelegramNotificationService:
|
||||
async def _send_msg(
|
||||
self,
|
||||
func_send: Callable,
|
||||
msg_error: str,
|
||||
message_tag: str | None,
|
||||
*args_msg: Any,
|
||||
context: Context | None = None,
|
||||
suppress_error: bool = False,
|
||||
**kwargs_msg: Any,
|
||||
) -> Any:
|
||||
"""Send one message."""
|
||||
out = await func_send(*args_msg, **kwargs_msg)
|
||||
if isinstance(out, Message):
|
||||
chat_id = out.chat_id
|
||||
message_id = out.message_id
|
||||
self._last_message_id[chat_id] = message_id
|
||||
_LOGGER.debug(
|
||||
"Last message ID: %s (from chat_id %s)",
|
||||
self._last_message_id,
|
||||
chat_id,
|
||||
)
|
||||
|
||||
event_data: dict[str, Any] = {
|
||||
ATTR_CHAT_ID: chat_id,
|
||||
ATTR_MESSAGEID: message_id,
|
||||
}
|
||||
if message_tag is not None:
|
||||
event_data[ATTR_MESSAGE_TAG] = message_tag
|
||||
if kwargs_msg.get(ATTR_MESSAGE_THREAD_ID) is not None:
|
||||
event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[ATTR_MESSAGE_THREAD_ID]
|
||||
|
||||
event_data["bot"] = _get_bot_info(self.bot, self.config)
|
||||
|
||||
self.hass.bus.async_fire(EVENT_TELEGRAM_SENT, event_data, context=context)
|
||||
async_dispatcher_send(
|
||||
self.hass, signal(self.bot), EVENT_TELEGRAM_SENT, event_data
|
||||
try:
|
||||
out = await func_send(*args_msg, **kwargs_msg)
|
||||
if isinstance(out, Message):
|
||||
chat_id = out.chat_id
|
||||
message_id = out.message_id
|
||||
self._last_message_id[chat_id] = message_id
|
||||
_LOGGER.debug(
|
||||
"Last message ID: %s (from chat_id %s)",
|
||||
self._last_message_id,
|
||||
chat_id,
|
||||
)
|
||||
|
||||
event_data: dict[str, Any] = {
|
||||
ATTR_CHAT_ID: chat_id,
|
||||
ATTR_MESSAGEID: message_id,
|
||||
}
|
||||
if message_tag is not None:
|
||||
event_data[ATTR_MESSAGE_TAG] = message_tag
|
||||
if kwargs_msg.get(ATTR_MESSAGE_THREAD_ID) is not None:
|
||||
event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[
|
||||
ATTR_MESSAGE_THREAD_ID
|
||||
]
|
||||
|
||||
event_data["bot"] = _get_bot_info(self.bot, self.config)
|
||||
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_TELEGRAM_SENT, event_data, context=context
|
||||
)
|
||||
async_dispatcher_send(
|
||||
self.hass, signal(self.bot), EVENT_TELEGRAM_SENT, event_data
|
||||
)
|
||||
except TelegramError as exc:
|
||||
if not suppress_error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="action_failed",
|
||||
translation_placeholders={"error": str(exc)},
|
||||
) from exc
|
||||
|
||||
_LOGGER.error(
|
||||
"%s: %s. Args: %s, kwargs: %s", msg_error, exc, args_msg, kwargs_msg
|
||||
)
|
||||
|
||||
return None
|
||||
return out
|
||||
|
||||
async def send_message(
|
||||
@@ -520,6 +542,7 @@ class TelegramNotificationService:
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
return await self._send_msgs(
|
||||
self.bot.send_message,
|
||||
"Error sending message",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
text,
|
||||
chat_id=chat_id,
|
||||
@@ -544,6 +567,7 @@ class TelegramNotificationService:
|
||||
_LOGGER.debug("Delete message %s in chat ID %s", message_id, chat_id)
|
||||
deleted: bool = await self._send_msg(
|
||||
self.bot.delete_message,
|
||||
"Error deleting message",
|
||||
None,
|
||||
chat_id,
|
||||
message_id,
|
||||
@@ -620,6 +644,7 @@ class TelegramNotificationService:
|
||||
|
||||
return await self._send_msg(
|
||||
self.bot.edit_message_media,
|
||||
"Error editing message media",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
media=media,
|
||||
chat_id=chat_id,
|
||||
@@ -653,6 +678,7 @@ class TelegramNotificationService:
|
||||
_LOGGER.debug("Editing message with ID %s", message_id or inline_message_id)
|
||||
return await self._send_msg(
|
||||
self.bot.edit_message_text,
|
||||
"Error editing text message",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
text,
|
||||
chat_id=chat_id,
|
||||
@@ -667,6 +693,7 @@ class TelegramNotificationService:
|
||||
if type_edit == SERVICE_EDIT_CAPTION:
|
||||
return await self._send_msg(
|
||||
self.bot.edit_message_caption,
|
||||
"Error editing message attributes",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
@@ -680,6 +707,7 @@ class TelegramNotificationService:
|
||||
|
||||
return await self._send_msg(
|
||||
self.bot.edit_message_reply_markup,
|
||||
"Error editing message attributes",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
@@ -707,6 +735,7 @@ class TelegramNotificationService:
|
||||
)
|
||||
await self._send_msg(
|
||||
self.bot.answer_callback_query,
|
||||
"Error sending answer callback query",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
callback_query_id,
|
||||
text=message,
|
||||
@@ -727,6 +756,7 @@ class TelegramNotificationService:
|
||||
_LOGGER.debug("Send action %s in chat ID %s", chat_action, chat_id)
|
||||
is_successful = await self._send_msg(
|
||||
self.bot.send_chat_action,
|
||||
"Error sending action",
|
||||
None,
|
||||
chat_id=chat_id,
|
||||
action=chat_action,
|
||||
@@ -761,6 +791,7 @@ class TelegramNotificationService:
|
||||
if file_type == SERVICE_SEND_PHOTO:
|
||||
return await self._send_msgs(
|
||||
self.bot.send_photo,
|
||||
"Error sending photo",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=kwargs[ATTR_CHAT_ID],
|
||||
photo=file_content,
|
||||
@@ -777,6 +808,7 @@ class TelegramNotificationService:
|
||||
if file_type == SERVICE_SEND_STICKER:
|
||||
return await self._send_msgs(
|
||||
self.bot.send_sticker,
|
||||
"Error sending sticker",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=kwargs[ATTR_CHAT_ID],
|
||||
sticker=file_content,
|
||||
@@ -791,6 +823,7 @@ class TelegramNotificationService:
|
||||
if file_type == SERVICE_SEND_VIDEO:
|
||||
return await self._send_msgs(
|
||||
self.bot.send_video,
|
||||
"Error sending video",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=kwargs[ATTR_CHAT_ID],
|
||||
video=file_content,
|
||||
@@ -807,6 +840,7 @@ class TelegramNotificationService:
|
||||
if file_type == SERVICE_SEND_DOCUMENT:
|
||||
return await self._send_msgs(
|
||||
self.bot.send_document,
|
||||
"Error sending document",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=kwargs[ATTR_CHAT_ID],
|
||||
document=file_content,
|
||||
@@ -823,6 +857,7 @@ class TelegramNotificationService:
|
||||
if file_type == SERVICE_SEND_VOICE:
|
||||
return await self._send_msgs(
|
||||
self.bot.send_voice,
|
||||
"Error sending voice",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=kwargs[ATTR_CHAT_ID],
|
||||
voice=file_content,
|
||||
@@ -838,6 +873,7 @@ class TelegramNotificationService:
|
||||
# SERVICE_SEND_ANIMATION
|
||||
return await self._send_msgs(
|
||||
self.bot.send_animation,
|
||||
"Error sending animation",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=kwargs[ATTR_CHAT_ID],
|
||||
animation=file_content,
|
||||
@@ -863,6 +899,7 @@ class TelegramNotificationService:
|
||||
if stickerid:
|
||||
return await self._send_msgs(
|
||||
self.bot.send_sticker,
|
||||
"Error sending sticker",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=kwargs[ATTR_CHAT_ID],
|
||||
sticker=stickerid,
|
||||
@@ -888,6 +925,7 @@ class TelegramNotificationService:
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
return await self._send_msgs(
|
||||
self.bot.send_location,
|
||||
"Error sending location",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=kwargs[ATTR_CHAT_ID],
|
||||
latitude=latitude,
|
||||
@@ -913,6 +951,7 @@ class TelegramNotificationService:
|
||||
openperiod = kwargs.get(ATTR_OPEN_PERIOD)
|
||||
return await self._send_msgs(
|
||||
self.bot.send_poll,
|
||||
"Error sending poll",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=kwargs[ATTR_CHAT_ID],
|
||||
question=question,
|
||||
@@ -935,7 +974,9 @@ class TelegramNotificationService:
|
||||
) -> Any:
|
||||
"""Remove bot from chat."""
|
||||
_LOGGER.debug("Leave from chat ID %s", chat_id)
|
||||
return await self._send_msg(self.bot.leave_chat, None, chat_id, context=context)
|
||||
return await self._send_msg(
|
||||
self.bot.leave_chat, "Error leaving chat", None, chat_id, context=context
|
||||
)
|
||||
|
||||
async def set_message_reaction(
|
||||
self,
|
||||
@@ -959,6 +1000,7 @@ class TelegramNotificationService:
|
||||
|
||||
await self._send_msg(
|
||||
self.bot.set_message_reaction,
|
||||
"Error setting message reaction",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id,
|
||||
message_id,
|
||||
@@ -981,6 +1023,7 @@ class TelegramNotificationService:
|
||||
directory_path = self.hass.config.path(DOMAIN)
|
||||
file: File = await self._send_msg(
|
||||
self.bot.get_file,
|
||||
"Error getting file",
|
||||
None,
|
||||
file_id=file_id,
|
||||
context=context,
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/telegram_bot",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["telegram"],
|
||||
"quality_scale": "silver",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@fredrike"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tellduslive",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["tellduslive==0.10.12"]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/tesla_wall_connector",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["tesla_wall_connector"],
|
||||
"requirements": ["tesla-wall-connector==1.1.0"]
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/thermobeacon",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["thermobeacon-ble==0.10.0"]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/thermopro",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["thermopro-ble==1.1.3"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials", "recorder"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tibber"],
|
||||
"requirements": ["pyTibber==0.35.0"]
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/tilt_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["tilt-ble==1.0.1"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@boralyl"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/todoist",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["todoist"],
|
||||
"requirements": ["todoist-api-python==3.1.0"]
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/togrill",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["togrill_bluetooth"],
|
||||
"quality_scale": "bronze",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/tolo",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["tololib"],
|
||||
"requirements": ["tololib==1.2.2"]
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/toon",
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["toonapi"],
|
||||
"requirements": ["toonapi==0.3.0"]
|
||||
|
||||
@@ -131,15 +131,34 @@ class TorqueReceiveDataView(HomeAssistantView):
|
||||
class TorqueSensor(SensorEntity):
|
||||
"""Representation of a Torque sensor."""
|
||||
|
||||
_attr_icon = "mdi:car"
|
||||
|
||||
def __init__(self, name, unit):
|
||||
"""Initialize the sensor."""
|
||||
self._attr_name = name
|
||||
self._attr_native_unit_of_measurement = unit
|
||||
self._name = name
|
||||
self._unit = unit
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the default icon of the sensor."""
|
||||
return "mdi:car"
|
||||
|
||||
@callback
|
||||
def async_on_update(self, value):
|
||||
"""Receive an update."""
|
||||
self._attr_native_value = value
|
||||
self._state = value
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@austinmroczek"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/totalconnect",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["total_connect_client"],
|
||||
"requirements": ["total-connect-client==2025.12.2"]
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"homekit": {
|
||||
"models": ["TRADFRI"]
|
||||
},
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pytradfri"],
|
||||
"requirements": ["pytradfri[async]==9.0.1"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@gjohansson-ST"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/trafikverket_camera",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pytrafikverket"],
|
||||
"requirements": ["pytrafikverket==1.1.1"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@gjohansson-ST"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/trafikverket_ferry",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pytrafikverket"],
|
||||
"requirements": ["pytrafikverket==1.1.1"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@gjohansson-ST"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/trafikverket_train",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pytrafikverket"],
|
||||
"requirements": ["pytrafikverket==1.1.1"]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"codeowners": ["@gjohansson-ST"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pytrafikverket"],
|
||||
"requirements": ["pytrafikverket==1.1.1"]
|
||||
|
||||
@@ -78,16 +78,25 @@ class TransportNSWSensor(SensorEntity):
|
||||
|
||||
_attr_attribution = "Data provided by Transport NSW"
|
||||
_attr_device_class = SensorDeviceClass.DURATION
|
||||
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, data, stop_id, name):
|
||||
"""Initialize the sensor."""
|
||||
self.data = data
|
||||
self._attr_name = name
|
||||
self._name = name
|
||||
self._stop_id = stop_id
|
||||
self._times = None
|
||||
self._attr_icon = ICONS[None]
|
||||
self._times = self._state = None
|
||||
self._icon = ICONS[None]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
@@ -104,12 +113,22 @@ class TransportNSWSensor(SensorEntity):
|
||||
}
|
||||
return None
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
"""Return the unit this state is expressed in."""
|
||||
return UnitOfTime.MINUTES
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
def update(self) -> None:
|
||||
"""Get the latest data from Transport NSW and update the states."""
|
||||
self.data.update()
|
||||
self._times = self.data.info
|
||||
self._attr_native_value = self._times[ATTR_DUE_IN]
|
||||
self._attr_icon = ICONS[self._times[ATTR_MODE]]
|
||||
self._state = self._times[ATTR_DUE_IN]
|
||||
self._icon = ICONS[self._times[ATTR_MODE]]
|
||||
|
||||
|
||||
def _get_value(value):
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
"""API for xbox bound to Home Assistant OAuth."""
|
||||
|
||||
from aiohttp import ClientError
|
||||
from httpx import AsyncClient, HTTPStatusError, RequestError
|
||||
from http import HTTPStatus
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from httpx import AsyncClient
|
||||
from pythonxbox.authentication.manager import AuthenticationManager
|
||||
from pythonxbox.authentication.models import OAuth2TokenResponse
|
||||
from pythonxbox.common.exceptions import AuthenticationException
|
||||
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
OAuth2TokenRequestReauthError,
|
||||
OAuth2TokenRequestTransientError,
|
||||
)
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
from homeassistant.util.dt import utc_from_timestamp
|
||||
|
||||
@@ -34,12 +30,16 @@ class AsyncConfigEntryAuth(AuthenticationManager):
|
||||
if not self._oauth_session.valid_token:
|
||||
try:
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
except OAuth2TokenRequestReauthError as e:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_exception",
|
||||
) from e
|
||||
except (OAuth2TokenRequestTransientError, ClientError) as e:
|
||||
except ClientResponseError as e:
|
||||
if (
|
||||
HTTPStatus.BAD_REQUEST
|
||||
<= e.status
|
||||
< HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
):
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_exception",
|
||||
) from e
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="request_exception",
|
||||
@@ -47,18 +47,7 @@ class AsyncConfigEntryAuth(AuthenticationManager):
|
||||
self.oauth = self._get_oauth_token()
|
||||
|
||||
# This will skip the OAuth refresh and only refresh User and XSTS tokens
|
||||
try:
|
||||
await super().refresh_tokens()
|
||||
except AuthenticationException as e:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_exception",
|
||||
) from e
|
||||
except (RequestError, HTTPStatusError) as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="request_exception",
|
||||
) from e
|
||||
await super().refresh_tokens()
|
||||
|
||||
def _get_oauth_token(self) -> OAuth2TokenResponse:
|
||||
tokens = {**self._oauth_session.token}
|
||||
|
||||
@@ -160,7 +160,6 @@ SENSOR_DESCRIPTIONS: tuple[XboxSensorEntityDescription, ...] = (
|
||||
key=XboxSensor.GAMER_SCORE,
|
||||
translation_key=XboxSensor.GAMER_SCORE,
|
||||
value_fn=lambda x, _: x.gamer_score,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
XboxSensorEntityDescription(
|
||||
key=XboxSensor.ACCOUNT_TIER,
|
||||
@@ -188,13 +187,11 @@ SENSOR_DESCRIPTIONS: tuple[XboxSensorEntityDescription, ...] = (
|
||||
key=XboxSensor.FOLLOWING,
|
||||
translation_key=XboxSensor.FOLLOWING,
|
||||
value_fn=lambda x, _: x.detail.following_count if x.detail else None,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
XboxSensorEntityDescription(
|
||||
key=XboxSensor.FOLLOWER,
|
||||
translation_key=XboxSensor.FOLLOWER,
|
||||
value_fn=lambda x, _: x.detail.follower_count if x.detail else None,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
XboxSensorEntityDescription(
|
||||
key=XboxSensor.NOW_PLAYING,
|
||||
@@ -207,7 +204,6 @@ SENSOR_DESCRIPTIONS: tuple[XboxSensorEntityDescription, ...] = (
|
||||
key=XboxSensor.FRIENDS,
|
||||
translation_key=XboxSensor.FRIENDS,
|
||||
value_fn=lambda x, _: x.detail.friend_count if x.detail else None,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
XboxSensorEntityDescription(
|
||||
key=XboxSensor.IN_PARTY,
|
||||
|
||||
@@ -158,7 +158,7 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity):
|
||||
super().__init__(device, name, xiaomi_hub, config_entry)
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
if self._data_key == "status":
|
||||
return "mdi:power-plug"
|
||||
|
||||
@@ -5948,7 +5948,7 @@
|
||||
"name": "Samsung Smart TV"
|
||||
},
|
||||
"syncthru": {
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Samsung SyncThru Printer"
|
||||
@@ -6411,7 +6411,7 @@
|
||||
},
|
||||
"snooz": {
|
||||
"name": "Snooz",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
@@ -6440,7 +6440,7 @@
|
||||
},
|
||||
"solax": {
|
||||
"name": "SolaX Power",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -6463,7 +6463,7 @@
|
||||
},
|
||||
"sonarr": {
|
||||
"name": "Sonarr",
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -6495,7 +6495,7 @@
|
||||
"name": "Sony Projector"
|
||||
},
|
||||
"songpal": {
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "Sony Songpal"
|
||||
@@ -6510,7 +6510,7 @@
|
||||
},
|
||||
"soundtouch": {
|
||||
"name": "Bose SoundTouch",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -6534,7 +6534,7 @@
|
||||
},
|
||||
"splunk": {
|
||||
"name": "Splunk",
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"single_config_entry": true
|
||||
@@ -6553,7 +6553,7 @@
|
||||
},
|
||||
"srp_energy": {
|
||||
"name": "SRP Energy",
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -6571,7 +6571,7 @@
|
||||
},
|
||||
"starlink": {
|
||||
"name": "Starlink",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -6595,13 +6595,13 @@
|
||||
},
|
||||
"steamist": {
|
||||
"name": "Steamist",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"stiebel_eltron": {
|
||||
"name": "STIEBEL ELTRON",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -6613,7 +6613,7 @@
|
||||
},
|
||||
"streamlabswater": {
|
||||
"name": "StreamLabs",
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -6625,7 +6625,7 @@
|
||||
},
|
||||
"suez_water": {
|
||||
"name": "Suez Water",
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -6678,7 +6678,7 @@
|
||||
},
|
||||
"swiss_public_transport": {
|
||||
"name": "Swiss public transport",
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -6729,7 +6729,7 @@
|
||||
},
|
||||
"syncthing": {
|
||||
"name": "Syncthing",
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -6801,7 +6801,7 @@
|
||||
},
|
||||
"tami4": {
|
||||
"name": "Tami4 Edge / Edge+",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -6869,7 +6869,7 @@
|
||||
"name": "Telegram"
|
||||
},
|
||||
"telegram_bot": {
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Telegram bot"
|
||||
@@ -6921,7 +6921,7 @@
|
||||
"name": "Tesla Powerwall"
|
||||
},
|
||||
"tesla_wall_connector": {
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Tesla Wall Connector"
|
||||
@@ -6959,7 +6959,7 @@
|
||||
},
|
||||
"thermobeacon": {
|
||||
"name": "ThermoBeacon",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
@@ -6970,7 +6970,7 @@
|
||||
},
|
||||
"thermopro": {
|
||||
"name": "ThermoPro",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
@@ -7040,7 +7040,7 @@
|
||||
"name": "Tilt",
|
||||
"integrations": {
|
||||
"tilt_ble": {
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "Tilt Hydrometer BLE"
|
||||
@@ -7066,19 +7066,19 @@
|
||||
},
|
||||
"todoist": {
|
||||
"name": "Todoist",
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"togrill": {
|
||||
"name": "ToGrill",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"tolo": {
|
||||
"name": "TOLO Sauna",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -7096,7 +7096,7 @@
|
||||
},
|
||||
"toon": {
|
||||
"name": "Toon",
|
||||
"integration_type": "device",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
@@ -7171,25 +7171,25 @@
|
||||
"name": "Trafikverket",
|
||||
"integrations": {
|
||||
"trafikverket_camera": {
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Trafikverket Camera"
|
||||
},
|
||||
"trafikverket_ferry": {
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Trafikverket Ferry"
|
||||
},
|
||||
"trafikverket_train": {
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Trafikverket Train"
|
||||
},
|
||||
"trafikverket_weatherstation": {
|
||||
"integration_type": "service",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Trafikverket Weather Station"
|
||||
|
||||
@@ -711,7 +711,6 @@ _ENTITY_MATCH: list[TypeHintMatch] = [
|
||||
TypeHintMatch(
|
||||
function_name="icon",
|
||||
return_type=["str", None],
|
||||
mandatory=True,
|
||||
),
|
||||
TypeHintMatch(
|
||||
function_name="entity_picture",
|
||||
|
||||
12
requirements_all.txt
generated
12
requirements_all.txt
generated
@@ -339,7 +339,7 @@ aionanoleaf2==1.0.2
|
||||
aionotion==2024.03.0
|
||||
|
||||
# homeassistant.components.ntfy
|
||||
aiontfy==0.8.0
|
||||
aiontfy==0.7.0
|
||||
|
||||
# homeassistant.components.nut
|
||||
aionut==4.3.4
|
||||
@@ -1466,9 +1466,6 @@ lxml==6.0.1
|
||||
# homeassistant.components.matrix
|
||||
matrix-nio==0.25.2
|
||||
|
||||
# homeassistant.components.matter
|
||||
matter-python-client==0.4.1
|
||||
|
||||
# homeassistant.components.maxcube
|
||||
maxcube-api==0.4.3
|
||||
|
||||
@@ -2227,7 +2224,7 @@ pyliebherrhomeapi==0.3.0
|
||||
pylitejet==0.6.3
|
||||
|
||||
# homeassistant.components.litterrobot
|
||||
pylitterbot==2025.1.0
|
||||
pylitterbot==2025.0.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.26.0
|
||||
@@ -2367,7 +2364,7 @@ pyplaato==0.0.19
|
||||
pypoint==3.0.0
|
||||
|
||||
# homeassistant.components.portainer
|
||||
pyportainer==1.0.27
|
||||
pyportainer==1.0.23
|
||||
|
||||
# homeassistant.components.probe_plus
|
||||
pyprobeplus==1.1.2
|
||||
@@ -2583,6 +2580,9 @@ python-kasa[speedups]==0.10.2
|
||||
# homeassistant.components.linkplay
|
||||
python-linkplay==0.2.12
|
||||
|
||||
# homeassistant.components.matter
|
||||
python-matter-server==8.1.2
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
python-melcloud==0.1.2
|
||||
|
||||
|
||||
12
requirements_test_all.txt
generated
12
requirements_test_all.txt
generated
@@ -324,7 +324,7 @@ aionanoleaf2==1.0.2
|
||||
aionotion==2024.03.0
|
||||
|
||||
# homeassistant.components.ntfy
|
||||
aiontfy==0.8.0
|
||||
aiontfy==0.7.0
|
||||
|
||||
# homeassistant.components.nut
|
||||
aionut==4.3.4
|
||||
@@ -1282,9 +1282,6 @@ lxml==6.0.1
|
||||
# homeassistant.components.matrix
|
||||
matrix-nio==0.25.2
|
||||
|
||||
# homeassistant.components.matter
|
||||
matter-python-client==0.4.1
|
||||
|
||||
# homeassistant.components.maxcube
|
||||
maxcube-api==0.4.3
|
||||
|
||||
@@ -1898,7 +1895,7 @@ pyliebherrhomeapi==0.3.0
|
||||
pylitejet==0.6.3
|
||||
|
||||
# homeassistant.components.litterrobot
|
||||
pylitterbot==2025.1.0
|
||||
pylitterbot==2025.0.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.26.0
|
||||
@@ -2017,7 +2014,7 @@ pyplaato==0.0.19
|
||||
pypoint==3.0.0
|
||||
|
||||
# homeassistant.components.portainer
|
||||
pyportainer==1.0.27
|
||||
pyportainer==1.0.23
|
||||
|
||||
# homeassistant.components.probe_plus
|
||||
pyprobeplus==1.1.2
|
||||
@@ -2179,6 +2176,9 @@ python-kasa[speedups]==0.10.2
|
||||
# homeassistant.components.linkplay
|
||||
python-linkplay==0.2.12
|
||||
|
||||
# homeassistant.components.matter
|
||||
python-matter-server==8.1.2
|
||||
|
||||
# homeassistant.components.melcloud
|
||||
python-melcloud==0.1.2
|
||||
|
||||
|
||||
@@ -203,6 +203,11 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
"sense": {"sense-energy": {"async-timeout"}},
|
||||
"slimproto": {"aioslimproto": {"async-timeout"}},
|
||||
"surepetcare": {"surepy": {"async-timeout"}},
|
||||
"tami4": {
|
||||
# https://github.com/SeleniumHQ/selenium/issues/16943
|
||||
# tami4 > selenium > types*
|
||||
"selenium": {"types-certifi", "types-urllib3"},
|
||||
},
|
||||
"travisci": {
|
||||
# https://github.com/menegazzo/travispy seems to be unmaintained
|
||||
# and unused https://www.home-assistant.io/integrations/travisci
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator, Generator
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -53,11 +53,9 @@ def mock_client(agent_backup: AgentBackup) -> Generator[AsyncMock]:
|
||||
client = create_client.return_value
|
||||
|
||||
tar_file, metadata_file = suggested_filenames(agent_backup)
|
||||
# Mock the paginator for list_objects_v2
|
||||
client.get_paginator = MagicMock()
|
||||
client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
|
||||
{"Contents": [{"Key": tar_file}, {"Key": metadata_file}]}
|
||||
]
|
||||
client.list_objects_v2.return_value = {
|
||||
"Contents": [{"Key": tar_file}, {"Key": metadata_file}]
|
||||
}
|
||||
client.create_multipart_upload.return_value = {"UploadId": "upload_id"}
|
||||
client.upload_part.return_value = {"ETag": "etag"}
|
||||
client.list_buckets.return_value = {
|
||||
|
||||
@@ -179,9 +179,7 @@ async def test_agents_get_backup_does_not_throw_on_not_found(
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test agent get backup does not throw on a backup not found."""
|
||||
mock_client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
|
||||
{"Contents": []}
|
||||
]
|
||||
mock_client.list_objects_v2.return_value = {"Contents": []}
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json_auto_id({"type": "backup/details", "backup_id": "random"})
|
||||
@@ -204,20 +202,18 @@ async def test_agents_list_backups_with_corrupted_metadata(
|
||||
agent = IDriveE2BackupAgent(hass, mock_config_entry)
|
||||
|
||||
# Set up mock responses for both valid and corrupted metadata files
|
||||
mock_client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
|
||||
{
|
||||
"Contents": [
|
||||
{
|
||||
"Key": "valid_backup.metadata.json",
|
||||
"LastModified": "2023-01-01T00:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"Key": "corrupted_backup.metadata.json",
|
||||
"LastModified": "2023-01-01T00:00:00+00:00",
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
mock_client.list_objects_v2.return_value = {
|
||||
"Contents": [
|
||||
{
|
||||
"Key": "valid_backup.metadata.json",
|
||||
"LastModified": "2023-01-01T00:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"Key": "corrupted_backup.metadata.json",
|
||||
"LastModified": "2023-01-01T00:00:00+00:00",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
# Mock responses for get_object calls
|
||||
valid_metadata = json.dumps(agent_backup.as_dict())
|
||||
@@ -274,9 +270,7 @@ async def test_agents_delete_not_throwing_on_not_found(
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test agent delete backup does not throw on a backup not found."""
|
||||
mock_client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
|
||||
{"Contents": []}
|
||||
]
|
||||
mock_client.list_objects_v2.return_value = {"Contents": []}
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
@@ -290,7 +284,7 @@ async def test_agents_delete_not_throwing_on_not_found(
|
||||
|
||||
assert response["success"]
|
||||
assert response["result"] == {"agent_errors": {}}
|
||||
assert mock_client.delete_objects.call_count == 0
|
||||
assert mock_client.delete_object.call_count == 0
|
||||
|
||||
|
||||
async def test_agents_upload(
|
||||
@@ -496,27 +490,20 @@ async def test_cache_expiration(
|
||||
metadata_content = json.dumps(agent_backup.as_dict())
|
||||
mock_body = AsyncMock()
|
||||
mock_body.read.return_value = metadata_content.encode()
|
||||
mock_client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
|
||||
{
|
||||
"Contents": [
|
||||
{
|
||||
"Key": "test.metadata.json",
|
||||
"LastModified": "2023-01-01T00:00:00+00:00",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
mock_client.get_object.return_value = {"Body": mock_body}
|
||||
mock_client.list_objects_v2.return_value = {
|
||||
"Contents": [
|
||||
{"Key": "test.metadata.json", "LastModified": "2023-01-01T00:00:00+00:00"}
|
||||
]
|
||||
}
|
||||
|
||||
# First call should query IDrive e2
|
||||
await agent.async_list_backups()
|
||||
assert mock_client.get_paginator.call_count == 1
|
||||
assert mock_client.list_objects_v2.call_count == 1
|
||||
assert mock_client.get_object.call_count == 1
|
||||
|
||||
# Second call should use cache
|
||||
await agent.async_list_backups()
|
||||
assert mock_client.get_paginator.call_count == 1
|
||||
assert mock_client.list_objects_v2.call_count == 1
|
||||
assert mock_client.get_object.call_count == 1
|
||||
|
||||
# Set cache to expire
|
||||
@@ -524,7 +511,7 @@ async def test_cache_expiration(
|
||||
|
||||
# Third call should query IDrive e2 again
|
||||
await agent.async_list_backups()
|
||||
assert mock_client.get_paginator.call_count == 2
|
||||
assert mock_client.list_objects_v2.call_count == 2
|
||||
assert mock_client.get_object.call_count == 2
|
||||
|
||||
|
||||
@@ -539,88 +526,3 @@ async def test_listeners_get_cleaned_up(hass: HomeAssistant) -> None:
|
||||
remove_listener()
|
||||
|
||||
assert DATA_BACKUP_AGENT_LISTENERS not in hass.data
|
||||
|
||||
|
||||
async def test_list_backups_with_pagination(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test listing backups when paginating through multiple pages."""
|
||||
# Create agent
|
||||
agent = IDriveE2BackupAgent(hass, mock_config_entry)
|
||||
|
||||
# Create two different backups
|
||||
backup1 = AgentBackup(
|
||||
backup_id="backup1",
|
||||
date="2023-01-01T00:00:00+00:00",
|
||||
addons=[],
|
||||
database_included=False,
|
||||
extra_metadata={},
|
||||
folders=[],
|
||||
homeassistant_included=False,
|
||||
homeassistant_version=None,
|
||||
name="Backup 1",
|
||||
protected=False,
|
||||
size=0,
|
||||
)
|
||||
backup2 = AgentBackup(
|
||||
backup_id="backup2",
|
||||
date="2023-01-02T00:00:00+00:00",
|
||||
addons=[],
|
||||
database_included=False,
|
||||
extra_metadata={},
|
||||
folders=[],
|
||||
homeassistant_included=False,
|
||||
homeassistant_version=None,
|
||||
name="Backup 2",
|
||||
protected=False,
|
||||
size=0,
|
||||
)
|
||||
|
||||
# Setup two pages of results
|
||||
page1 = {
|
||||
"Contents": [
|
||||
{
|
||||
"Key": "backup1.metadata.json",
|
||||
"LastModified": "2023-01-01T00:00:00+00:00",
|
||||
},
|
||||
{"Key": "backup1.tar", "LastModified": "2023-01-01T00:00:00+00:00"},
|
||||
]
|
||||
}
|
||||
page2 = {
|
||||
"Contents": [
|
||||
{
|
||||
"Key": "backup2.metadata.json",
|
||||
"LastModified": "2023-01-02T00:00:00+00:00",
|
||||
},
|
||||
{"Key": "backup2.tar", "LastModified": "2023-01-02T00:00:00+00:00"},
|
||||
]
|
||||
}
|
||||
|
||||
# Setup mock client
|
||||
mock_client = mock_config_entry.runtime_data
|
||||
mock_client.get_paginator.return_value.paginate.return_value.__aiter__.return_value = [
|
||||
page1,
|
||||
page2,
|
||||
]
|
||||
|
||||
# Mock get_object responses based on the key
|
||||
async def mock_get_object(**kwargs):
|
||||
"""Mock get_object with different responses based on the key."""
|
||||
key = kwargs.get("Key", "")
|
||||
if "backup1" in key:
|
||||
mock_body = AsyncMock()
|
||||
mock_body.read.return_value = json.dumps(backup1.as_dict()).encode()
|
||||
return {"Body": mock_body}
|
||||
# backup2
|
||||
mock_body = AsyncMock()
|
||||
mock_body.read.return_value = json.dumps(backup2.as_dict()).encode()
|
||||
return {"Body": mock_body}
|
||||
|
||||
mock_client.get_object.side_effect = mock_get_object
|
||||
|
||||
# List backups and verify we got both
|
||||
backups = await agent.async_list_backups()
|
||||
assert len(backups) == 2
|
||||
backup_ids = {backup.backup_id for backup in backups}
|
||||
assert backup_ids == {"backup1", "backup2"}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <VacuumEntityFeature: 12828>,
|
||||
'supported_features': <VacuumEntityFeature: 29212>,
|
||||
'translation_key': 'vacuum',
|
||||
'unique_id': '00000000000004D2-000000000000002F-MatterNodeDevice-1-MatterVacuumCleaner-84-1',
|
||||
'unit_of_measurement': None,
|
||||
@@ -39,7 +39,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'ecodeebot',
|
||||
'supported_features': <VacuumEntityFeature: 12828>,
|
||||
'supported_features': <VacuumEntityFeature: 29212>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'vacuum.ecodeebot',
|
||||
@@ -79,7 +79,7 @@
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <VacuumEntityFeature: 12828>,
|
||||
'supported_features': <VacuumEntityFeature: 29212>,
|
||||
'translation_key': 'vacuum',
|
||||
'unique_id': '00000000000004D2-0000000000000028-MatterNodeDevice-1-MatterVacuumCleaner-84-1',
|
||||
'unit_of_measurement': None,
|
||||
@@ -89,7 +89,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': '2BAVS-AB6031X-44PE',
|
||||
'supported_features': <VacuumEntityFeature: 12828>,
|
||||
'supported_features': <VacuumEntityFeature: 29212>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'vacuum.2bavs_ab6031x_44pe',
|
||||
@@ -129,7 +129,7 @@
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <VacuumEntityFeature: 12316>,
|
||||
'supported_features': <VacuumEntityFeature: 28700>,
|
||||
'translation_key': 'vacuum',
|
||||
'unique_id': '00000000000004D2-0000000000000042-MatterNodeDevice-1-MatterVacuumCleaner-84-1',
|
||||
'unit_of_measurement': None,
|
||||
@@ -139,7 +139,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Vacuum',
|
||||
'supported_features': <VacuumEntityFeature: 12316>,
|
||||
'supported_features': <VacuumEntityFeature: 28700>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'vacuum.mock_vacuum',
|
||||
@@ -179,7 +179,7 @@
|
||||
'platform': 'matter',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <VacuumEntityFeature: 12316>,
|
||||
'supported_features': <VacuumEntityFeature: 28700>,
|
||||
'translation_key': 'vacuum',
|
||||
'unique_id': '00000000000004D2-0000000000000061-MatterNodeDevice-1-MatterVacuumCleaner-84-1',
|
||||
'unit_of_measurement': None,
|
||||
@@ -189,7 +189,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'K11+',
|
||||
'supported_features': <VacuumEntityFeature: 12316>,
|
||||
'supported_features': <VacuumEntityFeature: 28700>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'vacuum.k11',
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"""Test Matter vacuum."""
|
||||
|
||||
from unittest.mock import MagicMock, call
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from matter_server.client.models.node import MatterNode
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN, VacuumEntityFeature
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -71,8 +72,13 @@ async def test_vacuum_actions(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert matter_client.send_device_command.call_count == 1
|
||||
assert matter_client.send_device_command.call_args == call(
|
||||
assert matter_client.send_device_command.call_count == 2
|
||||
assert matter_client.send_device_command.call_args_list[0] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.ServiceArea.Commands.SelectAreas(newAreas=[]),
|
||||
)
|
||||
assert matter_client.send_device_command.call_args_list[1] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.RvcRunMode.Commands.ChangeToMode(newMode=1),
|
||||
@@ -289,5 +295,100 @@ async def test_vacuum_actions_no_supported_run_modes(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
component = hass.data["vacuum"]
|
||||
entity = component.get_entity(entity_id)
|
||||
assert entity is not None
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="No supported run mode found to start the vacuum cleaner",
|
||||
):
|
||||
await entity.async_clean_segments(["7"])
|
||||
|
||||
# Ensure no commands were sent to the device
|
||||
assert matter_client.send_device_command.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_vacuum_cleaner"])
|
||||
async def test_vacuum_clean_area(
|
||||
hass: HomeAssistant,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test vacuum clean_area action."""
|
||||
# Fetch translations
|
||||
await async_setup_component(hass, "homeassistant", {})
|
||||
entity_id = "vacuum.mock_vacuum"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
|
||||
# Verify CLEAN_AREA feature is supported
|
||||
# Get the entity component to access entity
|
||||
component = hass.data["vacuum"]
|
||||
entity = component.get_entity(entity_id)
|
||||
assert entity is not None
|
||||
assert VacuumEntityFeature.CLEAN_AREA in entity.supported_features
|
||||
|
||||
# Get segments from the entity
|
||||
segments = await entity.async_get_segments()
|
||||
assert len(segments) == 3
|
||||
assert segments[0].id == "7"
|
||||
assert segments[0].name == "My Location A"
|
||||
assert segments[1].id == "1234567"
|
||||
assert segments[1].name == "My Location B"
|
||||
assert segments[2].id == "2290649224"
|
||||
assert segments[2].name == "My Location C"
|
||||
|
||||
# Test clean_segments method directly
|
||||
await entity.async_clean_segments(["7", "1234567"])
|
||||
|
||||
# Verify both commands were sent: SelectAreas followed by ChangeToMode
|
||||
assert matter_client.send_device_command.call_count == 2
|
||||
|
||||
# First call: SelectAreas with the area IDs
|
||||
assert matter_client.send_device_command.call_args_list[0] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.ServiceArea.Commands.SelectAreas(newAreas=[7, 1234567]),
|
||||
)
|
||||
|
||||
# Second call: ChangeToMode to start cleaning (mode 1 is the CLEANING mode)
|
||||
assert matter_client.send_device_command.call_args_list[1] == call(
|
||||
node_id=matter_node.node_id,
|
||||
endpoint_id=1,
|
||||
command=clusters.RvcRunMode.Commands.ChangeToMode(newMode=1),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("node_fixture", ["mock_vacuum_cleaner"])
|
||||
async def test_vacuum_create_segments_issue_when_segments_changed(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
matter_client: MagicMock,
|
||||
matter_node: MatterNode,
|
||||
) -> None:
|
||||
"""Test segments issue callback is triggered when segments changed."""
|
||||
entity_id = "vacuum.mock_vacuum"
|
||||
component = hass.data["vacuum"]
|
||||
entity = component.get_entity(entity_id)
|
||||
assert entity is not None
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
entity_id,
|
||||
VACUUM_DOMAIN,
|
||||
{
|
||||
"last_seen_segments": [
|
||||
{
|
||||
"id": "7",
|
||||
"name": "Old location A",
|
||||
"group": None,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
with patch.object(entity, "async_create_segments_issue") as mock_create_issue:
|
||||
set_node_attribute(matter_node, 1, 97, 4, 0x02)
|
||||
await trigger_subscription_callback(hass, matter_client)
|
||||
|
||||
assert mock_create_issue.call_count >= 1
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -41,7 +39,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'PublicUniversalFriend Bronze trophies',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'trophies',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -57,9 +54,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -94,7 +89,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'PublicUniversalFriend Gold trophies',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'trophies',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -160,9 +154,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -197,7 +189,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'PublicUniversalFriend Next level',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -374,9 +365,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -411,7 +400,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'PublicUniversalFriend Platinum trophies',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'trophies',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -427,9 +415,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -464,7 +450,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'PublicUniversalFriend Silver trophies',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'trophies',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -480,9 +465,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -517,7 +500,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'PublicUniversalFriend Trophy level',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.publicuniversalfriend_trophy_level',
|
||||
@@ -532,9 +514,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -569,7 +549,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'testuser Bronze trophies',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'trophies',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -585,9 +564,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -622,7 +599,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'testuser Gold trophies',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'trophies',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -688,9 +664,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -725,7 +699,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'testuser Next level',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -903,9 +876,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -940,7 +911,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'testuser Platinum trophies',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'trophies',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -956,9 +926,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -993,7 +961,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'testuser Silver trophies',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'trophies',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -1009,9 +976,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -1046,7 +1011,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'testuser Trophy level',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.testuser_trophy_level',
|
||||
|
||||
@@ -224,94 +224,6 @@ async def test_import_flow_already_configured(
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
||||
|
||||
async def test_reconfigure_flow_success(
|
||||
hass: HomeAssistant, mock_hass_splunk: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test successful reconfigure flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_TOKEN: "new-token-456",
|
||||
CONF_HOST: "new-splunk.example.com",
|
||||
CONF_PORT: 9088,
|
||||
CONF_SSL: True,
|
||||
CONF_VERIFY_SSL: False,
|
||||
CONF_NAME: "Updated Splunk",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data[CONF_HOST] == "new-splunk.example.com"
|
||||
assert mock_config_entry.data[CONF_PORT] == 9088
|
||||
assert mock_config_entry.data[CONF_TOKEN] == "new-token-456"
|
||||
assert mock_config_entry.data[CONF_SSL] is True
|
||||
assert mock_config_entry.data[CONF_VERIFY_SSL] is False
|
||||
assert mock_config_entry.data[CONF_NAME] == "Updated Splunk"
|
||||
assert mock_config_entry.title == "new-splunk.example.com:9088"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error"),
|
||||
[
|
||||
([False, True], "cannot_connect"),
|
||||
([True, False], "invalid_auth"),
|
||||
(Exception("Unexpected error"), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_reconfigure_flow_error_and_recovery(
|
||||
hass: HomeAssistant,
|
||||
mock_hass_splunk: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
side_effect: list[bool] | Exception,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test reconfigure flow errors and recovery."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_hass_splunk.check.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_TOKEN: "test-token-123",
|
||||
CONF_HOST: "new-splunk.example.com",
|
||||
CONF_PORT: 8088,
|
||||
CONF_SSL: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
# Test recovery
|
||||
mock_hass_splunk.check.side_effect = None
|
||||
mock_hass_splunk.check.return_value = True
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_TOKEN: "test-token-123",
|
||||
CONF_HOST: "new-splunk.example.com",
|
||||
CONF_PORT: 8088,
|
||||
CONF_SSL: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
|
||||
|
||||
async def test_reauth_flow_success(
|
||||
hass: HomeAssistant, mock_hass_splunk: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
|
||||
@@ -41,21 +41,12 @@ async def test_setup_entry_success(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_state", "expected_error_key"),
|
||||
("side_effect", "expected_state"),
|
||||
[
|
||||
([False, False], ConfigEntryState.SETUP_RETRY, "cannot_connect"),
|
||||
(
|
||||
ClientConnectionError("Connection failed"),
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
"connection_error",
|
||||
),
|
||||
(TimeoutError(), ConfigEntryState.SETUP_RETRY, "timeout_connect"),
|
||||
(
|
||||
Exception("Unexpected error"),
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
"unexpected_error",
|
||||
),
|
||||
([True, False], ConfigEntryState.SETUP_ERROR, "invalid_auth"),
|
||||
([False, False], ConfigEntryState.SETUP_RETRY),
|
||||
(ClientConnectionError("Connection failed"), ConfigEntryState.SETUP_RETRY),
|
||||
(TimeoutError(), ConfigEntryState.SETUP_RETRY),
|
||||
([True, False], ConfigEntryState.SETUP_ERROR),
|
||||
],
|
||||
)
|
||||
async def test_setup_entry_error(
|
||||
@@ -64,7 +55,6 @@ async def test_setup_entry_error(
|
||||
mock_config_entry: MockConfigEntry,
|
||||
side_effect: Exception | list[bool],
|
||||
expected_state: ConfigEntryState,
|
||||
expected_error_key: str,
|
||||
) -> None:
|
||||
"""Test setup with various errors results in appropriate states."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
@@ -75,7 +65,6 @@ async def test_setup_entry_error(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is expected_state
|
||||
assert mock_config_entry.error_reason_translation_key == expected_error_key
|
||||
|
||||
|
||||
async def test_unload_entry(
|
||||
|
||||
@@ -353,7 +353,7 @@ async def test_send_sticker_partial_error(
|
||||
assert mock_send_sticker.call_count == 2
|
||||
assert err.value.translation_key == "multiple_errors"
|
||||
assert err.value.translation_placeholders == {
|
||||
"errors": "`entity_id` notify.mock_title_mock_chat_1: mock network error\n`entity_id` notify.mock_title_mock_chat_2: mock network error"
|
||||
"errors": "`entity_id` notify.mock_title_mock_chat_1: Action failed. mock network error\n`entity_id` notify.mock_title_mock_chat_2: Action failed. mock network error"
|
||||
}
|
||||
|
||||
|
||||
@@ -364,7 +364,7 @@ async def test_send_sticker_error(hass: HomeAssistant, webhook_bot) -> None:
|
||||
) as mock_bot:
|
||||
mock_bot.side_effect = NetworkError("mock network error")
|
||||
|
||||
with pytest.raises(TelegramError) as err:
|
||||
with pytest.raises(HomeAssistantError) as err:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_STICKER,
|
||||
@@ -377,8 +377,8 @@ async def test_send_sticker_error(hass: HomeAssistant, webhook_bot) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_bot.assert_called_once()
|
||||
assert err.typename == "NetworkError"
|
||||
assert err.value.message == "mock network error"
|
||||
assert err.value.translation_domain == DOMAIN
|
||||
assert err.value.translation_key == "action_failed"
|
||||
|
||||
|
||||
async def test_send_message_with_invalid_inline_keyboard(
|
||||
@@ -2264,7 +2264,7 @@ async def test_download_file_when_bot_failed_to_get_file(
|
||||
"homeassistant.components.telegram_bot.bot.Bot.get_file",
|
||||
AsyncMock(side_effect=TelegramError("failed to get file")),
|
||||
),
|
||||
pytest.raises(TelegramError) as err,
|
||||
pytest.raises(HomeAssistantError) as err,
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
@@ -2273,9 +2273,7 @@ async def test_download_file_when_bot_failed_to_get_file(
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert err.typename == "TelegramError"
|
||||
assert err.value.message == "failed to get file"
|
||||
assert err.value.translation_key == "action_failed"
|
||||
|
||||
|
||||
async def test_download_file_when_empty_file_path(
|
||||
|
||||
@@ -104,30 +104,6 @@ def mock_authentication_manager() -> Generator[AsyncMock]:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture(name="oauth2_session")
|
||||
def mock_oauth2_session() -> Generator[AsyncMock]:
|
||||
"""Mock OAuth2 session."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.xbox.OAuth2Session", autospec=True
|
||||
) as mock_client:
|
||||
client = mock_client.return_value
|
||||
|
||||
client.token = {
|
||||
"access_token": "1234567890",
|
||||
"expires_at": 1760697327.7298331,
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "0987654321",
|
||||
"scope": "XboxLive.signin XboxLive.offline_access",
|
||||
"service": "xbox",
|
||||
"token_type": "bearer",
|
||||
"user_id": "AAAAAAAAAAAAAAAAAAAAA",
|
||||
}
|
||||
client.valid_token = False
|
||||
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture(name="xbox_live_client")
|
||||
def mock_xbox_live_client() -> Generator[AsyncMock]:
|
||||
"""Mock xbox-webapi XboxLiveClient."""
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -41,7 +39,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'erics273 Follower',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'people',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -57,9 +54,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -94,7 +89,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'erics273 Following',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'people',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -110,9 +104,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -147,7 +139,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'erics273 Friends',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'people',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -163,9 +154,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -200,7 +189,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'erics273 Gamerscore',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'points',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -482,9 +470,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -519,7 +505,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'GSR Ae Follower',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'people',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -535,9 +520,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -572,7 +555,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'GSR Ae Following',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'people',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -588,9 +570,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -625,7 +605,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'GSR Ae Friends',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'people',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -641,9 +620,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -678,7 +655,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'GSR Ae Gamerscore',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'points',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -961,9 +937,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -998,7 +972,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Ikken Hissatsuu Follower',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'people',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -1014,9 +987,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -1051,7 +1022,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Ikken Hissatsuu Following',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'people',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -1067,9 +1037,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -1104,7 +1072,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Ikken Hissatsuu Friends',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'people',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -1120,9 +1087,7 @@
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
@@ -1157,7 +1122,6 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Ikken Hissatsuu Gamerscore',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'points',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
"""Tests for the Xbox integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aiohttp import ClientError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from httpx import ConnectTimeout, HTTPStatusError, ProtocolError, RequestError, Response
|
||||
from httpx import ConnectTimeout, HTTPStatusError, ProtocolError
|
||||
import pytest
|
||||
from pythonxbox.api.provider.smartglass.models import SmartglassConsoleList
|
||||
from pythonxbox.common.exceptions import AuthenticationException
|
||||
import respx
|
||||
|
||||
from homeassistant.components.xbox.const import DOMAIN, OAUTH2_TOKEN
|
||||
from homeassistant.components.xbox.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
OAuth2TokenRequestReauthError,
|
||||
OAuth2TokenRequestTransientError,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
@@ -90,76 +82,6 @@ async def test_config_implementation_not_available(
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("state", "exception"),
|
||||
[
|
||||
(
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
OAuth2TokenRequestReauthError(domain=DOMAIN, request_info=Mock()),
|
||||
),
|
||||
(
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
OAuth2TokenRequestTransientError(domain=DOMAIN, request_info=Mock()),
|
||||
),
|
||||
(
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
ClientError,
|
||||
),
|
||||
],
|
||||
)
|
||||
@respx.mock
|
||||
async def test_oauth_session_refresh_failure_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
state: ConfigEntryState,
|
||||
exception: Exception | type[Exception],
|
||||
oauth2_session: AsyncMock,
|
||||
) -> None:
|
||||
"""Test OAuth2 session refresh failures."""
|
||||
|
||||
oauth2_session.async_ensure_token_valid.side_effect = exception
|
||||
oauth2_session.valid_token = False
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("state", "exception"),
|
||||
[
|
||||
(
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
HTTPStatusError(
|
||||
"", request=MagicMock(), response=Response(HTTPStatus.IM_A_TEAPOT)
|
||||
),
|
||||
),
|
||||
(ConfigEntryState.SETUP_RETRY, RequestError("", request=Mock())),
|
||||
(ConfigEntryState.SETUP_ERROR, AuthenticationException),
|
||||
],
|
||||
)
|
||||
@respx.mock
|
||||
async def test_oauth_session_refresh_user_and_xsts_token_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
state: ConfigEntryState,
|
||||
exception: Exception | type[Exception],
|
||||
oauth2_session: AsyncMock,
|
||||
) -> None:
|
||||
"""Test OAuth2 user and XSTS token refresh failures."""
|
||||
oauth2_session.valid_token = True
|
||||
|
||||
respx.post(OAUTH2_TOKEN).mock(side_effect=exception)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user