Compare commits

..

12 Commits

Author SHA1 Message Date
Daniel Hjelseth Høyer
556440a918 Merge branch 'dev' into hv_switch 2026-02-20 12:07:28 +01:00
Daniel Hjelseth Høyer
18f1a2705e tests
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-02-20 06:44:26 +01:00
Daniel Hjelseth Høyer
2ff115f3a7 tests
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-02-19 21:52:35 +01:00
Daniel Hjelseth Høyer
901220b4a1 more tests
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-02-19 11:58:55 +01:00
Daniel Hjelseth Høyer
085127ac58 more tests
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-02-19 09:58:35 +01:00
Daniel Hjelseth Høyer
498791253b snapshot
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-02-19 08:40:51 +01:00
Daniel Hjelseth Høyer
f0059b2ffb Merge branch 'dev' into hv_switch 2026-02-19 07:09:13 +01:00
Daniel Hjelseth Høyer
1a9177b097 snapshot
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-02-19 06:42:39 +01:00
Daniel Hjelseth Høyer
7a07d0c198 batt control
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-02-17 19:45:13 +01:00
Daniel Hjelseth Høyer
937cbb9d60 batt control
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-02-17 16:03:49 +01:00
Daniel Hjelseth Høyer
c46f73aafb Homevolt switch
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-02-16 19:13:07 +01:00
Daniel Hjelseth Høyer
9dcb3c6bb0 Homevolt switch
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-02-16 19:09:11 +01:00
181 changed files with 995 additions and 2985 deletions

View File

@@ -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"

4
CODEOWNERS generated
View File

@@ -403,8 +403,8 @@ build.json @home-assistant/supervisor
/tests/components/duke_energy/ @hunterjm
/homeassistant/components/duotecno/ @cereal2nd
/tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
/homeassistant/components/dynalite/ @ziv1234
/tests/components/dynalite/ @ziv1234
/homeassistant/components/eafm/ @Jc2k

View File

@@ -75,7 +75,7 @@
"name": "Go to preset"
},
"ptz_control": {
"description": "Moves (pan/tilt) and/or zooms a PTZ camera.",
"description": "Moves (pan/tilt) and/or zoom a PTZ camera.",
"fields": {
"entity_id": {
"description": "[%key:component::amcrest::services::enable_recording::fields::entity_id::description%]",

View File

@@ -38,6 +38,7 @@ def get_app_entity_description(
translation_key="apps",
name=name_slug,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement="active installations",
value_fn=lambda data: data.apps.get(name_slug),
)
@@ -51,6 +52,7 @@ def get_core_integration_entity_description(
translation_key="core_integrations",
name=name,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement="active installations",
value_fn=lambda data: data.core_integrations.get(domain),
)
@@ -64,6 +66,7 @@ def get_custom_integration_entity_description(
translation_key="custom_integrations",
translation_placeholders={"custom_integration_domain": domain},
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement="active installations",
value_fn=lambda data: data.custom_integrations.get(domain),
)
@@ -74,6 +77,7 @@ GENERAL_SENSORS = [
translation_key="total_active_installations",
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement="active installations",
value_fn=lambda data: data.active_installations,
),
AnalyticsSensorEntityDescription(
@@ -81,6 +85,7 @@ GENERAL_SENSORS = [
translation_key="total_reports_integrations",
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement="active installations",
value_fn=lambda data: data.reports_integrations,
),
]

View File

@@ -24,23 +24,14 @@
},
"entity": {
"sensor": {
"apps": {
"unit_of_measurement": "active installations"
},
"core_integrations": {
"unit_of_measurement": "[%key:component::analytics_insights::entity::sensor::apps::unit_of_measurement%]"
},
"custom_integrations": {
"name": "{custom_integration_domain} (custom)",
"unit_of_measurement": "[%key:component::analytics_insights::entity::sensor::apps::unit_of_measurement%]"
"name": "{custom_integration_domain} (custom)"
},
"total_active_installations": {
"name": "Total active installations",
"unit_of_measurement": "[%key:component::analytics_insights::entity::sensor::apps::unit_of_measurement%]"
"name": "Total active installations"
},
"total_reports_integrations": {
"name": "Total reported integrations",
"unit_of_measurement": "[%key:component::analytics_insights::entity::sensor::apps::unit_of_measurement%]"
"name": "Total reported integrations"
}
}
},

View File

@@ -74,7 +74,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
return self._feature.is_on
@property
def brightness(self) -> int | None:
def brightness(self):
"""Return the name."""
return self._feature.brightness

View File

@@ -31,7 +31,6 @@ _LOGGER = logging.getLogger(__name__)
def _convert_image_for_editing(data: bytes) -> tuple[bytes, str]:
"""Ensure the image data is in a format accepted by OpenAI image edits."""
img: Image.Image
stream = io.BytesIO(data)
with Image.open(stream) as img:
mode = img.mode

View File

@@ -199,7 +199,7 @@ class Control4Light(Control4Entity, LightEntity):
return self.coordinator.data[self._idx][CONTROL4_NON_DIMMER_VAR] > 0
@property
def brightness(self) -> int | None:
def brightness(self):
"""Return the brightness of this light between 0..255."""
if self._is_dimmer:
for var in CONTROL4_DIMMER_VARS:

View File

@@ -132,7 +132,7 @@ class DecoraWifiLight(LightEntity):
return self._switch.serial
@property
def brightness(self) -> int:
def brightness(self):
"""Return the brightness of the dimmer switch."""
return int(self._switch.brightness * 255 / 100)

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pydoods"],
"quality_scale": "legacy",
"requirements": ["pydoods==1.0.2", "Pillow==12.1.1"]
"requirements": ["pydoods==1.0.2", "Pillow==12.0.0"]
}

View File

@@ -1,7 +1,7 @@
{
"domain": "dwd_weather_warnings",
"name": "Deutscher Wetterdienst (DWD) Weather Warnings",
"codeowners": ["@runningman84", "@stephan192"],
"codeowners": ["@runningman84", "@stephan192", "@andarotajo"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings",
"integration_type": "service",

View File

@@ -75,7 +75,7 @@ class EufyHomeLight(LightEntity):
self._attr_is_on = self._bulb.power
@property
def brightness(self) -> int:
def brightness(self):
"""Return the brightness of this light between 0..255."""
return int(self._brightness * 255 / 100)
@@ -88,7 +88,7 @@ class EufyHomeLight(LightEntity):
)
@property
def hs_color(self) -> tuple[float, float] | None:
def hs_color(self):
"""Return the color of this light."""
return self._hs

View File

@@ -72,7 +72,7 @@ class FitbitApi(ABC):
configuration = Configuration()
configuration.pool_manager = async_get_clientsession(self._hass)
configuration.access_token = token[CONF_ACCESS_TOKEN]
return await self._hass.async_add_executor_job(ApiClient, configuration)
return ApiClient(configuration)
async def async_get_user_profile(self) -> FitbitProfile:
"""Return the user profile from the API."""

View File

@@ -63,7 +63,6 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
host=self.config_entry.data[CONF_HOST],
user=self.config_entry.data[CONF_USERNAME],
password=self.config_entry.data[CONF_PASSWORD],
timeout=20,
)
try:

View File

@@ -7,7 +7,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyfritzhome"],
"requirements": ["pyfritzhome==0.6.20"],
"requirements": ["pyfritzhome==0.6.19"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:fritzbox:1"

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/generic",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["av==16.0.1", "Pillow==12.1.1"]
"requirements": ["av==16.0.1", "Pillow==12.0.0"]
}

View File

@@ -15,7 +15,7 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DISCOVERY_TIMEOUT, DOMAIN
from .const import DISCOVERY_TIMEOUT
from .coordinator import GoveeLocalApiCoordinator, GoveeLocalConfigEntry
PLATFORMS: list[Platform] = [Platform.LIGHT]
@@ -52,11 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -
_LOGGER.error("Start failed, errno: %d", ex.errno)
return False
_LOGGER.error("Port %s already in use", LISTENING_PORT)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="port_in_use",
translation_placeholders={"port": LISTENING_PORT},
) from ex
raise ConfigEntryNotReady from ex
await coordinator.async_config_entry_first_refresh()
@@ -65,9 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -
while not coordinator.devices:
await asyncio.sleep(delay=1)
except TimeoutError as ex:
raise ConfigEntryNotReady(
translation_domain=DOMAIN, translation_key="no_devices_found"
) from ex
raise ConfigEntryNotReady from ex
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -33,13 +33,5 @@
}
}
}
},
"exceptions": {
"no_devices_found": {
"message": "[%key:common::config_flow::abort::no_devices_found%]"
},
"port_in_use": {
"message": "Port {port} is already in use"
}
}
}

