Compare commits

..

72 Commits

Author SHA1 Message Date
Franck Nijhof
a12915fc14 2025.3.2 (#140392)
* Don't allow creating backups if Home Assistant is not running (#139499)

* Don't allow creating backups if hass is not running

* Revert "Don't allow creating backups if hass is not running"

This reverts commit 1bf545eb25.

* Set backup manager to idle only after Home Assistant has started

* Update according to discussion, add tests

* Add more test

* Bump govee_ble to 0.43.1 (#139862)

Bump govee_ble to 0.43.0

* Label emergency heat switch (#139872)

* Add label to emergency heat switch

* Use sentence case names

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

* Bump sense-energy lib to 0.13.7 (#140068)

* Update jinja to 3.1.6 (#140069)

* Update evohome-async to 1.0.3 (#140083)

bump client to 1.0.3

* Fix HEOS discovery error when previously ignored (#140091)

Abort ignored discovery

* Map prewash job state in SmartThings (#140097)

* Check support for thermostat operating state in SmartThings (#140103)

* Handle None options in SmartThings (#140110)

* Handle None options in SmartThings

* Handle None options in SmartThings

* Fix MQTT JSON light not reporting color temp status if color is not supported (#140113)

* Fix HEOS user initiated setup when discovery is waiting confirmation (#140119)

* Support null supported Thermostat modes in SmartThings (#140101)

* Set device class for Oven Completion time in SmartThings (#140139)

* Revert "Check if the unit of measurement is valid before creating the entity" (#140155)

Revert "Check if the unit of measurement is valid before creating the entity …"

This reverts commit 99e1a7a676.

* Fix the order of the group members attribute of the Music Assistant integration (#140204)

* Fix events without user in Bring integration (#140213)

Fix events without publicUserUuid

* Log broad exception in Electricity Maps config flow (#140219)

* Bump evohome-async to 1.0.4 to fix  #140194 (#140230)

bump client, add test for fix  #140194

* Refresh Home Connect token during config entry setup (#140233)

* Refresh token during config entry setup

* Test 500 error

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Add 900 RPM option to washer spin speed options at Home Connect (#140234)

Add 900 RPM option to washer spin speed options

* Fix todo tool broken with Gemini 2.0 models. (#140246)

* Change tool name for addlist item

* Change to HasListAddItem

* extract to function

* Fix version not always available in onewire (#140260)

* Fix `client_id` not generated when connecting to the MQTT broker (#140264)

Fix client_id not generated when connecting to the MQTT broker

* Bump velbusaio to 2025.3.0 (#140267)

* Fix dryer operating state in SmartThings (#140277)

* FGLair : Upgrade to ayla-iot-unofficial 1.4.7 (#140296)

Upgrade to ayla-iot-unofficial 1.4.7

* Bump pyheos to v1.0.3 (#140310)

Bump pyheos v1.0.3

* Bump ZHA to 0.0.52 (#140325)

* Bump pydrawise to 2025.3.0 (#140330)

* Bump teslemetry-stream (#140335)

Bump

* Fix no temperature unit in SmartThings (#140363)

* Fix double space quoting in WebDAV (#140364)

* Bump python-roborock to 2.12.2 (#140368)

bump python roboorck to 2.12.2

* Handle incomplete power consumption reports in SmartThings (#140370)

* Fix browsing Audible Favorites in Sonos (#140378)

* initial commit

* updates

* update test data

* Make sure SmartThings light can deal with unknown states (#140190)

* Fix

* add comment

* Make light unknown

* Make light unknown

* Delete subscription on shutdown of SmartThings (#140135)

* Cache subscription url in SmartThings

* Cache subscription url in SmartThings

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Bump pysmartthings to 2.7.1

* 2.7.2

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Bump version to 2025.3.2

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Evan Farrell <evan@evanfarrell.com>
Co-authored-by: John Hillery <34005807+jrhillery@users.noreply.github.com>
Co-authored-by: Keilin Bickar <TrumpetGod@gmail.com>
Co-authored-by: David Bonnes <zxdavb@bonnes.me>
Co-authored-by: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: msm595 <msm595@users.noreply.github.com>
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
Co-authored-by: J. Diego Rodríguez Royo <jdrr1998@hotmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Luke Lashley <conway220@gmail.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Maikel Punie <maikel.punie@gmail.com>
Co-authored-by: Antoine Reversat <a.reversat@gmail.com>
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
Co-authored-by: David Knowles <dknowles2@gmail.com>
Co-authored-by: Brett Adams <Bre77@users.noreply.github.com>
Co-authored-by: Pete Sage <76050312+PeteRager@users.noreply.github.com>
2025-03-11 17:36:00 +01:00
Franck Nijhof
3d5e4b980f Bump version to 2025.3.2 2025-03-11 15:22:38 +00:00
Joost Lekkerkerker
f2f653efcf Delete subscription on shutdown of SmartThings (#140135)
* Cache subscription url in SmartThings

* Cache subscription url in SmartThings

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Bump pysmartthings to 2.7.1

* 2.7.2

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-03-11 15:21:27 +00:00
Joost Lekkerkerker
b5c7bdd98f Make sure SmartThings light can deal with unknown states (#140190)
* Fix

* add comment

* Make light unknown

* Make light unknown
2025-03-11 14:58:36 +00:00
Pete Sage
38e6133202 Fix browsing Audible Favorites in Sonos (#140378)
* initial commit

* updates

* update test data
2025-03-11 14:58:02 +00:00
Joost Lekkerkerker
8541dc5bde Handle incomplete power consumption reports in SmartThings (#140370) 2025-03-11 14:57:58 +00:00
Luke Lashley
5327996bad Bump python-roborock to 2.12.2 (#140368)
bump python roboorck to 2.12.2
2025-03-11 14:57:55 +00:00
Jan-Philipp Benecke
4ddc43a9d9 Fix double space quoting in WebDAV (#140364) 2025-03-11 14:57:51 +00:00
Joost Lekkerkerker
e6dea4179b Fix no temperature unit in SmartThings (#140363) 2025-03-11 14:57:47 +00:00
Brett Adams
0318b85517 Bump teslemetry-stream (#140335)
Bump
2025-03-11 14:57:43 +00:00
David Knowles
29987d443e Bump pydrawise to 2025.3.0 (#140330) 2025-03-11 14:57:27 +00:00
puddly
cbfd8707b9 Bump ZHA to 0.0.52 (#140325) 2025-03-11 14:57:18 +00:00
Andrew Sayre
5f158f5c87 Bump pyheos to v1.0.3 (#140310)
Bump pyheos v1.0.3
2025-03-11 14:57:14 +00:00
Antoine Reversat
d67ccd2fce FGLair : Upgrade to ayla-iot-unofficial 1.4.7 (#140296)
Upgrade to ayla-iot-unofficial 1.4.7
2025-03-11 14:57:10 +00:00
Joost Lekkerkerker
29c9d3804b Fix dryer operating state in SmartThings (#140277) 2025-03-11 14:57:07 +00:00
Maikel Punie
76d478c84f Bump velbusaio to 2025.3.0 (#140267) 2025-03-11 14:56:54 +00:00
Jan Bouwhuis
5d9d6f099c Fix client_id not generated when connecting to the MQTT broker (#140264)
Fix client_id not generated when connecting to the MQTT broker
2025-03-11 14:56:49 +00:00
epenet
e4b31640b3 Fix version not always available in onewire (#140260) 2025-03-11 14:56:43 +00:00
Luke Lashley
c43f6a67d0 Fix todo tool broken with Gemini 2.0 models. (#140246)
* Change tool name for addlist item

* Change to HasListAddItem

* extract to function
2025-03-11 14:55:51 +00:00
J. Diego Rodríguez Royo
0bbab63193 Add 900 RPM option to washer spin speed options at Home Connect (#140234)
Add 900 RPM option to washer spin speed options
2025-03-11 14:52:37 +00:00
J. Diego Rodríguez Royo
06188b8fbd Refresh Home Connect token during config entry setup (#140233)
* Refresh token during config entry setup

* Test 500 error

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-03-11 14:52:33 +00:00
David Bonnes
bbbb5cadd4 Bump evohome-async to 1.0.4 to fix #140194 (#140230)
bump client, add test for fix  #140194
2025-03-11 14:52:29 +00:00
Jan-Philipp Benecke
52fcdda429 Log broad exception in Electricity Maps config flow (#140219) 2025-03-11 14:52:25 +00:00
Manu
7d93ceb0f0 Fix events without user in Bring integration (#140213)
Fix events without publicUserUuid
2025-03-11 14:52:22 +00:00
msm595
873e4b77eb Fix the order of the group members attribute of the Music Assistant integration (#140204) 2025-03-11 14:52:17 +00:00
Jan Bouwhuis
61f0eabcbb Revert "Check if the unit of measurement is valid before creating the entity" (#140155)
Revert "Check if the unit of measurement is valid before creating the entity …"

This reverts commit 99e1a7a676.
2025-03-11 14:50:10 +00:00
Joost Lekkerkerker
134b5319e1 Set device class for Oven Completion time in SmartThings (#140139) 2025-03-11 14:50:05 +00:00
Joost Lekkerkerker
ee78e21950 Support null supported Thermostat modes in SmartThings (#140101) 2025-03-11 14:47:50 +00:00
Andrew Sayre
323bc54efc Fix HEOS user initiated setup when discovery is waiting confirmation (#140119) 2025-03-11 14:46:18 +00:00
Jan Bouwhuis
fd2dee3c11 Fix MQTT JSON light not reporting color temp status if color is not supported (#140113) 2025-03-11 14:46:14 +00:00
Joost Lekkerkerker
fc53322c07 Handle None options in SmartThings (#140110)
* Handle None options in SmartThings

* Handle None options in SmartThings
2025-03-11 14:46:08 +00:00
Joost Lekkerkerker
faf9977abb Check support for thermostat operating state in SmartThings (#140103) 2025-03-11 14:46:03 +00:00
Joost Lekkerkerker
7336c8fc07 Map prewash job state in SmartThings (#140097) 2025-03-11 14:36:17 +00:00
Andrew Sayre
5cfaeda95b Fix HEOS discovery error when previously ignored (#140091)
Abort ignored discovery
2025-03-11 14:36:13 +00:00
David Bonnes
a78e9039c6 Update evohome-async to 1.0.3 (#140083)
bump client to 1.0.3
2025-03-11 14:36:10 +00:00
Franck Nijhof
227f3cea25 Update jinja to 3.1.6 (#140069) 2025-03-11 14:36:06 +00:00
Keilin Bickar
cab4890246 Bump sense-energy lib to 0.13.7 (#140068) 2025-03-11 14:36:03 +00:00
John Hillery
95fd096bdd Label emergency heat switch (#139872)
* Add label to emergency heat switch

* Use sentence case names

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2025-03-11 14:35:59 +00:00
Evan Farrell
91cf8cb547 Bump govee_ble to 0.43.1 (#139862)
Bump govee_ble to 0.43.0
2025-03-11 14:35:56 +00:00
Erik Montnemery
3ce4f3f918 Don't allow creating backups if Home Assistant is not running (#139499)
* Don't allow creating backups if hass is not running

* Revert "Don't allow creating backups if hass is not running"

This reverts commit 1bf545eb25.

* Set backup manager to idle only after Home Assistant has started

* Update according to discussion, add tests

* Add more test
2025-03-11 14:35:46 +00:00
Franck Nijhof
4e89948b5c 2025.3.1 (#140061) 2025-03-07 18:54:39 +01:00
Franck Nijhof
9f95383201 Bump version to 2025.3.1 2025-03-07 17:03:29 +00:00
Joost Lekkerkerker
7e452521c8 Restore SmartThings button event (#140044)
* Restore SmartThings button event

* Fix
2025-03-07 17:03:16 +00:00
Michael
991de6f1d0 Bump py-synologydsm-api to 2.7.1 (#140052)
bump py-synologydsm-api to 2.7.1
2025-03-07 16:49:07 +00:00
Joost Lekkerkerker
be32e3fe8f Only keep valid powerConsumptionReports in SmartThings (#140049)
* power consumption report

* Only keep valid powerConsumptionReports in SmartThings
2025-03-07 16:49:03 +00:00
Joost Lekkerkerker
d6eb61e9ec Bump pysmartthings to 2.7.0 (#140047) 2025-03-07 16:49:00 +00:00
Joost Lekkerkerker
e74fe69d65 Fix SmartThings thermostat climate check (#140046)
* Fix SmartThings thermostat climate check

* Add tests
2025-03-07 16:48:55 +00:00
Joost Lekkerkerker
208406123e Fix SmartThings disabling working capabilities (#140039) 2025-03-07 16:03:40 +00:00
David Bonnes
8bcd135f3d Fix evohome to gracefully handle null schedules (#140036)
* extend tests to catch null schedules

* add fixture with null schedule

* remove null schedules for now

* fic the typing for _schedule attr (is list, not dict)

* add valid schedule to fixture

* update ssetpoints only if there is a schedule

* snapshot to match last change

* refactor: dont update switchpoints if no schedule

* add in warnings for null schedules

* add fixture for DHW without schedule
2025-03-07 16:03:36 +00:00
hahn-th
e7ea0e435e Add description for HomematicIP HCU1 in homematicip_cloud setup config flow (#140025)
add description for hcu1
2025-03-07 16:03:33 +00:00
Brett Adams
b15b680cfe Fix shift state default in Teslemetry and Tessie (#140018)
* Fix again

* Fix Tessie

* Update snap
2025-03-07 16:03:29 +00:00
Brett Adams
5e26d98bdf Fix powerwall 0% in Tessie and Tesla Fleet (#140017)
Fix powerwall zero
2025-03-07 16:03:26 +00:00
Martin Hjelmare
9f94ee280a Bump aiohomeconnect to 0.16.3 (#140014) 2025-03-07 16:03:23 +00:00
J. Diego Rodríguez Royo
efa98539fa Check operation state on Home Connect program sensor update (#140011)
Check operation state on program sensor update
2025-03-07 16:03:19 +00:00
David Bonnes
113cd4bfcc Fix regression to evohome debug logging (#140000)
* fix regression in debug logging

* lint
2025-03-07 16:03:15 +00:00
Ivan Lopez Hernandez
ccbaf76e44 Correctly retrieve only loaded Google Generative AI config_entries (#139999)
* Correctly retrieve only loaded config_entries

* Ruff
2025-03-07 16:03:08 +00:00
Jan-Philipp Benecke
5d9d93d3a1 Bump aiowebdav2 to 0.4.1 (#139988) 2025-03-07 16:03:04 +00:00
J. Nick Koston
c2c5274aac Bump nexia to 2.2.2 (#139986)
changelog: https://github.com/bdraco/nexia/compare/2.2.1...2.2.2
2025-03-07 16:03:01 +00:00
Joost Lekkerkerker
89756394c9 Fix SmartThings dust sensor UoM (#139977) 2025-03-07 16:02:57 +00:00
Bram Kragten
352aa88e79 Update frontend to 20250306.0 (#139965) 2025-03-07 16:02:54 +00:00
Joost Lekkerkerker
714962bd7a Fix SmartThings fan (#139962) 2025-03-07 16:02:50 +00:00
Luke Lashley
fb4c50b5dc Bump to python-snoo 0.6.1 (#139954) 2025-03-07 16:02:47 +00:00
Jan-Philipp Benecke
b4794b2029 Set content length when uploading files to WebDAV (#139950) 2025-03-07 16:02:43 +00:00
Joost Lekkerkerker
3a8c8accfe Add config entry level diagnostics to SmartThings (#139939)
* Add config entry level diagnostics to SmartThings

* Add config entry level diagnostics to SmartThings

* Add config entry level diagnostics to SmartThings
2025-03-07 16:02:40 +00:00
Jan-Philipp Benecke
844adfc590 Bump aiowebdav2 to 0.4.0 (#139938) 2025-03-07 16:02:36 +00:00
Joost Lekkerkerker
a279e23fb5 Bump pysmartthings to 2.6.1 (#139936)
* Bump pysmartthings to 2.6.1

* Bump pysmartthings to 2.6.1
2025-03-07 15:58:00 +00:00
Jan Bouwhuis
af9bbd0585 Check if the unit of measurement is valid before creating the entity (#139932) 2025-03-07 15:50:55 +00:00
Joost Lekkerkerker
1304194f09 Deduplicate climate modes in SmartThings (#139930)
* Deduplicate climate modes in SmartThings

* Deduplicate climate modes in SmartThings
2025-03-07 15:50:51 +00:00
J. Nick Koston
e909417a3f Bump thermobeacon-ble to 0.8.1 (#139919)
changelog: https://github.com/Bluetooth-Devices/thermobeacon-ble/compare/v0.8.0...v0.8.1

fixes #139917
2025-03-07 15:50:48 +00:00
Ivan Lopez Hernandez
02706c116d Trim the Schema allowed keys to match the Public Gemini API docs. (#139876)
* Trim the Schema allowed types to match the Public API docs, not the SDK types as those do not match

* Testing
2025-03-07 15:50:43 +00:00
peteS-UK
3af6b5cb4c Fix Unit of Measurement for Squeezebox duration sensor entity on LMS service (#139861)
UOM Fix
2025-03-07 15:42:02 +00:00
Ishima
35c1bb1ec5 Check support for demand load control in SmartThings AC (#139616)
* Check support for demand load control in SmartThings AC

* Fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-03-07 15:41:52 +00:00
135 changed files with 10825 additions and 1463 deletions

View File

@@ -118,6 +118,7 @@ class BackupManagerState(StrEnum):
IDLE = "idle"
CREATE_BACKUP = "create_backup"
BLOCKED = "blocked"
RECEIVE_BACKUP = "receive_backup"
RESTORE_BACKUP = "restore_backup"
@@ -226,6 +227,13 @@ class RestoreBackupEvent(ManagerStateEvent):
state: RestoreBackupState
@dataclass(frozen=True, kw_only=True, slots=True)
class BlockedEvent(ManagerStateEvent):
"""Backup manager blocked, Home Assistant is starting."""
manager_state: BackupManagerState = BackupManagerState.BLOCKED
class BackupPlatformProtocol(Protocol):
"""Define the format that backup platforms can have."""
@@ -340,7 +348,7 @@ class BackupManager:
self.remove_next_delete_event: Callable[[], None] | None = None
# Latest backup event and backup event subscribers
self.last_event: ManagerStateEvent = IdleEvent()
self.last_event: ManagerStateEvent = BlockedEvent()
self.last_non_idle_event: ManagerStateEvent | None = None
self._backup_event_subscriptions = hass.data[
DATA_BACKUP
@@ -354,10 +362,19 @@ class BackupManager:
self.known_backups.load(stored["backups"])
await self._reader_writer.async_validate_config(config=self.config)
await self._reader_writer.async_resume_restore_progress_after_restart(
on_progress=self.async_on_backup_event
)
async def set_manager_idle_after_start(hass: HomeAssistant) -> None:
"""Set manager to idle after start."""
self.async_on_backup_event(IdleEvent())
if self.state == BackupManagerState.BLOCKED:
# If we're not finishing a restore job, set the manager to idle after start
start.async_at_started(self.hass, set_manager_idle_after_start)
await self.load_platforms()
@property
@@ -1293,7 +1310,7 @@ class BackupManager:
if (current_state := self.state) != (new_state := event.manager_state):
LOGGER.debug("Backup state: %s -> %s", current_state, new_state)
self.last_event = event
if not isinstance(event, IdleEvent):
if not isinstance(event, (BlockedEvent, IdleEvent)):
self.last_non_idle_event = event
for subscription in self._backup_event_subscriptions:
subscription(event)

View File

@@ -77,9 +77,12 @@ class BringEventEntity(BringBaseEntity, EventEntity):
attributes = asdict(activity.content)
attributes["last_activity_by"] = next(
x.name
for x in bring_list.users.users
if x.publicUuid == activity.content.publicUserUuid
(
x.name
for x in bring_list.users.users
if x.publicUuid == activity.content.publicUserUuid
),
None,
)
self._trigger_event(

View File

@@ -3,11 +3,11 @@
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from aioelectricitymaps import (
ElectricityMaps,
ElectricityMapsError,
ElectricityMapsInvalidTokenError,
ElectricityMapsNoDataError,
)
@@ -36,6 +36,8 @@ TYPE_USE_HOME = "use_home_location"
TYPE_SPECIFY_COORDINATES = "specify_coordinates"
TYPE_SPECIFY_COUNTRY = "specify_country_code"
_LOGGER = logging.getLogger(__name__)
class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Co2signal."""
@@ -158,7 +160,8 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "invalid_auth"
except ElectricityMapsNoDataError:
errors["base"] = "no_data"
except ElectricityMapsError:
except Exception:
_LOGGER.exception("Unexpected error occurred while checking API key")
errors["base"] = "unknown"
else:
if self.source == SOURCE_REAUTH:

View File

@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.13.6"]
"requirements": ["sense-energy==0.13.7"]
}

View File

@@ -11,6 +11,7 @@ from typing import Any
import evohomeasync as ec1
import evohomeasync2 as ec2
from evohomeasync2.const import (
SZ_DHW,
SZ_GATEWAY_ID,
SZ_GATEWAY_INFO,
SZ_GATEWAYS,
@@ -19,8 +20,9 @@ from evohomeasync2.const import (
SZ_TEMPERATURE_CONTROL_SYSTEMS,
SZ_TIME_ZONE,
SZ_USE_DAYLIGHT_SAVE_SWITCHING,
SZ_ZONES,
)
from evohomeasync2.schemas.typedefs import EvoLocStatusResponseT
from evohomeasync2.schemas.typedefs import EvoLocStatusResponseT, EvoTcsConfigResponseT
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
@@ -113,17 +115,19 @@ class EvoDataUpdateCoordinator(DataUpdateCoordinator):
SZ_USE_DAYLIGHT_SAVE_SWITCHING
],
}
tcs_info: EvoTcsConfigResponseT = self.tcs.config # type: ignore[assignment]
tcs_info[SZ_ZONES] = [zone.config for zone in self.tcs.zones]
if self.tcs.hotwater:
tcs_info[SZ_DHW] = self.tcs.hotwater.config
gwy_info = {
SZ_GATEWAY_ID: self.loc.gateways[0].id,
SZ_TEMPERATURE_CONTROL_SYSTEMS: [
self.loc.gateways[0].systems[0].config
],
SZ_TEMPERATURE_CONTROL_SYSTEMS: [tcs_info],
}
config = {
SZ_LOCATION_INFO: loc_info,
SZ_GATEWAYS: [{SZ_GATEWAY_INFO: gwy_info}],
}
self.logger.debug("Config = %s", config)
self.logger.debug("Config = %s", [config])
async def call_client_api(
self,
@@ -203,10 +207,18 @@ class EvoDataUpdateCoordinator(DataUpdateCoordinator):
async def _update_v2_schedules(self) -> None:
for zone in self.tcs.zones:
await zone.get_schedule()
try:
await zone.get_schedule()
except ec2.InvalidScheduleError as err:
self.logger.warning(
"Zone '%s' has an invalid/missing schedule: %r", zone.name, err
)
if dhw := self.tcs.hotwater:
await dhw.get_schedule()
try:
await dhw.get_schedule()
except ec2.InvalidScheduleError as err:
self.logger.warning("DHW has an invalid/missing schedule: %r", err)
async def _async_update_data(self) -> EvoLocStatusResponseT: # type: ignore[override]
"""Fetch the latest state of an entire TCC Location.

View File

@@ -6,6 +6,7 @@ import logging
from typing import Any
import evohomeasync2 as evo
from evohomeasync2.schemas.typedefs import DayOfWeekDhwT
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -102,7 +103,7 @@ class EvoChild(EvoEntity):
self._evo_tcs = evo_device.tcs
self._schedule: dict[str, Any] | None = None
self._schedule: list[DayOfWeekDhwT] | None = None
self._setpoints: dict[str, Any] = {}
@property
@@ -123,6 +124,9 @@ class EvoChild(EvoEntity):
Only Zones & DHW controllers (but not the TCS) can have schedules.
"""
if not self._schedule:
return self._setpoints
this_sp_dtm, this_sp_val = self._evo_device.this_switchpoint
next_sp_dtm, next_sp_val = self._evo_device.next_switchpoint
@@ -152,10 +156,10 @@ class EvoChild(EvoEntity):
self._evo_device,
err,
)
self._schedule = {}
self._schedule = []
return
else:
self._schedule = schedule or {} # mypy hint
self._schedule = schedule # type: ignore[assignment]
_LOGGER.debug("Schedule['%s'] = %s", self.name, schedule)

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==1.0.2"]
"requirements": ["evohome-async==1.0.4"]
}

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250305.0"]
"requirements": ["home-assistant-frontend==20250306.0"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/fujitsu_fglair",
"iot_class": "cloud_polling",
"requirements": ["ayla-iot-unofficial==1.4.5"]
"requirements": ["ayla-iot-unofficial==1.4.7"]
}

View File

@@ -65,9 +65,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
prompt_parts = [call.data[CONF_PROMPT]]
config_entry: GoogleGenerativeAIConfigEntry = hass.config_entries.async_entries(
DOMAIN
)[0]
config_entry: GoogleGenerativeAIConfigEntry = (
hass.config_entries.async_loaded_entries(DOMAIN)[0]
)
client = config_entry.runtime_data

View File

@@ -64,28 +64,18 @@ async def async_setup_entry(
SUPPORTED_SCHEMA_KEYS = {
"min_items",
"example",
"property_ordering",
"pattern",
"minimum",
"default",
"any_of",
"max_length",
"title",
"min_properties",
"min_length",
"max_items",
"maximum",
"nullable",
"max_properties",
# Gemini API does not support all of the OpenAPI schema
# SoT: https://ai.google.dev/api/caching#Schema
"type",
"description",
"enum",
"format",
"items",
"description",
"nullable",
"enum",
"max_items",
"min_items",
"properties",
"required",
"items",
}
@@ -109,9 +99,7 @@ def _format_schema(schema: dict[str, Any]) -> Schema:
key = _camel_to_snake(key)
if key not in SUPPORTED_SCHEMA_KEYS:
continue
if key == "any_of":
val = [_format_schema(subschema) for subschema in val]
elif key == "type":
if key == "type":
val = val.upper()
elif key == "format":
# Gemini API does not support all formats, see: https://ai.google.dev/api/caching#Schema
@@ -288,6 +276,13 @@ class GoogleGenerativeAIConversationEntity(
):
return await self._async_handle_message(user_input, chat_log)
def _fix_tool_name(self, tool_name: str) -> str:
"""Fix tool name if needed."""
# The Gemini 2.0+ tokenizer seemingly has a issue with the HassListAddItem tool
# name. This makes sure when it incorrectly changes the name, that we change it
# back for HA to call.
return tool_name if tool_name != "HasListAddItem" else "HassListAddItem"
async def _async_handle_message(
self,
user_input: conversation.ConversationInput,
@@ -447,7 +442,10 @@ class GoogleGenerativeAIConversationEntity(
tool_name = tool_call.name
tool_args = _escape_decode(tool_call.args)
tool_calls.append(
llm.ToolInput(tool_name=tool_name, tool_args=tool_args)
llm.ToolInput(
tool_name=self._fix_tool_name(tool_name),
tool_args=tool_args,
)
)
chat_request = _create_google_tool_response_content(

View File

@@ -135,5 +135,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/govee_ble",
"iot_class": "local_push",
"requirements": ["govee-ble==0.43.0"]
"requirements": ["govee-ble==0.43.1"]
}

View File

@@ -14,7 +14,12 @@ from pyheos import (
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.config_entries import (
SOURCE_IGNORE,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import selector
@@ -141,8 +146,10 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
hostname = urlparse(discovery_info.ssdp_location).hostname
assert hostname is not None
# Abort early when discovered host is part of the current system
if entry and hostname in _get_current_hosts(entry):
# Abort early when discovery is ignored or host is part of the current system
if entry and (
entry.source == SOURCE_IGNORE or hostname in _get_current_hosts(entry)
):
return self.async_abort(reason="single_instance_allowed")
# Connect to discovered host and get system information
@@ -198,7 +205,7 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Obtain host and validate connection."""
await self.async_set_unique_id(DOMAIN)
await self.async_set_unique_id(DOMAIN, raise_on_progress=False)
self._abort_if_unique_id_configured(error="single_instance_allowed")
# Try connecting to host if provided
errors: dict[str, str] = {}

View File

@@ -159,13 +159,12 @@ class HeosCoordinator(DataUpdateCoordinator[None]):
async def _async_on_reconnected(self) -> None:
"""Handle when reconnected so resources are updated and entities marked available."""
await self._async_update_players()
await self._async_update_sources()
_LOGGER.warning("Successfully reconnected to HEOS host %s", self.host)
self.async_update_listeners()
async def _async_on_controller_event(
self, event: str, data: PlayerUpdateResult | None
self, event: str, data: PlayerUpdateResult | None = None
) -> None:
"""Handle a controller event, such as players or groups changed."""
if event == const.EVENT_PLAYERS_CHANGED:

View File

@@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["pyheos"],
"quality_scale": "platinum",
"requirements": ["pyheos==1.0.2"],
"requirements": ["pyheos==1.0.3"],
"ssdp": [
{
"st": "urn:schemas-denon-com:device:ACT-Denon:1"

View File

@@ -16,11 +16,17 @@ from aiohomeconnect.model import (
SettingKey,
)
from aiohomeconnect.model.error import HomeConnectError
import aiohttp
import voluptuous as vol
from homeassistant.const import ATTR_DEVICE_ID, Platform
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
ServiceValidationError,
)
from homeassistant.helpers import (
config_entry_oauth2_flow,
config_validation as cv,
@@ -611,6 +617,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeConnectConfigEntry)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
config_entry_auth = AsyncConfigEntryAuth(hass, session)
try:
await config_entry_auth.async_get_access_token()
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
home_connect_client = HomeConnectClient(config_entry_auth)

View File

@@ -285,6 +285,7 @@ SPIN_SPEED_OPTIONS = {
"LaundryCare.Washer.EnumType.SpinSpeed.RPM400",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM600",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM800",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM900",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1000",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1200",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1400",

View File

@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],
"requirements": ["aiohomeconnect==0.16.2"],
"requirements": ["aiohomeconnect==0.16.3"],
"single_config_entry": true
}

View File

@@ -386,6 +386,13 @@ class HomeConnectProgramSensor(HomeConnectSensor):
def update_native_value(self) -> None:
"""Update the program sensor's status."""
self.program_running = (
status := self.appliance.status.get(StatusKey.BSH_COMMON_OPERATION_STATE)
) is not None and status.value in [
BSH_OPERATION_STATE_RUN,
BSH_OPERATION_STATE_PAUSE,
BSH_OPERATION_STATE_FINISHED,
]
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
if event:
self._update_native_value(event.value)

View File

@@ -461,6 +461,7 @@
"laundry_care_washer_enum_type_spin_speed_r_p_m400": "400 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m600": "600 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "800 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m900": "900 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "1000 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "1200 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "1400 rpm",
@@ -1430,6 +1431,7 @@
"laundry_care_washer_enum_type_spin_speed_r_p_m400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m400%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m600%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m800%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m900": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m900%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1000%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1200%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1400%]",

View File

@@ -3,6 +3,7 @@
"step": {
"init": {
"title": "Pick Homematic IP access point",
"description": "If you are about to register a **Homematic IP HCU1**, please press the button on top of the device before you continue.\n\nThe registration process must be completed within 5 minutes.",
"data": {
"hapid": "Access point ID (SGTIN)",
"pin": "[%key:common::config_flow::data::pin%]",

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2025.2.0"]
"requirements": ["pydrawise==2025.3.0"]
}

View File

@@ -15,6 +15,7 @@ import socket
import ssl
import time
from typing import TYPE_CHECKING, Any
from uuid import uuid4
import certifi
@@ -292,7 +293,7 @@ class MqttClientSetup:
"""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
from paho.mqtt import client as mqtt # pylint: disable=import-outside-toplevel
# pylint: disable-next=import-outside-toplevel
from .async_client import AsyncMQTTClient
@@ -309,9 +310,10 @@ class MqttClientSetup:
clean_session = True
if (client_id := config.get(CONF_CLIENT_ID)) is None:
# PAHO MQTT relies on the MQTT server to generate random client IDs.
# However, that feature is not mandatory so we generate our own.
client_id = None
# PAHO MQTT relies on the MQTT server to generate random client ID
# for protocol version 3.1, however, that feature is not mandatory
# so we generate our own.
client_id = mqtt._base62(uuid4().int, padding=22) # noqa: SLF001
transport: str = config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT)
self._client = AsyncMQTTClient(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,

View File

@@ -31,7 +31,6 @@ from homeassistant.components.light import (
LightEntity,
LightEntityFeature,
brightness_supported,
color_supported,
valid_supported_color_modes,
)
from homeassistant.const import (
@@ -293,7 +292,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
elif values["state"] is None:
self._attr_is_on = None
if color_supported(self.supported_color_modes) and "color_mode" in values:
if "color_mode" in values:
self._update_color(values)
if brightness_supported(self.supported_color_modes):

View File

@@ -276,22 +276,26 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
self._attr_state = MediaPlayerState(player.state.value)
else:
self._attr_state = MediaPlayerState(STATE_OFF)
group_members_entity_ids: list[str] = []
group_members: list[str] = []
if player.group_childs:
# translate MA group_childs to HA group_members as entity id's
entity_registry = er.async_get(self.hass)
group_members_entity_ids = [
entity_id
for child_id in player.group_childs
if (
entity_id := entity_registry.async_get_entity_id(
self.platform.domain, DOMAIN, child_id
)
group_members = player.group_childs
elif player.synced_to and (parent := self.mass.players.get(player.synced_to)):
group_members = parent.group_childs
# translate MA group_childs to HA group_members as entity id's
entity_registry = er.async_get(self.hass)
group_members_entity_ids: list[str] = [
entity_id
for child_id in group_members
if (
entity_id := entity_registry.async_get_entity_id(
self.platform.domain, DOMAIN, child_id
)
]
# NOTE: we sort the group_members for now,
# until the MA API returns them sorted (group_childs is now a set)
self._attr_group_members = sorted(group_members_entity_ids)
)
]
self._attr_group_members = group_members_entity_ids
self._attr_volume_level = (
player.volume_level / 100 if player.volume_level is not None else None
)

View File

@@ -12,5 +12,5 @@
"documentation": "https://www.home-assistant.io/integrations/nexia",
"iot_class": "cloud_polling",
"loggers": ["nexia"],
"requirements": ["nexia==2.2.1"]
"requirements": ["nexia==2.2.2"]
}

View File

@@ -58,6 +58,9 @@
"switch": {
"hold": {
"name": "Hold"
},
"emergency_heat": {
"name": "Emergency heat"
}
}
},

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import contextlib
from datetime import datetime, timedelta
import logging
import os
@@ -58,7 +59,7 @@ class OneWireHub:
owproxy: protocol._Proxy
devices: list[OWDeviceDescription]
_version: str
_version: str | None = None
def __init__(self, hass: HomeAssistant, config_entry: OneWireConfigEntry) -> None:
"""Initialize."""
@@ -74,7 +75,9 @@ class OneWireHub:
port = self._config_entry.data[CONF_PORT]
_LOGGER.debug("Initializing connection to %s:%s", host, port)
self.owproxy = protocol.proxy(host, port)
self._version = self.owproxy.read(protocol.PTH_VERSION).decode()
with contextlib.suppress(protocol.OwnetError):
# Version is not available on all servers
self._version = self.owproxy.read(protocol.PTH_VERSION).decode()
self.devices = _discover_devices(self.owproxy)
async def initialize(self) -> None:

View File

@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": [
"python-roborock==2.11.1",
"python-roborock==2.12.2",
"vacuum-map-parser-roborock==0.1.2"
]
}

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/sense",
"iot_class": "cloud_polling",
"loggers": ["sense_energy"],
"requirements": ["sense-energy==0.13.6"]
"requirements": ["sense-energy==0.13.7"]
}

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING, cast
@@ -11,15 +12,22 @@ from pysmartthings import (
Attribute,
Capability,
Device,
DeviceEvent,
Scene,
SmartThings,
SmartThingsAuthenticationFailedError,
SmartThingsSinkError,
Status,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_TOKEN,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -28,7 +36,15 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
async_get_config_entry_implementation,
)
from .const import CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, DOMAIN, MAIN, OLD_DATA
from .const import (
CONF_INSTALLED_APP_ID,
CONF_LOCATION_ID,
CONF_SUBSCRIPTION_ID,
DOMAIN,
EVENT_BUTTON,
MAIN,
OLD_DATA,
)
_LOGGER = logging.getLogger(__name__)
@@ -90,6 +106,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
client.refresh_token_function = _refresh_token
def _handle_max_connections() -> None:
_LOGGER.debug("We hit the limit of max connections")
hass.config_entries.async_schedule_reload(entry.entry_id)
client.max_connections_reached_callback = _handle_max_connections
def _handle_new_subscription_identifier(identifier: str | None) -> None:
"""Handle a new subscription identifier."""
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_SUBSCRIPTION_ID: identifier,
},
)
if identifier is not None:
_LOGGER.debug("Updating subscription ID to %s", identifier)
else:
_LOGGER.debug("Removing subscription ID")
client.new_subscription_id_callback = _handle_new_subscription_identifier
if (old_identifier := entry.data.get(CONF_SUBSCRIPTION_ID)) is not None:
_LOGGER.debug("Trying to delete old subscription %s", old_identifier)
await client.delete_subscription(old_identifier)
_LOGGER.debug("Trying to create a new subscription")
try:
subscription = await client.create_subscription(
entry.data[CONF_LOCATION_ID],
entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID],
)
except SmartThingsSinkError as err:
_LOGGER.debug("Couldn't create a new subscription: %s", err)
raise ConfigEntryNotReady from err
subscription_id = subscription.subscription_id
_handle_new_subscription_identifier(subscription_id)
entry.async_create_background_task(
hass,
client.subscribe(
entry.data[CONF_LOCATION_ID],
entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID],
subscription,
),
"smartthings_socket",
)
device_status: dict[str, FullDevice] = {}
try:
devices = await client.get_devices()
@@ -114,12 +178,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
scenes=scenes,
)
entry.async_create_background_task(
hass,
client.subscribe(
entry.data[CONF_LOCATION_ID], entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID]
),
"smartthings_webhook",
def handle_button_press(event: DeviceEvent) -> None:
"""Handle a button press."""
if (
event.capability is Capability.BUTTON
and event.attribute is Attribute.BUTTON
):
hass.bus.async_fire(
EVENT_BUTTON,
{
"component_id": event.component_id,
"device_id": event.device_id,
"location_id": event.location_id,
"value": event.value,
"name": entry.runtime_data.devices[event.device_id].device.label,
"data": event.data,
},
)
entry.async_on_unload(
client.add_unspecified_device_event_listener(handle_button_press)
)
async def _handle_shutdown(_: Event) -> None:
"""Handle shutdown."""
await client.delete_subscription(subscription_id)
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _handle_shutdown)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -145,6 +231,9 @@ async def async_unload_entry(
hass: HomeAssistant, entry: SmartThingsConfigEntry
) -> bool:
"""Unload a config entry."""
client = entry.runtime_data.client
if (subscription_id := entry.data.get(CONF_SUBSCRIPTION_ID)) is not None:
await client.delete_subscription(subscription_id)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -160,26 +249,39 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
KEEP_CAPABILITY_QUIRK: dict[
Capability | str, Callable[[dict[Attribute | str, Status]], bool]
] = {
Capability.DRYER_OPERATING_STATE: (
lambda status: status[Attribute.SUPPORTED_MACHINE_STATES].value is not None
),
Capability.WASHER_OPERATING_STATE: (
lambda status: status[Attribute.SUPPORTED_MACHINE_STATES].value is not None
),
Capability.DEMAND_RESPONSE_LOAD_CONTROL: lambda _: True,
}
def process_status(
status: dict[str, dict[Capability | str, dict[Attribute | str, Status]]],
) -> dict[str, dict[Capability | str, dict[Attribute | str, Status]]]:
"""Remove disabled capabilities from status."""
if (main_component := status.get("main")) is None or (
if (main_component := status.get(MAIN)) is None:
return status
if (
disabled_capabilities_capability := main_component.get(
Capability.CUSTOM_DISABLED_CAPABILITIES
)
) is None:
return status
disabled_capabilities = cast(
list[Capability | str],
disabled_capabilities_capability[Attribute.DISABLED_CAPABILITIES].value,
)
if disabled_capabilities is not None:
for capability in disabled_capabilities:
# We still need to make sure the climate entity can work without this capability
if (
capability in main_component
and capability != Capability.DEMAND_RESPONSE_LOAD_CONTROL
):
del main_component[capability]
) is not None:
disabled_capabilities = cast(
list[Capability | str],
disabled_capabilities_capability[Attribute.DISABLED_CAPABILITIES].value,
)
if disabled_capabilities is not None:
for capability in disabled_capabilities:
if capability in main_component and (
capability not in KEEP_CAPABILITY_QUIRK
or not KEEP_CAPABILITY_QUIRK[capability](main_component[capability])
):
del main_component[capability]
return status

View File

@@ -161,9 +161,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
if self.get_attribute_value(
Capability.THERMOSTAT_FAN_MODE, Attribute.THERMOSTAT_FAN_MODE
):
if self.supports_capability(Capability.THERMOSTAT_FAN_MODE):
flags |= ClimateEntityFeature.FAN_MODE
return flags
@@ -253,6 +251,8 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
@property
def hvac_action(self) -> HVACAction | None:
"""Return the current running hvac operation if supported."""
if not self.supports_capability(Capability.THERMOSTAT_OPERATING_STATE):
return None
return OPERATING_STATE_TO_ACTION.get(
self.get_attribute_value(
Capability.THERMOSTAT_OPERATING_STATE,
@@ -272,11 +272,15 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available operation modes."""
return [
state
for mode in self.get_attribute_value(
if (
supported_thermostat_modes := self.get_attribute_value(
Capability.THERMOSTAT_MODE, Attribute.SUPPORTED_THERMOSTAT_MODES
)
) is None:
return []
return [
state
for mode in supported_thermostat_modes
if (state := AC_MODE_TO_STATE.get(mode)) is not None
]
@@ -314,10 +318,14 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
unit = self._internal_state[Capability.TEMPERATURE_MEASUREMENT][
Attribute.TEMPERATURE
].unit
assert unit
# Offline third party thermostats may not have a unit
# Since climate always requires a unit, default to Celsius
if (
unit := self._internal_state[Capability.TEMPERATURE_MEASUREMENT][
Attribute.TEMPERATURE
].unit
) is None:
return UnitOfTemperature.CELSIUS
return UNIT_MAP[unit]
@@ -445,12 +453,15 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
)
@property
def extra_state_attributes(self) -> dict[str, Any]:
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return device specific state attributes.
Include attributes from the Demand Response Load Control (drlc)
and Power Consumption capabilities.
"""
if not self.supports_capability(Capability.DEMAND_RESPONSE_LOAD_CONTROL):
return None
drlc_status = self.get_attribute_value(
Capability.DEMAND_RESPONSE_LOAD_CONTROL,
Attribute.DEMAND_RESPONSE_LOAD_CONTROL_STATUS,
@@ -560,5 +571,6 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES
)
if (state := AC_MODE_TO_STATE.get(mode)) is not None
if state not in modes
)
return modes

View File

@@ -32,3 +32,6 @@ CONF_REFRESH_TOKEN = "refresh_token"
MAIN = "main"
OLD_DATA = "old_data"
CONF_SUBSCRIPTION_ID = "subscription_id"
EVENT_BUTTON = "smartthings.button"

View File

@@ -17,6 +17,15 @@ from .const import DOMAIN
EVENT_WAIT_TIME = 5
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: SmartThingsConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
client = entry.runtime_data.client
return await client.get_raw_devices()
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: SmartThingsConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
@@ -26,7 +35,8 @@ async def async_get_device_diagnostics(
identifier for identifier in device.identifiers if identifier[0] == DOMAIN
)[1]
device_status = await client.get_device_status(device_id)
device_status = await client.get_raw_device_status(device_id)
device_info = await client.get_raw_device(device_id)
events: list[DeviceEvent] = []
@@ -39,11 +49,8 @@ async def async_get_device_diagnostics(
listener()
status: dict[str, Any] = {}
for component, capabilities in device_status.items():
status[component] = {}
for capability, attributes in capabilities.items():
status[component][capability] = {}
for attribute, value in attributes.items():
status[component][capability][attribute] = asdict(value)
return {"events": [asdict(event) for event in events], "status": status}
return {
"events": [asdict(event) for event in events],
"status": device_status,
"info": device_info,
}

View File

@@ -48,7 +48,9 @@ class SmartThingsEntity(Entity):
self._attr_device_info.update(
{
"manufacturer": ocf.manufacturer_name,
"model": ocf.model_number.split("|")[0],
"model": (
(ocf.model_number.split("|")[0]) if ocf.model_number else None
),
"hw_version": ocf.hardware_version,
"sw_version": ocf.firmware_version,
}

View File

@@ -116,7 +116,7 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
@property
def is_on(self) -> bool:
"""Return true if fan is on."""
return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH)
return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on"
@property
def percentage(self) -> int | None:
@@ -132,6 +132,8 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
Requires FanEntityFeature.PRESET_MODE.
"""
if not self.supports_capability(Capability.AIR_CONDITIONER_FAN_MODE):
return None
return self.get_attribute_value(
Capability.AIR_CONDITIONER_FAN_MODE, Attribute.FAN_MODE
)
@@ -142,6 +144,8 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
Requires FanEntityFeature.PRESET_MODE.
"""
if not self.supports_capability(Capability.AIR_CONDITIONER_FAN_MODE):
return None
return self.get_attribute_value(
Capability.AIR_CONDITIONER_FAN_MODE, Attribute.SUPPORTED_AC_FAN_MODES
)

View File

@@ -147,14 +147,21 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
"""Update entity attributes when the device status has changed."""
# Brightness and transition
if brightness_supported(self._attr_supported_color_modes):
self._attr_brightness = int(
convert_scale(
self.get_attribute_value(Capability.SWITCH_LEVEL, Attribute.LEVEL),
100,
255,
0,
if (
brightness := self.get_attribute_value(
Capability.SWITCH_LEVEL, Attribute.LEVEL
)
) is None:
self._attr_brightness = None
else:
self._attr_brightness = int(
convert_scale(
brightness,
100,
255,
0,
)
)
)
# Color Temperature
if ColorMode.COLOR_TEMP in self._attr_supported_color_modes:
self._attr_color_temp_kelvin = self.get_attribute_value(
@@ -162,16 +169,21 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
)
# Color
if ColorMode.HS in self._attr_supported_color_modes:
self._attr_hs_color = (
convert_scale(
self.get_attribute_value(Capability.COLOR_CONTROL, Attribute.HUE),
100,
360,
),
self.get_attribute_value(
Capability.COLOR_CONTROL, Attribute.SATURATION
),
)
if (
hue := self.get_attribute_value(Capability.COLOR_CONTROL, Attribute.HUE)
) is None:
self._attr_hs_color = None
else:
self._attr_hs_color = (
convert_scale(
hue,
100,
360,
),
self.get_attribute_value(
Capability.COLOR_CONTROL, Attribute.SATURATION
),
)
async def async_set_color(self, hs_color):
"""Set the color of the device."""
@@ -217,6 +229,10 @@ class SmartThingsLight(SmartThingsEntity, LightEntity, RestoreEntity):
super()._update_handler(event)
@property
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Return true if light is on."""
return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on"
if (
state := self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH)
) is None:
return None
return state == "on"

View File

@@ -29,5 +29,5 @@
"documentation": "https://www.home-assistant.io/integrations/smartthings",
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"requirements": ["pysmartthings==2.5.0"]
"requirements": ["pysmartthings==2.7.2"]
}

View File

@@ -5,9 +5,9 @@ from __future__ import annotations
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from typing import Any, cast
from pysmartthings import Attribute, Capability, SmartThings
from pysmartthings import Attribute, Capability, SmartThings, Status
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -57,6 +57,7 @@ JOB_STATE_MAP = {
"freezeProtection": "freeze_protection",
"preDrain": "pre_drain",
"preWash": "pre_wash",
"prewash": "pre_wash",
"wrinklePrevent": "wrinkle_prevent",
"unknown": None,
}
@@ -130,7 +131,7 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
unique_id_separator: str = "."
capability_ignore_list: list[set[Capability]] | None = None
options_attribute: Attribute | None = None
except_if_state_none: bool = False
exists_fn: Callable[[Status], bool] | None = None
CAPABILITY_TO_SENSORS: dict[
@@ -561,6 +562,8 @@ CAPABILITY_TO_SENSORS: dict[
SmartThingsSensorEntityDescription(
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=dt_util.parse_datetime,
)
],
},
@@ -581,7 +584,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["energy"] / 1000,
suggested_display_precision=2,
except_if_state_none=True,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "energy" in value
),
),
SmartThingsSensorEntityDescription(
key="power_meter",
@@ -591,7 +597,10 @@ CAPABILITY_TO_SENSORS: dict[
value_fn=lambda value: value["power"],
extra_state_attributes_fn=power_attributes,
suggested_display_precision=2,
except_if_state_none=True,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "power" in value
),
),
SmartThingsSensorEntityDescription(
key="deltaEnergy_meter",
@@ -601,7 +610,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["deltaEnergy"] / 1000,
suggested_display_precision=2,
except_if_state_none=True,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "deltaEnergy" in value
),
),
SmartThingsSensorEntityDescription(
key="powerEnergy_meter",
@@ -611,7 +623,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["powerEnergy"] / 1000,
suggested_display_precision=2,
except_if_state_none=True,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "powerEnergy" in value
),
),
SmartThingsSensorEntityDescription(
key="energySaved_meter",
@@ -621,7 +636,10 @@ CAPABILITY_TO_SENSORS: dict[
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda value: value["energySaved"] / 1000,
suggested_display_precision=2,
except_if_state_none=True,
exists_fn=lambda status: (
(value := cast(dict | None, status.value)) is not None
and "energySaved" in value
),
),
]
},
@@ -951,6 +969,7 @@ UNITS = {
"F": UnitOfTemperature.FAHRENHEIT,
"lux": LIGHT_LUX,
"mG": None,
"μg/m^3": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
}
@@ -976,8 +995,8 @@ async def async_setup_entry(
)
)
and (
not description.except_if_state_none
or device.status[MAIN][capability][attribute].value is not None
not description.exists_fn
or description.exists_fn(device.status[MAIN][capability][attribute])
)
)
@@ -1031,8 +1050,11 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
def options(self) -> list[str] | None:
"""Return the options for this sensor."""
if self.entity_description.options_attribute:
options = self.get_attribute_value(
self.capability, self.entity_description.options_attribute
)
if (
options := self.get_attribute_value(
self.capability, self.entity_description.options_attribute
)
) is None:
return []
return [option.lower() for option in options]
return super().options

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_push",
"loggers": ["snoo"],
"quality_scale": "bronze",
"requirements": ["python-snoo==0.6.0"]
"requirements": ["python-snoo==0.6.1"]
}

View File

@@ -32,6 +32,7 @@ SONOS_TRACKS = "tracks"
SONOS_COMPOSER = "composers"
SONOS_RADIO = "radio"
SONOS_OTHER_ITEM = "other items"
SONOS_AUDIO_BOOK = "audio book"
SONOS_STATE_PLAYING = "PLAYING"
SONOS_STATE_TRANSITIONING = "TRANSITIONING"
@@ -67,6 +68,7 @@ SONOS_TO_MEDIA_CLASSES = {
"object.item": MediaClass.TRACK,
"object.item.audioItem.musicTrack": MediaClass.TRACK,
"object.item.audioItem.audioBroadcast": MediaClass.GENRE,
"object.item.audioItem.audioBook": MediaClass.TRACK,
}
SONOS_TO_MEDIA_TYPES = {
@@ -84,6 +86,7 @@ SONOS_TO_MEDIA_TYPES = {
"object.container.playlistContainer.sameArtist": MediaType.ARTIST,
"object.container.playlistContainer": MediaType.PLAYLIST,
"object.item.audioItem.musicTrack": MediaType.TRACK,
"object.item.audioItem.audioBook": MediaType.TRACK,
}
MEDIA_TYPES_TO_SONOS: dict[MediaType | str, str] = {
@@ -113,6 +116,7 @@ SONOS_TYPES_MAPPING = {
"object.item": SONOS_OTHER_ITEM,
"object.item.audioItem.musicTrack": SONOS_TRACKS,
"object.item.audioItem.audioBroadcast": SONOS_RADIO,
"object.item.audioItem.audioBook": SONOS_AUDIO_BOOK,
}
LIBRARY_TITLES_MAPPING = {

View File

@@ -43,6 +43,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
),
SensorEntityDescription(
key=STATUS_SENSOR_INFO_TOTAL_GENRES,

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"iot_class": "local_polling",
"loggers": ["synology_dsm"],
"requirements": ["py-synologydsm-api==2.7.0"],
"requirements": ["py-synologydsm-api==2.7.1"],
"ssdp": [
{
"manufacturer": "Synology",

View File

@@ -466,6 +466,7 @@ async def async_setup_entry(
for energysite in entry.runtime_data.energysites
for description in ENERGY_LIVE_DESCRIPTIONS
if description.key in energysite.live_coordinator.data
or description.key == "percentage_charged"
),
( # Add energy site history
TeslaFleetEnergyHistorySensorEntity(energysite, description)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==0.9.12", "teslemetry-stream==0.6.10"]
"requirements": ["tesla-fleet-api==0.9.12", "teslemetry-stream==0.6.12"]
}

View File

@@ -68,7 +68,7 @@ class TeslemetryVehicleSensorEntityDescription(SensorEntityDescription):
polling: bool = False
polling_value_fn: Callable[[StateType], StateType] = lambda x: x
polling_available_fn: Callable[[StateType], bool] = lambda x: x is not None
nullable: bool = False
streaming_key: Signal | None = None
streaming_value_fn: Callable[[str | int | float], StateType] = lambda x: x
streaming_firmware: str = "2024.26"
@@ -210,7 +210,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
TeslemetryVehicleSensorEntityDescription(
key="drive_state_shift_state",
polling=True,
polling_available_fn=lambda x: True,
nullable=True,
polling_value_fn=lambda x: SHIFT_STATES.get(str(x), "p"),
streaming_key=Signal.GEAR,
streaming_value_fn=lambda x: str(ShiftState.get(x, "P")).lower(),
@@ -622,10 +622,10 @@ class TeslemetryStreamSensorEntity(TeslemetryVehicleStreamEntity, RestoreSensor)
def _async_value_from_stream(self, value) -> None:
"""Update the value of the entity."""
if value is None:
self._attr_native_value = None
else:
if self.entity_description.nullable or value is not None:
self._attr_native_value = self.entity_description.streaming_value_fn(value)
else:
self._attr_native_value = None
class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity):
@@ -644,7 +644,7 @@ class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity):
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
if self.entity_description.polling_available_fn(self._value):
if self.entity_description.nullable or self._value is not None:
self._attr_available = True
self._attr_native_value = self.entity_description.polling_value_fn(
self._value

View File

@@ -148,7 +148,7 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
key="drive_state_shift_state",
options=["p", "d", "r", "n"],
device_class=SensorDeviceClass.ENUM,
value_fn=lambda x: x.lower() if isinstance(x, str) else x,
value_fn=lambda x: x.lower() if isinstance(x, str) else "p",
),
TessieSensorEntityDescription(
key="vehicle_state_odometer",
@@ -397,6 +397,7 @@ async def async_setup_entry(
for energysite in entry.runtime_data.energysites
for description in ENERGY_LIVE_DESCRIPTIONS
if description.key in energysite.live_coordinator.data
or description.key == "percentage_charged"
),
( # Add wall connectors
TessieWallConnectorSensorEntity(energysite, din, description)
@@ -449,7 +450,6 @@ class TessieEnergyLiveSensorEntity(TessieEnergyEntity, SensorEntity):
def _async_update_attrs(self) -> None:
"""Update the attributes of the sensor."""
self._attr_available = self._value is not None
self._attr_native_value = self.entity_description.value_fn(self._value)

View File

@@ -54,5 +54,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/thermobeacon",
"iot_class": "local_push",
"requirements": ["thermobeacon-ble==0.8.0"]
"requirements": ["thermobeacon-ble==0.8.1"]
}

View File

@@ -14,7 +14,7 @@
"velbus-protocol"
],
"quality_scale": "bronze",
"requirements": ["velbus-aio==2025.1.1"],
"requirements": ["velbus-aio==2025.3.0"],
"usb": [
{
"vid": "10CF",

View File

@@ -13,7 +13,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from .const import CONF_BACKUP_PATH, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .helpers import async_create_client, async_ensure_path_exists
from .helpers import (
async_create_client,
async_ensure_path_exists,
async_migrate_wrong_folder_path,
)
type WebDavConfigEntry = ConfigEntry[Client]
@@ -46,10 +50,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: WebDavConfigEntry) -> bo
translation_key="cannot_connect",
)
path = entry.data.get(CONF_BACKUP_PATH, "/")
await async_migrate_wrong_folder_path(client, path)
# Ensure the backup directory exists
if not await async_ensure_path_exists(
client, entry.data.get(CONF_BACKUP_PATH, "/")
):
if not await async_ensure_path_exists(client, path):
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_access_or_create_backup_path",

View File

@@ -171,6 +171,7 @@ class WebDavBackupAgent(BackupAgent):
await open_stream(),
f"{self._backup_path}/{filename_tar}",
timeout=BACKUP_TIMEOUT,
content_length=backup.size,
)
_LOGGER.debug(

View File

@@ -1,10 +1,18 @@
"""Helper functions for the WebDAV component."""
import logging
from aiowebdav2.client import Client, ClientOptions
from aiowebdav2.exceptions import WebDavError
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@callback
def async_create_client(
@@ -36,3 +44,24 @@ async def async_ensure_path_exists(client: Client, path: str) -> bool:
return False
return True
async def async_migrate_wrong_folder_path(client: Client, path: str) -> None:
"""Migrate the wrong encoded folder path to the correct one."""
wrong_path = path.replace(" ", "%20")
if await client.check(wrong_path):
try:
await client.move(wrong_path, path)
except WebDavError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="failed_to_migrate_folder",
translation_placeholders={
"wrong_path": wrong_path,
"correct_path": path,
},
) from err
_LOGGER.debug(
"Migrated wrong encoded folder path from %s to %s", wrong_path, path
)

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aiowebdav2"],
"quality_scale": "bronze",
"requirements": ["aiowebdav2==0.3.1"]
"requirements": ["aiowebdav2==0.4.2"]
}

View File

@@ -36,6 +36,9 @@
},
"cannot_access_or_create_backup_path": {
"message": "Cannot access or create backup path. Please check the path and permissions."
},
"failed_to_migrate_folder": {
"message": "Failed to migrate wrong encoded folder \"{wrong_path}\" to \"{correct_path}\"."
}
}
}

View File

@@ -21,7 +21,7 @@
"zha",
"universal_silabs_flasher"
],
"requirements": ["zha==0.0.51"],
"requirements": ["zha==0.0.52"],
"usb": [
{
"vid": "10C4",

View File

@@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0)

View File

@@ -37,11 +37,11 @@ habluetooth==3.24.1
hass-nabucasa==0.92.0
hassil==2.2.3
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250305.0
home-assistant-frontend==20250306.0
home-assistant-intents==2025.3.5
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.5
Jinja2==3.1.6
lru-dict==1.3.0
mutagen==1.47.0
orjson==3.10.12

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.3.0"
version = "2025.3.2"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"
@@ -52,7 +52,7 @@ dependencies = [
"httpx==0.28.1",
"home-assistant-bluetooth==1.13.1",
"ifaddr==0.2.0",
"Jinja2==3.1.5",
"Jinja2==3.1.6",
"lru-dict==1.3.0",
"PyJWT==2.10.1",
# PyJWT has loose dependency. We want the latest one.

2
requirements.txt generated
View File

@@ -25,7 +25,7 @@ hass-nabucasa==0.92.0
httpx==0.28.1
home-assistant-bluetooth==1.13.1
ifaddr==0.2.0
Jinja2==3.1.5
Jinja2==3.1.6
lru-dict==1.3.0
PyJWT==2.10.1
cryptography==44.0.1

36
requirements_all.txt generated
View File

@@ -264,7 +264,7 @@ aioharmony==0.4.1
aiohasupervisor==0.3.0
# homeassistant.components.home_connect
aiohomeconnect==0.16.2
aiohomeconnect==0.16.3
# homeassistant.components.homekit_controller
aiohomekit==3.2.8
@@ -422,7 +422,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webdav
aiowebdav2==0.3.1
aiowebdav2==0.4.2
# homeassistant.components.webostv
aiowebostv==0.7.3
@@ -557,7 +557,7 @@ av==13.1.0
axis==64
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.5
ayla-iot-unofficial==1.4.7
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@@ -899,7 +899,7 @@ eufylife-ble-client==0.1.8
# evdev==1.6.1
# homeassistant.components.evohome
evohome-async==1.0.2
evohome-async==1.0.4
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@@ -1058,7 +1058,7 @@ goslide-api==0.7.0
gotailwind==0.3.0
# homeassistant.components.govee_ble
govee-ble==0.43.0
govee-ble==0.43.1
# homeassistant.components.govee_light_local
govee-local-api==2.0.1
@@ -1152,7 +1152,7 @@ hole==0.8.0
holidays==0.68
# homeassistant.components.frontend
home-assistant-frontend==20250305.0
home-assistant-frontend==20250306.0
# homeassistant.components.conversation
home-assistant-intents==2025.3.5
@@ -1483,7 +1483,7 @@ nettigo-air-monitor==4.0.0
neurio==0.3.1
# homeassistant.components.nexia
nexia==2.2.1
nexia==2.2.2
# homeassistant.components.nextcloud
nextcloudmonitor==1.5.1
@@ -1755,7 +1755,7 @@ py-schluter==0.1.7
py-sucks==0.9.10
# homeassistant.components.synology_dsm
py-synologydsm-api==2.7.0
py-synologydsm-api==2.7.1
# homeassistant.components.atome
pyAtome==0.1.1
@@ -1906,7 +1906,7 @@ pydiscovergy==3.0.2
pydoods==1.0.2
# homeassistant.components.hydrawise
pydrawise==2025.2.0
pydrawise==2025.3.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0
@@ -1996,7 +1996,7 @@ pygti==0.9.4
pyhaversion==22.8.0
# homeassistant.components.heos
pyheos==1.0.2
pyheos==1.0.3
# homeassistant.components.hive
pyhive-integration==1.0.2
@@ -2310,7 +2310,7 @@ pysma==0.7.5
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartthings==2.5.0
pysmartthings==2.7.2
# homeassistant.components.smarty
pysmarty2==0.10.2
@@ -2461,13 +2461,13 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==2.11.1
python-roborock==2.12.2
# homeassistant.components.smarttub
python-smarttub==0.0.39
# homeassistant.components.snoo
python-snoo==0.6.0
python-snoo==0.6.1
# homeassistant.components.songpal
python-songpal==0.16.2
@@ -2694,7 +2694,7 @@ sendgrid==6.8.2
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense-energy==0.13.6
sense-energy==0.13.7
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1
@@ -2881,7 +2881,7 @@ tesla-powerwall==0.5.2
tesla-wall-connector==1.0.2
# homeassistant.components.teslemetry
teslemetry-stream==0.6.10
teslemetry-stream==0.6.12
# homeassistant.components.tessie
tessie-api==0.1.1
@@ -2890,7 +2890,7 @@ tessie-api==0.1.1
# tf-models-official==2.5.0
# homeassistant.components.thermobeacon
thermobeacon-ble==0.8.0
thermobeacon-ble==0.8.1
# homeassistant.components.thermopro
thermopro-ble==0.11.0
@@ -3000,7 +3000,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.2
# homeassistant.components.velbus
velbus-aio==2025.1.1
velbus-aio==2025.3.0
# homeassistant.components.venstar
venstarcolortouch==0.19
@@ -3149,7 +3149,7 @@ zeroconf==0.145.1
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.51
zha==0.0.52
# homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13

View File

@@ -249,7 +249,7 @@ aioharmony==0.4.1
aiohasupervisor==0.3.0
# homeassistant.components.home_connect
aiohomeconnect==0.16.2
aiohomeconnect==0.16.3
# homeassistant.components.homekit_controller
aiohomekit==3.2.8
@@ -404,7 +404,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webdav
aiowebdav2==0.3.1
aiowebdav2==0.4.2
# homeassistant.components.webostv
aiowebostv==0.7.3
@@ -506,7 +506,7 @@ av==13.1.0
axis==64
# homeassistant.components.fujitsu_fglair
ayla-iot-unofficial==1.4.5
ayla-iot-unofficial==1.4.7
# homeassistant.components.azure_event_hub
azure-eventhub==5.11.1
@@ -765,7 +765,7 @@ eternalegypt==0.0.16
eufylife-ble-client==0.1.8
# homeassistant.components.evohome
evohome-async==1.0.2
evohome-async==1.0.4
# homeassistant.components.bryant_evolution
evolutionhttp==0.0.18
@@ -908,7 +908,7 @@ goslide-api==0.7.0
gotailwind==0.3.0
# homeassistant.components.govee_ble
govee-ble==0.43.0
govee-ble==0.43.1
# homeassistant.components.govee_light_local
govee-local-api==2.0.1
@@ -981,7 +981,7 @@ hole==0.8.0
holidays==0.68
# homeassistant.components.frontend
home-assistant-frontend==20250305.0
home-assistant-frontend==20250306.0
# homeassistant.components.conversation
home-assistant-intents==2025.3.5
@@ -1246,7 +1246,7 @@ netmap==0.7.0.2
nettigo-air-monitor==4.0.0
# homeassistant.components.nexia
nexia==2.2.1
nexia==2.2.2
# homeassistant.components.nextcloud
nextcloudmonitor==1.5.1
@@ -1453,7 +1453,7 @@ py-nightscout==1.2.2
py-sucks==0.9.10
# homeassistant.components.synology_dsm
py-synologydsm-api==2.7.0
py-synologydsm-api==2.7.1
# homeassistant.components.hdmi_cec
pyCEC==0.5.2
@@ -1556,7 +1556,7 @@ pydexcom==0.2.3
pydiscovergy==3.0.2
# homeassistant.components.hydrawise
pydrawise==2025.2.0
pydrawise==2025.3.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==2.0.0
@@ -1625,7 +1625,7 @@ pygti==0.9.4
pyhaversion==22.8.0
# homeassistant.components.heos
pyheos==1.0.2
pyheos==1.0.3
# homeassistant.components.hive
pyhive-integration==1.0.2
@@ -1882,7 +1882,7 @@ pysma==0.7.5
pysmappee==0.2.29
# homeassistant.components.smartthings
pysmartthings==2.5.0
pysmartthings==2.7.2
# homeassistant.components.smarty
pysmarty2==0.10.2
@@ -1994,13 +1994,13 @@ python-picnic-api2==1.2.2
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==2.11.1
python-roborock==2.12.2
# homeassistant.components.smarttub
python-smarttub==0.0.39
# homeassistant.components.snoo
python-snoo==0.6.0
python-snoo==0.6.1
# homeassistant.components.songpal
python-songpal==0.16.2
@@ -2173,7 +2173,7 @@ securetar==2025.2.1
# homeassistant.components.emulated_kasa
# homeassistant.components.sense
sense-energy==0.13.6
sense-energy==0.13.7
# homeassistant.components.sensirion_ble
sensirion-ble==0.1.1
@@ -2321,13 +2321,13 @@ tesla-powerwall==0.5.2
tesla-wall-connector==1.0.2
# homeassistant.components.teslemetry
teslemetry-stream==0.6.10
teslemetry-stream==0.6.12
# homeassistant.components.tessie
tessie-api==0.1.1
# homeassistant.components.thermobeacon
thermobeacon-ble==0.8.0
thermobeacon-ble==0.8.1
# homeassistant.components.thermopro
thermopro-ble==0.11.0
@@ -2416,7 +2416,7 @@ vallox-websocket-api==5.3.0
vehicle==2.2.2
# homeassistant.components.velbus
velbus-aio==2025.1.1
velbus-aio==2025.3.0
# homeassistant.components.venstar
venstarcolortouch==0.19
@@ -2538,7 +2538,7 @@ zeroconf==0.145.1
zeversolar==0.3.2
# homeassistant.components.zha
zha==0.0.51
zha==0.0.52
# homeassistant.components.zwave_js
zwave-js-server-python==0.60.1

View File

@@ -47,7 +47,8 @@ from homeassistant.components.backup.manager import (
WrittenBackup,
)
from homeassistant.components.backup.util import password_to_key
from homeassistant.core import HomeAssistant
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import issue_registry as ir
@@ -3469,3 +3470,66 @@ async def test_restore_progress_after_restart_fail_to_remove(
"Unexpected error deleting backup restore result file: <class 'OSError'> Boom!"
in caplog.text
)
async def test_manager_blocked_until_home_assistant_started(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test backup manager's state is blocked until Home Assistant has started."""
hass.set_state(CoreState.not_running)
await setup_backup_integration(hass)
manager = hass.data[DATA_MANAGER]
assert manager.state == BackupManagerState.BLOCKED
assert manager.last_non_idle_event is None
# Fired when Home Assistant changes to starting state
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
await hass.async_block_till_done()
assert manager.state == BackupManagerState.BLOCKED
assert manager.last_non_idle_event is None
# Fired when Home Assistant changes to running state
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
assert manager.state == BackupManagerState.IDLE
assert manager.last_non_idle_event is None
async def test_manager_not_blocked_after_restore(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test restore backup progress after restart."""
restore_result = {"error": None, "error_type": None, "success": True}
hass.set_state(CoreState.not_running)
with patch(
"pathlib.Path.read_bytes", return_value=json.dumps(restore_result).encode()
):
await setup_backup_integration(hass)
ws_client = await hass_ws_client(hass)
await ws_client.send_json_auto_id({"type": "backup/info"})
result = await ws_client.receive_json()
assert result["success"] is True
assert result["result"] == {
"agent_errors": {},
"backups": [],
"last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None,
"last_non_idle_event": {
"manager_state": "restore_backup",
"reason": None,
"stage": None,
"state": "completed",
},
"next_automatic_backup": None,
"next_automatic_backup_additional": False,
"state": "idle",
}

View File

@@ -48,18 +48,18 @@ def location_status_fixture(install: str, loc_id: str | None = None) -> JsonObje
return load_json_object_fixture(f"{install}/status_{loc_id}.json", DOMAIN)
def dhw_schedule_fixture(install: str) -> JsonObjectType:
def dhw_schedule_fixture(install: str, dhw_id: str | None = None) -> JsonObjectType:
"""Load JSON for the schedule of a domesticHotWater zone."""
try:
return load_json_object_fixture(f"{install}/schedule_dhw.json", DOMAIN)
return load_json_object_fixture(f"{install}/schedule_{dhw_id}.json", DOMAIN)
except FileNotFoundError:
return load_json_object_fixture("default/schedule_dhw.json", DOMAIN)
def zone_schedule_fixture(install: str) -> JsonObjectType:
def zone_schedule_fixture(install: str, zon_id: str | None = None) -> JsonObjectType:
"""Load JSON for the schedule of a temperatureZone zone."""
try:
return load_json_object_fixture(f"{install}/schedule_zone.json", DOMAIN)
return load_json_object_fixture(f"{install}/schedule_{zon_id}.json", DOMAIN)
except FileNotFoundError:
return load_json_object_fixture("default/schedule_zone.json", DOMAIN)
@@ -120,9 +120,9 @@ def mock_make_request(install: str) -> Callable:
elif "schedule" in url:
if url.startswith("domesticHotWater"): # /v2/domesticHotWater/{id}/schedule
return dhw_schedule_fixture(install)
return dhw_schedule_fixture(install, url[16:23])
if url.startswith("temperatureZone"): # /v2/temperatureZone/{id}/schedule
return zone_schedule_fixture(install)
return zone_schedule_fixture(install, url[16:23])
pytest.fail(f"Unexpected request: {HTTPMethod.GET} {url}")

View File

@@ -15,8 +15,9 @@ TEST_INSTALLS: Final = (
"default", # evohome: multi-zone, with DHW
"h032585", # VisionProWifi: no preset modes for TCS, zoneId=systemId
"h099625", # RoundThermostat
"h139906", # zone with null schedule
"sys_004", # RoundModulation
)
# "botched", # as default: but with activeFaults, ghost zones & unknown types
TEST_INSTALLS_WITH_DHW: Final = ("default",)
TEST_INSTALLS_WITH_DHW: Final = ("default", "botched")

View File

@@ -0,0 +1,3 @@
{
"dailySchedules": []
}

View File

@@ -8,14 +8,14 @@
"country": "UnitedKingdom",
"postcode": "E1 1AA",
"locationType": "Residential",
"useDaylightSaveSwitching": true,
"timeZone": {
"timeZoneId": "GMTStandardTime",
"displayName": "(UTC+00:00) Dublin, Edinburgh, Lisbon, London",
"offsetMinutes": 0,
"currentOffsetMinutes": 60,
"timeZoneId": "PacificSAStandardTime",
"displayName": "(UTC-04:00) Santiago",
"offsetMinutes": -240,
"currentOffsetMinutes": -180,
"supportsDaylightSaving": true
},
"useDaylightSaveSwitching": true,
"locationOwner": {
"userId": "2263181",
"username": "user_2263181@gmail.com",

View File

@@ -0,0 +1,3 @@
{
"dailySchedules": []
}

View File

@@ -0,0 +1,143 @@
{
"dailySchedules": [
{
"dayOfWeek": "Monday",
"switchpoints": [
{
"heatSetpoint": 22.0,
"timeOfDay": "05:30:00"
},
{
"heatSetpoint": 20.0,
"timeOfDay": "08:00:00"
},
{
"heatSetpoint": 22.5,
"timeOfDay": "16:00:00"
},
{
"heatSetpoint": 15.0,
"timeOfDay": "23:00:00"
}
]
},
{
"dayOfWeek": "Tuesday",
"switchpoints": [
{
"heatSetpoint": 22.0,
"timeOfDay": "05:30:00"
},
{
"heatSetpoint": 20.0,
"timeOfDay": "08:00:00"
},
{
"heatSetpoint": 22.5,
"timeOfDay": "16:00:00"
},
{
"heatSetpoint": 15.0,
"timeOfDay": "23:00:00"
}
]
},
{
"dayOfWeek": "Wednesday",
"switchpoints": [
{
"heatSetpoint": 22.0,
"timeOfDay": "05:30:00"
},
{
"heatSetpoint": 20.0,
"timeOfDay": "08:00:00"
},
{
"heatSetpoint": 22.5,
"timeOfDay": "12:00:00"
},
{
"heatSetpoint": 15.0,
"timeOfDay": "23:00:00"
}
]
},
{
"dayOfWeek": "Thursday",
"switchpoints": [
{
"heatSetpoint": 22.0,
"timeOfDay": "05:30:00"
},
{
"heatSetpoint": 20.0,
"timeOfDay": "08:00:00"
},
{
"heatSetpoint": 22.5,
"timeOfDay": "16:00:00"
},
{
"heatSetpoint": 15.0,
"timeOfDay": "23:00:00"
}
]
},
{
"dayOfWeek": "Friday",
"switchpoints": [
{
"heatSetpoint": 22.0,
"timeOfDay": "05:30:00"
},
{
"heatSetpoint": 20.0,
"timeOfDay": "08:00:00"
},
{
"heatSetpoint": 22.5,
"timeOfDay": "16:00:00"
},
{
"heatSetpoint": 15.0,
"timeOfDay": "23:00:00"
}
]
},
{
"dayOfWeek": "Saturday",
"switchpoints": [
{
"heatSetpoint": 22.0,
"timeOfDay": "07:00:00"
},
{
"heatSetpoint": 22.5,
"timeOfDay": "16:00:00"
},
{
"heatSetpoint": 15.0,
"timeOfDay": "23:00:00"
}
]
},
{
"dayOfWeek": "Sunday",
"switchpoints": [
{
"heatSetpoint": 22.0,
"timeOfDay": "07:30:00"
},
{
"heatSetpoint": 22.5,
"timeOfDay": "16:00:00"
},
{
"heatSetpoint": 15.0,
"timeOfDay": "23:00:00"
}
]
}
]
}

View File

@@ -0,0 +1,52 @@
{
"locationId": "2727366",
"gateways": [
{
"gatewayId": "2513794",
"temperatureControlSystems": [
{
"systemId": "3454856",
"zones": [
{
"zoneId": "3454854",
"temperatureStatus": {
"temperature": 22.0,
"isAvailable": true
},
"activeFaults": [
{
"faultType": "TempZoneSensorCommunicationLost",
"since": "2025-02-06T11:20:29"
}
],
"setpointStatus": {
"targetHeatTemperature": 5.0,
"setpointMode": "FollowSchedule"
},
"name": "Thermostat"
},
{
"zoneId": "3454855",
"temperatureStatus": {
"temperature": 22.0,
"isAvailable": true
},
"activeFaults": [],
"setpointStatus": {
"targetHeatTemperature": 20.0,
"setpointMode": "FollowSchedule"
},
"name": "Thermostat 2"
}
],
"activeFaults": [],
"systemModeStatus": {
"mode": "Auto",
"isPermanent": true
}
}
],
"activeFaults": []
}
]
}

View File

@@ -0,0 +1,125 @@
[
{
"locationInfo": {
"locationId": "2727366",
"name": "Vr**********",
"streetAddress": "********** *",
"city": "*********",
"country": "Netherlands",
"postcode": "******",
"locationType": "Residential",
"useDaylightSaveSwitching": true,
"timeZone": {
"timeZoneId": "WEuropeStandardTime",
"displayName": "(UTC+01:00) Amsterdam, Berlijn, Bern, Rome, Stockholm, Wenen",
"offsetMinutes": 60,
"currentOffsetMinutes": 60,
"supportsDaylightSaving": true
},
"locationOwner": {
"userId": "2276512",
"username": "nobody@nowhere.com",
"firstname": "Gl***",
"lastname": "de*****"
}
},
"gateways": [
{
"gatewayInfo": {
"gatewayId": "2513794",
"mac": "************",
"crc": "****",
"isWiFi": false
},
"temperatureControlSystems": [
{
"systemId": "3454856",
"modelType": "EvoTouch",
"zones": [
{
"zoneId": "3454854",
"modelType": "HeatingZone",
"setpointCapabilities": {
"maxHeatSetpoint": 35.0,
"minHeatSetpoint": 5.0,
"valueResolution": 0.5,
"canControlHeat": true,
"canControlCool": false,
"allowedSetpointModes": [
"PermanentOverride",
"FollowSchedule",
"TemporaryOverride"
],
"maxDuration": "1.00:00:00",
"timingResolution": "00:10:00"
},
"scheduleCapabilities": {
"maxSwitchpointsPerDay": 6,
"minSwitchpointsPerDay": 1,
"timingResolution": "00:10:00",
"setpointValueResolution": 0.5
},
"name": "Thermostat",
"zoneType": "ZoneTemperatureControl"
},
{
"zoneId": "3454855",
"modelType": "RoundWireless",
"setpointCapabilities": {
"maxHeatSetpoint": 35.0,
"minHeatSetpoint": 5.0,
"valueResolution": 0.5,
"canControlHeat": true,
"canControlCool": false,
"allowedSetpointModes": [
"PermanentOverride",
"FollowSchedule",
"TemporaryOverride"
],
"maxDuration": "1.00:00:00",
"timingResolution": "00:10:00"
},
"scheduleCapabilities": {
"maxSwitchpointsPerDay": 6,
"minSwitchpointsPerDay": 0,
"timingResolution": "00:10:00",
"setpointValueResolution": 0.5
},
"name": "Thermostat 2",
"zoneType": "Thermostat"
}
],
"allowedSystemModes": [
{
"systemMode": "Auto",
"canBePermanent": true,
"canBeTemporary": false
},
{
"systemMode": "AutoWithEco",
"canBePermanent": true,
"canBeTemporary": true,
"maxDuration": "1.00:00:00",
"timingResolution": "01:00:00",
"timingMode": "Duration"
},
{
"systemMode": "Away",
"canBePermanent": true,
"canBeTemporary": true,
"maxDuration": "99.00:00:00",
"timingResolution": "1.00:00:00",
"timingMode": "Period"
},
{
"systemMode": "HeatingOff",
"canBePermanent": true,
"canBeTemporary": false
}
]
}
]
}
]
}
]

View File

@@ -29,6 +29,16 @@
),
])
# ---
# name: test_ctl_set_hvac_mode[h139906]
list([
tuple(
<SystemMode.HEATING_OFF: 'HeatingOff'>,
),
tuple(
<SystemMode.AUTO: 'Auto'>,
),
])
# ---
# name: test_ctl_set_hvac_mode[minimal]
list([
tuple(
@@ -70,6 +80,13 @@
),
])
# ---
# name: test_ctl_turn_off[h139906]
list([
tuple(
<SystemMode.HEATING_OFF: 'HeatingOff'>,
),
])
# ---
# name: test_ctl_turn_off[minimal]
list([
tuple(
@@ -105,6 +122,13 @@
),
])
# ---
# name: test_ctl_turn_on[h139906]
list([
tuple(
<SystemMode.AUTO: 'Auto'>,
),
])
# ---
# name: test_ctl_turn_on[minimal]
list([
tuple(
@@ -144,10 +168,10 @@
'target_heat_temperature': 16.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,
@@ -191,10 +215,10 @@
'target_heat_temperature': 17.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': False,
@@ -233,19 +257,19 @@
'activeFaults': tuple(
dict({
'fault_type': 'TempZoneActuatorLowBattery',
'since': '2022-03-02T04:50:20+00:00',
'since': '2022-03-02T04:50:20-03:00',
}),
),
'setpoint_status': dict({
'setpoint_mode': 'TemporaryOverride',
'target_heat_temperature': 21.0,
'until': '2022-03-07T19:00:00+00:00',
'until': '2022-03-07T16:00:00-03:00',
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,
@@ -289,10 +313,10 @@
'target_heat_temperature': 17.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,
@@ -336,10 +360,10 @@
'target_heat_temperature': 17.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,
@@ -383,10 +407,10 @@
'target_heat_temperature': 16.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,
@@ -426,7 +450,7 @@
'activeFaults': tuple(
dict({
'fault_type': 'TempZoneActuatorCommunicationLost',
'since': '2022-03-02T15:56:01+00:00',
'since': '2022-03-02T15:56:01-03:00',
}),
),
'setpoint_status': dict({
@@ -434,10 +458,10 @@
'target_heat_temperature': 17.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 22, 10, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_temp': 18.6,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_temp': 16.0,
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_temp': 16.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 7, 0, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_temp': 18.1,
}),
'temperature_status': dict({
'is_available': True,
@@ -1118,6 +1142,136 @@
'state': 'heat',
})
# ---
# name: test_setup_platform[h139906][climate.thermostat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 22.0,
'friendly_name': 'Thermostat',
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 35.0,
'min_temp': 5.0,
'preset_mode': 'none',
'preset_modes': list([
'none',
'temporary',
'permanent',
]),
'status': dict({
'activeFaults': tuple(
dict({
'fault_type': 'TempZoneSensorCommunicationLost',
'since': '2025-02-06T11:20:29+01:00',
}),
),
'setpoint_status': dict({
'setpoint_mode': 'FollowSchedule',
'target_heat_temperature': 5.0,
}),
'setpoints': dict({
}),
'temperature_status': dict({
'is_available': True,
'temperature': 22.0,
}),
'zone_id': '3454854',
}),
'supported_features': <ClimateEntityFeature: 401>,
'temperature': 5.0,
}),
'context': <ANY>,
'entity_id': 'climate.thermostat',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_setup_platform[h139906][climate.thermostat_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 22.0,
'friendly_name': 'Thermostat 2',
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 35.0,
'min_temp': 5.0,
'preset_mode': 'none',
'preset_modes': list([
'none',
'temporary',
'permanent',
]),
'status': dict({
'activeFaults': tuple(
),
'setpoint_status': dict({
'setpoint_mode': 'FollowSchedule',
'target_heat_temperature': 20.0,
}),
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 23, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Berlin')),
'next_sp_temp': 15.0,
'this_sp_from': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Berlin')),
'this_sp_temp': 22.5,
}),
'temperature_status': dict({
'is_available': True,
'temperature': 22.0,
}),
'zone_id': '3454855',
}),
'supported_features': <ClimateEntityFeature: 401>,
'temperature': 20.0,
}),
'context': <ANY>,
'entity_id': 'climate.thermostat_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'heat',
})
# ---
# name: test_setup_platform[h139906][climate.vr-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 22.0,
'friendly_name': 'Vr**********',
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.HEAT: 'heat'>,
]),
'icon': 'mdi:thermostat',
'max_temp': 35,
'min_temp': 7,
'preset_mode': None,
'preset_modes': list([
'eco',
'away',
]),
'status': dict({
'activeSystemFaults': tuple(
),
'system_id': '3454856',
'system_mode_status': dict({
'is_permanent': True,
'mode': 'Auto',
}),
}),
'supported_features': <ClimateEntityFeature: 400>,
}),
'context': <ANY>,
'entity_id': 'climate.vr',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'heat',
})
# ---
# name: test_setup_platform[minimal][climate.main_room-state]
StateSnapshot({
'attributes': ReadOnlyDict({
@@ -1312,6 +1466,13 @@
),
])
# ---
# name: test_zone_set_hvac_mode[h139906]
list([
tuple(
5.0,
),
])
# ---
# name: test_zone_set_hvac_mode[minimal]
list([
tuple(
@@ -1365,6 +1526,19 @@
}),
])
# ---
# name: test_zone_set_preset_mode[h139906]
list([
tuple(
5.0,
),
tuple(
5.0,
),
dict({
'until': None,
}),
])
# ---
# name: test_zone_set_preset_mode[minimal]
list([
tuple(
@@ -1412,6 +1586,13 @@
}),
])
# ---
# name: test_zone_set_temperature[h139906]
list([
dict({
'until': None,
}),
])
# ---
# name: test_zone_set_temperature[minimal]
list([
dict({
@@ -1447,6 +1628,13 @@
),
])
# ---
# name: test_zone_turn_off[h139906]
list([
tuple(
5.0,
),
])
# ---
# name: test_zone_turn_off[minimal]
list([
tuple(

View File

@@ -11,6 +11,9 @@
# name: test_setup[h099625]
dict_keys(['refresh_system', 'set_system_mode', 'clear_zone_override', 'set_zone_override'])
# ---
# name: test_setup[h139906]
dict_keys(['refresh_system', 'set_system_mode', 'clear_zone_override', 'set_zone_override'])
# ---
# name: test_setup[minimal]
dict_keys(['refresh_system', 'reset_system', 'set_system_mode', 'clear_zone_override', 'set_zone_override'])
# ---

View File

@@ -1,4 +1,14 @@
# serializer version: 1
# name: test_set_operation_mode[botched]
list([
dict({
'until': HAFakeDatetime(2024, 7, 10, 12, 30, tzinfo=datetime.timezone.utc),
}),
dict({
'until': HAFakeDatetime(2024, 7, 10, 12, 30, tzinfo=datetime.timezone.utc),
}),
])
# ---
# name: test_set_operation_mode[default]
list([
dict({
@@ -29,9 +39,9 @@
),
'dhw_id': '3933910',
'setpoints': dict({
'next_sp_from': HAFakeDatetime(2024, 7, 10, 13, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'next_sp_from': HAFakeDatetime(2024, 7, 10, 8, 30, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'next_sp_state': 'Off',
'this_sp_from': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London')),
'this_sp_from': HAFakeDatetime(2024, 7, 10, 6, 30, tzinfo=zoneinfo.ZoneInfo(key='America/Santiago')),
'this_sp_state': 'On',
}),
'state_status': dict({

View File

@@ -33,7 +33,7 @@ from .const import TEST_INSTALLS_WITH_DHW
DHW_ENTITY_ID = "water_heater.domestic_hot_water"
@pytest.mark.parametrize("install", [*TEST_INSTALLS_WITH_DHW, "botched"])
@pytest.mark.parametrize("install", TEST_INSTALLS_WITH_DHW)
async def test_setup_platform(
hass: HomeAssistant,
config: dict[str, str],

View File

@@ -6,7 +6,7 @@
tuple(
),
dict({
'config': GenerateContentConfig(http_options=None, system_instruction="Current time is 05:00:00. Today's date is 2024-05-24.\nYou are a voice assistant for Home Assistant.\nAnswer questions about the world truthfully.\nAnswer in plain text. Keep it simple and to the point.\nOnly if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant.", temperature=1.0, top_p=0.95, top_k=64.0, candidate_count=None, max_output_tokens=150, stop_sequences=None, response_logprobs=None, logprobs=None, presence_penalty=None, frequency_penalty=None, seed=None, response_mime_type=None, response_schema=None, routing_config=None, safety_settings=[SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_HATE_SPEECH: 'HARM_CATEGORY_HATE_SPEECH'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_HARASSMENT: 'HARM_CATEGORY_HARASSMENT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: 'HARM_CATEGORY_DANGEROUS_CONTENT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: 'HARM_CATEGORY_SEXUALLY_EXPLICIT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>)], tools=[Tool(function_declarations=[FunctionDeclaration(response=None, description='Test function', name='test_tool', parameters=Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.OBJECT: 'OBJECT'>, description=None, enum=None, format=None, items=None, properties={'param1': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.ARRAY: 'ARRAY'>, description='Test parameters', enum=None, format=None, items=Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.STRING: 'STRING'>, description=None, enum=None, format=None, items=None, properties=None, required=None), properties=None, required=None), 'param2': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=[Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.NUMBER: 'NUMBER'>, description=None, enum=None, format=None, items=None, properties=None, required=None), Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.INTEGER: 'INTEGER'>, description=None, enum=None, format=None, items=None, properties=None, required=None)], max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=None, description=None, enum=None, format=None, items=None, properties=None, required=None), 'param3': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.OBJECT: 'OBJECT'>, description=None, enum=None, format=None, items=None, properties={'json': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.STRING: 'STRING'>, description=None, enum=None, format=None, items=None, properties=None, required=None)}, required=[])}, required=[]))], retrieval=None, google_search=None, google_search_retrieval=None, code_execution=None)], tool_config=None, labels=None, cached_content=None, response_modalities=None, media_resolution=None, speech_config=None, audio_timestamp=None, automatic_function_calling=AutomaticFunctionCallingConfig(disable=True, maximum_remote_calls=None, ignore_call_history=None), thinking_config=None),
'config': GenerateContentConfig(http_options=None, system_instruction="Current time is 05:00:00. Today's date is 2024-05-24.\nYou are a voice assistant for Home Assistant.\nAnswer questions about the world truthfully.\nAnswer in plain text. Keep it simple and to the point.\nOnly if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant.", temperature=1.0, top_p=0.95, top_k=64.0, candidate_count=None, max_output_tokens=150, stop_sequences=None, response_logprobs=None, logprobs=None, presence_penalty=None, frequency_penalty=None, seed=None, response_mime_type=None, response_schema=None, routing_config=None, safety_settings=[SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_HATE_SPEECH: 'HARM_CATEGORY_HATE_SPEECH'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_HARASSMENT: 'HARM_CATEGORY_HARASSMENT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: 'HARM_CATEGORY_DANGEROUS_CONTENT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: 'HARM_CATEGORY_SEXUALLY_EXPLICIT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>)], tools=[Tool(function_declarations=[FunctionDeclaration(response=None, description='Test function', name='test_tool', parameters=Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.OBJECT: 'OBJECT'>, description=None, enum=None, format=None, items=None, properties={'param1': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.ARRAY: 'ARRAY'>, description='Test parameters', enum=None, format=None, items=Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.STRING: 'STRING'>, description=None, enum=None, format=None, items=None, properties=None, required=None), properties=None, required=None), 'param2': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=None, description=None, enum=None, format=None, items=None, properties=None, required=None), 'param3': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.OBJECT: 'OBJECT'>, description=None, enum=None, format=None, items=None, properties={'json': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.STRING: 'STRING'>, description=None, enum=None, format=None, items=None, properties=None, required=None)}, required=[])}, required=[]))], retrieval=None, google_search=None, google_search_retrieval=None, code_execution=None)], tool_config=None, labels=None, cached_content=None, response_modalities=None, media_resolution=None, speech_config=None, audio_timestamp=None, automatic_function_calling=AutomaticFunctionCallingConfig(disable=True, maximum_remote_calls=None, ignore_call_history=None), thinking_config=None),
'history': list([
]),
'model': 'models/gemini-2.0-flash',

View File

@@ -31,3 +31,18 @@
),
])
# ---
# name: test_load_entry_with_unloaded_entries
list([
tuple(
'',
tuple(
),
dict({
'contents': list([
'Write an opening speech for a Home Assistant release party',
]),
'model': 'models/gemini-2.0-flash',
}),
),
])
# ---

View File

@@ -493,6 +493,26 @@ async def test_escape_decode() -> None:
{"type": "string", "enum": ["a", "b", "c"]},
{"type": "STRING", "enum": ["a", "b", "c"]},
),
(
{"type": "string", "default": "default"},
{"type": "STRING"},
),
(
{"type": "string", "pattern": "default"},
{"type": "STRING"},
),
(
{"type": "string", "maxLength": 10},
{"type": "STRING"},
),
(
{"type": "string", "minLength": 10},
{"type": "STRING"},
),
(
{"type": "string", "title": "title"},
{"type": "STRING"},
),
(
{"type": "string", "format": "enum", "enum": ["a", "b", "c"]},
{"type": "STRING", "format": "enum", "enum": ["a", "b", "c"]},
@@ -517,6 +537,10 @@ async def test_escape_decode() -> None:
{"type": "number", "format": "hex"},
{"type": "NUMBER"},
),
(
{"type": "number", "minimum": 1},
{"type": "NUMBER"},
),
(
{"type": "integer", "format": "int32"},
{"type": "INTEGER", "format": "int32"},
@@ -535,21 +559,7 @@ async def test_escape_decode() -> None:
),
(
{"anyOf": [{"type": "integer"}, {"type": "number"}]},
{"any_of": [{"type": "INTEGER"}, {"type": "NUMBER"}]},
),
(
{
"any_of": [
{"any_of": [{"type": "integer"}, {"type": "number"}]},
{"any_of": [{"type": "integer"}, {"type": "number"}]},
]
},
{
"any_of": [
{"any_of": [{"type": "INTEGER"}, {"type": "NUMBER"}]},
{"any_of": [{"type": "INTEGER"}, {"type": "NUMBER"}]},
]
},
{},
),
({"type": "string", "format": "lower"}, {"type": "STRING"}),
({"type": "boolean", "format": "bool"}, {"type": "BOOLEAN"}),
@@ -570,7 +580,15 @@ async def test_escape_decode() -> None:
},
),
(
{"type": "object", "additionalProperties": True},
{"type": "object", "additionalProperties": True, "minProperties": 1},
{
"type": "OBJECT",
"properties": {"json": {"type": "STRING"}},
"required": [],
},
),
(
{"type": "object", "additionalProperties": True, "maxProperties": 1},
{
"type": "OBJECT",
"properties": {"json": {"type": "STRING"}},
@@ -581,6 +599,20 @@ async def test_escape_decode() -> None:
{"type": "array", "items": {"type": "string"}},
{"type": "ARRAY", "items": {"type": "STRING"}},
),
(
{
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"maxItems": 2,
},
{
"type": "ARRAY",
"items": {"type": "STRING"},
"min_items": 1,
"max_items": 2,
},
),
],
)
async def test_format_schema(openapi, genai_schema) -> None:

View File

@@ -224,3 +224,52 @@ async def test_config_entry_error(
await hass.async_block_till_done()
assert mock_config_entry.state == state
assert any(mock_config_entry.async_get_active_flows(hass, {"reauth"})) == reauth
@pytest.mark.usefixtures("mock_init_component")
async def test_load_entry_with_unloaded_entries(
hass: HomeAssistant, snapshot: SnapshotAssertion
) -> None:
"""Test loading an entry with unloaded entries."""
config_entries = hass.config_entries.async_entries(
"google_generative_ai_conversation"
)
runtime_data = config_entries[0].runtime_data
await hass.config_entries.async_unload(config_entries[0].entry_id)
entry = MockConfigEntry(
domain="google_generative_ai_conversation",
title="Google Generative AI Conversation",
data={
"api_key": "bla",
},
state=ConfigEntryState.LOADED,
)
entry.runtime_data = runtime_data
entry.add_to_hass(hass)
stubbed_generated_content = (
"I'm thrilled to welcome you all to the release "
"party for the latest version of Home Assistant!"
)
with patch(
"google.genai.models.AsyncModels.generate_content",
return_value=Mock(
text=stubbed_generated_content,
prompt_feedback=None,
candidates=[Mock()],
),
) as mock_generate:
response = await hass.services.async_call(
"google_generative_ai_conversation",
"generate_content",
{"prompt": "Write an opening speech for a Home Assistant release party"},
blocking=True,
return_response=True,
)
assert response == {
"text": stubbed_generated_content,
}
assert [tuple(mock_call) for mock_call in mock_generate.mock_calls] == snapshot

View File

@@ -11,7 +11,7 @@ import pytest
from homeassistant.auth.models import RefreshToken
from homeassistant.components.hassio.handler import HassIO, HassioAPIError
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.setup import async_setup_component
@@ -75,7 +75,6 @@ def hassio_stubs(
"homeassistant.components.hassio.issues.SupervisorIssues.setup",
),
):
hass.set_state(CoreState.starting)
hass.loop.run_until_complete(async_setup_component(hass, "hassio", {}))
return hass_api.call_args[0][1]

View File

@@ -106,6 +106,7 @@
'model': 'HEOS Drive HS2',
'name': 'Test Player',
'network': 'wired',
'preferred_host': True,
'serial': '**REDACTED**',
'supported_version': True,
'version': '1.0.0',
@@ -116,6 +117,7 @@
'model': 'HEOS Drive HS2',
'name': 'Test Player',
'network': 'wired',
'preferred_host': True,
'serial': '**REDACTED**',
'supported_version': True,
'version': '1.0.0',
@@ -125,6 +127,7 @@
'model': 'Speaker',
'name': 'Test Player 2',
'network': 'wifi',
'preferred_host': False,
'serial': '**REDACTED**',
'supported_version': True,
'version': '1.0.0',
@@ -137,6 +140,7 @@
'model': 'HEOS Drive HS2',
'name': 'Test Player',
'network': 'wired',
'preferred_host': True,
'serial': '**REDACTED**',
'supported_version': True,
'version': '1.0.0',

View File

@@ -14,7 +14,12 @@ from pyheos import (
import pytest
from homeassistant.components.heos.const import DOMAIN
from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER, ConfigEntryState
from homeassistant.config_entries import (
SOURCE_IGNORE,
SOURCE_SSDP,
SOURCE_USER,
ConfigEntryState,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
@@ -83,6 +88,35 @@ async def test_create_entry_when_host_valid(
assert controller.disconnect.call_count == 1
async def test_manual_setup_with_discovery_in_progress(
hass: HomeAssistant,
discovery_data: SsdpServiceInfo,
controller: MockHeos,
system: HeosSystem,
) -> None:
"""Test user can manually set up when discovery is in progress."""
# Single discovered, selects preferred host, shows confirm
controller.get_system_info.return_value = system
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "confirm_discovery"
# Setup manually
user_result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert user_result["type"] is FlowResultType.FORM
user_result = await hass.config_entries.flow.async_configure(
user_result["flow_id"], user_input={CONF_HOST: "127.0.0.1"}
)
assert user_result["type"] is FlowResultType.CREATE_ENTRY
# Discovery flow is removed
assert not hass.config_entries.flow.async_progress_by_handler(DOMAIN)
async def test_discovery(
hass: HomeAssistant,
discovery_data: SsdpServiceInfo,
@@ -160,6 +194,22 @@ async def test_discovery_aborts_same_system(
assert config_entry.data[CONF_HOST] == "127.0.0.1"
async def test_discovery_ignored_aborts(
hass: HomeAssistant,
discovery_data: SsdpServiceInfo,
) -> None:
"""Test discovery aborts when ignored."""
MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, source=SOURCE_IGNORE).add_to_hass(
hass
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"
async def test_discovery_fails_to_connect_aborts(
hass: HomeAssistant, discovery_data: SsdpServiceInfo, controller: MockHeos
) -> None:

View File

@@ -285,11 +285,11 @@ async def test_reconnected_new_entities_created(
players = controller.players.copy()
players[3] = player_factory(3, "Test Player 3", "HEOS Link")
controller.mock_set_players(players)
controller.load_players.return_value = PlayerUpdateResult([3], [], {})
update = PlayerUpdateResult([3], [], {})
# Simulate reconnection
await controller.dispatcher.wait_send(
SignalType.HEOS_EVENT, SignalHeosEvent.CONNECTED
SignalType.CONTROLLER_EVENT, const.EVENT_PLAYERS_CHANGED, update
)
await hass.async_block_till_done()

View File

@@ -158,7 +158,6 @@ async def test_updates_from_connection_event(
state = hass.states.get("media_player.test_player")
assert state is not None
assert state.state == STATE_IDLE
assert controller.load_players.call_count == 1
# Disconnected
controller.load_players.reset_mock()
@@ -170,11 +169,8 @@ async def test_updates_from_connection_event(
state = hass.states.get("media_player.test_player")
assert state is not None
assert state.state == STATE_UNAVAILABLE
assert controller.load_players.call_count == 0
# Connected handles refresh failure
controller.load_players.reset_mock()
controller.load_players.side_effect = CommandFailedError("", "Failure", 1)
# Reconnect and state updates
player.available = True
await controller.dispatcher.wait_send(
SignalType.HEOS_EVENT, SignalHeosEvent.CONNECTED
@@ -183,38 +179,6 @@ async def test_updates_from_connection_event(
state = hass.states.get("media_player.test_player")
assert state is not None
assert state.state == STATE_IDLE
assert controller.load_players.call_count == 1
assert "Unable to refresh players" in caplog.text
async def test_updates_from_connection_event_new_player_ids(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
config_entry: MockConfigEntry,
controller: MockHeos,
change_data_mapped_ids: PlayerUpdateResult,
) -> None:
"""Test player ids changed after reconnection updates ids."""
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
# Assert current IDs
assert device_registry.async_get_device(identifiers={(DOMAIN, "1")})
assert entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "1")
# Send event which will result in updated IDs.
controller.load_players.return_value = change_data_mapped_ids
await controller.dispatcher.wait_send(
SignalType.HEOS_EVENT, SignalHeosEvent.CONNECTED
)
await hass.async_block_till_done()
# Assert updated IDs and previous don't exist
assert not device_registry.async_get_device(identifiers={(DOMAIN, "1")})
assert device_registry.async_get_device(identifiers={(DOMAIN, "101")})
assert not entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "1")
assert entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "101")
async def test_updates_from_sources_updated(

View File

@@ -8,9 +8,8 @@ from unittest.mock import MagicMock, patch
from aiohomeconnect.const import OAUTH2_TOKEN
from aiohomeconnect.model import OptionKey, ProgramKey, SettingKey, StatusKey
from aiohomeconnect.model.error import HomeConnectError, UnauthorizedError
import aiohttp
import pytest
import requests_mock
import respx
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
@@ -221,14 +220,12 @@ async def test_exception_handling(
@pytest.mark.parametrize("token_expiration_time", [12345])
@respx.mock
async def test_token_refresh_success(
hass: HomeAssistant,
platforms: list[Platform],
integration_setup: Callable[[MagicMock], Awaitable[bool]],
config_entry: MockConfigEntry,
aioclient_mock: AiohttpClientMocker,
requests_mock: requests_mock.Mocker,
setup_credentials: None,
client: MagicMock,
) -> None:
@@ -236,7 +233,6 @@ async def test_token_refresh_success(
assert config_entry.data["token"]["access_token"] == FAKE_ACCESS_TOKEN
requests_mock.post(OAUTH2_TOKEN, json=SERVER_ACCESS_TOKEN)
aioclient_mock.post(
OAUTH2_TOKEN,
json=SERVER_ACCESS_TOKEN,
@@ -280,6 +276,61 @@ async def test_token_refresh_success(
)
@pytest.mark.parametrize("token_expiration_time", [12345])
@pytest.mark.parametrize(
("aioclient_mock_args", "expected_config_entry_state"),
[
(
{
"status": 400,
"json": {"error": "invalid_grant"},
},
ConfigEntryState.SETUP_ERROR,
),
(
{
"status": 500,
},
ConfigEntryState.SETUP_RETRY,
),
(
{
"exc": aiohttp.ClientError,
},
ConfigEntryState.SETUP_RETRY,
),
],
)
async def test_token_refresh_error(
aioclient_mock_args: dict[str, Any],
expected_config_entry_state: ConfigEntryState,
hass: HomeAssistant,
platforms: list[Platform],
integration_setup: Callable[[MagicMock], Awaitable[bool]],
config_entry: MockConfigEntry,
aioclient_mock: AiohttpClientMocker,
setup_credentials: None,
client: MagicMock,
) -> None:
"""Test where token is expired and the refresh attempt fails."""
config_entry.data["token"]["access_token"] = FAKE_ACCESS_TOKEN
aioclient_mock.post(
OAUTH2_TOKEN,
**aioclient_mock_args,
)
assert config_entry.state == ConfigEntryState.NOT_LOADED
with patch(
"homeassistant.components.home_connect.HomeConnectClient", return_value=client
):
assert not await integration_setup(client)
await hass.async_block_till_done()
assert config_entry.state == expected_config_entry_state
@pytest.mark.parametrize(
("exception", "expected_state"),
[

View File

@@ -27,7 +27,7 @@ from homeassistant.components.home_connect.const import (
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -302,7 +302,7 @@ ENTITY_ID_STATES = {
)
),
)
async def test_event_sensors(
async def test_program_sensors(
client: MagicMock,
appliance_ha_id: str,
states: tuple,
@@ -313,7 +313,7 @@ async def test_event_sensors(
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
) -> None:
"""Test sequence for sensors that are only available after an event happens."""
"""Test sequence for sensors that expose information about a program."""
entity_ids = ENTITY_ID_STATES.keys()
time_to_freeze = "2021-01-09 12:00:00+00:00"
@@ -358,6 +358,82 @@ async def test_event_sensors(
assert hass.states.is_state(entity_id, state)
@pytest.mark.parametrize("appliance_ha_id", [TEST_HC_APP], indirect=True)
@pytest.mark.parametrize(
("initial_operation_state", "initial_state", "event_order", "entity_states"),
[
(
"BSH.Common.EnumType.OperationState.Ready",
STATE_UNAVAILABLE,
(EventType.STATUS, EventType.EVENT),
(STATE_UNKNOWN, "60"),
),
(
"BSH.Common.EnumType.OperationState.Run",
STATE_UNKNOWN,
(EventType.EVENT, EventType.STATUS),
("60", "60"),
),
],
)
async def test_program_sensor_edge_case(
initial_operation_state: str,
initial_state: str,
event_order: tuple[EventType, EventType],
entity_states: tuple[str, str],
appliance_ha_id: str,
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
) -> None:
"""Test edge case for the program related entities."""
entity_id = "sensor.dishwasher_program_progress"
client.get_status = AsyncMock(
return_value=ArrayOfStatus(
[
Status(
StatusKey.BSH_COMMON_OPERATION_STATE,
StatusKey.BSH_COMMON_OPERATION_STATE.value,
initial_operation_state,
)
]
)
)
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
assert hass.states.is_state(entity_id, initial_state)
for event_type, state in zip(event_order, entity_states, strict=True):
await client.add_events(
[
EventMessage(
appliance_ha_id,
event_type,
ArrayOfEvents(
[
Event(
key=event_key,
raw_key=event_key.value,
timestamp=0,
level="",
handling="",
value=value,
)
],
),
)
for event_key, value in EVENT_PROG_RUN[event_type].items()
]
)
await hass.async_block_till_done()
assert hass.states.is_state(entity_id, state)
# Program sequence for SensorDeviceClass.TIMESTAMP edge cases.
PROGRAM_SEQUENCE_EDGE_CASE = [
EVENT_PROG_DELAYED_START,

View File

@@ -1556,6 +1556,42 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure(
assert insecure_check["insecure"] == insecure_param
@pytest.mark.parametrize(
("mqtt_config_entry_data", "client_id"),
[
(
{
mqtt.CONF_BROKER: "mock-broker",
"client_id": "random01234random0124",
},
"random01234random0124",
),
(
{
mqtt.CONF_BROKER: "mock-broker",
},
None,
),
],
)
async def test_client_id_is_set(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
client_id: str | None,
) -> None:
"""Test setup defaults for tls."""
with patch(
"homeassistant.components.mqtt.async_client.AsyncMQTTClient"
) as async_client_mock:
await mqtt_mock_entry()
await hass.async_block_till_done()
assert async_client_mock.call_count == 1
call_params: dict[str, Any] = async_client_mock.call_args[1]
assert "client_id" in call_params
assert client_id is None or client_id == call_params["client_id"]
assert call_params["client_id"] is not None
@pytest.mark.parametrize(
"mqtt_config_entry_data",
[

View File

@@ -432,6 +432,65 @@ async def test_brightness_only(
assert state.state == STATE_OFF
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
light.DOMAIN: {
"schema": "json",
"name": "test",
"state_topic": "test_light_rgb",
"command_topic": "test_light_rgb/set",
"supported_color_modes": ["color_temp"],
}
}
},
],
)
async def test_color_temp_only(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:
"""Test a light that only support color_temp as supported color mode."""
await mqtt_mock_entry()
state = hass.states.get("light.test")
assert state.state == STATE_UNKNOWN
assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == [
light.ColorMode.COLOR_TEMP
]
expected_features = (
light.LightEntityFeature.FLASH | light.LightEntityFeature.TRANSITION
)
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features
assert state.attributes.get("rgb_color") is None
assert state.attributes.get("brightness") is None
assert state.attributes.get("color_temp_kelvin") is None
assert state.attributes.get("effect") is None
assert state.attributes.get("xy_color") is None
assert state.attributes.get("hs_color") is None
async_fire_mqtt_message(
hass,
"test_light_rgb",
'{"state":"ON", "color_mode": "color_temp", "color_temp": 250, "brightness": 50}',
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
assert state.attributes.get("rgb_color") == (255, 206, 166)
assert state.attributes.get("brightness") == 50
assert state.attributes.get("color_temp_kelvin") == 4000
assert state.attributes.get("effect") is None
assert state.attributes.get("xy_color") == (0.42, 0.365)
assert state.attributes.get("hs_color") == (26.812, 34.87)
async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"OFF"}')
state = hass.states.get("light.test")
assert state.state == STATE_OFF
@pytest.mark.parametrize(
"hass_config",
[

View File

@@ -109,8 +109,8 @@
'entity_picture_local': None,
'friendly_name': 'Test Group Player 1',
'group_members': list([
'media_player.my_super_test_player_2',
'media_player.test_player_1',
'media_player.my_super_test_player_2',
]),
'icon': 'mdi:speaker-multiple',
'is_volume_muted': False,

View File

@@ -68,6 +68,8 @@ async def trigger_update(
value,
data,
)
for call in mock.add_unspecified_device_event_listener.call_args_list:
call[0][0](event)
for call in mock.add_device_event_listener.call_args_list:
if call[0][0] == device_id:
call[0][3](event)

View File

@@ -9,6 +9,7 @@ from pysmartthings.models import (
DeviceStatus,
LocationResponse,
SceneResponse,
Subscription,
)
import pytest
@@ -78,12 +79,16 @@ def mock_smartthings() -> Generator[AsyncMock]:
client.get_locations.return_value = LocationResponse.from_json(
load_fixture("locations.json", DOMAIN)
).items
client.create_subscription.return_value = Subscription.from_json(
load_fixture("subscription.json", DOMAIN)
)
yield client
@pytest.fixture(
params=[
"da_ac_rac_000001",
"da_ac_rac_100001",
"da_ac_rac_01001",
"multipurpose_sensor",
"contact_sensor",
@@ -99,7 +104,9 @@ def mock_smartthings() -> Generator[AsyncMock]:
"iphone",
"da_wm_dw_000001",
"da_wm_wd_000001",
"da_wm_wd_000001_1",
"da_wm_wm_000001",
"da_wm_wm_000001_1",
"da_rvc_normal_000001",
"da_ks_microwave_0101x",
"hue_color_temperature_bulb",
@@ -113,7 +120,15 @@ def mock_smartthings() -> Generator[AsyncMock]:
"sensibo_airconditioner_1",
"ecobee_sensor",
"ecobee_thermostat",
"ecobee_thermostat_offline",
"fake_fan",
"generic_fan_3_speed",
"heatit_ztrm3_thermostat",
"generic_ef00_v1",
"bosch_radiator_thermostat_ii",
"im_speaker_ai_0001",
"abl_light_b_001",
"tplink_p110",
]
)
def device_fixture(

View File

@@ -0,0 +1,27 @@
{
"components": {
"main": {
"switchLevel": {
"levelRange": {
"value": null
},
"level": {
"value": null
}
},
"switch": {
"switch": {
"value": null
}
},
"colorTemperature": {
"colorTemperatureRange": {
"value": null
},
"colorTemperature": {
"value": null
}
}
}
}
}

View File

@@ -0,0 +1,89 @@
{
"components": {
"main": {
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 23.9,
"unit": "C",
"timestamp": "2025-03-07T19:55:13.328Z"
}
},
"thermostatHeatingSetpoint": {
"heatingSetpoint": {
"value": 22.0,
"unit": "C",
"timestamp": "2025-03-05T03:05:26.510Z"
},
"heatingSetpointRange": {
"value": {
"minimum": 5.0,
"maximum": 40.0,
"step": 0.1
},
"unit": "C",
"timestamp": "2025-03-05T03:05:26.510Z"
}
},
"refresh": {},
"thermostatMode": {
"thermostatMode": {
"value": "heat",
"data": {
"supportedThermostatModes": ["off", "heat"]
},
"timestamp": "2025-03-05T03:05:26.489Z"
},
"supportedThermostatModes": {
"value": ["off", "heat"],
"timestamp": "2025-03-05T03:05:26.509Z"
}
},
"battery": {
"quantity": {
"value": null
},
"battery": {
"value": 94,
"unit": "%",
"timestamp": "2025-03-07T20:47:27.362Z"
},
"type": {
"value": null
}
},
"firmwareUpdate": {
"lastUpdateStatusReason": {
"value": null
},
"availableVersion": {
"value": "2.00.09 (20009)",
"timestamp": "2024-11-29T19:55:02.005Z"
},
"lastUpdateStatus": {
"value": null
},
"supportedCommands": {
"value": null
},
"state": {
"value": "normalOperation",
"timestamp": "2024-11-29T19:55:02.009Z"
},
"updateAvailable": {
"value": false,
"timestamp": "2024-11-29T19:55:02.004Z"
},
"currentVersion": {
"value": "2.00.09 (20009)",
"timestamp": "2024-11-29T19:55:02.037Z"
},
"lastUpdateTime": {
"value": null
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
{
"components": {
"main": {
"button": {
"button": {
"value": "pushed",
"timestamp": "2025-03-07T12:20:43.363Z"
},
"numberOfButtons": {
"value": 1,
"timestamp": "2025-03-07T12:20:43.363Z"
},
"supportedButtonValues": {
"value": ["pushed", "held", "pushed_2x"],
"timestamp": "2025-03-07T12:20:43.363Z"
}
},
"refresh": {}
}
}
}

View File

@@ -58,6 +58,15 @@
"timestamp": "2025-02-08T21:56:09.761Z"
}
},
"powerConsumptionReport": {
"powerConsumption": {
"value": {
"start": "2025-02-09T15:45:29Z",
"end": "2025-02-09T16:15:33Z"
},
"timestamp": "2025-02-09T16:15:33.639Z"
}
},
"battery": {
"quantity": {
"value": null

View File

@@ -32,7 +32,7 @@
"timestamp": "2025-02-09T14:35:56.800Z"
},
"supportedAcModes": {
"value": ["auto", "cool", "dry", "wind", "heat"],
"value": ["auto", "cool", "dry", "wind", "heat", "dryClean"],
"timestamp": "2025-02-09T15:42:13.444Z"
},
"airConditionerMode": {

View File

@@ -0,0 +1,171 @@
{
"components": {
"main": {
"refresh": {},
"custom.thermostatSetpointControl": {
"minimumSetpoint": {
"value": 16,
"timestamp": "2024-11-25T22:17:38.251Z"
},
"maximumSetpoint": {
"value": 30,
"timestamp": "2024-11-25T22:17:38.251Z"
}
},
"airConditionerMode": {
"availableAcModes": {
"value": null
},
"supportedAcModes": {
"value": ["cool", "dry", "wind", "auto"],
"timestamp": "2025-03-02T10:16:19.519Z"
},
"airConditionerMode": {
"value": "cool",
"timestamp": "2025-03-02T10:16:19.519Z"
}
},
"execute": {
"data": {
"value": null
}
},
"airQualitySensor": {
"airQuality": {
"value": null
}
},
"switch": {
"switch": {
"value": "off",
"timestamp": "2025-03-02T06:54:52.852Z"
}
},
"ocf": {
"st": {
"value": null
},
"mndt": {
"value": null
},
"mnfv": {
"value": null
},
"mnhw": {
"value": null
},
"di": {
"value": "F8042E25-0E53-0000-0000-000000000000",
"timestamp": "2025-02-28T21:15:28.920Z"
},
"mnsl": {
"value": null
},
"dmv": {
"value": "res.1.1.0,sh.1.1.0",
"timestamp": "2025-02-28T21:15:28.920Z"
},
"n": {
"value": "Room A/C",
"timestamp": "2025-02-28T21:15:28.920Z"
},
"mnmo": {
"value": "TP6X_RAC_15K",
"timestamp": "2025-02-28T21:15:28.920Z"
},
"vid": {
"value": "DA-AC-RAC-100001",
"timestamp": "2025-02-28T21:15:28.920Z"
},
"mnmn": {
"value": "Samsung Electronics",
"timestamp": "2025-02-28T21:15:28.920Z"
},
"mnml": {
"value": null
},
"mnpv": {
"value": null
},
"mnos": {
"value": null
},
"pi": {
"value": "shp",
"timestamp": "2025-02-28T21:15:28.920Z"
},
"icv": {
"value": "core.1.1.0",
"timestamp": "2025-02-28T21:15:28.920Z"
}
},
"odorSensor": {
"odorLevel": {
"value": null
}
},
"airConditionerFanMode": {
"fanMode": {
"value": "auto",
"timestamp": "2025-02-28T21:15:28.941Z"
},
"supportedAcFanModes": {
"value": ["auto", "low", "medium", "high", "turbo"],
"timestamp": "2025-02-28T21:15:28.941Z"
},
"availableAcFanModes": {
"value": null
}
},
"samsungce.driverState": {
"driverState": {
"value": null
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": ["odorSensor"],
"timestamp": "2024-11-25T22:17:38.251Z"
}
},
"samsungce.driverVersion": {
"versionNumber": {
"value": 22090101,
"timestamp": "2024-11-25T22:17:38.251Z"
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 27,
"unit": "C",
"timestamp": "2025-03-02T08:28:39.409Z"
}
},
"dustSensor": {
"dustLevel": {
"value": 46,
"unit": "\u03bcg/m^3",
"timestamp": "2025-03-06T16:01:49.656000+00:00"
},
"fineDustLevel": {
"value": 10,
"unit": "\u03bcg/m^3",
"timestamp": "2025-03-06T16:01:49.656000+00:00"
}
},
"thermostatCoolingSetpoint": {
"coolingSetpointRange": {
"value": null
},
"coolingSetpoint": {
"value": 18,
"unit": "C",
"timestamp": "2025-03-02T06:54:23.887Z"
}
}
}
}
}

View File

@@ -0,0 +1,692 @@
{
"components": {
"hca.main": {
"hca.dryerMode": {
"mode": {
"value": "normal",
"timestamp": "2025-03-09T16:31:41.247Z"
},
"supportedModes": {
"value": ["normal", "quickDry", "mix", "timeDry"],
"timestamp": "2025-03-09T16:31:40.486Z"
}
}
},
"main": {
"custom.dryerWrinklePrevent": {
"operatingState": {
"value": "ready",
"timestamp": "2025-03-09T16:31:40.486Z"
},
"dryerWrinklePrevent": {
"value": "off",
"timestamp": "2025-03-09T16:31:41.077Z"
}
},
"samsungce.dryerDryingTemperature": {
"dryingTemperature": {
"value": null,
"timestamp": "2021-04-02T18:31:36.756Z"
},
"supportedDryingTemperature": {
"value": null,
"timestamp": "2021-04-02T18:29:52.258Z"
}
},
"samsungce.welcomeMessage": {
"welcomeMessage": {
"value": null,
"timestamp": "2021-04-02T18:32:37.913Z"
}
},
"samsungce.dongleSoftwareInstallation": {
"status": {
"value": "completed",
"timestamp": "2022-06-17T17:07:35.734Z"
}
},
"samsungce.dryerCyclePreset": {
"maxNumberOfPresets": {
"value": 10,
"timestamp": "2025-03-09T16:31:41.229Z"
},
"presets": {
"value": null,
"timestamp": "2021-04-02T18:30:36.772Z"
}
},
"samsungce.deviceIdentification": {
"micomAssayCode": {
"value": "20221341",
"timestamp": "2025-03-09T16:31:40.834Z"
},
"modelName": {
"value": null,
"timestamp": "2021-04-02T18:29:53.622Z"
},
"serialNumber": {
"value": null,
"timestamp": "2021-04-02T18:29:52.641Z"
},
"serialNumberExtra": {
"value": null,
"timestamp": "2021-04-02T18:29:51.653Z"
},
"modelClassificationCode": {
"value": "30010102001211000103000000000000",
"timestamp": "2025-03-09T16:31:40.834Z"
},
"description": {
"value": "DA_WM_A51_20_COMMON_DV6800N/DC92-01967B_0404",
"timestamp": "2025-03-09T16:31:40.834Z"
},
"releaseYear": {
"value": null
},
"binaryId": {
"value": "DA_WM_A51_20_COMMON",
"timestamp": "2025-03-09T19:07:40.295Z"
}
},
"switch": {
"switch": {
"value": "off",
"timestamp": "2025-03-09T19:47:36.549Z"
}
},
"samsungce.quickControl": {
"version": {
"value": null
}
},
"samsungce.dryerFreezePrevent": {
"operatingState": {
"value": null
}
},
"ocf": {
"st": {
"value": null,
"timestamp": "2020-06-20T10:01:02.741Z"
},
"mndt": {
"value": null,
"timestamp": "2020-06-25T01:53:25.278Z"
},
"mnfv": {
"value": "DA_WM_A51_20_COMMON_30230708",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnhw": {
"value": "ARTIK051",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"di": {
"value": "3a6c4e05-811d-5041-e956-3d04c424cbcd",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnsl": {
"value": "http://www.samsung.com",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"dmv": {
"value": "res.1.1.0,sh.1.1.0",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"n": {
"value": "[dryer] Samsung",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnmo": {
"value": "DA_WM_A51_20_COMMON|20221341|30010102001211000103000000000000",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"vid": {
"value": "DA-WM-WD-000001",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnmn": {
"value": "Samsung Electronics",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnml": {
"value": "http://www.samsung.com",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnpv": {
"value": "DAWIT 2.0",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"mnos": {
"value": "TizenRT 1.0 + IPv6",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"pi": {
"value": "3a6c4e05-811d-5041-e956-3d04c424cbcd",
"timestamp": "2024-12-15T10:53:49.561Z"
},
"icv": {
"value": "core.1.1.0",
"timestamp": "2024-12-15T10:53:49.561Z"
}
},
"custom.dryerDryLevel": {
"dryerDryLevel": {
"value": "2",
"timestamp": "2025-03-09T19:47:36.806Z"
},
"supportedDryerDryLevel": {
"value": ["none", "1", "2", "3"],
"timestamp": "2020-11-18T20:16:43.428Z"
}
},
"samsungce.dryerAutoCycleLink": {
"dryerAutoCycleLink": {
"value": null,
"timestamp": "2020-08-11T12:41:38.646Z"
}
},
"samsungce.dryerCycle": {
"dryerCycle": {
"value": "Table_00_Course_9A",
"timestamp": "2025-03-09T16:31:41.247Z"
},
"supportedCycles": {
"value": [
{
"cycle": "9A",
"supportedOptions": {
"dryingLevel": {
"raw": "D20E",
"default": "2",
"options": ["1", "2", "3"]
}
}
},
{
"cycle": "CA",
"supportedOptions": {
"dryingLevel": {
"raw": "D10E",
"default": "1",
"options": ["1", "2", "3"]
}
}
},
{
"cycle": "DB",
"supportedOptions": {
"dryingLevel": {
"raw": "D204",
"default": "2",
"options": ["2"]
}
}
},
{
"cycle": "99",
"supportedOptions": {
"dryingLevel": {
"raw": "D20E",
"default": "2",
"options": ["1", "2", "3"]
}
}
},
{
"cycle": "93",
"supportedOptions": {
"dryingLevel": {
"raw": "D102",
"default": "1",
"options": ["1"]
}
}
},
{
"cycle": "B5",
"supportedOptions": {
"dryingLevel": {
"raw": "D102",
"default": "1",
"options": ["1"]
}
}
},
{
"cycle": "D7",
"supportedOptions": {
"dryingLevel": {
"raw": "D204",
"default": "2",
"options": ["2"]
}
}
},
{
"cycle": "A5",
"supportedOptions": {
"dryingLevel": {
"raw": "D204",
"default": "2",
"options": ["2"]
}
}
},
{
"cycle": "96",
"supportedOptions": {
"dryingLevel": {
"raw": "D000",
"default": "none",
"options": []
}
}
},
{
"cycle": "97",
"supportedOptions": {
"dryingLevel": {
"raw": "D000",
"default": "none",
"options": []
}
}
},
{
"cycle": "7F",
"supportedOptions": {
"dryingLevel": {
"raw": "D000",
"default": "none",
"options": []
}
}
},
{
"cycle": "98",
"supportedOptions": {
"dryingLevel": {
"raw": "D000",
"default": "none",
"options": []
}
}
},
{
"cycle": "EB",
"supportedOptions": {
"dryingLevel": {
"raw": "D204",
"default": "2",
"options": ["2"]
}
}
},
{
"cycle": "B6",
"supportedOptions": {
"dryingLevel": {
"raw": "D20E",
"default": "2",
"options": ["1", "2", "3"]
}
}
}
],
"timestamp": "2025-02-10T02:24:03.524Z"
},
"referenceTable": {
"value": {
"id": "Table_00"
},
"timestamp": "2025-03-09T16:31:41.247Z"
},
"specializedFunctionClassification": {
"value": 4,
"timestamp": "2025-03-09T16:31:40.486Z"
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [
"samsungce.dryerDelayEnd",
"dryerOperatingState",
"samsungce.dryerCyclePreset",
"samsungce.welcomeMessage",
"samsungce.dongleSoftwareInstallation",
"sec.wifiConfiguration",
"samsungce.quickControl",
"samsungce.deviceInfoPrivate",
"demandResponseLoadControl",
"samsungce.dryerFreezePrevent",
"samsungce.dryerDryingTemperature",
"sec.diagnosticsInformation"
],
"timestamp": "2024-07-02T14:42:38.334Z"
}
},
"samsungce.driverVersion": {
"versionNumber": {
"value": 24110101,
"timestamp": "2024-12-02T07:43:41.263Z"
}
},
"sec.diagnosticsInformation": {
"logType": {
"value": null
},
"endpoint": {
"value": null
},
"minVersion": {
"value": null
},
"signinPermission": {
"value": null
},
"setupId": {
"value": null
},
"protocolType": {
"value": null
},
"tsId": {
"value": null
},
"mnId": {
"value": null
},
"dumpType": {
"value": null
}
},
"samsungce.kidsLock": {
"lockState": {
"value": "unlocked",
"timestamp": "2025-03-09T16:31:40.882Z"
}
},
"demandResponseLoadControl": {
"drlcStatus": {
"value": null
}
},
"samsungce.detergentOrder": {
"alarmEnabled": {
"value": false,
"timestamp": "2025-03-09T16:31:40.486Z"
},
"orderThreshold": {
"value": 0,
"unit": "cc",
"timestamp": "2025-03-09T16:31:40.486Z"
}
},
"powerConsumptionReport": {
"powerConsumption": {
"value": {
"energy": 796400,
"deltaEnergy": 0,
"power": 0,
"powerEnergy": 0.0,
"persistedEnergy": 0,
"energySaved": 0,
"start": "2025-03-09T19:47:26Z",
"end": "2025-03-09T19:47:37Z"
},
"timestamp": "2025-03-09T19:47:37.283Z"
}
},
"dryerOperatingState": {
"completionTime": {
"value": "2025-03-09T22:55:37Z",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"machineState": {
"value": "stop",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"supportedMachineStates": {
"value": ["stop", "run", "pause"],
"timestamp": "2025-03-09T16:31:41.172Z"
},
"dryerJobState": {
"value": "none",
"timestamp": "2025-03-09T19:47:37.015Z"
}
},
"samsungce.detergentState": {
"remainingAmount": {
"value": 0,
"unit": "cc",
"timestamp": "2025-03-09T16:31:40.486Z"
},
"dosage": {
"value": 0,
"unit": "cc",
"timestamp": "2025-03-09T16:31:40.486Z"
},
"initialAmount": {
"value": 0,
"unit": "cc",
"timestamp": "2025-03-09T16:31:40.486Z"
},
"detergentType": {
"value": "none",
"timestamp": "2021-04-02T18:29:51.428Z"
}
},
"samsungce.dryerDelayEnd": {
"remainingTime": {
"value": 0,
"unit": "min",
"timestamp": "2025-03-09T16:31:41.172Z"
}
},
"refresh": {},
"custom.jobBeginningStatus": {
"jobBeginningStatus": {
"value": null,
"timestamp": "2020-06-25T01:53:34.974Z"
}
},
"execute": {
"data": {
"value": {
"payload": {
"rt": ["x.com.samsung.da.information"],
"if": ["oic.if.baseline", "oic.if.a"],
"x.com.samsung.da.modelNum": "DA_WM_A51_20_COMMON|20221341|30010102001211000103000000000000",
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON_DV6800N/DC92-01967B_0404",
"x.com.samsung.da.serialNum": "0T625AEN100200N",
"x.com.samsung.da.otnDUID": "SHCDM6YAPCCXC",
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON|20221341|30010102001211000103000000000000",
"x.com.samsung.da.type": "Software",
"x.com.samsung.da.number": "02198A220728(E256)",
"x.com.samsung.da.newVersionAvailable": "0"
},
{
"x.com.samsung.da.id": "1",
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON",
"x.com.samsung.da.type": "Firmware",
"x.com.samsung.da.number": "17111305,19060420",
"x.com.samsung.da.newVersionAvailable": "0"
}
]
}
},
"data": {
"href": "/information/vs/0"
},
"timestamp": "2023-08-07T00:06:05.984Z"
}
},
"sec.wifiConfiguration": {
"autoReconnection": {
"value": null
},
"minVersion": {
"value": null
},
"supportedWiFiFreq": {
"value": null
},
"supportedAuthType": {
"value": null
},
"protocolType": {
"value": null
}
},
"remoteControlStatus": {
"remoteControlEnabled": {
"value": "false",
"timestamp": "2025-03-09T16:31:41.180Z"
}
},
"custom.supportedOptions": {
"course": {
"value": null
},
"referenceTable": {
"value": {
"id": "Table_00"
},
"timestamp": "2025-03-09T16:31:41.247Z"
},
"supportedCourses": {
"value": [
"9A",
"CA",
"DB",
"99",
"93",
"B5",
"D7",
"A5",
"96",
"97",
"7F",
"98",
"EB",
"B6"
],
"timestamp": "2025-03-09T16:31:40.486Z"
}
},
"custom.energyType": {
"energyType": {
"value": "2.0",
"timestamp": "2022-06-17T17:07:35.734Z"
},
"energySavingSupport": {
"value": false,
"timestamp": "2022-06-17T17:07:35.734Z"
},
"drMaxDuration": {
"value": null
},
"energySavingLevel": {
"value": null
},
"energySavingInfo": {
"value": null
},
"supportedEnergySavingLevels": {
"value": null
},
"energySavingOperation": {
"value": null
},
"notificationTemplateID": {
"value": null
},
"energySavingOperationSupport": {
"value": null
}
},
"samsungce.dryerOperatingState": {
"operatingState": {
"value": "ready",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"supportedOperatingStates": {
"value": ["ready", "running", "paused"],
"timestamp": "2022-11-01T12:48:22.390Z"
},
"scheduledJobs": {
"value": [
{
"jobName": "drying",
"timeInMin": 192
},
{
"jobName": "cooling",
"timeInMin": 1
}
],
"timestamp": "2025-03-09T16:31:40.486Z"
},
"progress": {
"value": 1,
"unit": "%",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"remainingTimeStr": {
"value": "03:08",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"dryerJobState": {
"value": "none",
"timestamp": "2025-03-09T19:47:37.015Z"
},
"remainingTime": {
"value": 188,
"unit": "min",
"timestamp": "2025-03-09T19:47:37.015Z"
}
},
"samsungce.softwareUpdate": {
"targetModule": {
"value": null
},
"otnDUID": {
"value": "SHCDM6YAPCCXC",
"timestamp": "2025-03-09T16:31:40.834Z"
},
"lastUpdatedDate": {
"value": null
},
"availableModules": {
"value": [],
"timestamp": "2024-12-01T21:16:50.598Z"
},
"newVersionAvailable": {
"value": false,
"timestamp": "2024-12-01T21:16:50.598Z"
},
"operatingState": {
"value": null
},
"progress": {
"value": null
}
},
"samsungce.dryerDryingTime": {
"supportedDryingTime": {
"value": ["0", "30", "60", "90", "120", "150"],
"timestamp": "2021-04-02T18:29:51.428Z"
},
"dryingTime": {
"value": "0",
"unit": "min",
"timestamp": "2025-03-09T16:31:41.077Z"
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
{
"components": {
"main": {
"relativeHumidityMeasurement": {
"humidity": {
"value": null
}
},
"thermostatOperatingState": {
"thermostatOperatingState": {
"value": null
}
},
"healthCheck": {
"checkInterval": {
"value": 60,
"unit": "s",
"data": {
"deviceScheme": "UNTRACKED",
"protocol": "cloud"
},
"timestamp": "2025-03-10T00:57:26.866Z"
},
"healthStatus": {
"value": null
},
"DeviceWatch-Enroll": {
"value": null
},
"DeviceWatch-DeviceStatus": {
"value": "offline",
"data": {
"reason": "DEVICE-OFFLINE"
},
"timestamp": "2025-03-11T10:22:17.013Z"
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": null
}
},
"thermostatHeatingSetpoint": {
"heatingSetpoint": {
"value": null
},
"heatingSetpointRange": {
"value": null
}
},
"thermostatFanMode": {
"thermostatFanMode": {
"value": null
},
"supportedThermostatFanModes": {
"value": null
}
},
"refresh": {},
"thermostatMode": {
"thermostatMode": {
"value": null
},
"supportedThermostatModes": {
"value": null
}
},
"thermostatCoolingSetpoint": {
"coolingSetpointRange": {
"value": null
},
"coolingSetpoint": {
"value": null
}
}
}
}
}

View File

@@ -0,0 +1,76 @@
{
"components": {
"main02": {
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 200.0,
"unit": "C",
"timestamp": "2024-12-02T20:18:52.095Z"
}
}
},
"main": {
"thermostatOperatingState": {
"thermostatOperatingState": {
"value": null
}
},
"signalStrength": {
"rssi": {
"value": -84,
"unit": "dBm",
"timestamp": "2025-03-07T20:53:55.346Z"
},
"lqi": {
"value": 255,
"timestamp": "2025-03-07T20:53:55.387Z"
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 21.0,
"unit": "C",
"timestamp": "2025-03-07T16:58:23.773Z"
}
},
"thermostatHeatingSetpoint": {
"heatingSetpoint": {
"value": 23.0,
"unit": "C",
"timestamp": "2025-02-10T17:48:38.299Z"
},
"heatingSetpointRange": {
"value": null
}
},
"refresh": {},
"valleyboard16460.debug": {
"value": {
"value": "<table style=\"font-size:0.6em;min-width:100%\"><tbody>\n <tr><th align=\"left\" style=\"width:35%\">Actual</th><td style=\"width:65%\">_TZE200_rxntag7i</td></tr>\n <tr><th align=\"left\">Expected</th><td>_TZE200_4hbx5cvx</td></tr>\n <tr><th align=\"left\">Profile</th><td>normal-thermostat-v3</td></tr>\n <tr><th align=\"left\">Mode</th><td>Similarity</td></tr>\n <tr><th align=\"left\">Preferences</th><td>Modified</td></tr>\n <tr><th align=\"left\">Exposes EF00</th><td>Yes</td></tr>\n <tr><th align=\"left\">Default DP</th><td>No</td></tr>\n </tbody></table>",
"timestamp": "2025-03-05T03:04:54.025Z"
}
},
"thermostatMode": {
"thermostatMode": {
"value": "heat",
"data": {},
"timestamp": "2024-12-30T08:22:19.273Z"
},
"supportedThermostatModes": {
"value": null
}
},
"switch": {
"switch": {
"value": null
}
}
}
}
}

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