Compare commits

..

69 Commits

Author SHA1 Message Date
Franck Nijhof 0723d8d83f 2026.5.4 (#171859) 2026-05-22 19:26:21 +02:00
Franck Nijhof 73c9edd3e8 Ran gen_requirements_all 2026-05-22 16:18:20 +00:00
Franck Nijhof 18f30bd97b Bump version to 2026.5.4 2026-05-22 16:06:32 +00:00
Manu eae6e79b61 Fix dead link in System Bridge service action (#171855) 2026-05-22 16:04:27 +00:00
Franck Nijhof 5bb42801d9 Fix Hue device trigger crash for devices removed from bridge (#171844) 2026-05-22 16:04:25 +00:00
Franck Nijhof 98271265d3 Fix OpenHome config flow crash when UDN is a list (#171841) 2026-05-22 16:04:23 +00:00
Franck Nijhof 92d20477bc Register Insteon modem device before platform setup (#171839) 2026-05-22 16:04:21 +00:00
Franck Nijhof 9352a0057e Fix invalid MDI icon references (#171831)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-22 16:04:19 +00:00
Franck Nijhof 5fb874277a Fix Lutron Caseta battery sensor crash on unsupported devices (#171829) 2026-05-22 16:04:17 +00:00
Franck Nijhof d65f605398 Fix ZBT-2 hardware page crash when entry data is missing VID (#171828) 2026-05-22 16:04:16 +00:00
Simone Chemelli 7e5b448f70 Add missing exception translation keys in alexa_devices (#171749) 2026-05-22 16:04:14 +00:00
Simone Chemelli ef5da5ef36 Fix exception translation placeholder mismatches in comelit (#171748) 2026-05-22 16:04:11 +00:00
epenet 410f00c4ed Bump renault-api to 0.5.10 (#171692) 2026-05-22 15:58:41 +00:00
Kamil Breguła 33c205dc04 Bump wled to 0.23.0 and remove backoff exception (#171622) 2026-05-22 15:58:39 +00:00
dontinelli 267b3e279d Fix update error message key in solarlog (#171611) 2026-05-22 15:58:37 +00:00
Maciej Bieniek 9c1cd8093d Fix media_image_hash and validate the MIME type in the Shelly media player (#171585) 2026-05-22 15:58:35 +00:00
Josef Zweck 201c0c2470 Fix string ref for tedee (#171548) 2026-05-22 15:58:33 +00:00
Franck Nijhof 281d6e0e8b Fix Wyoming satellite crash when TTS is not configured (#171513) 2026-05-22 15:58:31 +00:00
Franck Nijhof 88746534a4 Fix PowerView cover crash when shade position is unavailable (#171471) 2026-05-22 15:58:29 +00:00
Franck Nijhof 135f91c3c5 Fix habitica ignoring zero values for interval and streak (#171468) 2026-05-22 15:58:27 +00:00
Franck Nijhof 49d8dc88d9 Fix SmartThings crash when timestamp attribute is None (#171467) 2026-05-22 15:58:25 +00:00
epenet a7a2c1eb02 Bump renault-api to 0.5.9 (#171428) 2026-05-22 15:58:23 +00:00
J. Nick Koston 6596f956d2 Bump aiodns to 4.0.4 (#171420)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: frenck <195327+frenck@users.noreply.github.com>
2026-05-22 15:57:27 +00:00
TheJulianJES 9d8859833b Fix ZHA blocking minor version downgrades (#171319) 2026-05-22 15:48:41 +00:00
Aidan Timson 65a4c10660 Bump aiolyric to 2.1.1, Update OAuth URL for lyric (#171181) 2026-05-22 15:48:39 +00:00
Åke Strandberg 1737b50558 Add missing Miele Dishwasher codes (#171175) 2026-05-22 15:48:37 +00:00
Luke Lashley 614c7006f6 Bump python-roborock to 5.12.0 (#171112)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-05-22 15:46:06 +00:00
Jonathan Segev 8c901cc405 Bump aiolyric to 2.1.0 (#171007)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-05-22 15:46:04 +00:00
Franck Nijhof 5d0fdfd38b Apply web search citation stripping for GPT-5.x models in OpenAI conversation (#170956) 2026-05-22 15:46:02 +00:00
Franck Nijhof c9ed57bc56 2026.5.3 (#171185) 2026-05-19 11:49:12 +02:00
bkobus-bbx 0e0901993d Fix blebox light temperature scaling (#170573) 2026-05-19 08:47:23 +00:00
Franck Nijhof 54aba11091 Bump version to 2026.5.3 2026-05-19 08:39:50 +00:00
Mick Vleeshouwer dc9116a7a7 Fix tilt and position support for VenetianBlind covers in Overkiz (#170974) 2026-05-19 08:39:38 +00:00
Mick Vleeshouwer 1e90882918 Fix is_closed state and position for DynamicPergola covers in Overkiz (#170983) 2026-05-19 08:37:54 +00:00
puddly e8295e14b1 Fix ZHA config entries using a URI without a port (#171164) 2026-05-19 08:35:43 +00:00
Mick Vleeshouwer 7ebaaf129a Fix controls for UpDownGarageDoor4T and additional 4T covers in Overkiz (#171144) 2026-05-19 08:35:00 +00:00
Michael ee734dede6 Bump aioimmich to 0.14.1 (#171138) 2026-05-19 08:33:58 +00:00
Franck Nijhof ebc582c813 Return media_content_id as string in forked_daapd (#171059) 2026-05-19 08:33:56 +00:00
James Nimmo 311e5a9bd2 Bump pyIntesishome to 1.8.8 (#171041) 2026-05-19 08:33:54 +00:00
Franck Nijhof cd6c3c878b Fix WeatherFlow websocket crash when data payload is None (#171037) 2026-05-19 08:33:52 +00:00
Franck Nijhof 51589ec2ff Add stop command to Overkiz pergola horizontal awning covers (#171034) 2026-05-19 08:33:50 +00:00
Franck Nijhof 8e1a04dc82 Fix Verisure alarm crash when cloud rejects arm/disarm command (#171024) 2026-05-19 08:33:48 +00:00
Mick Vleeshouwer 6b15f9a2ec Add additional overrides to cover entity in Overkiz (#171019) 2026-05-19 08:33:46 +00:00
Franck Nijhof 8d66752556 Fix shorthand template conditions in choose blocks crashing all automations (#171018) 2026-05-19 08:33:44 +00:00
Franck Nijhof 266767e37d Handle Daikin connection errors gracefully in coordinator (#171017) 2026-05-19 08:33:42 +00:00
Franck Nijhof d39775ac34 Fix manual alarm panel crash on restore with invalid state (#171016) 2026-05-19 08:33:40 +00:00
Franck Nijhof a314f7bf64 Fix Control4 climate crash when humidity is 'Undefined' (#171015) 2026-05-19 08:33:38 +00:00
Franck Nijhof 37478d33eb Fix SleepIQ timer units: seconds should be minutes for core climate and foot warmer (#171013) 2026-05-19 08:33:36 +00:00
Franck Nijhof 5a76f3bd19 Fix Growatt mix device IndexError when chart data is empty (#171012) 2026-05-19 08:33:34 +00:00
Franck Nijhof 17e105083e Fix threshold preview crash when hysteresis is not provided (#171009) 2026-05-19 08:33:32 +00:00
Franck Nijhof db8589b2bc Fix time trigger crash when using entity_id dict format without offset (#171006)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-05-19 08:33:30 +00:00
Franck Nijhof 771b016f33 Fix Netatmo valve KeyError when hvac_action state is unavailable in Overkiz (#171004) 2026-05-19 08:33:28 +00:00
Franck Nijhof 0bc0745e8c Use asyncio.get_running_loop() in emulated_hue UPnP responder (#171000)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:31:43 +00:00
Franck Nijhof ea084797d3 Load template extensions by class to prevent import deadlock (#170995)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:31:41 +00:00
Franck Nijhof 2456753caf Prevent Google Assistant entity sync from blocking startup (#170991)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:31:39 +00:00
Mick Vleeshouwer 070de13c14 Fix controls for OpenCloseGate4T (rts:GateOpenerRTS4TComponent) in Overkiz (#170987) 2026-05-19 08:30:30 +00:00
Mick Vleeshouwer 5e45f37ee6 Fix is_closed state for DiscretePositionableGarageDoor in Overkiz (#170981) 2026-05-19 08:25:10 +00:00
Franck Nijhof 4a96880f51 Reduce GoodWe connect retries to avoid blocking startup (#170964)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:20:26 +00:00
Franck Nijhof 228ac01124 Use correct state_class for utility meters with device classes that don't support total_increasing (#170962)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:20:24 +00:00
Franck Nijhof d366027e6b Fix utility meter next_reset shifting forward on entity rename (#170957)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:20:22 +00:00
puddly 2f35ad2a8a Disable USB discovery for teleinfo (#170933) 2026-05-19 08:20:20 +00:00
Mick Vleeshouwer 95cc9aed64 Fix is_closed state for SlidingDiscreteGateWithPedestrianPosition covers in Overkiz (#170913) 2026-05-19 08:19:03 +00:00
Franck Nijhof 37d6449a49 Populate uid and recurrence_id in CalDAV calendar events (#170910) 2026-05-19 08:14:10 +00:00
J. Nick Koston 249b5435d9 Bump aiodns to 4.0.3 (#170865)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-05-19 08:07:21 +00:00
bkobus-bbx 3293ebcea5 Fix ValueError when turning on blebox light with brightness set to 0 (#170769)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-19 08:07:18 +00:00
Daniil Karpenko 47d8adc77c Add tilt controls for UpDownSheerScreen in Overkiz (#170563)
Co-authored-by: Mick Vleeshouwer <mick@imick.nl>
2026-05-19 08:01:00 +00:00
Keith Roehrenbeck 356e6a691b Fix Apple TV keyboard focus binary_sensor missing on cold start (#170360) 2026-05-19 08:00:58 +00:00
Florent Thoumie b26c2f3854 Improve iaqualink 429 handling (#170231) 2026-05-19 08:00:56 +00:00
Luka Matijević 0830988687 Bump qbittorrent-api to 2026.5.1 (#170181) 2026-05-19 08:00:54 +00:00
117 changed files with 4454 additions and 714 deletions
@@ -11,7 +11,7 @@
"service": "mdi:dialpad"
},
"alarm_toggle_chime": {
"service": "mdi:abc"
"service": "mdi:bell-ring"
}
}
}
@@ -102,6 +102,9 @@
"entry_not_loaded": {
"message": "Entry not loaded: {entry}"
},
"invalid_auth": {
"message": "Invalid authentication credentials: {error}"
},
"invalid_device_id": {
"message": "Invalid device ID specified: {device_id}"
},
@@ -7,7 +7,7 @@ from pyatv.interface import AppleTV, KeyboardListener
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import CONF_NAME
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -23,23 +23,33 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Load Apple TV binary sensor based on a config entry."""
# apple_tv config entries always have a unique id
manager = config_entry.runtime_data
cb: CALLBACK_TYPE
added = False
@callback
def setup_entities(atv: AppleTV) -> None:
nonlocal added
if added:
return
if atv.features.in_state(FeatureState.Available, FeatureName.TextFocusState):
assert config_entry.unique_id is not None
name: str = config_entry.data[CONF_NAME]
async_add_entities(
[AppleTVKeyboardFocused(name, config_entry.unique_id, manager)]
)
cb()
added = True
cb = async_dispatcher_connect(
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
config_entry.async_on_unload(
async_dispatcher_connect(
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
)
)
config_entry.async_on_unload(cb)
# The manager may have already connected (and dispatched SIGNAL_CONNECTED)
# before this platform was forwarded, in which case the signal above was
# missed; handle that case directly.
if manager.atv is not None:
setup_entities(manager.atv)
class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener):
+4
View File
@@ -13,3 +13,7 @@ UNKNOWN = "unknown"
DEFAULT_HOST = "192.168.0.2"
DEFAULT_PORT = 80
LIGHT_MAX_KELVINS = 6500 # 154 Mireds
LIGHT_MIN_KELVINS = 2700 # 370 Mireds
+46 -9
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
import math
from typing import Any
import blebox_uniapi.light
@@ -22,9 +23,9 @@ from homeassistant.components.light import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util
from . import BleBoxConfigEntry
from .const import LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS
from .entity import BleBoxEntity
_LOGGER = logging.getLogger(__name__)
@@ -59,8 +60,8 @@ COLOR_MODE_MAP = {
class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
"""Representation of BleBox lights."""
_attr_min_color_temp_kelvin = 2700 # 370 Mireds
_attr_max_color_temp_kelvin = 6500 # 154 Mireds
_attr_min_color_temp_kelvin = LIGHT_MIN_KELVINS
_attr_max_color_temp_kelvin = LIGHT_MAX_KELVINS
def __init__(self, feature: blebox_uniapi.light.Light) -> None:
"""Initialize a BleBox light."""
@@ -78,10 +79,43 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
"""Return the name."""
return self._feature.brightness
def _color_temp_to_native_scale(self, x: int) -> int:
"""Convert color temperature from Kelvin to native BleBox scale (0-255).
BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
"""
scaled = (
(self._attr_max_color_temp_kelvin - x)
/ (self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin)
) * 255
# note: within the operating temperature range here the Kelvin
# scale has less "integer steps" than the native scale used
# by blebox devices. Thus we need to use rounding method that is opposite
# to the one used in _color_temp_from_native_scale in order to avoid
# temperature value jumping by one step when the temperature value is read
# back from the device
bounded = max(min(math.floor(scaled), 255), 0)
return int(bounded)
def _color_temp_from_native_scale(self, x: int) -> int:
"""Convert color temperature from native BleBox scale (0-255) to Kelvin.
BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
"""
scaled = self._attr_max_color_temp_kelvin - (x / 255) * (
self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin
)
# note: see _color_temp_to_native_scale for explanation of rounding method
bounded = max(
min(math.ceil(scaled), self._attr_max_color_temp_kelvin),
self._attr_min_color_temp_kelvin,
)
return int(bounded)
@property
def color_temp_kelvin(self) -> int:
"""Return the color temperature value in Kelvin."""
return color_util.color_temperature_mired_to_kelvin(self._feature.color_temp)
return self._color_temp_from_native_scale(self._feature.color_temp)
@property
def color_mode(self) -> ColorMode:
@@ -139,15 +173,16 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
effect = kwargs.get(ATTR_EFFECT)
color_temp_kelvin = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
rgbww = kwargs.get(ATTR_RGBWW_COLOR)
rgb = kwargs.get(ATTR_RGB_COLOR)
feature = self._feature
value = feature.sensible_on_value
rgb = kwargs.get(ATTR_RGB_COLOR)
if rgbw is not None:
value = list(rgbw)
if color_temp_kelvin is not None:
value = feature.return_color_temp_with_brightness(
int(color_util.color_temperature_kelvin_to_mired(color_temp_kelvin)),
self._color_temp_to_native_scale(color_temp_kelvin),
self.brightness,
)
@@ -162,14 +197,16 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
if brightness is not None:
if self.color_mode == ColorMode.COLOR_TEMP:
value = feature.return_color_temp_with_brightness(
color_util.color_temperature_kelvin_to_mired(
self.color_temp_kelvin
),
self._color_temp_to_native_scale(self.color_temp_kelvin),
brightness,
)
else:
value = feature.apply_brightness(value, brightness)
if isinstance(value, (list, tuple)) and not any(value):
await self._feature.async_off()
return
try:
await self._feature.async_on(value)
except ValueError as exc:
@@ -81,6 +81,12 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
end=self.to_local(self.get_end_date(vevent)),
location=get_attr_value(vevent, "location"),
description=get_attr_value(vevent, "description"),
uid=get_attr_value(vevent, "uid"),
recurrence_id=(
str(v)
if (v := get_attr_value(vevent, "recurrence_id")) is not None
else None
),
)
)
@@ -176,6 +182,12 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
end=self.to_local(self.get_end_date(vevent)),
location=get_attr_value(vevent, "location"),
description=get_attr_value(vevent, "description"),
uid=get_attr_value(vevent, "uid"),
recurrence_id=(
str(v)
if (v := get_attr_value(vevent, "recurrence_id")) is not None
else None
),
)
@staticmethod
@@ -70,7 +70,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
raise InvalidAuth(
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
translation_placeholders={"error": repr(err)},
) from err
finally:
await api.logout()
+4 -1
View File
@@ -273,7 +273,10 @@ class Control4Climate(Control4Entity, ClimateEntity):
if data is None:
return None
humidity = data.get(CONTROL4_HUMIDITY)
return int(humidity) if humidity is not None else None
try:
return int(humidity) if humidity is not None else None
except ValueError, TypeError:
return None
@property
def hvac_mode(self) -> HVACMode:
@@ -63,7 +63,7 @@ class CurrencylayerSensor(SensorEntity):
"""Implementing the Currencylayer sensor."""
_attr_attribution = "Data provided by currencylayer.com"
_attr_icon = "mdi:currency"
_attr_icon = "mdi:currency-usd"
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
"""Initialize the sensor."""
+10 -2
View File
@@ -4,10 +4,11 @@ from datetime import timedelta
import logging
from pydaikin.daikin_base import Appliance
from pydaikin.exceptions import DaikinException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, TIMEOUT_SEC
@@ -33,4 +34,11 @@ class DaikinCoordinator(DataUpdateCoordinator[None]):
self.device = device
async def _async_update_data(self) -> None:
await self.device.update_status()
try:
await self.device.update_status()
except DaikinException as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="error_communicating",
translation_placeholders={"error": str(err)},
) from err
@@ -59,6 +59,9 @@
}
},
"exceptions": {
"error_communicating": {
"message": "Error communicating with Daikin device: {error}"
},
"zone_hvac_mode_unsupported": {
"message": "Zone temperature can only be changed when the main climate mode is heat or cool."
},
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["aiodns==4.0.0"]
"requirements": ["aiodns==4.0.4"]
}
@@ -171,7 +171,7 @@ async def async_create_upnp_datagram_endpoint(
ssdp_socket.bind(("" if upnp_bind_multicast else host_ip_addr, BROADCAST_PORT))
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
transport_protocol = await loop.create_datagram_endpoint(
lambda: UPNPResponderProtocol(loop, ssdp_socket, advertise_ip, advertise_port),
+1 -1
View File
@@ -23,7 +23,7 @@
"service": "mdi:refresh"
},
"set_dhw_override": {
"service": "mdi:water-heater"
"service": "mdi:water-boiler"
},
"set_system_mode": {
"service": "mdi:pencil"
+1 -1
View File
@@ -16,7 +16,7 @@ class DeviceType(Enum):
GAME_CONSOLE = "mdi:nintendo-game-boy"
STREAMING_DONGLE = "mdi:cast"
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
DISC_PLAYER = "mdi:disk-player"
DISC_PLAYER = "mdi:disc-player"
REMOTE_CONTROL = "mdi:remote-tv"
RADIO = "mdi:radio"
PHOTO_CAMERA = PHOTOS = "mdi:camera"
@@ -470,9 +470,11 @@ class ForkedDaapdMaster(MediaPlayerEntity):
return self._player["volume"] == 0
@property
def media_content_id(self):
def media_content_id(self) -> str | None:
"""Content ID of current playing media."""
return self._player["item_id"]
if (item_id := self._player["item_id"]) == 0:
return None
return str(item_id)
@property
def media_content_type(self):
+1 -1
View File
@@ -2,7 +2,7 @@
"entity": {
"button": {
"sync_clock": {
"default": "mdi:clock-sync"
"default": "mdi:clock-check"
}
},
"number": {
+1 -1
View File
@@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoodweConfigEntry) -> bo
host=host,
port=port,
family=model_family,
retries=10,
retries=3,
)
except InverterError as err:
try:
@@ -73,8 +73,8 @@ class GoodweFlowHandler(ConfigFlow, domain=DOMAIN):
"""Detects the port of the Inverter."""
port = GOODWE_UDP_PORT
try:
inverter = await connect(host=host, port=port, retries=10)
inverter = await connect(host=host, port=port, retries=3)
except InverterError:
port = GOODWE_TCP_PORT
inverter = await connect(host=host, port=port, retries=10)
inverter = await connect(host=host, port=port, retries=3)
return inverter, port
@@ -114,7 +114,7 @@ class AbstractConfig(ABC):
"""Sync entities to Google."""
await self.async_sync_entities_all()
self._on_deinitialize.append(start.async_at_start(self.hass, sync_google))
self._on_deinitialize.append(start.async_at_started(self.hass, sync_google))
@callback
def async_deinitialize(self) -> None:
@@ -209,14 +209,15 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
mix_chart_entries = mix_detail["chartData"]
sorted_keys = sorted(mix_chart_entries)
# Create datetime from the latest entry
date_now = dt_util.now().date()
last_updated_time = dt_util.parse_time(str(sorted_keys[-1]))
mix_detail["lastdataupdate"] = datetime.datetime.combine(
date_now,
last_updated_time, # type: ignore[arg-type]
dt_util.get_default_time_zone(),
)
if sorted_keys:
# Create datetime from the latest entry
date_now = dt_util.now().date()
last_updated_time = dt_util.parse_time(str(sorted_keys[-1]))
mix_detail["lastdataupdate"] = datetime.datetime.combine(
date_now,
last_updated_time, # type: ignore[arg-type]
dt_util.get_default_time_zone(),
)
# Dashboard data for mix system
dashboard_data = self.api.dashboard_data(self.plant_id)
@@ -808,10 +808,10 @@ async def _create_or_update_task(call: ServiceCall) -> ServiceResponse: # noqa:
data["daysOfMonth"] = [start_date.day]
data["weeksOfMonth"] = []
if interval := call.data.get(ATTR_INTERVAL):
if (interval := call.data.get(ATTR_INTERVAL)) is not None:
data["everyX"] = interval
if streak := call.data.get(ATTR_STREAK):
if (streak := call.data.get(ATTR_STREAK)) is not None:
data["streak"] = streak
try:
@@ -265,9 +265,9 @@ async def async_attach_trigger( # noqa: C901
# entity
update_entity_trigger(at_time, new_state=hass.states.get(at_time))
to_track.append(TrackEntity(at_time, update_entity_trigger_event))
elif isinstance(at_time, dict) and CONF_OFFSET in at_time:
# entity with offset
entity_id: str = at_time.get(CONF_ENTITY_ID, "")
elif isinstance(at_time, dict):
# entity with optional offset
entity_id: str = at_time[CONF_ENTITY_ID]
offset: timedelta = at_time.get(CONF_OFFSET, timedelta(0))
update_entity_trigger(
entity_id, new_state=hass.states.get(entity_id), offset=offset
@@ -21,7 +21,9 @@ EXPECTED_ENTRY_VERSION = (
@callback
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
"""Return board info."""
entries = hass.config_entries.async_entries(DOMAIN)
entries = hass.config_entries.async_entries(
DOMAIN, include_ignore=False, include_disabled=False
)
return [
HardwareInfo(
board=None,
@@ -89,6 +89,8 @@ def async_get_triggers(
# Get Hue device id from device identifier
hue_dev_id = get_hue_device_id(device_entry)
if hue_dev_id is None or hue_dev_id not in api.devices:
return []
# extract triggers from all button resources of this Hue device
triggers: list[dict[str, Any]] = []
model_id = api.devices[hue_dev_id].product_data.product_name
@@ -135,6 +135,11 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
"""
return self._is_hard_wired
@property
def available(self) -> bool:
"""Return True if shade position data is available."""
return super().available and self.positions.primary is not None
@property
def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes."""
@@ -8,6 +8,7 @@ from typing import Any
import httpx
from iaqualink.exception import (
AqualinkServiceException,
AqualinkServiceThrottledException,
AqualinkServiceUnauthorizedException,
)
@@ -46,6 +47,12 @@ class AqualinkDataUpdateCoordinator(DataUpdateCoordinator[None]):
await self.system.update()
except AqualinkServiceUnauthorizedException as err:
raise ConfigEntryAuthFailed("Invalid credentials for iAquaLink") from err
except AqualinkServiceThrottledException:
_LOGGER.warning(
"Rate limited by iAquaLink system %s, will retry later",
self.system.serial,
)
return
except (AqualinkServiceException, httpx.HTTPError) as err:
raise UpdateFailed(
f"Unable to update iAquaLink system {self.system.serial}: {err}"
@@ -9,5 +9,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "platinum",
"requirements": ["aioimmich==0.14.0"]
"requirements": ["aioimmich==0.14.1"]
}
+2 -2
View File
@@ -118,6 +118,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
device = devices.add_x10_device(housecode, unitcode, x10_type, steps)
create_insteon_device(hass, devices.modem, entry.entry_id)
await hass.config_entries.async_forward_entry_setups(entry, INSTEON_PLATFORMS)
for address in devices:
@@ -131,8 +133,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
register_new_device_callback(hass)
async_setup_services(hass)
create_insteon_device(hass, devices.modem, entry.entry_id)
entry.async_create_background_task(
hass, async_get_device_config(hass, entry), "insteon-get-device-config"
)
@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["pyintesishome"],
"quality_scale": "legacy",
"requirements": ["pyintesishome==1.8.7"]
"requirements": ["pyintesishome==1.8.8"]
}
@@ -1,7 +1,7 @@
{
"entity": {
"binary_sensor": {
"erev_shabbat_hag": { "default": "mdi:candle-light" },
"erev_shabbat_hag": { "default": "mdi:candle" },
"issur_melacha_in_effect": { "default": "mdi:power-plug-off" },
"motzei_shabbat_hag": { "default": "mdi:fire" }
},
+1 -1
View File
@@ -7,7 +7,7 @@
"service": "mdi:lock-open"
},
"disable": {
"service": "mdi:fash-off"
"service": "mdi:flash-off"
},
"enable": {
"service": "mdi:flash"
+4 -4
View File
@@ -28,25 +28,25 @@
"ice_maker": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_bottom_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_middle_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
},
"ice_maker_top_zone": {
"default": "mdi:cube-outline",
"state": {
"off": "mdi:cube-outline-off"
"off": "mdi:cube-off-outline"
}
}
},
@@ -5,7 +5,7 @@ from __future__ import annotations
from datetime import timedelta
from typing import Any
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED, BridgeResponseError
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -133,7 +133,11 @@ class LutronCasetaBatterySensor(LutronCasetaEntity, BinarySensorEntity):
async def async_update(self) -> None:
"""Fetch the latest battery status from the bridge."""
status = await self._smartbridge.get_battery_status(self.device_id)
try:
status = await self._smartbridge.get_battery_status(self.device_id)
except BridgeResponseError:
self._attr_is_on = None
return
normalized_status = status.strip().casefold() if status else None
if normalized_status == BATTERY_STATUS_LOW:
self._attr_is_on = True
+2 -2
View File
@@ -5,8 +5,8 @@ from aiolyric.exceptions import LyricAuthenticationException, LyricException
DOMAIN = "lyric"
OAUTH2_AUTHORIZE = "https://api.honeywell.com/oauth2/authorize"
OAUTH2_TOKEN = "https://api.honeywell.com/oauth2/token"
OAUTH2_AUTHORIZE = "https://api.honeywellhome.com/oauth2/authorize"
OAUTH2_TOKEN = "https://api.honeywellhome.com/oauth2/token"
PRESET_NO_HOLD = "NoHold"
PRESET_TEMPORARY_HOLD = "TemporaryHold"
+1 -1
View File
@@ -22,5 +22,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aiolyric"],
"requirements": ["aiolyric==2.0.2"]
"requirements": ["aiolyric==2.1.1"]
}
@@ -455,12 +455,15 @@ class ManualAlarm(AlarmControlPanelEntity, RestoreEntity):
await super().async_added_to_hass()
if state := await self.async_get_last_state():
self._state_ts = state.last_updated
if next_state := state.attributes.get(ATTR_NEXT_STATE):
# If in arming or pending state we record the transition,
# not the current state
self._state = AlarmControlPanelState(next_state)
else:
self._state = AlarmControlPanelState(state.state)
try:
if next_state := state.attributes.get(ATTR_NEXT_STATE):
# If in arming or pending state we record the transition,
# not the current state
self._state = AlarmControlPanelState(next_state)
else:
self._state = AlarmControlPanelState(state.state)
except ValueError:
return
if prev_state := state.attributes.get(ATTR_PREVIOUS_STATE):
self._previous_state = prev_state
+1 -1
View File
@@ -102,7 +102,7 @@
"default": "mdi:home-lightning-bolt"
},
"eve_weather_trend": {
"default": "mdi:weather",
"default": "mdi:weather-cloudy",
"state": {
"cloudy": "mdi:weather-cloudy",
"rainy": "mdi:weather-rainy",
+3 -1
View File
@@ -511,7 +511,9 @@ class DishWasherProgramId(MieleEnum, missing_to_none=True):
tall_items = 17, 42
glasses_warm = 19
quick_intense = 21
normal = 30
normal = 23, 30
pre_wash = 24
pot_rests_and_filters = 25
power_wash = 44, 204
comfort_wash = 203
comfort_wash_plus = 209
@@ -709,6 +709,7 @@
"pork_tenderloin_medaillons_4_cm": "Pork tenderloin (medaillons, 4 cm)",
"pork_tenderloin_medaillons_5_cm": "Pork tenderloin (medaillons, 5 cm)",
"pork_with_crackling": "Pork with crackling",
"pot_rests_and_filters": "Pot rests and filters",
"potato_cheese_gratin": "Potato cheese gratin",
"potato_dumplings_half_half_boil_in_bag": "Potato dumplings (half/half, boil-in-bag)",
"potato_dumplings_half_half_deep_frozen": "Potato dumplings (half/half, deep-frozen)",
@@ -751,6 +752,7 @@
"powerfresh": "PowerFresh",
"prawns": "Prawns",
"pre_ironing": "Pre-ironing",
"pre_wash": "Pre-wash",
"proofing": "Proofing",
"prove_15_min": "Prove for 15 min",
"prove_30_min": "Prove for 30 min",
@@ -597,8 +597,8 @@ class OpenAIBaseLLMEntity(Entity):
)
)
if "reasoning" not in model_args:
# Reasoning models handle this correctly with just a prompt
if not model_args["model"].startswith("o"):
# o-series models handle this correctly with just a prompt
remove_citations = True
tools.append(web_search)
@@ -37,11 +37,15 @@ class OpenhomeConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.debug("async_step_ssdp: Incomplete discovery, ignoring")
return self.async_abort(reason="incomplete_discovery")
_LOGGER.debug(
"async_step_ssdp: setting unique id %s", discovery_info.upnp[ATTR_UPNP_UDN]
)
udn = discovery_info.upnp[ATTR_UPNP_UDN]
if isinstance(udn, list):
if not udn:
return self.async_abort(reason="incomplete_discovery")
udn = udn[0]
await self.async_set_unique_id(discovery_info.upnp[ATTR_UPNP_UDN])
_LOGGER.debug("async_step_ssdp: setting unique id %s", udn)
await self.async_set_unique_id(udn)
self._abort_if_unique_id_configured({CONF_HOST: discovery_info.ssdp_location})
_LOGGER.debug(
@@ -74,11 +74,13 @@ class ValveHeatingTemperatureInterface(OverkizEntity, ClimateEntity):
)
@property
def hvac_action(self) -> HVACAction:
def hvac_action(self) -> HVACAction | None:
"""Return the current running hvac operation."""
return OVERKIZ_TO_HVAC_ACTION[
cast(str, self.executor.select_state(OverkizState.CORE_OPEN_CLOSED_VALVE))
]
if (
state := self.executor.select_state(OverkizState.CORE_OPEN_CLOSED_VALVE)
) is None:
return None
return OVERKIZ_TO_HVAC_ACTION[cast(str, state)]
@property
def target_temperature(self) -> float:
+159 -6
View File
@@ -70,6 +70,7 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
set_position_command=OverkizCommand.SET_DEPLOYMENT,
open_command=OverkizCommand.DEPLOY,
close_command=OverkizCommand.UNDEPLOY,
stop_command=OverkizCommand.STOP,
invert_position=False,
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
),
@@ -80,6 +81,7 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
set_position_command=OverkizCommand.SET_DEPLOYMENT,
open_command=OverkizCommand.DEPLOY,
close_command=OverkizCommand.UNDEPLOY,
stop_command=OverkizCommand.STOP,
invert_position=False,
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
),
@@ -146,6 +148,36 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
close_tilt_command_args=(15, 1), # position (1-127), speed (1-15)
stop_tilt_command=OverkizCommand.STOP,
),
# Needs override to support very specific tilt commands (rts:SheerBlindRTSComponent)
# uiClass is VenetianBlind
OverkizCoverDescription(
key=UIWidget.UP_DOWN_SHEER_SCREEN,
device_class=CoverDeviceClass.BLIND,
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
stop_command=OverkizCommand.STOP,
open_tilt_command=OverkizCommand.TILT_POSITIVE,
open_tilt_command_args=(15, 1), # position (1-127), speed (1-15)
close_tilt_command=OverkizCommand.TILT_NEGATIVE,
close_tilt_command_args=(15, 1), # position (1-127), speed (1-15)
stop_tilt_command=OverkizCommand.STOP,
),
# Needs override since BioclimaticPergola uses core:SlatsOpenClosedState
# and core:SlateOrientationState (tilt-only, no position)
# uiClass is Pergola
OverkizCoverDescription(
key=UIWidget.BIOCLIMATIC_PERGOLA,
device_class=CoverDeviceClass.AWNING,
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
stop_command=OverkizCommand.STOP,
is_closed_state=OverkizState.CORE_SLATS_OPEN_CLOSED,
current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION,
set_tilt_position_command=OverkizCommand.SET_ORIENTATION,
open_tilt_command=OverkizCommand.OPEN_SLATS,
close_tilt_command=OverkizCommand.CLOSE_SLATS,
stop_tilt_command=OverkizCommand.STOP,
),
# Needs override since PositionableGarageDoor reports
# core:OpenClosedUnknownState instead of core:OpenClosedState
# uiClass is GarageDoor
@@ -159,6 +191,17 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
stop_command=OverkizCommand.STOP,
is_closed_state=OverkizState.CORE_OPEN_CLOSED_UNKNOWN,
),
# Needs override since DiscretePositionableGarageDoor reports
# core:OpenClosedUnknownState instead of core:OpenClosedState
# uiClass is GarageDoor
OverkizCoverDescription(
key=UIWidget.DISCRETE_POSITIONABLE_GARAGE_DOOR,
device_class=CoverDeviceClass.GARAGE,
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
stop_command=OverkizCommand.STOP,
is_closed_state=OverkizState.CORE_OPEN_CLOSED_UNKNOWN,
),
# Needs override since PositionableGarageDoorWithPartialPosition reports
# core:OpenClosedPartialState instead of core:OpenClosedState
# uiClass is GarageDoor
@@ -183,6 +226,111 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN,
stop_command=OverkizCommand.STOP,
),
# Needs override since OpenCloseGate4T only supports the cycle command
# (rts:GateOpenerRTS4TComponent)
# uiClass is Gate
OverkizCoverDescription(
key=UIWidget.OPEN_CLOSE_GATE_4T,
device_class=CoverDeviceClass.GATE,
open_command=OverkizCommand.CYCLE,
close_command=OverkizCommand.CYCLE,
),
# Needs override since UpDownGarageDoor4T only supports the cycle command
# (rts:GarageDoor4TRTSComponent)
# uiClass is GarageDoor
OverkizCoverDescription(
key=UIWidget.UP_DOWN_GARAGE_DOOR_4T,
device_class=CoverDeviceClass.GARAGE,
open_command=OverkizCommand.CYCLE,
close_command=OverkizCommand.CYCLE,
),
# Needs override since OpenCloseSlidingGarageDoor4T only supports the cycle command
# uiClass is GarageDoor
OverkizCoverDescription(
key=UIWidget.OPEN_CLOSE_SLIDING_GARAGE_DOOR_4T,
device_class=CoverDeviceClass.GARAGE,
open_command=OverkizCommand.CYCLE,
close_command=OverkizCommand.CYCLE,
),
# Needs override since OpenCloseSlidingGate4T only supports the cycle command
# uiClass is Gate
OverkizCoverDescription(
key=UIWidget.OPEN_CLOSE_SLIDING_GATE_4T,
device_class=CoverDeviceClass.GATE,
open_command=OverkizCommand.CYCLE,
close_command=OverkizCommand.CYCLE,
),
# Needs override since CyclicGarageDoor only supports the cycle command
# (io:CyclicGarageOpenerIOComponent)
# uiClass is GarageDoor
OverkizCoverDescription(
key=UIWidget.CYCLIC_GARAGE_DOOR,
device_class=CoverDeviceClass.GARAGE,
open_command=OverkizCommand.CYCLE,
close_command=OverkizCommand.CYCLE,
),
# Needs override since CyclicSlidingGateOpener only supports the cycle command
# uiClass is Gate
OverkizCoverDescription(
key=UIWidget.CYCLIC_SLIDING_GATE_OPENER,
device_class=CoverDeviceClass.GATE,
open_command=OverkizCommand.CYCLE,
close_command=OverkizCommand.CYCLE,
),
# Needs override since CyclicSwingingGateOpener only supports the cycle command
# uiClass is Gate
OverkizCoverDescription(
key=UIWidget.CYCLIC_SWINGING_GATE_OPENER,
device_class=CoverDeviceClass.GATE,
open_command=OverkizCommand.CYCLE,
close_command=OverkizCommand.CYCLE,
),
# Needs override since SlidingDiscreteGateWithPedestrianPosition reports
# core:OpenClosedPedestrianState instead of core:OpenClosedState
# uiClass is Gate
OverkizCoverDescription(
key=UIWidget.SLIDING_DISCRETE_GATE_WITH_PEDESTRIAN_POSITION,
device_class=CoverDeviceClass.GATE,
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN,
stop_command=OverkizCommand.STOP,
),
# Needs override since OpenCloseGateWithPedestrianPosition reports
# core:OpenClosedPedestrianState instead of core:OpenClosedState
# uiClass is Gate
OverkizCoverDescription(
key=UIWidget.OPEN_CLOSE_GATE_WITH_PEDESTRIAN_POSITION,
device_class=CoverDeviceClass.GATE,
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN,
stop_command=OverkizCommand.STOP,
),
# Needs override since OpenCloseSlidingGateWithPedestrianPosition reports
# core:OpenClosedPedestrianState instead of core:OpenClosedState
# uiClass is Gate
OverkizCoverDescription(
key=UIWidget.OPEN_CLOSE_SLIDING_GATE_WITH_PEDESTRIAN_POSITION,
device_class=CoverDeviceClass.GATE,
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN,
stop_command=OverkizCommand.STOP,
),
# Needs override since PositionableGateWithPedestrianPosition reports
# core:OpenClosedPedestrianState instead of core:OpenClosedState
# uiClass is Gate
OverkizCoverDescription(
key=UIWidget.POSITIONABLE_GATE_WITH_PEDESTRIAN_POSITION,
device_class=CoverDeviceClass.GATE,
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN,
current_position_state=OverkizState.CORE_CLOSURE,
set_position_command=OverkizCommand.SET_CLOSURE,
stop_command=OverkizCommand.STOP,
),
# Needs override to support this Generic device (rts:GenericRTSComponent)
# uiClass is Generic (not mapped to cover as this is a Generic device class)
OverkizCoverDescription(
@@ -273,15 +421,12 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
OverkizCoverDescription(
key=UIClass.PERGOLA,
device_class=CoverDeviceClass.AWNING,
current_position_state=OverkizState.CORE_CLOSURE,
set_position_command=OverkizCommand.SET_CLOSURE,
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
stop_command=OverkizCommand.STOP,
is_closed_state=OverkizState.CORE_SLATS_OPEN_CLOSED,
current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION,
set_tilt_position_command=OverkizCommand.SET_ORIENTATION,
open_tilt_command=OverkizCommand.OPEN_SLATS,
close_tilt_command=OverkizCommand.CLOSE_SLATS,
stop_tilt_command=OverkizCommand.STOP,
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
),
OverkizCoverDescription(
key=UIClass.ROLLER_SHUTTER,
@@ -292,6 +437,9 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
close_command=OverkizCommand.CLOSE,
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
stop_command=OverkizCommand.STOP,
current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION,
set_tilt_position_command=OverkizCommand.SET_ORIENTATION,
stop_tilt_command=OverkizCommand.STOP,
),
OverkizCoverDescription(
key=UIClass.SCREEN,
@@ -326,9 +474,14 @@ COVER_DESCRIPTIONS: list[OverkizCoverDescription] = [
OverkizCoverDescription(
key=UIClass.VENETIAN_BLIND,
device_class=CoverDeviceClass.BLIND,
current_position_state=OverkizState.CORE_CLOSURE,
set_position_command=OverkizCommand.SET_CLOSURE,
open_command=OverkizCommand.OPEN,
close_command=OverkizCommand.CLOSE,
stop_command=OverkizCommand.STOP,
is_closed_state=OverkizState.CORE_OPEN_CLOSED,
current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION,
set_tilt_position_command=OverkizCommand.SET_ORIENTATION,
open_tilt_command=OverkizCommand.TILT_UP,
close_tilt_command=OverkizCommand.TILT_DOWN,
stop_tilt_command=OverkizCommand.STOP,
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["qbittorrent"],
"requirements": ["qbittorrent-api==2024.9.67"]
"requirements": ["qbittorrent-api==2026.5.1"]
}
+16 -16
View File
@@ -29,29 +29,29 @@
}
},
"sensor": {
"translation_key_0": {
"default": "mdi:abc"
"flow_sensor_clicks_cubic_meter": {
"default": "mdi:water-pump"
},
"translation_key_1": {
"default": "mdi:abc"
"flow_sensor_consumed_liters": {
"default": "mdi:water-pump"
},
"translation_key_2": {
"default": "mdi:abc"
"flow_sensor_leak_clicks": {
"default": "mdi:pipe-leak"
},
"translation_key_3": {
"default": "mdi:abc"
"flow_sensor_leak_volume": {
"default": "mdi:pipe-leak"
},
"translation_key_4": {
"default": "mdi:abc"
"flow_sensor_start_index": {
"default": "mdi:water-pump"
},
"translation_key_5": {
"default": "mdi:abc"
"flow_sensor_watering_clicks": {
"default": "mdi:water-pump"
},
"translation_key_6": {
"default": "mdi:abc"
"last_leak_detected": {
"default": "mdi:pipe-leak"
},
"translation_key_7": {
"default": "mdi:abc"
"rain_sensor_rain_start": {
"default": "mdi:weather-pouring"
}
},
"switch": {
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "silver",
"requirements": ["renault-api==0.5.8"]
"requirements": ["renault-api==0.5.10"]
}
@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==5.5.1",
"python-roborock==5.12.0",
"vacuum-map-parser-roborock==0.1.4"
]
}
+43 -10
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import base64
import binascii
import contextlib
from dataclasses import dataclass
import datetime
import hashlib
@@ -40,6 +41,15 @@ from .utils import get_device_entry_gen
CONTENT_TYPE_AUDIO = "audio"
CONTENT_TYPE_RADIO = "radio"
ALLOWED_IMAGE_MIME_TYPES: Final = frozenset(
{
"image/gif",
"image/jpeg",
"image/png",
"image/webp",
}
)
PARALLEL_UPDATES = 0
@@ -104,6 +114,9 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
_last_media_position: int | None = None
_last_media_position_updated_at: datetime.datetime | None = None
_cached_thumb: str | None = None
_cached_thumb_result: tuple[bytes, str] | None = None
def __init__(
self,
coordinator: ShellyRpcCoordinator,
@@ -217,9 +230,11 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
@property
def media_image_hash(self) -> str | None:
"""Hash value for media image."""
if (thumb := self._media_meta.get("thumb")) and thumb.startswith("data"):
return hashlib.sha256(thumb.encode("utf-8")).hexdigest()[:16]
return super().media_image_hash
thumb = self._media_meta.get("thumb")
if not thumb or self._decode_image_data(thumb) is None:
return super().media_image_hash
return hashlib.sha256(thumb.encode("utf-8")).hexdigest()[:16]
def _get_updated_media_position(self) -> int | None:
"""Return the current playback position and update its timestamp."""
@@ -237,15 +252,11 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
"""Fetch media image of current playing track."""
thumb = self._media_meta["thumb"]
try:
prefix, image_data = thumb.split(",", 1)
image = base64.b64decode(image_data, validate=True)
mime = prefix.split(";", 1)[0].rsplit(":", 1)[-1]
except binascii.Error, ValueError:
thumb = self._media_meta.get("thumb")
if not thumb or (result := self._decode_image_data(thumb)) is None:
return await super().async_get_media_image()
return image, mime
return result
@rpc_call
async def async_media_play(self) -> None:
@@ -436,3 +447,25 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
translation_key="unsupported_media_type",
translation_placeholders={"media_type": str(media_type)},
)
def _decode_image_data(self, thumb: str) -> tuple[bytes, str] | None:
"""Return image_bytes and mime_type for a valid image data or None."""
if thumb == self._cached_thumb:
return self._cached_thumb_result
result: tuple[bytes, str] | None = None
if thumb.startswith("data"):
try:
prefix, image_data = thumb.split(",", 1)
mime = prefix.split(";", 1)[0].rsplit(":", 1)[-1]
except IndexError, ValueError:
pass
else:
if mime in ALLOWED_IMAGE_MIME_TYPES:
with contextlib.suppress(binascii.Error):
result = base64.b64decode(image_data, validate=True), mime
self._cached_thumb = thumb
self._cached_thumb_result = result
return result
+3 -1
View File
@@ -158,6 +158,8 @@ NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = {
set_value_fn=_async_set_foot_warmer_time,
get_name_fn=_get_foot_warming_name,
get_unique_id_fn=_get_foot_warming_unique_id,
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=NumberDeviceClass.DURATION,
),
CORE_CLIMATE_TIMER: SleepIQNumberEntityDescription(
key=CORE_CLIMATE_TIMER,
@@ -170,7 +172,7 @@ NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = {
set_value_fn=_async_set_core_climate_time,
get_name_fn=_get_core_climate_name,
get_unique_id_fn=_get_core_climate_unique_id,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=NumberDeviceClass.DURATION,
),
}
@@ -396,7 +396,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
)
],
},
@@ -449,7 +449,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
)
],
},
@@ -567,7 +567,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.GAS_METER_TIME,
translation_key="gas_meter_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
)
],
Attribute.GAS_METER_VOLUME: [
@@ -728,7 +728,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
component_fn=lambda component: component == "cavity-01",
component_translation_key={
"cavity-01": "oven_completion_time_cavity_01",
@@ -1198,7 +1198,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
component_fn=lambda component: component == "sub",
component_translation_key={
"sub": "washer_sub_completion_time",
@@ -145,7 +145,7 @@
"config_entry_not_ready": {
"message": "Error while loading the config entry."
},
"update_error": {
"update_failed": {
"message": "Error while updating data from the API."
}
}
@@ -425,7 +425,7 @@ async def async_setup_entry(
),
supports_response=SupportsResponse.ONLY,
description_placeholders={
"syntax_keys_documentation_url": "http://robotjs.io/docs/syntax#keys"
"syntax_keys_documentation_url": "https://robotjs.dev/docs/syntax#keys"
},
)
@@ -100,7 +100,7 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]):
except TedeeLocalAuthException as ex:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="authentification_failed",
translation_key="authentication_failed",
) from ex
except TedeeDataUpdateException as ex:
@@ -92,16 +92,6 @@ class TeleinfoConfigFlow(ConfigFlow, domain=DOMAIN):
usb.get_serial_by_id, discovery_info.device
)
# Validate by reading a real Teleinfo frame — silent abort on failure
errors, decoded_data = await self._validate_serial_port(dev_path)
if errors or decoded_data is None:
return self.async_abort(reason="not_teleinfo_device")
# Use ADCO (meter serial number) as unique_id — same as manual entry
adco = decoded_data["ADCO"]
await self.async_set_unique_id(adco)
self._abort_if_unique_id_configured(updates={CONF_SERIAL_PORT: dev_path})
self._discovered_device = dev_path
self.context["title_placeholders"] = {
"name": human_readable_device_name(
@@ -122,6 +112,20 @@ class TeleinfoConfigFlow(ConfigFlow, domain=DOMAIN):
if TYPE_CHECKING:
assert self._discovered_device is not None
if user_input is not None:
# Validate by reading a real Teleinfo frame — silent abort on failure
errors, decoded_data = await self._validate_serial_port(
self._discovered_device
)
if errors or decoded_data is None:
return self.async_abort(reason="not_teleinfo_device")
# Use ADCO (meter serial number) as unique_id — same as manual entry
adco = decoded_data["ADCO"]
await self.async_set_unique_id(adco)
self._abort_if_unique_id_configured(
updates={CONF_SERIAL_PORT: self._discovered_device}
)
return self.async_create_entry(
title=f"Teleinfo ({self._discovered_device})",
data={CONF_SERIAL_PORT: self._discovered_device},
@@ -8,15 +8,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["pyteleinfo==0.4.0"],
"usb": [
{
"pid": "6015",
"vid": "0403"
},
{
"pid": "EA60",
"vid": "10C4"
}
]
"requirements": ["pyteleinfo==0.4.0"]
}
@@ -497,7 +497,7 @@
"default": "mdi:battery-clock"
},
"forward_collision_warning": {
"default": "mdi:car-crash",
"default": "mdi:car-emergency",
"state": {
"average": "mdi:alert-circle",
"early": "mdi:alert-octagon",
@@ -634,7 +634,7 @@
"default": "mdi:key"
},
"pedal_position": {
"default": "mdi:pedestal"
"default": "mdi:gauge"
},
"powershare_hours_left": {
"default": "mdi:clock-time-eight-outline"
@@ -794,7 +794,7 @@
"service": "mdi:calendar-plus"
},
"add_precondition_schedule": {
"service": "mdi:hvac-outline"
"service": "mdi:hvac"
},
"navigation_gps_request": {
"service": "mdi:crosshairs-gps"
@@ -803,7 +803,7 @@
"service": "mdi:calendar-minus"
},
"remove_precondition_schedule": {
"service": "mdi:hvac-off-outline"
"service": "mdi:hvac-off"
},
"set_scheduled_charging": {
"service": "mdi:timeline-clock-outline"
@@ -139,7 +139,7 @@ def ws_start_preview(
name=name,
lower=msg["user_input"].get(CONF_LOWER),
upper=msg["user_input"].get(CONF_UPPER),
hysteresis=msg["user_input"].get(CONF_HYSTERESIS),
hysteresis=msg["user_input"].get(CONF_HYSTERESIS, DEFAULT_HYSTERESIS),
device_class=None,
unique_id=None,
)
@@ -15,6 +15,7 @@ import voluptuous as vol
from homeassistant.components.sensor import (
ATTR_LAST_RESET,
DEVICE_CLASS_STATE_CLASSES,
DEVICE_CLASS_UNITS,
RestoreSensor,
SensorDeviceClass,
@@ -408,11 +409,12 @@ class UtilityMeterSensor(RestoreSensor):
self._current_tz = None
self._config_scheduler()
def _config_scheduler(self):
def _config_scheduler(self, start_time: datetime | None = None) -> None:
self.scheduler = (
CronSim(
self._cron_pattern,
dt_util.now(
start_time
or dt_util.now(
dt_util.get_default_time_zone()
), # we need timezone for DST purposes (see issue #102984)
)
@@ -610,8 +612,6 @@ class UtilityMeterSensor(RestoreSensor):
# and we need to reconfigure the scheduler
self._current_tz = self.hass.config.time_zone
await self._program_reset()
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_RESET_METER, self.async_reset_meter
@@ -630,6 +630,13 @@ class UtilityMeterSensor(RestoreSensor):
if last_sensor_data.status == COLLECTING:
# Null lambda to allow cancelling the collection on tariff change
self._collecting = lambda: None
# Reconfigure the scheduler from the restored last_reset so that
# next_reset is not shifted forward on entity restore/rename.
self._config_scheduler(
dt_util.as_local(self._last_reset) if self._last_reset else None
)
await self._program_reset()
@callback
def async_source_tracking(event):
@@ -697,12 +704,18 @@ class UtilityMeterSensor(RestoreSensor):
@property
def state_class(self) -> SensorStateClass:
"""Return the device class of the sensor."""
return (
SensorStateClass.TOTAL
if self._sensor_net_consumption
else SensorStateClass.TOTAL_INCREASING
)
"""Return the state class of the sensor."""
if self._sensor_net_consumption:
return SensorStateClass.TOTAL
if (
self._input_device_class is not None
and SensorStateClass.TOTAL_INCREASING
not in DEVICE_CLASS_STATE_CLASSES.get(
self._input_device_class, {SensorStateClass.TOTAL_INCREASING}
)
):
return SensorStateClass.TOTAL
return SensorStateClass.TOTAL_INCREASING
@property
def extra_state_attributes(self) -> dict[str, Any]:
@@ -11,6 +11,7 @@ from homeassistant.components.alarm_control_panel import (
CodeFormat,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -65,6 +66,12 @@ class VerisureAlarm(
self.coordinator.verisure.request, command_data
)
LOGGER.debug("Verisure set arm state %s", state)
if arm_state is None or "data" not in arm_state:
await self.coordinator.async_refresh()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="arm_state_failed",
)
result = None
attempts = 0
while result is None:
@@ -79,6 +86,8 @@ class VerisureAlarm(
list(arm_state["data"].values())[0], state
),
)
if transaction is None:
continue
result = (
transaction.get("data", {})
.get("installation", {})
@@ -51,6 +51,11 @@
}
}
},
"exceptions": {
"arm_state_failed": {
"message": "Failed to change alarm state. Verify your code is correct and that your account is not temporarily locked."
}
},
"options": {
"step": {
"init": {
@@ -185,8 +185,10 @@ class WeatherFlowWindCoordinator(BaseWebsocketCoordinator[EventDataRapidWind]):
"""Create rapid wind listen message."""
return RapidWindListenStartMessage(device_id=str(device_id))
async def _handle_websocket_message(self, data: RapidWindWS) -> None:
async def _handle_websocket_message(self, data: RapidWindWS | None) -> None:
"""Handle rapid wind websocket data."""
if data is None:
return
device_id = data.device_id
station_id = self.device_to_station_map[device_id]
@@ -204,8 +206,12 @@ class WeatherFlowObservationCoordinator(BaseWebsocketCoordinator[WebsocketObserv
"""Create observation listen message."""
return ListenStartMessage(device_id=str(device_id))
async def _handle_websocket_message(self, data: ObservationTempestWS) -> None:
async def _handle_websocket_message(
self, data: ObservationTempestWS | None
) -> None:
"""Handle observation websocket data."""
if data is None:
return
device_id = data.device_id
station_id = self.device_to_station_map[device_id]
@@ -65,7 +65,7 @@
"state": {
"lightning": "mdi:weather-lightning-rainy",
"rain": "mdi:weather-rainy",
"rain_snow": "mdi:weather-snoy-rainy",
"rain_snow": "mdi:weather-snowy-rainy",
"snow": "mdi:weather-snowy"
}
},
+1 -1
View File
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["wled==0.22.0"],
"requirements": ["wled==0.23.0"],
"zeroconf": ["_wled._tcp.local."]
}
@@ -195,7 +195,7 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
return
if event.type == assist_pipeline.PipelineEventType.RUN_START:
if event.data and (tts_output := event.data["tts_output"]):
if event.data and (tts_output := event.data.get("tts_output")):
# Get stream token early.
# If "tts_start_streaming" is True in INTENT_PROGRESS event, we
# can start streaming TTS before the TTS_END event.
+32 -2
View File
@@ -5,6 +5,7 @@ import logging
from zoneinfo import ZoneInfo
import voluptuous as vol
from yarl import URL
from zha.application.const import BAUD_RATES, RadioType
from zha.application.gateway import Gateway
from zha.application.helpers import ZHAData
@@ -32,6 +33,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from . import homeassistant_hardware, repairs, websocket_api
from .config_flow import ZhaConfigFlowHandler
from .const import (
CONF_BAUDRATE,
CONF_CUSTOM_QUIRKS_PATH,
@@ -43,6 +45,7 @@ from .const import (
CONF_ZIGPY,
DATA_ZHA,
DOMAIN,
LEGACY_ZEROCONF_PORT,
)
from .helpers import (
SIGNAL_ADD_ENTITIES,
@@ -301,7 +304,15 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
_LOGGER.debug(
"Migrating from version %s.%s",
config_entry.version,
config_entry.minor_version,
)
if config_entry.version > ZhaConfigFlowHandler.VERSION:
# This means the user has downgraded from a future major version
return False
if config_entry.version == 1:
data = {
@@ -361,5 +372,24 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
version=5,
)
_LOGGER.info("Migration to version %s successful", config_entry.version)
if config_entry.version == 5 and config_entry.minor_version < 2:
data = {**config_entry.data, CONF_DEVICE: {**config_entry.data[CONF_DEVICE]}}
device_path = data[CONF_DEVICE][CONF_DEVICE_PATH]
if device_path.startswith(("socket://", "tcp://")):
url = URL(device_path)
if url.explicit_port is None:
data[CONF_DEVICE][CONF_DEVICE_PATH] = str(
url.with_port(LEGACY_ZEROCONF_PORT)
)
hass.config_entries.async_update_entry(
config_entry, data=data, version=5, minor_version=2
)
_LOGGER.info(
"Migration to version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True
+8 -2
View File
@@ -47,7 +47,13 @@ from homeassistant.helpers.service_info.usb import UsbServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.util import dt as dt_util
from .const import CONF_BAUDRATE, CONF_FLOW_CONTROL, CONF_RADIO_TYPE, DOMAIN
from .const import (
CONF_BAUDRATE,
CONF_FLOW_CONTROL,
CONF_RADIO_TYPE,
DOMAIN,
LEGACY_ZEROCONF_PORT,
)
from .helpers import get_config_entry_unique_id, get_zha_gateway
from .radio_manager import (
DEVICE_SCHEMA,
@@ -89,7 +95,6 @@ UPLOADED_BACKUP_FILE = "uploaded_backup_file"
REPAIR_MY_URL = "https://my.home-assistant.io/redirect/repairs/"
LEGACY_ZEROCONF_PORT = 6638
LEGACY_ZEROCONF_ESPHOME_API_PORT = 6053
ZEROCONF_SERVICE_TYPE = "_zigbee-coordinator._tcp.local."
@@ -760,6 +765,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
VERSION = 5
MINOR_VERSION = 2
async def _set_unique_id_and_update_ignored_flow(
self, unique_id: str, device_path: str
+2
View File
@@ -64,6 +64,8 @@ DEVICE_PAIRING_STATUS = "pairing_status"
DOMAIN = "zha"
LEGACY_ZEROCONF_PORT = 6638
GROUP_ID = "group_id"
-25
View File
@@ -1265,19 +1265,6 @@ def async_add_entities(
entities.clear()
def _clean_serial_port_path(path: str) -> str:
"""Clean the serial port path, applying corrections where necessary."""
if path.startswith("socket://"):
path = path.strip()
# Removes extraneous brackets from IP addresses (they don't parse in CPython 3.11.4)
if re.match(r"^socket://\[\d+\.\d+\.\d+\.\d+\]:\d+$", path):
path = path.replace("[", "").replace("]", "")
return path
CONF_ZHA_OPTIONS_SCHEMA = vol.Schema(
{
vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION, default=0): vol.All(
@@ -1316,18 +1303,6 @@ def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData:
assert ha_zha_data.config_entry is not None
assert ha_zha_data.yaml_config is not None
# Remove brackets around IP addresses, this no longer works in CPython 3.11.4
# This will be removed in 2023.11.0
path = ha_zha_data.config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
cleaned_path = _clean_serial_port_path(path)
if path != cleaned_path:
_LOGGER.debug("Cleaned serial port path %r -> %r", path, cleaned_path)
ha_zha_data.config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] = cleaned_path
hass.config_entries.async_update_entry(
ha_zha_data.config_entry, data=ha_zha_data.config_entry.data
)
# deep copy the yaml config to avoid modifying the original and to safely
# pass it to the ZHA library
app_config = copy.deepcopy(ha_zha_data.yaml_config.get(CONF_ZIGPY, {}))
+1 -1
View File
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2026
MINOR_VERSION: Final = 5
PATCH_VERSION: Final = "2"
PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
-10
View File
@@ -58,16 +58,6 @@ USB = [
"pid": "0003",
"vid": "04B4",
},
{
"domain": "teleinfo",
"pid": "6015",
"vid": "0403",
},
{
"domain": "teleinfo",
"pid": "EA60",
"vid": "10C4",
},
{
"domain": "velbus",
"pid": "0B1B",
+6 -1
View File
@@ -1722,9 +1722,14 @@ def state_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType
async def async_validate_condition_config(
hass: HomeAssistant, config: ConfigType
hass: HomeAssistant, config: ConfigType | str
) -> ConfigType:
"""Validate config."""
if isinstance(config, str):
config = {
CONF_CONDITION: "template",
CONF_VALUE_TEMPLATE: cv.dynamic_template(config),
}
condition_key: str = config[CONF_CONDITION]
if condition_key in ("and", "not", "or"):
+40 -31
View File
@@ -37,6 +37,27 @@ from .context import (
template_context_manager,
template_cv,
)
from .extensions import (
AreaExtension,
Base64Extension,
CollectionExtension,
ConfigEntryExtension,
CryptoExtension,
DateTimeExtension,
DeviceExtension,
EntityExtension,
FloorExtension,
FunctionalExtension,
IssuesExtension,
LabelExtension,
MathExtension,
RegexExtension,
SerializationExtension,
StateExtension,
StringExtension,
TypeCastExtension,
VersionExtension,
)
from .helpers import result_as_boolean as result_as_boolean
from .render_info import RenderInfo, render_info_cv
from .states import (
@@ -722,37 +743,25 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
] = weakref.WeakValueDictionary()
self.add_extension("jinja2.ext.loopcontrols")
self.add_extension("jinja2.ext.do")
self.add_extension("homeassistant.helpers.template.extensions.AreaExtension")
self.add_extension("homeassistant.helpers.template.extensions.Base64Extension")
self.add_extension(
"homeassistant.helpers.template.extensions.CollectionExtension"
)
self.add_extension(
"homeassistant.helpers.template.extensions.ConfigEntryExtension"
)
self.add_extension("homeassistant.helpers.template.extensions.CryptoExtension")
self.add_extension(
"homeassistant.helpers.template.extensions.DateTimeExtension"
)
self.add_extension("homeassistant.helpers.template.extensions.DeviceExtension")
self.add_extension("homeassistant.helpers.template.extensions.EntityExtension")
self.add_extension("homeassistant.helpers.template.extensions.FloorExtension")
self.add_extension(
"homeassistant.helpers.template.extensions.FunctionalExtension"
)
self.add_extension("homeassistant.helpers.template.extensions.IssuesExtension")
self.add_extension("homeassistant.helpers.template.extensions.LabelExtension")
self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
self.add_extension("homeassistant.helpers.template.extensions.RegexExtension")
self.add_extension(
"homeassistant.helpers.template.extensions.SerializationExtension"
)
self.add_extension("homeassistant.helpers.template.extensions.StateExtension")
self.add_extension("homeassistant.helpers.template.extensions.StringExtension")
self.add_extension(
"homeassistant.helpers.template.extensions.TypeCastExtension"
)
self.add_extension("homeassistant.helpers.template.extensions.VersionExtension")
self.add_extension(AreaExtension)
self.add_extension(Base64Extension)
self.add_extension(CollectionExtension)
self.add_extension(ConfigEntryExtension)
self.add_extension(CryptoExtension)
self.add_extension(DateTimeExtension)
self.add_extension(DeviceExtension)
self.add_extension(EntityExtension)
self.add_extension(FloorExtension)
self.add_extension(FunctionalExtension)
self.add_extension(IssuesExtension)
self.add_extension(LabelExtension)
self.add_extension(MathExtension)
self.add_extension(RegexExtension)
self.add_extension(SerializationExtension)
self.add_extension(StateExtension)
self.add_extension(StringExtension)
self.add_extension(TypeCastExtension)
self.add_extension(VersionExtension)
if hass is not None:
# This environment has access to hass, attach its loader
+1 -1
View File
@@ -2,7 +2,7 @@
aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
aiodns==4.0.0
aiodns==4.0.4
aiogithubapi==26.0.0
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.3.0
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2026.5.2"
version = "2026.5.4"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@@ -23,7 +23,7 @@ classifiers = [
]
requires-python = ">=3.14.2"
dependencies = [
"aiodns==4.0.0",
"aiodns==4.0.4",
# aiogithubapi is needed by frontend; frontend is unconditionally imported at
# module level in `bootstrap.py` and its requirements thus need to be in
# requirements.txt to ensure they are always installed
+1 -1
View File
@@ -3,7 +3,7 @@
-c homeassistant/package_constraints.txt
# Home Assistant Core
aiodns==4.0.0
aiodns==4.0.4
aiogithubapi==26.0.0
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.3.0
+8 -8
View File
@@ -233,7 +233,7 @@ aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
# homeassistant.components.dnsip
aiodns==4.0.0
aiodns==4.0.4
# homeassistant.components.eafm
aioeafm==0.1.2
@@ -294,7 +294,7 @@ aiohue==4.8.1
aioimaplib==2.0.1
# homeassistant.components.immich
aioimmich==0.14.0
aioimmich==0.14.1
# homeassistant.components.apache_kafka
aiokafka==0.10.0
@@ -321,7 +321,7 @@ aiolifx==1.2.1
aiolookin==1.0.0
# homeassistant.components.lyric
aiolyric==2.0.2
aiolyric==2.1.1
# homeassistant.components.mealie
aiomealie==1.2.4
@@ -2192,7 +2192,7 @@ pyinsteon==1.6.4
pyintelliclima==0.3.1
# homeassistant.components.intesishome
pyintesishome==1.8.7
pyintesishome==1.8.8
# homeassistant.components.ipma
pyipma==3.0.9
@@ -2675,7 +2675,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==5.5.1
python-roborock==5.12.0
# homeassistant.components.smarttub
python-smarttub==0.0.47
@@ -2796,7 +2796,7 @@ pyzbar==0.1.7
pyzerproc==0.4.8
# homeassistant.components.qbittorrent
qbittorrent-api==2024.9.67
qbittorrent-api==2026.5.1
# homeassistant.components.qbus
qbusmqttapi==1.4.3
@@ -2835,7 +2835,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.5.8
renault-api==0.5.10
# homeassistant.components.renson
renson-endura-delta==1.7.2
@@ -3328,7 +3328,7 @@ wiim==0.1.2
wirelesstagpy==0.8.1
# homeassistant.components.wled
wled==0.22.0
wled==0.23.0
# homeassistant.components.wolflink
wolf-comm==0.0.48
+7 -7
View File
@@ -224,7 +224,7 @@ aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
# homeassistant.components.dnsip
aiodns==4.0.0
aiodns==4.0.4
# homeassistant.components.eafm
aioeafm==0.1.2
@@ -282,7 +282,7 @@ aiohue==4.8.1
aioimaplib==2.0.1
# homeassistant.components.immich
aioimmich==0.14.0
aioimmich==0.14.1
# homeassistant.components.apache_kafka
aiokafka==0.10.0
@@ -306,7 +306,7 @@ aiolifx==1.2.1
aiolookin==1.0.0
# homeassistant.components.lyric
aiolyric==2.0.2
aiolyric==2.1.1
# homeassistant.components.mealie
aiomealie==1.2.4
@@ -2280,7 +2280,7 @@ python-qube-heatpump==1.8.0
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==5.5.1
python-roborock==5.12.0
# homeassistant.components.smarttub
python-smarttub==0.0.47
@@ -2386,7 +2386,7 @@ pyyardian==1.1.1
pyzerproc==0.4.8
# homeassistant.components.qbittorrent
qbittorrent-api==2024.9.67
qbittorrent-api==2026.5.1
# homeassistant.components.qbus
qbusmqttapi==1.4.3
@@ -2419,7 +2419,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.5.8
renault-api==0.5.10
# homeassistant.components.renson
renson-endura-delta==1.7.2
@@ -2828,7 +2828,7 @@ wiffi==1.1.2
wiim==0.1.2
# homeassistant.components.wled
wled==0.22.0
wled==0.23.0
# homeassistant.components.wolflink
wolf-comm==0.0.48
-2
View File
@@ -285,8 +285,6 @@ FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = {
"lacrosse": {"homeassistant": {"pylacrosse"}},
# ???
"linode": {"homeassistant": {"linode-api"}},
# https://github.com/timmo001/aiolyric
"lyric": {"homeassistant": {"aiolyric"}},
# https://github.com/microBeesTech/pythonSDK/
"microbees": {
"homeassistant": {"microbeespy"},
@@ -0,0 +1,62 @@
"""Tests for Apple TV binary sensor."""
from unittest.mock import AsyncMock, MagicMock, patch
from pyatv.const import DeviceModel, KeyboardFocusState, Protocol
from homeassistant.components.apple_tv.const import DOMAIN
from homeassistant.const import CONF_ADDRESS, CONF_NAME
from homeassistant.core import HomeAssistant
from .common import create_conf, mrp_service
from tests.common import MockConfigEntry
async def test_keyboard_focus_entity_created_on_setup(
hass: HomeAssistant,
mock_async_zeroconf: MagicMock,
) -> None:
"""Test the keyboard focus binary sensor is created when the device supports it.
Regression test for https://github.com/home-assistant/core/issues/170075 the
initial SIGNAL_CONNECTED dispatch happens in async_first_connect (before platform
forwarding), so the binary_sensor platform must also handle the already-connected
case rather than relying solely on the dispatcher signal.
"""
atv = AsyncMock()
atv.close = MagicMock()
atv.features = MagicMock()
atv.features.in_state = MagicMock(return_value=True)
atv.keyboard = AsyncMock()
atv.keyboard.text_focus_state = KeyboardFocusState.Unfocused
atv.push_updater = MagicMock()
atv.device_info.model = DeviceModel.Gen4K
atv.device_info.raw_model = "AppleTV6,2"
atv.device_info.version = "15.0"
atv.device_info.mac = "AA:BB:CC:DD:EE:FF"
entry = MockConfigEntry(
domain=DOMAIN,
title="Living Room",
unique_id="mrpid",
data={
CONF_ADDRESS: "127.0.0.1",
CONF_NAME: "Living Room",
"credentials": {str(Protocol.MRP.value): "mrp_creds"},
"identifiers": ["mrpid"],
},
)
entry.add_to_hass(hass)
scan_result = create_conf("127.0.0.1", "Living Room", mrp_service())
with (
patch("homeassistant.components.apple_tv.scan", return_value=[scan_result]),
patch("homeassistant.components.apple_tv.connect", return_value=atv),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.living_room_keyboard_focus")
assert state is not None
+112 -1
View File
@@ -1,13 +1,15 @@
"""BleBox light entities tests."""
import logging
from unittest.mock import AsyncMock, PropertyMock
from unittest.mock import AsyncMock, MagicMock, PropertyMock
import blebox_uniapi
import pytest
from homeassistant.components.blebox.const import LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT,
ATTR_RGBW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
@@ -329,6 +331,73 @@ def wlightbox_fixture():
return (feature, "light.my_wlightbox_wlightbox_color")
@pytest.fixture(name="wlightbox_ct")
def wlightbox_ct_fixture() -> tuple[MagicMock, str]:
"""Return a default light entity mock for color temperature testing."""
feature = mock_feature(
"lights",
blebox_uniapi.light.Light,
unique_id="BleBox-wLightBox-1afe34e750b8-color",
full_name="wLightBox-ct",
device_class=None,
is_on=None,
supports_color=True,
supports_white=True,
white_value=None,
rgbw_hex=None,
color_mode=blebox_uniapi.light.BleboxColorMode.CT,
effect="NONE",
effect_list=["NONE", "PL", "POLICE"],
)
product = feature.product
type(product).name = PropertyMock(return_value="My wLightBox")
type(product).model = PropertyMock(return_value="wLightBox")
return feature, "light.my_wlightbox_wlightbox_ct"
@pytest.mark.parametrize("kelvin_requested", [1000, 2700, 3000, 4000, 5000, 6500, 8000])
async def test_wlightbox_on_color_temp(
hass: HomeAssistant,
wlightbox_ct: tuple[MagicMock, str],
kelvin_requested: int,
) -> None:
"""Test light on with color temperature change."""
feature_mock, entity_id = wlightbox_ct
# Capture the native scale value passed to the device to verify the
# conversion is correct without depending on blebox_uniapi internals.
transient_temp: int = -1
def return_color_temp_with_brightness(value: int, _brightness: int) -> list[int]:
nonlocal transient_temp
transient_temp = value
return [0x00, 0x39, 0xB0, 0xFF]
def turn_on(_: list[int]) -> None:
feature_mock.is_on = True
feature_mock.color_temp = transient_temp
feature_mock.return_color_temp_with_brightness = return_color_temp_with_brightness
feature_mock.async_on = AsyncMock(side_effect=turn_on)
await async_setup_entity(hass, entity_id)
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{"entity_id": entity_id, ATTR_COLOR_TEMP_KELVIN: kelvin_requested},
blocking=True,
)
state = hass.states.get(entity_id)
assert state.state == STATE_ON
assert 0 <= transient_temp <= 255
kelvin_actual = state.attributes[ATTR_COLOR_TEMP_KELVIN]
assert LIGHT_MIN_KELVINS <= kelvin_actual <= LIGHT_MAX_KELVINS
async def test_wlightbox_init(
wlightbox, hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
@@ -461,6 +530,48 @@ async def test_wlightbox_on_to_last_color(wlightbox, hass: HomeAssistant) -> Non
assert state.state == STATE_ON
async def test_wlightbox_turn_on_with_zero_brightness_turns_off(
wlightbox, hass: HomeAssistant
) -> None:
"""Test that setting brightness to 0 turns the light off instead of raising ValueError."""
feature_mock, entity_id = wlightbox
def initial_update():
feature_mock.is_on = True
feature_mock.rgbw_hex = "c1d2f3c7"
feature_mock.white_value = 0xC7
feature_mock.async_update = AsyncMock(side_effect=initial_update)
await async_setup_entity(hass, entity_id)
feature_mock.async_update = AsyncMock()
state = hass.states.get(entity_id)
assert state.state == STATE_ON
feature_mock.apply_brightness = MagicMock(return_value=[0, 0, 0, 0])
def turn_off():
feature_mock.is_on = False
feature_mock.white_value = 0x0
feature_mock.rgbw_hex = "00000000"
feature_mock.async_off = AsyncMock(side_effect=turn_off)
await hass.services.async_call(
"light",
SERVICE_TURN_ON,
{"entity_id": entity_id, ATTR_BRIGHTNESS: 0},
blocking=True,
)
feature_mock.async_off.assert_called_once()
feature_mock.async_on.assert_not_called()
state = hass.states.get(entity_id)
assert state.state == STATE_OFF
async def test_wlightbox_off(wlightbox, hass: HomeAssistant) -> None:
"""Test light off."""
+56 -2
View File
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable
import datetime
from http import HTTPStatus
from typing import Any
from unittest.mock import MagicMock, Mock
from unittest.mock import MagicMock, Mock, patch
import zoneinfo
from caldav.objects import Event
@@ -1063,13 +1063,67 @@ async def test_get_events_custom_calendars(
"summary": "This is a normal event",
"location": "Hamburg",
"description": "Surprisingly rainy",
"uid": None,
"uid": "0",
"recurrence_id": None,
"rrule": None,
}
]
async def test_get_events_with_recurrence_id(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
) -> None:
"""Test that uid and recurrence_id are populated from VEVENT data."""
vevent_with_recurrence_id = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//E-Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:original-event-uid
RECURRENCE-ID:20171127T170000Z
DTSTAMP:20171125T000000Z
DTSTART:20171127T180000Z
DTEND:20171127T190000Z
SUMMARY:Modified occurrence
LOCATION:Hamburg
DESCRIPTION:This occurrence was moved
END:VEVENT
END:VCALENDAR"""
calendar = Mock()
calendar.name = "Example"
calendar.get_supported_components = MagicMock(return_value=["VEVENT"])
calendar.search = MagicMock(
return_value=[
Event(
None, "0.ics", vevent_with_recurrence_id, calendar, "original-event-uid"
)
]
)
with patch(
"homeassistant.components.caldav.calendar.caldav.DAVClient"
) as mock_client:
mock_client.return_value.principal.return_value.calendars.return_value = [
calendar
]
assert await async_setup_component(
hass, "calendar", {"calendar": CALDAV_CONFIG}
)
await hass.async_block_till_done()
client = await hass_client()
response = await client.get(
f"/api/calendars/{TEST_ENTITY}?start=2017-11-27&end=2017-11-28"
)
assert response.status == HTTPStatus.OK
events = await response.json()
assert len(events) == 1
assert events[0]["uid"] == "original-event-uid"
assert events[0]["recurrence_id"] == "2017-11-27 17:00:00+00:00"
assert events[0]["summary"] == "Modified occurrence"
@pytest.mark.parametrize(
("calendars"),
[
@@ -372,6 +372,10 @@ async def test_sync_google_on_home_assistant_start(
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert len(mock_sync.mock_calls) == 0
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert len(mock_sync.mock_calls) == 1
+34
View File
@@ -385,6 +385,40 @@ async def test_climate_missing_variables(
assert state.attributes["temperature"] == 68.0
@pytest.mark.parametrize(
"mock_climate_variables",
[
{
123: {
"HVAC_STATE": "Off",
"HVAC_MODE": "Heat",
"TEMPERATURE_F": 72.0,
"HUMIDITY": "Undefined",
"COOL_SETPOINT_F": 75.0,
"HEAT_SETPOINT_F": 68.0,
"SCALE": "FAHRENHEIT",
}
}
],
)
@pytest.mark.usefixtures(
"mock_c4_account",
"mock_c4_director",
"mock_climate_update_variables",
"init_integration",
)
async def test_climate_undefined_humidity(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test climate entity handles 'Undefined' humidity string gracefully."""
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == HVACMode.HEAT
assert state.attributes.get("current_temperature") == 72.0
assert state.attributes.get("current_humidity") is None
@pytest.mark.parametrize(
"mock_climate_variables",
[
@@ -367,7 +367,7 @@ def test_master_state(hass: HomeAssistant) -> None:
assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORTED_FEATURES
assert not state.attributes[ATTR_MEDIA_VOLUME_MUTED]
assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.2
assert state.attributes[ATTR_MEDIA_CONTENT_ID] == 12322
assert state.attributes[ATTR_MEDIA_CONTENT_ID] == "12322"
assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC
assert state.attributes[ATTR_MEDIA_DURATION] == 0.05
assert state.attributes[ATTR_MEDIA_POSITION] == 0.005
+35 -1
View File
@@ -1,8 +1,11 @@
"""Test the GoodWe initialization."""
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
from goodwe import InverterError
from homeassistant.components.goodwe import CONF_MODEL_FAMILY, DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@@ -34,3 +37,34 @@ async def test_migration(
assert config_entry.data[CONF_HOST] == TEST_HOST
assert config_entry.data[CONF_MODEL_FAMILY] == "MagicMock"
assert config_entry.data[CONF_PORT] == TEST_PORT
async def test_setup_connect_not_ready(hass: HomeAssistant) -> None:
"""Test that setup raises ConfigEntryNotReady when inverter is unreachable."""
config_entry = MockConfigEntry(
version=2,
domain=DOMAIN,
data={
CONF_HOST: TEST_HOST,
CONF_PORT: TEST_PORT,
CONF_MODEL_FAMILY: "ET",
},
)
config_entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.goodwe.connect",
side_effect=InverterError,
) as mock_connect,
patch(
"homeassistant.components.goodwe.config_flow.connect",
side_effect=InverterError,
),
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY
# Verify connect is called with limited retries to avoid blocking startup
assert mock_connect.call_args.kwargs["retries"] <= 3
+25 -2
View File
@@ -28,8 +28,12 @@ from homeassistant.components.google_assistant.http import (
_get_homegraph_token,
async_get_users,
)
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.core import HomeAssistant, State
from homeassistant.const import (
CLOUD_NEVER_EXPOSED_ENTITIES,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STARTED,
)
from homeassistant.core import CoreState, HomeAssistant, State
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@@ -60,6 +64,25 @@ MOCK_HEADER = {
}
async def test_sync_google_does_not_block_startup(hass: HomeAssistant) -> None:
"""Test that Google entity sync runs after startup, not during."""
hass.set_state(CoreState.not_running)
config = GoogleConfig(hass, DUMMY_CONFIG)
with patch.object(config, "async_sync_entities_all") as mock_sync:
await config.async_initialize()
# Fire EVENT_HOMEASSISTANT_START - sync should NOT run yet
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
mock_sync.assert_not_called()
# Fire EVENT_HOMEASSISTANT_STARTED - now sync should run
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
mock_sync.assert_called_once()
async def test_get_jwt(hass: HomeAssistant) -> None:
"""Test signing of key."""
@@ -125,6 +125,29 @@ async def test_sensors_classic_api(
)
@pytest.mark.freeze_time("2023-10-21")
async def test_mix_empty_chart_data(
hass: HomeAssistant,
mock_growatt_classic_api,
mock_config_entry_classic: MockConfigEntry,
) -> None:
"""Test mix device handles empty chart data without crashing."""
mock_growatt_classic_api.device_list.return_value = [
{"deviceSn": "MIX123456", "deviceType": "mix"}
]
mock_growatt_classic_api.mix_detail.return_value = {
"deviceSn": "MIX123456",
"chartData": {},
}
with patch("homeassistant.components.growatt_server.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry_classic)
# Should not crash - entities should still be created
states = hass.states.async_entity_ids("sensor")
assert len(states) > 0
async def test_sensor_coordinator_updates(
hass: HomeAssistant,
mock_growatt_v1_api,
@@ -1766,6 +1766,12 @@ async def test_create_todo(
},
Task(everyX=5),
),
(
{
ATTR_INTERVAL: 0,
},
Task(everyX=0),
),
(
{
ATTR_FREQUENCY: "weekly",
@@ -717,6 +717,52 @@ async def test_if_fires_using_at_sensor_with_offset(
)
async def test_if_fires_using_at_sensor_dict_without_offset(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
service_calls: list[ServiceCall],
) -> None:
"""Test for firing at sensor time using dict format without offset."""
now = dt_util.now()
trigger_dt = now.replace(hour=5, minute=0, second=0, microsecond=0) + timedelta(2)
hass.states.async_set(
"sensor.next_alarm",
trigger_dt.isoformat(),
{ATTR_DEVICE_CLASS: SensorDeviceClass.TIMESTAMP},
)
time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1)
freezer.move_to(dt_util.as_utc(time_that_will_not_match_right_away))
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger": {
"platform": "time",
"at": {
"entity_id": "sensor.next_alarm",
},
},
"action": {
"service": "test.automation",
"data_template": {"some": "{{ trigger.entity_id }}"},
},
}
},
)
await hass.async_block_till_done()
async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1))
await hass.async_block_till_done()
assert len(service_calls) == 1
assert service_calls[0].data["some"] == "sensor.next_alarm"
@pytest.mark.parametrize(
"conf",
[
@@ -2,6 +2,7 @@
from homeassistant.components.homeassistant_connect_zbt2.const import DOMAIN
from homeassistant.components.usb import DOMAIN as USB_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@@ -65,3 +66,66 @@ async def test_hardware_info(
}
]
}
async def test_hardware_info_ignored_entry(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, addon_store_info
) -> None:
"""Test ignored discovery entries don't crash hardware info.
Regression test for https://github.com/home-assistant/core/issues/170270
"""
assert await async_setup_component(hass, USB_DOMAIN, {})
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
# Setup the normal entry so the hardware platform is loaded
normal_entry = MockConfigEntry(
data=CONFIG_ENTRY_DATA,
domain=DOMAIN,
options={},
title="Home Assistant Connect ZBT-2",
unique_id="normal_1",
version=1,
minor_version=1,
)
normal_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(normal_entry.entry_id)
# Setup an ignored config entry without USB data
ignored_entry = MockConfigEntry(
data={},
domain=DOMAIN,
options={},
title="Home Assistant Connect ZBT-2",
unique_id="ignored_1",
version=1,
minor_version=2,
source="ignore",
)
ignored_entry.add_to_hass(hass)
assert ignored_entry.state is ConfigEntryState.NOT_LOADED
client = await hass_ws_client(hass)
await client.send_json({"id": 1, "type": "hardware/info"})
msg = await client.receive_json()
assert msg["id"] == 1
assert msg["success"]
assert msg["result"] == {
"hardware": [
{
"board": None,
"config_entries": [normal_entry.entry_id],
"dongle": {
"vid": "303A",
"pid": "4001",
"serial_number": "80B54EEFAE18",
"manufacturer": "Nabu Casa",
"description": "ZBT-2",
},
"name": "Home Assistant Connect ZBT-2",
"url": "https://support.nabucasa.com/hc/en-us/categories/24734620813469-Home-Assistant-Connect-ZBT-1",
}
]
}
@@ -116,3 +116,30 @@ async def test_get_triggers(
]
assert triggers == unordered(expected_triggers)
async def test_get_triggers_for_removed_device(
hass: HomeAssistant,
mock_bridge_v2: Mock,
v2_resources_test_data: JsonArrayType,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test triggers for a device removed from the bridge.
Regression test for https://github.com/home-assistant/core/issues/152937
"""
await mock_bridge_v2.api.load_test_data(v2_resources_test_data)
await setup_platform(
hass, mock_bridge_v2, [Platform.BINARY_SENSOR, Platform.SENSOR]
)
# Create a device entry with a Hue ID that doesn't exist on the bridge
orphaned_device = device_registry.async_get_or_create(
config_entry_id=mock_bridge_v2.config_entry.entry_id,
identifiers={(hue.DOMAIN, "non-existent-hue-device-id")},
)
triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, orphaned_device.id
)
assert triggers == []
+56 -1
View File
@@ -6,6 +6,7 @@ from freezegun.api import FrozenDateTimeFactory
from iaqualink.client import AqualinkClient
from iaqualink.exception import (
AqualinkServiceException,
AqualinkServiceThrottledException,
AqualinkServiceUnauthorizedException,
)
from iaqualink.systems.iaqua.device import (
@@ -16,6 +17,7 @@ from iaqualink.systems.iaqua.device import (
IaquaThermostat,
)
from iaqualink.systems.iaqua.system import IaquaSystem
import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
@@ -46,7 +48,9 @@ async def _advance_coordinator_time(
hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
"""Advance time to trigger coordinator update interval."""
freezer.tick(delta=UPDATE_INTERVAL_BY_SYSTEM_TYPE["iaqua"])
update_interval = UPDATE_INTERVAL_BY_SYSTEM_TYPE["iaqua"]
freezer.tick(delta=update_interval)
async_fire_time_changed(hass, dt_util.utcnow())
await hass.async_block_till_done(wait_background_tasks=True)
@@ -104,6 +108,57 @@ async def test_system_refresh_failure_marks_entities_unavailable(
assert state.state == STATE_UNAVAILABLE
async def test_system_rate_limited_keeps_entities_available(
hass: HomeAssistant,
config_entry: MockConfigEntry,
client: AqualinkClient,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test a rate-limited update keeps entities at their last known state."""
config_entry.add_to_hass(hass)
system = get_aqualink_system(client, cls=IaquaSystem)
system.online = True
system.update = AsyncMock()
systems = {system.serial: system}
light = get_aqualink_device(
system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"}
)
devices = {light.name: light}
system.get_devices = AsyncMock(return_value=devices)
with (
patch(
"homeassistant.components.iaqualink.AqualinkClient.login",
return_value=None,
),
patch(
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
return_value=systems,
),
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
assert len(entity_ids) == 1
entity_id = entity_ids[0]
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_ON
system.update = AsyncMock(side_effect=AqualinkServiceThrottledException)
await _advance_coordinator_time(hass, freezer)
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_ON
assert "Rate limited by iAquaLink" in caplog.text
async def test_light_service_calls_update_entity_state(
hass: HomeAssistant,
config_entry: MockConfigEntry,
@@ -1,9 +1,10 @@
"""Tests for the Lutron Caseta binary sensors."""
from typing import Any
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, MagicMock
from freezegun.api import FrozenDateTimeFactory
from pylutron_caseta import BridgeResponseError
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.lutron_caseta.binary_sensor import SCAN_INTERVAL
@@ -114,3 +115,32 @@ async def test_battery_sensor_updates_on_schedule(
assert unknown_state is not None
assert unknown_state.state == STATE_UNKNOWN
assert instance.get_battery_status.await_count == 3
async def test_battery_sensor_handles_bridge_response_error(
hass: HomeAssistant,
) -> None:
"""Test battery sensor handles BridgeResponseError gracefully.
Regression test for https://github.com/home-assistant/core/issues/169965
"""
instance = MockBridge()
def factory(*args: Any, **kwargs: Any) -> MockBridge:
"""Return the mock bridge instance."""
return instance
mock_response = MagicMock()
mock_response.Header.StatusCode = "404 NotFound"
instance.get_battery_status = AsyncMock(
side_effect=BridgeResponseError(mock_response)
)
await async_setup_integration(hass, factory)
await hass.async_block_till_done()
state = hass.states.get(
"binary_sensor.basement_bedroom_basement_bedroom_left_shade_battery"
)
assert state is not None
assert state.state == STATE_UNKNOWN
@@ -1338,6 +1338,33 @@ async def test_restore_state(hass: HomeAssistant, expected_state) -> None:
assert state.state == expected_state
async def test_restore_state_invalid(hass: HomeAssistant) -> None:
"""Ensure invalid restored state does not crash entity setup."""
mock_restore_cache(hass, (State("alarm_control_panel.test", "unknown"),))
hass.set_state(CoreState.starting)
mock_component(hass, "recorder")
assert await async_setup_component(
hass,
alarm_control_panel.DOMAIN,
{
"alarm_control_panel": {
"platform": "manual",
"name": "test",
"arming_time": 0,
"trigger_time": 0,
"disarm_after_trigger": False,
}
},
)
await hass.async_block_till_done()
state = hass.states.get("alarm_control_panel.test")
assert state
assert state.state == AlarmControlPanelState.DISARMED
@pytest.mark.parametrize(
"expected_state",
[
@@ -659,6 +659,52 @@ async def test_web_search(
assert mock_create_stream.mock_calls[1][2]["input"][1:] == snapshot
async def test_web_search_remove_citations_gpt5(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_init_component,
mock_create_stream,
mock_chat_log: MockChatLog, # noqa: F811
) -> None:
"""Test that citations are stripped for GPT-5 models with inline_citations disabled."""
subentry = next(iter(mock_config_entry.subentries.values()))
hass.config_entries.async_update_subentry(
mock_config_entry,
subentry,
data={
**subentry.data,
CONF_CHAT_MODEL: "gpt-5-mini",
CONF_WEB_SEARCH: True,
CONF_WEB_SEARCH_INLINE_CITATIONS: False,
},
)
await hass.config_entries.async_reload(mock_config_entry.entry_id)
message = [
"The match ended 0-2",
" ([legaseriea.it](https://www.legaseriea.it/))",
".",
]
mock_create_stream.return_value = [
(
*create_web_search_item(id="ws_A", output_index=0),
*create_message_item(id="msg_A", text=message, output_index=1),
)
]
result = await conversation.async_converse(
hass,
"What was the score?",
mock_chat_log.conversation_id,
Context(),
agent_id="conversation.openai_conversation",
)
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
# Citation should be stripped from the response
assert result.response.speech["plain"]["speech"] == "The match ended 0-2."
async def test_code_interpreter(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
@@ -116,3 +116,54 @@ async def test_host_updated(hass: HomeAssistant) -> None:
assert result["reason"] == "already_configured"
assert entry.data[CONF_HOST] == MOCK_SSDP_LOCATION
async def test_ssdp_udn_as_list(hass: HomeAssistant) -> None:
"""Test SSDP discovery when UDN is a list instead of a string.
Regression test for https://github.com/home-assistant/core/issues/171837
"""
list_udn_discovery = SsdpServiceInfo(
ssdp_usn="usn",
ssdp_st="st",
ssdp_location=MOCK_SSDP_LOCATION,
upnp={
ATTR_UPNP_FRIENDLY_NAME: MOCK_FRIENDLY_NAME,
ATTR_UPNP_UDN: [MOCK_UDN, "uuid:other"],
},
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_SSDP},
data=list_udn_discovery,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "confirm"
assert result["description_placeholders"] == {CONF_NAME: MOCK_FRIENDLY_NAME}
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == MOCK_FRIENDLY_NAME
assert result2["data"] == {CONF_HOST: MOCK_SSDP_LOCATION}
async def test_ssdp_udn_as_empty_list(hass: HomeAssistant) -> None:
"""Test SSDP discovery when UDN is an empty list."""
empty_udn_discovery = SsdpServiceInfo(
ssdp_usn="usn",
ssdp_st="st",
ssdp_location=MOCK_SSDP_LOCATION,
upnp={
ATTR_UPNP_FRIENDLY_NAME: MOCK_FRIENDLY_NAME,
ATTR_UPNP_UDN: [],
},
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_SSDP},
data=empty_udn_discovery,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "incomplete_discovery"
@@ -464,6 +464,86 @@
"type": 1,
"oid": "3496a041-cafc-4d5d-ab3b-7947985812dc",
"uiClass": "VenetianBlind"
},
{
"creationTime": 1613676710000,
"lastUpdateTime": 1613676710000,
"label": "Kitchen Sheer Screen",
"deviceURL": "rts://1234-1234-6362/16753206",
"shortcut": false,
"controllableName": "rts:SheerBlindRTSComponent",
"definition": {
"commands": [
{
"commandName": "open",
"nparams": 0
},
{
"commandName": "up",
"nparams": 0
},
{
"commandName": "tiltPositive",
"nparams": 2
},
{
"commandName": "tiltNegative",
"nparams": 2
},
{
"commandName": "down",
"nparams": 0
},
{
"commandName": "rest",
"nparams": 0
},
{
"commandName": "openConfiguration",
"nparams": 0
},
{
"commandName": "test",
"nparams": 0
},
{
"commandName": "close",
"nparams": 0
},
{
"commandName": "identify",
"nparams": 0
},
{
"commandName": "moveOf",
"nparams": 1
},
{
"commandName": "my",
"nparams": 0
},
{
"commandName": "stop",
"nparams": 0
}
],
"states": [],
"dataProperties": [],
"widgetName": "UpDownSheerScreen",
"uiProfiles": ["OpenCloseBlind", "OpenClose"],
"uiClass": "VenetianBlind",
"qualifiedName": "rts:SheerBlindRTSComponent",
"type": "ACTUATOR"
},
"states": [],
"attributes": [],
"available": true,
"enabled": true,
"placeOID": "6133b4a0-f514-4553-b635-d1b7beb7e7b2",
"widget": "UpDownSheerScreen",
"type": 1,
"oid": "c198bcdd-8b8b-4dc6-a2b0-f86f7dc7c001",
"uiClass": "VenetianBlind"
}
],
"zones": [],
File diff suppressed because it is too large Load Diff
@@ -1674,6 +1674,706 @@
],
"uiClass": "Light"
}
},
{
"deviceURL": "io://1234-5678-3293/12745774",
"available": true,
"synced": true,
"type": 1,
"states": [
{
"type": 3,
"name": "core:StatusState",
"value": "available"
},
{
"type": 11,
"name": "core:CommandLockLevelsState",
"value": []
},
{
"type": 3,
"name": "core:DiscreteRSSILevelState",
"value": "good"
},
{
"type": 1,
"name": "core:RSSILevelState",
"value": 100
},
{
"type": 3,
"name": "core:OpenClosedUnknownState",
"value": "closed"
},
{
"type": 3,
"name": "core:NameState",
"value": "Garage Door Rollixo"
},
{
"type": 1,
"name": "core:PriorityLockTimerState",
"value": 0
},
{
"type": 3,
"name": "io:PriorityLockOriginatorState",
"value": "unknown"
}
],
"attributes": [
{
"name": "core:Manufacturer",
"type": 3,
"value": "Somfy"
},
{
"name": "core:FirmwareRevision",
"type": 3,
"value": "5105491C15"
}
],
"enabled": true,
"label": "Garage Door Rollixo",
"controllableName": "io:DiscreteGarageOpenerIOComponent",
"subsystemId": 0,
"definition": {
"commands": [
{
"nparams": 0,
"commandName": "up"
},
{
"nparams": 0,
"commandName": "close"
},
{
"nparams": 1,
"commandName": "addLockLevel",
"paramsSig": "p1,*p2"
},
{
"nparams": 0,
"commandName": "resetLockLevels"
},
{
"nparams": 1,
"commandName": "removeLockLevel",
"paramsSig": "p1"
},
{
"nparams": 1,
"commandName": "executeManufacturerProcedure",
"paramsSig": "p1,*p2"
},
{
"nparams": 1,
"commandName": "writeManufacturerData",
"paramsSig": "p1"
},
{
"nparams": 1,
"commandName": "readManufacturerData",
"paramsSig": "p1"
},
{
"nparams": 0,
"commandName": "unpairAllOneWayControllers"
},
{
"nparams": 0,
"commandName": "stopIdentify"
},
{
"nparams": 0,
"commandName": "startIdentify"
},
{
"nparams": 1,
"commandName": "pairOneWayController",
"paramsSig": "p1,*p2"
},
{
"nparams": 1,
"commandName": "delayedStopIdentify",
"paramsSig": "p1"
},
{
"nparams": 0,
"commandName": "sendIOKey"
},
{
"nparams": 0,
"commandName": "unpairAllOneWayControllersAndDeleteNode"
},
{
"nparams": 1,
"commandName": "wink",
"paramsSig": "p1"
},
{
"nparams": 1,
"commandName": "setConfigState",
"paramsSig": "p1"
},
{
"nparams": 1,
"commandName": "advancedRefresh",
"paramsSig": "p1,*p2"
},
{
"nparams": 1,
"commandName": "setName",
"paramsSig": "p1"
},
{
"nparams": 1,
"commandName": "unpairOneWayController",
"paramsSig": "p1,*p2"
},
{
"nparams": 2,
"commandName": "runManufacturerSettingsCommand",
"paramsSig": "p1,p2"
},
{
"nparams": 0,
"commandName": "getName"
},
{
"nparams": 0,
"commandName": "stop"
},
{
"nparams": 0,
"commandName": "open"
},
{
"nparams": 0,
"commandName": "keepOneWayControllersAndDeleteNode"
},
{
"nparams": 0,
"commandName": "identify"
},
{
"nparams": 0,
"commandName": "down"
}
],
"states": [
{
"name": "core:AdditionalStatusState"
},
{
"name": "core:PriorityLockTimerState"
},
{
"name": "io:PriorityLockLevelState"
},
{
"name": "io:PriorityLockOriginatorState"
},
{
"name": "core:StatusState"
},
{
"name": "core:ManufacturerSettingsState"
},
{
"name": "core:ManufacturerDiagnosticsState"
},
{
"name": "core:CommandLockLevelsState"
},
{
"name": "core:NameState"
},
{
"name": "core:DiscreteRSSILevelState"
},
{
"name": "core:RSSILevelState"
},
{
"name": "core:OpenClosedUnknownState"
}
],
"widgetName": "DiscretePositionableGarageDoor",
"uiClass": "GarageDoor",
"type": "ACTUATOR"
}
},
{
"attributes": [
{
"name": "zigbee:Role",
"type": 3,
"value": "endDevice"
},
{
"name": "core:ManufacturerId",
"type": 3,
"value": 4640
},
{
"name": "zigbee:NotificationEnable",
"type": 6,
"value": true
},
{
"name": "zigbee:InputClusters",
"type": 10,
"value": ["identify", "scenes", "windowCovering"]
},
{
"name": "core:OrientationPhysicalUpperBound",
"type": 1,
"value": 90
},
{
"name": "core:Compatibilities",
"type": 10,
"value": ["homekit"]
},
{
"name": "zigbee:SomfyEndProduct",
"type": 3,
"value": "2DInteriorBlind"
},
{
"name": "core:MacAddress",
"type": 3,
"value": "dXsY/v8Gwkw="
},
{
"name": "core:OrientationPhysicalLowerBound",
"type": 1,
"value": -90
}
],
"available": true,
"definition": {
"uiClass": "VenetianBlind",
"commands": [
{
"commandName": "my",
"nparams": 0
},
{
"commandName": "setPosition",
"nparams": 1,
"paramsSig": "p1"
},
{
"commandName": "close",
"nparams": 0
},
{
"commandName": "removeLockLevel",
"nparams": 1,
"paramsSig": "p1"
},
{
"commandName": "setPositionByStep",
"nparams": 2,
"paramsSig": "p1,p2"
},
{
"commandName": "setOrientation",
"nparams": 1,
"paramsSig": "p1"
},
{
"commandName": "advancedRefresh",
"nparams": 1,
"paramsSig": "p1"
},
{
"commandName": "resetFactoryDefaultsAndConfiguration",
"nparams": 0
},
{
"commandName": "storeCurrentPositionToMy",
"nparams": 0
},
{
"commandName": "open",
"nparams": 0
},
{
"commandName": "leaveNetworkOnly",
"nparams": 0
},
{
"commandName": "ping",
"nparams": 0
},
{
"commandName": "setTiltingRangeConfiguration",
"nparams": 2,
"paramsSig": "p1,p2"
},
{
"commandName": "stopIdentify",
"nparams": 0
},
{
"commandName": "addLockLevel",
"nparams": 1,
"paramsSig": "p1,*p2"
},
{
"commandName": "unbind",
"nparams": 2,
"paramsSig": "p1,p2"
},
{
"commandName": "resetLockLevels",
"nparams": 0
},
{
"commandName": "setPositionNoLimit",
"nparams": 2,
"paramsSig": "p1,p2"
},
{
"commandName": "stop",
"nparams": 0
},
{
"commandName": "reverseDirection",
"nparams": 0
},
{
"commandName": "bind",
"nparams": 2,
"paramsSig": "p1,p2"
},
{
"commandName": "identify",
"nparams": 0
},
{
"commandName": "setPositionLimitation",
"nparams": 1,
"paramsSig": "p1"
},
{
"commandName": "setClosureAndOrientation",
"nparams": 2,
"paramsSig": "p1,p2"
},
{
"commandName": "setName",
"nparams": 1,
"paramsSig": "p1"
},
{
"commandName": "getName",
"nparams": 0
},
{
"commandName": "setClosure",
"nparams": 1,
"paramsSig": "p1"
}
],
"type": "ACTUATOR",
"widgetName": "PositionableVenetianBlind",
"states": [
{
"name": "core:OperationalStatusState"
},
{
"name": "core:SlateOrientationState"
},
{
"name": "zigbee:DirectionState"
},
{
"name": "core:TargetClosureState"
},
{
"name": "zigbee:EncoderLiftState"
},
{
"name": "core:OrientationOperationalStatusState"
},
{
"name": "core:TargetOrientationState"
},
{
"name": "zigbee:EncoderTiltState"
},
{
"name": "core:OpenLimitTiltState"
},
{
"name": "core:ClosedLimitTiltState"
},
{
"name": "zigbee:MotorRunModeState"
},
{
"name": "core:ClosureOperationalStatusState"
},
{
"name": "core:OpenLimitLiftState"
},
{
"name": "zigbee:MotorRunningModeState"
},
{
"name": "core:ErrorState"
},
{
"name": "core:ConfigStatusState"
},
{
"name": "zigbee:MotorLEDFeedbackState"
},
{
"name": "zigbee:ControlTiltState"
},
{
"name": "zigbee:ControlLiftState"
},
{
"name": "zigbee:ReversalLiftState"
},
{
"name": "zigbee:NetworkOnlineState"
},
{
"name": "core:ClosureState"
},
{
"name": "core:ClosedLimitLiftState"
},
{
"name": "core:StatusState"
},
{
"name": "core:DiscreteRSSILevelState"
},
{
"name": "core:RSSILevelState"
},
{
"name": "zigbee:LinkQualityIndicatorState"
},
{
"name": "zigbee:ZigbeeUpdateDownloadProgressState"
},
{
"name": "zigbee:ZigbeeUpdateState"
},
{
"name": "core:FirmwareRevisionState"
},
{
"name": "core:CommandLockLevelsState"
},
{
"name": "core:ProductSoftwareVersionState"
},
{
"name": "core:NameState"
},
{
"name": "core:ProductHardwareVersionState"
},
{
"name": "zigbee:PowerSourceState"
},
{
"name": "core:ManufacturerNameState"
},
{
"name": "core:ProductSoftwareBuildIdState"
},
{
"name": "core:MotorBoardSoftwareVersionState"
},
{
"name": "core:MotorBoardSoftwareBuildIdState"
},
{
"name": "core:MotorBoardHardwareVersionState"
},
{
"name": "core:ProductModelNameState"
}
],
"attributes": [
{
"name": "zigbee:Role"
},
{
"name": "core:ManufacturerId"
},
{
"name": "zigbee:NotificationEnable"
},
{
"name": "zigbee:InputClusters"
},
{
"name": "core:OrientationPhysicalUpperBound"
},
{
"name": "core:Compatibilities"
},
{
"name": "zigbee:SomfyEndProduct"
},
{
"name": "core:MacAddress"
},
{
"name": "core:OrientationPhysicalLowerBound"
},
{
"name": "zigbee:OutputClusters"
}
]
},
"controllableName": "zigbee:SomfyVenetianBlindComponent",
"deviceURL": "zigbee://1234-5678-3293/16730099",
"states": [
{
"name": "core:CommandLockLevelsState",
"type": 11,
"value": []
},
{
"name": "core:ClosureOperationalStatusState",
"type": 3,
"value": "not moving"
},
{
"name": "core:OrientationOperationalStatusState",
"type": 3,
"value": "not moving"
},
{
"name": "core:OperationalStatusState",
"type": 3,
"value": "not moving"
},
{
"name": "core:DiscreteRSSILevelState",
"type": 3,
"value": "verylow"
},
{
"name": "zigbee:LinkQualityIndicatorState",
"type": 1,
"value": 54
},
{
"name": "core:RSSILevelState",
"type": 1,
"value": -86
},
{
"name": "core:StatusState",
"type": 3,
"value": "available"
},
{
"name": "core:SlateOrientationState",
"type": 1,
"value": 0
},
{
"name": "core:ClosureState",
"type": 1,
"value": 100
},
{
"name": "zigbee:MotorRunningModeState",
"type": 3,
"value": "motor is running normally"
},
{
"name": "zigbee:MotorLEDFeedbackState",
"type": 3,
"value": "LEDs are off"
},
{
"name": "zigbee:DirectionState",
"type": 3,
"value": "counterClockwise"
},
{
"name": "zigbee:MotorRunModeState",
"type": 3,
"value": "run in normal mode"
},
{
"name": "zigbee:EncoderTiltState",
"type": 3,
"value": "encoder controlled"
},
{
"name": "core:ConfigStatusState",
"type": 3,
"value": "operational"
},
{
"name": "zigbee:ControlLiftState",
"type": 3,
"value": "lift control is closed loop"
},
{
"name": "zigbee:ReversalLiftState",
"type": 3,
"value": "not reversal lift commands"
},
{
"name": "zigbee:EncoderLiftState",
"type": 3,
"value": "encoder controlled"
},
{
"name": "zigbee:NetworkOnlineState",
"type": 3,
"value": "online"
},
{
"name": "zigbee:ControlTiltState",
"type": 3,
"value": "tilt control is closed loop"
},
{
"name": "core:NameState",
"type": 3,
"value": "Living Room Venetian Blind"
},
{
"name": "core:TargetOrientationState",
"type": 1,
"value": 0
},
{
"name": "core:TargetClosureState",
"type": 1,
"value": 100
},
{
"name": "core:ErrorState",
"type": 3,
"value": "no error"
}
],
"label": "Living Room Venetian Blind",
"enabled": true,
"subsystemId": 0,
"synced": true,
"type": 1
}
]
}

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