View File

@@ -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": {

View File

@@ -25,7 +25,7 @@
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
"read_hw_settings_error": "Failed to read hardware settings",
"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.",
"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.",
"write_hw_settings_error": "Failed to write hardware settings",
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]",
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]"

View File

@@ -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]

View File

@@ -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
@@ -200,14 +199,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 +216,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 +231,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"

View File

@@ -10,7 +10,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import HomevoltConfigEntry, HomevoltDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: HomevoltConfigEntry) -> bool:

View File

@@ -0,0 +1,67 @@
"""Shared entity helpers for Homevolt."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from typing import Any, Concatenate
from homevolt import HomevoltAuthenticationError, HomevoltConnectionError, HomevoltError
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER
from .coordinator import HomevoltDataUpdateCoordinator
class HomevoltEntity(CoordinatorEntity[HomevoltDataUpdateCoordinator]):
"""Base Homevolt entity."""
_attr_has_entity_name = True
def __init__(
self, coordinator: HomevoltDataUpdateCoordinator, device_identifier: str
) -> None:
"""Initialize the Homevolt entity."""
super().__init__(coordinator)
device_id = coordinator.data.unique_id
device_metadata = coordinator.data.device_metadata.get(device_identifier)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{device_id}_{device_identifier}")},
configuration_url=coordinator.client.base_url,
manufacturer=MANUFACTURER,
model=device_metadata.model if device_metadata else None,
name=device_metadata.name if device_metadata else None,
)
def homevolt_exception_handler[_HomevoltEntityT: HomevoltEntity, **_P](
func: Callable[Concatenate[_HomevoltEntityT, _P], Coroutine[Any, Any, Any]],
) -> Callable[Concatenate[_HomevoltEntityT, _P], Coroutine[Any, Any, None]]:
"""Decorate Homevolt calls to handle exceptions."""
async def handler(
self: _HomevoltEntityT, *args: _P.args, **kwargs: _P.kwargs
) -> None:
try:
await func(self, *args, **kwargs)
except HomevoltAuthenticationError as error:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_failed",
) from error
except HomevoltConnectionError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
translation_placeholders={"error": str(error)},
) from error
except HomevoltError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unknown_error",
translation_placeholders={"error": str(error)},
) from error
return handler

View File

@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["homevolt==0.4.4"],
"requirements": ["homevolt==0.5.0"],
"zeroconf": [
{
"name": "homevolt*",

View File

@@ -22,13 +22,11 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER
from .coordinator import HomevoltConfigEntry, HomevoltDataUpdateCoordinator
from .entity import HomevoltEntity
PARALLEL_UPDATES = 0 # Coordinator-based updates
@@ -309,11 +307,10 @@ async def async_setup_entry(
async_add_entities(entities)
class HomevoltSensor(CoordinatorEntity[HomevoltDataUpdateCoordinator], SensorEntity):
class HomevoltSensor(HomevoltEntity, SensorEntity):
"""Representation of a Homevolt sensor."""
entity_description: SensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
@@ -322,24 +319,12 @@ class HomevoltSensor(CoordinatorEntity[HomevoltDataUpdateCoordinator], SensorEnt
sensor_key: str,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
unique_id = coordinator.data.unique_id
self._attr_unique_id = f"{unique_id}_{sensor_key}"
sensor_data = coordinator.data.sensors[sensor_key]
super().__init__(coordinator, sensor_data.device_identifier)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.unique_id}_{sensor_key}"
self._sensor_key = sensor_key
device_metadata = coordinator.data.device_metadata.get(
sensor_data.device_identifier
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{unique_id}_{sensor_data.device_identifier}")},
configuration_url=coordinator.client.base_url,
manufacturer=MANUFACTURER,
model=device_metadata.model if device_metadata else None,
name=device_metadata.name if device_metadata else None,
)
@property
def available(self) -> bool:
"""Return if entity is available."""

View File

@@ -160,6 +160,22 @@
"tmin": {
"name": "Minimum temperature"
}
},
"switch": {
"local_mode": {
"name": "Local mode"
}
}
},
"exceptions": {
"auth_failed": {
"message": "[%key:common::config_flow::error::invalid_auth%]"
},
"communication_error": {
"message": "[%key:common::config_flow::error::cannot_connect%]"
},
"unknown_error": {
"message": "[%key:common::config_flow::error::unknown%]"
}
}
}

View File

@@ -0,0 +1,55 @@
"""Support for Homevolt switch entities."""
from __future__ import annotations
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import HomevoltConfigEntry, HomevoltDataUpdateCoordinator
from .entity import HomevoltEntity, homevolt_exception_handler
PARALLEL_UPDATES = 0 # Coordinator-based updates
async def async_setup_entry(
hass: HomeAssistant,
entry: HomevoltConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Homevolt switch entities."""
coordinator = entry.runtime_data
async_add_entities([HomevoltLocalModeSwitch(coordinator)])
class HomevoltLocalModeSwitch(HomevoltEntity, SwitchEntity):
"""Switch entity for Homevolt local mode."""
_attr_entity_category = EntityCategory.CONFIG
_attr_translation_key = "local_mode"
def __init__(self, coordinator: HomevoltDataUpdateCoordinator) -> None:
"""Initialize the switch entity."""
self._attr_unique_id = f"{coordinator.data.unique_id}_local_mode"
device_id = coordinator.data.unique_id
super().__init__(coordinator, f"ems_{device_id}")
@property
def is_on(self) -> bool:
"""Return true if local mode is enabled."""
return self.coordinator.client.local_mode_enabled
@homevolt_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Enable local mode."""
await self.coordinator.client.enable_local_mode()
await self.coordinator.async_request_refresh()
@homevolt_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Disable local mode."""
await self.coordinator.client.disable_local_mode()
await self.coordinator.async_request_refresh()

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["keyrings.alt", "pyicloud"],
"requirements": ["pyicloud==2.4.1"]
"requirements": ["pyicloud==2.3.0"]
}

View File

@@ -68,7 +68,7 @@ class IGloLamp(LightEntity):
return self._name
@property
def brightness(self) -> int:
def brightness(self):
"""Return the brightness of this light between 0..255."""
return int((self._lamp.state()["brightness"] / 200.0) * 255)
@@ -97,17 +97,17 @@ class IGloLamp(LightEntity):
return self._lamp.min_kelvin
@property
def hs_color(self) -> tuple[float, float]:
def hs_color(self):
"""Return the hs value."""
return color_util.color_RGB_to_hs(*self._lamp.state()["rgb"])
@property
def effect(self) -> str:
def effect(self):
"""Return the current effect."""
return self._lamp.state()["effect"]
@property
def effect_list(self) -> list[str]:
def effect_list(self):
"""Return the list of supported effects."""
return self._lamp.effect_list()

View File

@@ -1,7 +1,6 @@
"""Implementation of the lock platform."""
from datetime import timedelta
from typing import Any
from aiohttp import ClientError
from igloohome_api import (
@@ -64,7 +63,7 @@ class IgloohomeLockEntity(IgloohomeBaseEntity, LockEntity):
)
self.bridge_id = bridge_id
async def async_lock(self, **kwargs: Any) -> None:
async def async_lock(self, **kwargs):
"""Lock this lock."""
try:
await self.api.create_bridge_proxied_job(
@@ -73,7 +72,7 @@ class IgloohomeLockEntity(IgloohomeBaseEntity, LockEntity):
except (ApiException, ClientError) as err:
raise HomeAssistantError from err
async def async_unlock(self, **kwargs: Any) -> None:
async def async_unlock(self, **kwargs):
"""Unlock this lock."""
try:
await self.api.create_bridge_proxied_job(
@@ -82,7 +81,7 @@ class IgloohomeLockEntity(IgloohomeBaseEntity, LockEntity):
except (ApiException, ClientError) as err:
raise HomeAssistantError from err
async def async_open(self, **kwargs: Any) -> None:
async def async_open(self, **kwargs):
"""Open (unlatch) this lock."""
try:
await self.api.create_bridge_proxied_job(

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/image_upload",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["Pillow==12.1.1"]
"requirements": ["Pillow==12.0.0"]
}

View File

@@ -61,7 +61,7 @@ class InsteonDimmerEntity(InsteonEntity, LightEntity):
self._attr_supported_color_modes = {ColorMode.ONOFF}
@property
def brightness(self) -> int:
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._insteon_device_group.value

View File

@@ -6,7 +6,7 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any, Generic
from pylitterbot import FeederRobot, LitterRobot3, LitterRobot4, LitterRobot5, Robot
from pylitterbot import FeederRobot, LitterRobot3, LitterRobot4, Robot
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.const import EntityCategory
@@ -24,24 +24,20 @@ class RobotButtonEntityDescription(ButtonEntityDescription, Generic[_WhiskerEnti
press_fn: Callable[[_WhiskerEntityT], Coroutine[Any, Any, bool]]
ROBOT_BUTTON_MAP: dict[tuple[type[Robot], ...], RobotButtonEntityDescription] = {
(LitterRobot3, LitterRobot5): RobotButtonEntityDescription[
LitterRobot3 | LitterRobot5
](
ROBOT_BUTTON_MAP: dict[type[Robot], RobotButtonEntityDescription] = {
LitterRobot3: RobotButtonEntityDescription[LitterRobot3](
key="reset_waste_drawer",
translation_key="reset_waste_drawer",
entity_category=EntityCategory.CONFIG,
press_fn=lambda robot: robot.reset_waste_drawer(),
),
(LitterRobot4, LitterRobot5): RobotButtonEntityDescription[
LitterRobot4 | LitterRobot5
](
LitterRobot4: RobotButtonEntityDescription[LitterRobot4](
key="reset",
translation_key="reset",
entity_category=EntityCategory.CONFIG,
press_fn=lambda robot: robot.reset(),
),
(FeederRobot,): RobotButtonEntityDescription[FeederRobot](
FeederRobot: RobotButtonEntityDescription[FeederRobot](
key="give_snack",
translation_key="give_snack",
press_fn=lambda robot: robot.give_snack(),

View File

@@ -13,5 +13,5 @@
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"quality_scale": "bronze",
"requirements": ["pylitterbot==2025.1.0"]
"requirements": ["pylitterbot==2025.0.0"]
}

View File

@@ -6,7 +6,7 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any, Generic, TypeVar
from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, LitterRobot5, Robot
from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot
from pylitterbot.robot.litterrobot4 import BrightnessLevel, NightLightMode
from homeassistant.components.select import SelectEntity, SelectEntityDescription
@@ -32,11 +32,9 @@ class RobotSelectEntityDescription(
select_fn: Callable[[_WhiskerEntityT, str], Coroutine[Any, Any, bool]]
ROBOT_SELECT_MAP: dict[
type[Robot] | tuple[type[Robot], ...], tuple[RobotSelectEntityDescription, ...]
] = {
ROBOT_SELECT_MAP: dict[type[Robot], tuple[RobotSelectEntityDescription, ...]] = {
LitterRobot: (
RobotSelectEntityDescription[LitterRobot, int](
RobotSelectEntityDescription[LitterRobot, int]( # type: ignore[type-abstract] # only used for isinstance check
key="cycle_delay",
translation_key="cycle_delay",
unit_of_measurement=UnitOfTime.MINUTES,
@@ -45,8 +43,8 @@ ROBOT_SELECT_MAP: dict[
select_fn=lambda robot, opt: robot.set_wait_time(int(opt)),
),
),
(LitterRobot4, LitterRobot5): (
RobotSelectEntityDescription[LitterRobot4 | LitterRobot5, str](
LitterRobot4: (
RobotSelectEntityDescription[LitterRobot4, str](
key="globe_brightness",
translation_key="globe_brightness",
current_fn=(
@@ -63,7 +61,7 @@ ROBOT_SELECT_MAP: dict[
)
),
),
RobotSelectEntityDescription[LitterRobot4 | LitterRobot5, str](
RobotSelectEntityDescription[LitterRobot4, str](
key="globe_light",
translation_key="globe_light",
current_fn=(
@@ -80,7 +78,7 @@ ROBOT_SELECT_MAP: dict[
)
),
),
RobotSelectEntityDescription[LitterRobot4 | LitterRobot5, str](
RobotSelectEntityDescription[LitterRobot4, str](
key="panel_brightness",
translation_key="brightness_level",
current_fn=(

View File

@@ -7,7 +7,7 @@ from dataclasses import dataclass
from datetime import datetime
from typing import Any, Generic
from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, LitterRobot5, Pet, Robot
from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Pet, Robot
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -44,10 +44,8 @@ class RobotSensorEntityDescription(SensorEntityDescription, Generic[_WhiskerEnti
value_fn: Callable[[_WhiskerEntityT], float | datetime | str | None]
ROBOT_SENSOR_MAP: dict[
type[Robot] | tuple[type[Robot], ...], list[RobotSensorEntityDescription]
] = {
LitterRobot: [
ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = {
LitterRobot: [ # type: ignore[type-abstract] # only used for isinstance check
RobotSensorEntityDescription[LitterRobot](
key="waste_drawer_level",
translation_key="waste_drawer",
@@ -147,9 +145,7 @@ ROBOT_SENSOR_MAP: dict[
)
),
),
],
(LitterRobot4, LitterRobot5): [
RobotSensorEntityDescription[LitterRobot4 | LitterRobot5](
RobotSensorEntityDescription[LitterRobot4](
key="litter_level",
translation_key="litter_level",
native_unit_of_measurement=PERCENTAGE,
@@ -157,7 +153,7 @@ ROBOT_SENSOR_MAP: dict[
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda robot: robot.litter_level,
),
RobotSensorEntityDescription[LitterRobot4 | LitterRobot5](
RobotSensorEntityDescription[LitterRobot4](
key="pet_weight",
translation_key="pet_weight",
native_unit_of_measurement=UnitOfMass.POUNDS,

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["matrix_client"],
"quality_scale": "legacy",
"requirements": ["matrix-nio==0.25.2", "Pillow==12.1.1", "aiofiles==24.1.0"]
"requirements": ["matrix-nio==0.25.2", "Pillow==12.0.0", "aiofiles==24.1.0"]
}

View File

@@ -498,7 +498,6 @@ DISCOVERY_SCHEMAS = [
required_attributes=(
custom_clusters.InovelliCluster.Attributes.LEDIndicatorIntensityOff,
),
product_id=(2, 16),
),
MatterDiscoverySchema(
platform=Platform.NUMBER,
@@ -515,7 +514,6 @@ DISCOVERY_SCHEMAS = [
required_attributes=(
custom_clusters.InovelliCluster.Attributes.LEDIndicatorIntensityOn,
),
product_id=(2, 16),
),
MatterDiscoverySchema(
platform=Platform.NUMBER,

View File

@@ -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."
}
}
},

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import TYPE_CHECKING, Any
@@ -120,31 +119,6 @@ class NRGkickConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovered_name: str | None = None
self._pending_host: str | None = None
async def _async_validate_credentials(
self,
host: str,
errors: dict[str, str],
username: str | None = None,
password: str | None = None,
) -> dict[str, Any] | None:
"""Validate credentials and populate errors dict on failure."""
try:
return await validate_input(
self.hass, host, username=username, password=password
)
except NRGkickApiClientApiDisabledError:
errors["base"] = "json_api_disabled"
except NRGkickApiClientAuthenticationError:
errors["base"] = "invalid_auth"
except NRGkickApiClientInvalidResponseError:
errors["base"] = "invalid_response"
except NRGkickApiClientCommunicationError:
errors["base"] = "cannot_connect"
except NRGkickApiClientError:
_LOGGER.exception("Unexpected error")
errors["base"] = "unknown"
return None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -195,20 +169,36 @@ class NRGkickConfigFlow(ConfigFlow, domain=DOMAIN):
assert self._pending_host is not None
if user_input is not None:
if info := await self._async_validate_credentials(
self._pending_host,
errors,
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
):
username = user_input.get(CONF_USERNAME)
password = user_input.get(CONF_PASSWORD)
try:
info = await validate_input(
self.hass,
self._pending_host,
username=username,
password=password,
)
except NRGkickApiClientApiDisabledError:
errors["base"] = "json_api_disabled"
except NRGkickApiClientAuthenticationError:
errors["base"] = "invalid_auth"
except NRGkickApiClientInvalidResponseError:
errors["base"] = "invalid_response"
except NRGkickApiClientCommunicationError:
errors["base"] = "cannot_connect"
except NRGkickApiClientError:
_LOGGER.exception("Unexpected error")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(info["serial"], raise_on_progress=False)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=info["title"],
data={
CONF_HOST: self._pending_host,
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_USERNAME: username,
CONF_PASSWORD: password,
},
)
@@ -221,42 +211,6 @@ class NRGkickConfigFlow(ConfigFlow, domain=DOMAIN):
},
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle initiation of reauthentication."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauthentication."""
errors: dict[str, str] = {}
if user_input is not None:
reauth_entry = self._get_reauth_entry()
if info := await self._async_validate_credentials(
reauth_entry.data[CONF_HOST],
errors,
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
):
await self.async_set_unique_id(info["serial"], raise_on_progress=False)
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
reauth_entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(
STEP_AUTH_DATA_SCHEMA,
self._get_reauth_entry().data,
),
errors=errors,
)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
@@ -281,9 +235,8 @@ class NRGkickConfigFlow(ConfigFlow, domain=DOMAIN):
# Store discovery info for the confirmation step.
self._discovered_host = discovery_info.host
# Fallback: device_name -> model_type -> "NRGkick".
discovered_name = device_name or model_type or "NRGkick"
self._discovered_name = discovered_name
self.context["title_placeholders"] = {"name": discovered_name}
self._discovered_name = device_name or model_type or "NRGkick"
self.context["title_placeholders"] = {"name": self._discovered_name}
# If JSON API is disabled, guide the user through enabling it.
if json_api_enabled != "1":

View File

@@ -18,7 +18,7 @@ from nrgkick_api import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
@@ -65,7 +65,7 @@ class NRGkickDataUpdateCoordinator(DataUpdateCoordinator[NRGkickData]):
control = await self.api.get_control()
values = await self.api.get_values(raw=True)
except NRGkickAuthenticationError as error:
raise ConfigEntryAuthFailed(
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="authentication_error",
) from error

View File

@@ -43,7 +43,7 @@ rules:
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: done
reauthentication-flow: todo
test-coverage: done
# Gold

View File

@@ -4,9 +4,7 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"json_api_disabled": "JSON API is disabled on the device. Enable it in the NRGkick mobile app under Extended \u2192 Local API \u2192 API Variants.",
"no_serial_number": "Device does not provide a serial number",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unique_id_mismatch": "The device does not match the previous device"
"no_serial_number": "Device does not provide a serial number"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -17,17 +15,6 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"password": "[%key:component::nrgkick::config::step::user_auth::data_description::password%]",
"username": "[%key:component::nrgkick::config::step::user_auth::data_description::username%]"
},
"description": "Reauthenticate with your NRGkick device.\n\nGet your username and password in the NRGkick mobile app:\n1. Open the NRGkick mobile app \u2192 Extended \u2192 Local API\n2. Under Authentication (JSON), check or set your username and password"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"

View File

@@ -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"]
}

View File

@@ -244,7 +244,7 @@ class Luminary(LightEntity):
return self._luminary.name()
@property
def hs_color(self) -> tuple[float, float]:
def hs_color(self):
"""Return last hs color value set."""
return color_util.color_RGB_to_hs(*self._rgb_color)

View File

@@ -62,7 +62,7 @@ class PilightLight(PilightBaseDevice, LightEntity):
self._dimlevel_max = config.get(CONF_DIMLEVEL_MAX)
@property
def brightness(self) -> int | None:
def brightness(self):
"""Return the brightness."""
return self._brightness

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyportainer==1.0.28"]
"requirements": ["pyportainer==1.0.23"]
}

View File

@@ -176,7 +176,7 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
list[dict[str, Any]], list[tuple[list[dict[str, Any]], list[dict[str, Any]]]]
]:
"""Fetch all nodes, and then proceed to the VMs and containers."""
nodes = self.proxmox.nodes.get() or []
nodes = self.proxmox.nodes.get()
vms_containers = [self._get_vms_containers(node) for node in nodes]
return nodes, vms_containers

View File

@@ -4,5 +4,5 @@
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/proxy",
"quality_scale": "legacy",
"requirements": ["Pillow==12.1.1"]
"requirements": ["Pillow==12.0.0"]
}

View File

@@ -6,5 +6,5 @@
"iot_class": "calculated",
"loggers": ["pyzbar"],
"quality_scale": "legacy",
"requirements": ["Pillow==12.1.1", "pyzbar==0.1.7"]
"requirements": ["Pillow==12.0.0", "pyzbar==0.1.7"]
}

View File

@@ -30,7 +30,7 @@ class QSLight(QSToggleEntity, LightEntity):
"""Light based on a Qwikswitch relay/dimmer module."""
@property
def brightness(self) -> int | None:
def brightness(self):
"""Return the brightness of this light (0-255)."""
return self.device.value if self.device.is_dimmer else None

View File

@@ -5,7 +5,7 @@
"title": "Database backup failed due to lack of resources"
},
"maria_db_range_index_regression": {
"description": "Older versions of MariaDB suffer from a significant performance regression when retrieving history data or purging the database. Update to MariaDB version {min_version} or later and restart Home Assistant. If you are using the MariaDB Core app, make sure to update it to the latest version.",
"description": "Older versions of MariaDB suffer from a significant performance regression when retrieving history data or purging the database. Update to MariaDB version {min_version} or later and restart Home Assistant. If you are using the MariaDB core add-on, make sure to update it to the latest version.",
"title": "Update MariaDB to {min_version} or later resolve a significant performance issue"
}
},

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "silver",
"requirements": ["renault-api==0.5.5"]
"requirements": ["renault-api==0.5.3"]
}

View File

@@ -226,7 +226,7 @@ class DimmableRflinkLight(SwitchableRflinkDevice, LightEntity):
self._state = True
@property
def brightness(self) -> int:
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness

View File

@@ -144,18 +144,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
for coord in coordinators
if isinstance(coord, RoborockDataUpdateCoordinatorA01)
]
b01_q7_coords = [
b01_coords = [
coord
for coord in coordinators
if isinstance(coord, RoborockB01Q7UpdateCoordinator)
if isinstance(coord, RoborockDataUpdateCoordinatorB01)
]
if len(v1_coords) + len(a01_coords) + len(b01_q7_coords) == 0:
if len(v1_coords) + len(a01_coords) + len(b01_coords) == 0:
raise ConfigEntryNotReady(
"No devices were able to successfully setup",
translation_domain=DOMAIN,
translation_key="no_coordinators",
)
entry.runtime_data = RoborockCoordinators(v1_coords, a01_coords, b01_q7_coords)
entry.runtime_data = RoborockCoordinators(v1_coords, a01_coords, b01_coords)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -64,17 +64,17 @@ class RoborockCoordinators:
v1: list[RoborockDataUpdateCoordinator]
a01: list[RoborockDataUpdateCoordinatorA01]
b01_q7: list[RoborockB01Q7UpdateCoordinator]
b01: list[RoborockDataUpdateCoordinatorB01]
def values(
self,
) -> list[
RoborockDataUpdateCoordinator
| RoborockDataUpdateCoordinatorA01
| RoborockB01Q7UpdateCoordinator
| RoborockDataUpdateCoordinatorB01
]:
"""Return all coordinators."""
return self.v1 + self.a01 + self.b01_q7
return self.v1 + self.a01 + self.b01
type RoborockConfigEntry = ConfigEntry[RoborockCoordinators]

View File

@@ -2,8 +2,8 @@
from typing import Any
from roborock.data import Status
from roborock.devices.traits.v1.command import CommandTrait
from roborock.devices.traits.v1.status import StatusTrait
from roborock.exceptions import RoborockException
from roborock.roborock_typing import RoborockCommand
@@ -14,9 +14,9 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import (
RoborockB01Q7UpdateCoordinator,
RoborockDataUpdateCoordinator,
RoborockDataUpdateCoordinatorA01,
RoborockDataUpdateCoordinatorB01,
)
@@ -94,7 +94,7 @@ class RoborockCoordinatedEntityV1(
self._attr_unique_id = unique_id
@property
def _device_status(self) -> StatusTrait:
def _device_status(self) -> Status:
"""Return the status of the device."""
data = self.coordinator.data
return data.status
@@ -130,21 +130,21 @@ class RoborockCoordinatedEntityA01(
self._attr_unique_id = unique_id
class RoborockCoordinatedEntityB01Q7(
RoborockEntity, CoordinatorEntity[RoborockB01Q7UpdateCoordinator]
class RoborockCoordinatedEntityB01(
RoborockEntity, CoordinatorEntity[RoborockDataUpdateCoordinatorB01]
):
"""Representation of coordinated Roborock Entity."""
def __init__(
self,
unique_id: str,
coordinator: RoborockB01Q7UpdateCoordinator,
coordinator: RoborockDataUpdateCoordinatorB01,
) -> None:
"""Initialize the coordinated Roborock Device."""
CoordinatorEntity.__init__(self, coordinator=coordinator)
RoborockEntity.__init__(
self,
unique_id=unique_id,
device_info=coordinator.device_info,
)
CoordinatorEntity.__init__(self, coordinator=coordinator)
self._attr_unique_id = unique_id

View File

@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==4.17.1",
"python-roborock==4.14.0",
"vacuum-map-parser-roborock==0.1.4"
]
}

View File

@@ -12,8 +12,8 @@ from roborock.data import (
HomeDataDevice,
HomeDataProduct,
NetworkInfo,
Status,
)
from roborock.devices.traits.v1.status import StatusTrait
from vacuum_map_parser_base.map_data import MapData
_LOGGER = logging.getLogger(__name__)
@@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__)
class DeviceState:
"""Data about the current state of a device."""
status: StatusTrait
status: Status
dnd_timer: DnDTimer
consumable: Consumable
clean_summary: CleanSummaryWithDetail

View File

@@ -26,7 +26,7 @@ from .coordinator import (
RoborockConfigEntry,
RoborockDataUpdateCoordinator,
)
from .entity import RoborockCoordinatedEntityB01Q7, RoborockCoordinatedEntityV1
from .entity import RoborockCoordinatedEntityB01, RoborockCoordinatedEntityV1
PARALLEL_UPDATES = 0
@@ -92,31 +92,25 @@ SELECT_DESCRIPTIONS: list[RoborockSelectDescription] = [
key="water_box_mode",
translation_key="mop_intensity",
api_command=RoborockCommand.SET_WATER_BOX_CUSTOM_MODE,
value_fn=lambda api: api.status.water_mode_name,
value_fn=lambda api: api.status.water_box_mode_name,
entity_category=EntityCategory.CONFIG,
options_lambda=lambda api: (
[mode.value for mode in api.status.water_mode_options]
if api.status.water_mode_options
api.status.water_box_mode.keys()
if api.status.water_box_mode is not None
else None
),
parameter_lambda=lambda key, api: [
{v: k for k, v in api.status.water_mode_mapping.items()}[key]
],
parameter_lambda=lambda key, api: [api.status.get_mop_intensity_code(key)],
),
RoborockSelectDescription(
key="mop_mode",
translation_key="mop_mode",
api_command=RoborockCommand.SET_MOP_MODE,
value_fn=lambda api: api.status.mop_route_name,
value_fn=lambda api: api.status.mop_mode_name,
entity_category=EntityCategory.CONFIG,
options_lambda=lambda api: (
[mode.value for mode in api.status.mop_route_options]
if api.status.mop_route_options
else None
api.status.mop_mode.keys() if api.status.mop_mode is not None else None
),
parameter_lambda=lambda key, api: [
{v: k for k, v in api.status.mop_route_mapping.items()}[key]
],
parameter_lambda=lambda key, api: [api.status.get_mop_mode_code(key)],
),
RoborockSelectDescription(
key="dust_collection_mode",
@@ -165,13 +159,14 @@ async def async_setup_entry(
)
async_add_entities(
RoborockB01SelectEntity(coordinator, description, options)
for coordinator in config_entry.runtime_data.b01_q7
for coordinator in config_entry.runtime_data.b01
for description in B01_SELECT_DESCRIPTIONS
if isinstance(coordinator, RoborockB01Q7UpdateCoordinator)
if (options := description.options_lambda(coordinator.api)) is not None
)
class RoborockB01SelectEntity(RoborockCoordinatedEntityB01Q7, SelectEntity):
class RoborockB01SelectEntity(RoborockCoordinatedEntityB01, SelectEntity):
"""Select entity for Roborock B01 devices."""
entity_description: RoborockB01SelectDescription

View File

@@ -33,14 +33,14 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from .coordinator import (
RoborockB01Q7UpdateCoordinator,
RoborockConfigEntry,
RoborockDataUpdateCoordinator,
RoborockDataUpdateCoordinatorA01,
RoborockDataUpdateCoordinatorB01,
)
from .entity import (
RoborockCoordinatedEntityA01,
RoborockCoordinatedEntityB01Q7,
RoborockCoordinatedEntityB01,
RoborockCoordinatedEntityV1,
RoborockEntity,
)
@@ -422,8 +422,8 @@ async def async_setup_entry(
if description.data_protocol in coordinator.request_protocols
)
entities.extend(
RoborockSensorEntityB01Q7(coordinator, description)
for coordinator in coordinators.b01_q7
RoborockSensorEntityB01(coordinator, description)
for coordinator in coordinators.b01
for description in Q7_B01_SENSOR_DESCRIPTIONS
if description.value_fn(coordinator.data) is not None
)
@@ -515,14 +515,14 @@ class RoborockSensorEntityA01(RoborockCoordinatedEntityA01, SensorEntity):
return self.coordinator.data[self.entity_description.data_protocol]
class RoborockSensorEntityB01Q7(RoborockCoordinatedEntityB01Q7, SensorEntity):
"""Representation of a B01 Q7 Roborock sensor."""
class RoborockSensorEntityB01(RoborockCoordinatedEntityB01, SensorEntity):
"""Representation of a B01 Roborock sensor."""
entity_description: RoborockSensorDescriptionB01
def __init__(
self,
coordinator: RoborockB01Q7UpdateCoordinator,
coordinator: RoborockDataUpdateCoordinatorB01,
description: RoborockSensorDescriptionB01,
) -> None:
"""Initialize the entity."""

View File

@@ -118,12 +118,9 @@
"max": "Max",
"medium": "[%key:common::state::medium%]",
"mild": "Mild",
"min": "Min",
"moderate": "Moderate",
"off": "[%key:common::state::off%]",
"slight": "Slight",
"smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]",
"standard": "[%key:component::roborock::entity::select::mop_mode::state::standard%]",
"vac_followed_by_mop": "Vacuum followed by mop"
}
},
@@ -451,7 +448,6 @@
"max_plus": "Max plus",
"medium": "[%key:common::state::medium%]",
"off": "[%key:common::state::off%]",
"off_raise_main_brush": "Off (raised brush)",
"quiet": "Quiet",
"silent": "Silent",
"smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]",
@@ -482,9 +478,6 @@
"mqtt_unauthorized": {
"message": "Roborock MQTT servers rejected the connection due to rate limiting or invalid credentials. You may either attempt to reauthenticate or wait and reload the integration."
},
"multiple_maps_in_clean": {
"message": "All segments must belong to the same map. Got segments from maps: {map_flags}"
},
"no_coordinators": {
"message": "No devices were able to successfully setup"
},
@@ -494,9 +487,6 @@
"position_not_found": {
"message": "Robot position not found"
},
"segment_id_parse_error": {
"message": "Invalid segment ID format: {segment_id}"
},
"update_data_fail": {
"message": "Failed to update data"
},

View File

@@ -1,6 +1,5 @@
"""Support for Roborock vacuum class."""
import asyncio
import logging
from typing import Any
@@ -9,22 +8,21 @@ from roborock.exceptions import RoborockException
from roborock.roborock_typing import RoborockCommand
from homeassistant.components.vacuum import (
Segment,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.core import HomeAssistant, ServiceResponse
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, MAP_SLEEP
from .const import DOMAIN
from .coordinator import (
RoborockB01Q7UpdateCoordinator,
RoborockConfigEntry,
RoborockDataUpdateCoordinator,
)
from .entity import RoborockCoordinatedEntityB01Q7, RoborockCoordinatedEntityV1
from .entity import RoborockCoordinatedEntityB01, RoborockCoordinatedEntityV1
_LOGGER = logging.getLogger(__name__)
@@ -84,7 +82,8 @@ async def async_setup_entry(
)
async_add_entities(
RoborockQ7Vacuum(coordinator)
for coordinator in config_entry.runtime_data.b01_q7
for coordinator in config_entry.runtime_data.b01
if isinstance(coordinator, RoborockB01Q7UpdateCoordinator)
)
@@ -102,7 +101,6 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.STATE
| VacuumEntityFeature.START
| VacuumEntityFeature.CLEAN_AREA
)
_attr_translation_key = DOMAIN
_attr_name = None
@@ -118,13 +116,11 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
coordinator.duid_slug,
coordinator,
)
self._home_trait = coordinator.properties_api.home
self._maps_trait = coordinator.properties_api.maps
@property
def fan_speed_list(self) -> list[str]:
"""Get the list of available fan speeds."""
return [mode.value for mode in self._device_status.fan_speed_options]
return self._device_status.fan_power_options
@property
def activity(self) -> VacuumActivity | None:
@@ -135,7 +131,7 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
@property
def fan_speed(self) -> str | None:
"""Return the fan speed of the vacuum cleaner."""
return self._device_status.fan_speed_name
return self._device_status.fan_power_name
async def async_start(self) -> None:
"""Start the vacuum."""
@@ -174,83 +170,13 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
"""Set vacuum fan speed."""
await self.send(
RoborockCommand.SET_CUSTOM_MODE,
[
{v: k for k, v in self._device_status.fan_speed_mapping.items()}[
fan_speed
]
],
[self._device_status.get_fan_speed_code(fan_speed)],
)
async def async_set_vacuum_goto_position(self, x: int, y: int) -> None:
"""Send vacuum to a specific target point."""
await self.send(RoborockCommand.APP_GOTO_TARGET, [x, y])
async def async_get_segments(self) -> list[Segment]:
"""Get the segments that can be cleaned."""
home_map_info = self._home_trait.home_map_info
if not home_map_info:
return []
return [
Segment(
id=f"{map_flag}:{room.segment_id}",
name=room.name,
group=map_info.name,
)
for map_flag, map_info in home_map_info.items()
for room in map_info.rooms
]
async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
"""Clean the specified segments."""
parsed: list[tuple[int, int]] = []
for seg_id in segment_ids:
# Segment id is mapflag:segment_id
parts = seg_id.split(":")
if len(parts) != 2:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="segment_id_parse_error",
translation_placeholders={"segment_id": seg_id},
)
try:
# We need to make sure both parts are ints.
parsed.append((int(parts[0]), int(parts[1])))
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="segment_id_parse_error",
translation_placeholders={"segment_id": seg_id},
) from err
# Because segment_ids can overlap for each map,
# we need to make sure that only one map is passed in.
unique_map_flags = {map_flag for map_flag, _ in parsed}
if len(unique_map_flags) > 1:
map_flags_str = ", ".join(str(flag) for flag in sorted(unique_map_flags))
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="multiple_maps_in_clean",
translation_placeholders={"map_flags": map_flags_str},
)
target_map_flag = next(iter(unique_map_flags))
if self._maps_trait.current_map != target_map_flag:
# If the user is attempting to clean an area on a map that is not selected, we should try to change.
try:
await self._maps_trait.set_current_map(target_map_flag)
except RoborockException as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="command_failed",
translation_placeholders={"command": "load_multi_map"},
) from err
await asyncio.sleep(MAP_SLEEP)
# We can now confirm all segments are on our current map, so clean them all.
await self.send(
RoborockCommand.APP_SEGMENT_CLEAN,
[{"segments": [seg_id for _, seg_id in parsed]}],
)
async def async_send_command(
self,
command: str,
@@ -306,7 +232,7 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
}
class RoborockQ7Vacuum(RoborockCoordinatedEntityB01Q7, StateVacuumEntity):
class RoborockQ7Vacuum(RoborockCoordinatedEntityB01, StateVacuumEntity):
"""General Representation of a Roborock vacuum."""
_attr_icon = "mdi:robot-vacuum"
@@ -330,7 +256,7 @@ class RoborockQ7Vacuum(RoborockCoordinatedEntityB01Q7, StateVacuumEntity):
) -> None:
"""Initialize a vacuum."""
StateVacuumEntity.__init__(self)
RoborockCoordinatedEntityB01Q7.__init__(
RoborockCoordinatedEntityB01.__init__(
self,
coordinator.duid_slug,
coordinator,

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["Pillow==12.1.1"]
"requirements": ["Pillow==12.0.0"]
}

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["simplehound"],
"quality_scale": "legacy",
"requirements": ["Pillow==12.1.1", "simplehound==0.3"]
"requirements": ["Pillow==12.0.0", "simplehound==0.3"]
}

View File

@@ -78,7 +78,7 @@ class SisyphusLight(LightEntity):
return not self._table.is_sleeping
@property
def brightness(self) -> int:
def brightness(self):
"""Return the current brightness of the table's ring light."""
return self._table.brightness * 255

View File

@@ -65,7 +65,7 @@ class SmartTubLight(SmartTubEntity, LightEntity):
return self.coordinator.data[self.spa.id][ATTR_LIGHTS][self.light_zone]
@property
def brightness(self) -> int:
def brightness(self):
"""Return the brightness of this light between 0..255."""
# SmartTub intensity is 0..100
@@ -87,7 +87,7 @@ class SmartTubLight(SmartTubEntity, LightEntity):
return self.light.mode != SpaLight.LightMode.OFF
@property
def effect(self) -> str | None:
def effect(self):
"""Return the current effect."""
mode = self.light.mode.name.lower()
if mode in self.effect_list:
@@ -95,7 +95,7 @@ class SmartTubLight(SmartTubEntity, LightEntity):
return None
@property
def effect_list(self) -> list[str]:
def effect_list(self):
"""Return the list of supported effects."""
return [
effect

View File

@@ -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",

View File

@@ -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"]
}

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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"],

View File

@@ -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"],

View File

@@ -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",

View File

@@ -6,5 +6,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sql",
"iot_class": "local_polling",
"requirements": ["SQLAlchemy==2.0.41", "sqlparse==0.5.5"]
"requirements": ["SQLAlchemy==2.0.41", "sqlparse==0.5.0"]
}

View File

@@ -261,7 +261,7 @@ def check_and_render_sql_query(hass: HomeAssistant, query: Template | str) -> st
raise MultipleQueryError("Multiple SQL statements are not allowed")
if (
len(rendered_queries) == 0
or (query_type := rendered_queries[0].get_type()) == "UNKNOWN" # type: ignore[no-untyped-call]
or (query_type := rendered_queries[0].get_type()) == "UNKNOWN"
):
raise UnknownQueryTypeError("SQL query is empty or unknown type")
if query_type != "SELECT":

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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"]
}

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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",

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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()

View File

@@ -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"]

View File

@@ -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"]
}

View File

@@ -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",

View File

@@ -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"]

View File

@@ -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"],

View File

@@ -9,6 +9,6 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["systembridgeconnector"],
"requirements": ["systembridgeconnector==5.4.3"],
"requirements": ["systembridgeconnector==5.3.1"],
"zeroconf": ["_system-bridge._tcp.local."]
}

View File

@@ -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"]
}

View File

@@ -71,9 +71,9 @@ from .const import (
ATTR_KEYBOARD_INLINE,
ATTR_MEDIA_TYPE,
ATTR_MESSAGE,
ATTR_MESSAGE_ID,
ATTR_MESSAGE_TAG,
ATTR_MESSAGE_THREAD_ID,
ATTR_MESSAGEID,
ATTR_ONE_TIME_KEYBOARD,
ATTR_OPEN_PERIOD,
ATTR_OPTIONS,
@@ -264,7 +264,7 @@ SERVICE_SCHEMA_EDIT_MESSAGE = vol.All(
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
vol.Optional(ATTR_TITLE): cv.string,
vol.Required(ATTR_MESSAGE): cv.string,
vol.Required(ATTR_MESSAGE_ID): vol.Any(
vol.Required(ATTR_MESSAGEID): vol.Any(
cv.positive_int, vol.All(cv.string, "last")
),
vol.Optional(ATTR_CHAT_ID): vol.Coerce(int),
@@ -281,7 +281,7 @@ SERVICE_SCHEMA_EDIT_MESSAGE_MEDIA = vol.All(
{
vol.Optional(ATTR_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
vol.Required(ATTR_MESSAGE_ID): vol.Any(
vol.Required(ATTR_MESSAGEID): vol.Any(
cv.positive_int, vol.All(cv.string, "last")
),
vol.Optional(ATTR_CHAT_ID): vol.Coerce(int),
@@ -311,7 +311,7 @@ SERVICE_SCHEMA_EDIT_CAPTION = vol.Schema(
{
vol.Optional(ATTR_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
vol.Required(ATTR_MESSAGE_ID): vol.Any(
vol.Required(ATTR_MESSAGEID): vol.Any(
cv.positive_int, vol.All(cv.string, "last")
),
vol.Optional(ATTR_CHAT_ID): vol.Coerce(int),
@@ -325,7 +325,7 @@ SERVICE_SCHEMA_EDIT_REPLYMARKUP = vol.Schema(
{
vol.Optional(ATTR_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
vol.Required(ATTR_MESSAGE_ID): vol.Any(
vol.Required(ATTR_MESSAGEID): vol.Any(
cv.positive_int, vol.All(cv.string, "last")
),
vol.Optional(ATTR_CHAT_ID): vol.Coerce(int),
@@ -347,7 +347,7 @@ SERVICE_SCHEMA_DELETE_MESSAGE = vol.Schema(
vol.Optional(ATTR_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
vol.Optional(ATTR_CHAT_ID): vol.Coerce(int),
vol.Required(ATTR_MESSAGE_ID): vol.Any(
vol.Required(ATTR_MESSAGEID): vol.Any(
cv.positive_int, vol.All(cv.string, "last")
),
}
@@ -364,7 +364,7 @@ SERVICE_SCHEMA_LEAVE_CHAT = vol.Schema(
SERVICE_SCHEMA_SET_MESSAGE_REACTION = vol.Schema(
{
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
vol.Required(ATTR_MESSAGE_ID): vol.Any(
vol.Required(ATTR_MESSAGEID): vol.Any(
cv.positive_int, vol.All(cv.string, "last")
),
vol.Optional(ATTR_CHAT_ID): vol.Coerce(int),
@@ -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:
@@ -485,7 +485,7 @@ async def _async_send_telegram_message(service: ServiceCall) -> ServiceResponse:
for chat_id, message_id in service_response.items():
formatted_response = {
ATTR_CHAT_ID: int(chat_id),
ATTR_MESSAGE_ID: message_id,
ATTR_MESSAGEID: message_id,
}
if target_notify_entity_id:
@@ -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))

View File

@@ -73,9 +73,9 @@ from .const import (
ATTR_KEYBOARD,
ATTR_KEYBOARD_INLINE,
ATTR_MESSAGE,
ATTR_MESSAGE_ID,
ATTR_MESSAGE_TAG,
ATTR_MESSAGE_THREAD_ID,
ATTR_MESSAGEID,
ATTR_MSG,
ATTR_MSGID,
ATTR_ONE_TIME_KEYBOARD,
@@ -319,8 +319,8 @@ class TelegramNotificationService:
"""
message_id: Any | None = None
inline_message_id: int | None = None
if ATTR_MESSAGE_ID in msg_data:
message_id = msg_data[ATTR_MESSAGE_ID]
if ATTR_MESSAGEID in msg_data:
message_id = msg_data[ATTR_MESSAGEID]
if (
isinstance(message_id, str)
and (message_id == "last")
@@ -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_MESSAGE_ID: 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,

View File

@@ -102,7 +102,7 @@ ATTR_KEYBOARD = "keyboard"
ATTR_RESIZE_KEYBOARD = "resize_keyboard"
ATTR_ONE_TIME_KEYBOARD = "one_time_keyboard"
ATTR_KEYBOARD_INLINE = "inline_keyboard"
ATTR_MESSAGE_ID = "message_id"
ATTR_MESSAGEID = "message_id"
ATTR_INLINE_MESSAGE_ID = "inline_message_id"
ATTR_MEDIA_TYPE = "media_type"
ATTR_MSG = "message"

Some files were not shown because too many files have changed in this diff Show More