Compare commits

...

246 Commits

Author SHA1 Message Date
Ludovic BOUÉ
7d97424a22 Remove invalid segment ID test from vacuum clean area tests 2026-02-19 23:20:55 +01:00
Ludovic BOUÉ
76114c9ded Remove redundant error handling for segment ID conversion in MatterVacuum 2026-02-19 23:20:02 +01:00
Ludovic BOUÉ
0334dad2f8 Add service area feature map tracking to MatterVacuum for improved state management 2026-02-19 23:19:10 +01:00
Ludovic BOUÉ
2dd65172b0 Refactor MatterVacuum to use property for current segments and simplify segment comparison logic 2026-02-19 23:17:08 +01:00
Ludovic BOUÉ
4532fd379e Add validation for segment IDs in Matter vacuum clean area action 2026-02-19 22:55:10 +01:00
Ludovic BOUÉ
578b2b3d43 Refactor Matter vacuum area selection to resolve run mode before sending commands 2026-02-19 22:52:31 +01:00
Ludovic BOUÉ
9bb2f56fbe Merge branch 'dev' into matter_clean_area 2026-02-19 22:40:31 +01:00
Ludovic BOUÉ
a7d209f1f5 Enhance Matter vacuum support for clean area by checking Map feature availability 2026-02-19 22:38:53 +01:00
Ludovic BOUÉ
83d73dce5c Add SupportedAreas as optional_attributes 2026-02-19 22:18:45 +01:00
Ludovic BOUÉ
d84f81daf2 Enhance vacuum tests to verify segment issue handling and update command assertions 2026-02-19 21:55:17 +01:00
Ludovic BOUÉ
79d4f5c8cf Refactor segment handling to add last_seen_segments 2026-02-19 21:53:54 +01:00
Ludovic BOUÉ
e9e1abb604 Remove debug log for area reset before cleaning
Removed debug logging for resetting selected areas.
2026-02-19 21:42:01 +01:00
Ludovic BOUÉ
9a97541253 Revert unwanted change 2026-02-19 21:39:19 +01:00
Ludovic BOUÉ
fd39f3c431 Add support for unconstrained area selection in vacuum
Reset selected areas for full clean operation when starting the vacuum.
2026-02-19 21:37:43 +01:00
Ludovic BOUÉ
2cc4a77746 Remove Segment group
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2026-02-19 20:02:41 +01:00
wollew
e8885de8c2 add number platform to Velux integration for ExteriorHeating nodes (#162857) 2026-02-19 19:58:13 +01:00
J. Nick Koston
03d9c2cf7b Add Trane Local integration (#163301) 2026-02-19 12:39:58 -06:00
epenet
7f3583587d Combine matter snapshot tests (#162695)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-19 19:38:33 +01:00
Brett Adams
e009440bf9 Mark action-setup quality scale rule as done for Advantage Air (#163208)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 19:25:41 +01:00
Noah Husby
43dccf15ba Add room correction intensity to Cambridge Audio (#163306) 2026-02-19 19:25:14 +01:00
Josef Zweck
c647ab1877 Add proper ImplementationUnvailable handling to onedrive for business (#163258)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-19 19:24:31 +01:00
JannisPohle
6b395b2703 Add test for device_class inheritance in the min/max integration (#161123) 2026-02-19 19:18:41 +01:00
Thomas Sejr Madsen
882a44a1c2 Fix touchline_sl zone availability when alarm state is set (#163338) 2026-02-19 19:13:44 +01:00
Christopher Fenner
3c9a505fc3 Handle gateway issues during setup in EnOcean integration (#163168)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-19 17:53:29 +00:00
Sab44
b2679ddc42 Update json fixture to reflect response from current LHM versions (#163248) 2026-02-19 18:15:16 +01:00
Andrew Jackson
2055082993 Handle Mastodon auth fail in coordinator (#163234) 2026-02-19 18:14:14 +01:00
Andreas Jakl
6f49f9a12a NRGkick: do not update vehicle connected timestamp when vehicle is not connected (#163292) 2026-02-19 18:08:50 +01:00
Petar Petrov
36c560b7bf Add flow rate (stat_rate) tracking for gas and water (#163274) 2026-02-19 18:08:16 +01:00
hanwg
05abe7efe0 Add callback inline keyboard tests for Telegram bot (#163328) 2026-02-19 17:50:51 +01:00
Manu
865ec96429 Add notify platform to HTML5 integration (#163229) 2026-02-19 17:50:04 +01:00
epenet
e6dbed0a87 Use shorthand attributes in geonetnz_quakes (#163568)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-19 17:46:37 +01:00
A. Gideonse
a3fd2f692e Add switch platform to Indevolt integration (#163522)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-19 17:46:13 +01:00
konsulten
eb7e00346d Fixing minor case errors in strings for systemnexa2 (#163567) 2026-02-19 17:39:00 +01:00
Manu
77159e612e Improve error handling in Uptime Kuma (#163477) 2026-02-19 17:23:10 +01:00
mettolen
05f9e25f29 Pump pyliebherrhomeapi to 0.3.0 (#163450) 2026-02-19 17:10:10 +01:00
Denis Shulyaka
7fa51117a9 Update Anthropic repair flow (#163303) 2026-02-19 17:09:09 +01:00
epenet
9e87fa75f8 Mark entity capability/state attribute type hints as mandatory (#163300)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-02-19 17:02:38 +01:00
epenet
0188f2ffec Mark is_on property as mandatory in binary sensors and toggle entities (#163556) 2026-02-19 17:01:50 +01:00
epenet
c144aec03e Use shorthand attributes in opple light (#163519) 2026-02-19 15:50:15 +01:00
epenet
1cb44aef64 Use shorthand attributes in pilight (#163542) 2026-02-19 15:50:00 +01:00
epenet
900f2300ad Use shorthand attributes in eufy light (#163521) 2026-02-19 15:49:48 +01:00
epenet
b075fba594 Use shorthand attributes in greenwave light (#163526) 2026-02-19 15:49:33 +01:00
epenet
c2ba97fb79 Use shorthand attributes in futurenow light (#163523) 2026-02-19 15:49:16 +01:00
epenet
d0a373aecc Use shorthand attributes in lw12wifi light (#163532) 2026-02-19 15:48:56 +01:00
epenet
758225edad Use shorthand attributes in scsgate light (#163537) 2026-02-19 15:48:43 +01:00
epenet
8ab1a527a4 Use shorthand attributes in rflink (#163555) 2026-02-19 15:48:05 +01:00
epenet
c7582b2f25 Use shorthand attributes in mystrom binary sensor (#163518) 2026-02-19 15:29:39 +01:00
epenet
91b8a67ce2 Use shorthand attributes in scsgate switch (#163510) 2026-02-19 15:23:20 +01:00
epenet
2b13ff98da Use shorthand attributes in itach remote (#163516) 2026-02-19 15:07:50 +01:00
epenet
fd2d9c2ee2 Use shorthand attributes in raincloud (#163515) 2026-02-19 14:56:52 +01:00
Manu
61b5466dcc Add state_class to sensors in Uptime Kuma (#163495) 2026-02-19 14:54:29 +01:00
epenet
bc4af64bea Use shorthand attributes in pencom switch (#163509) 2026-02-19 14:54:00 +01:00
epenet
3323f84c22 Use shorthand attributes in hikvisioncam switch (#163504) 2026-02-19 14:47:10 +01:00
epenet
b1f48a5886 Use shorthand attributes in kankun switch (#163505) 2026-02-19 14:46:55 +01:00
epenet
a14b1db886 Use shorthand attribute in eufy switch (#163503) 2026-02-19 14:46:22 +01:00
epenet
9de89b923e Use shorthand attributes in orvibo switch (#163508) 2026-02-19 14:46:07 +01:00
epenet
21cf5dc321 Use shorthand attribute in elv switch (#163488) 2026-02-19 14:30:27 +01:00
epenet
fe32582233 Use shorthand attribute in edimax switch (#163487) 2026-02-19 14:30:10 +01:00
epenet
6ebf19c4ba Use shorthand attribute in danfoss_air switch (#163486) 2026-02-19 14:29:39 +01:00
Willem-Jan van Rootselaar
5794189f8d Add strict typing for BSB-Lan integration (#163236) 2026-02-19 14:19:10 +01:00
A. Gideonse
c336e58afc Add numbers platform to Indevolt integration (#163298)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-19 13:55:50 +01:00
Manu
cdad602af0 Add new sensor to Uptime Kuma (#163468) 2026-02-19 13:53:04 +01:00
Stefan Agner
520046cd82 Ignore WAKEUP_CHANNEL addition in Thread dataset with same timestamp (#163440)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 13:42:37 +01:00
Willem-Jan van Rootselaar
e0b2ff0b2a Bump python-bsblan version to 4.2.1 (#163439) 2026-02-19 13:41:31 +01:00
epenet
6164198bde Use shorthand attributes in versasense switch (#163442) 2026-02-19 13:40:41 +01:00
epenet
dd41b4cefd Use shorthand attribute in tellstick toggle entities (#163443) 2026-02-19 13:40:09 +01:00
epenet
ccb8d6af44 Use shorthand attribute in x10 light (#163444) 2026-02-19 13:39:55 +01:00
epenet
6e8c064474 Improve type hints in tesla_wall_connector binary sensor (#163445) 2026-02-19 13:39:35 +01:00
epenet
7079eda8d9 Improve type hints in philips_js light (#163448) 2026-02-19 13:39:20 +01:00
Brett Adams
4e3832758b Add charge cable and charge port latch sensors to Tessie (#163207)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-19 13:38:55 +01:00
Brett Adams
773c3c4f07 Add diagnostics support to Splunk integration (#163453) 2026-02-19 13:38:17 +01:00
konsulten
b73beba152 System Nexa 2 Core Integration (#159140)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-19 13:31:17 +01:00
epenet
82589b613d Fix pytest warnings in screenlogic (#163455) 2026-02-19 12:57:55 +01:00
J. Diego Rodríguez Royo
c9b5f5f2c1 Use a coordinator per appliance in Home Connect (#152518)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-19 12:35:19 +01:00
Erwin Douna
725b45db7f Add config URL to Proxmox (#163414)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-19 12:31:44 +01:00
Pierre PÉRONNET
b194741a13 Add custom headers support to downloader (#160541)
Signed-off-by: Pierre PÉRONNET <pierre.peronnet@gmail.com>
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-02-19 12:29:30 +01:00
epenet
4615b4d104 Add return type hint to is_on property (#163441) 2026-02-19 11:24:38 +01:00
A. Gideonse
2c7d9cb62e Bump indevolt-api requirement to 1.2.3 (#163429) 2026-02-19 11:22:50 +01:00
AlCalzone
e229ba591a Use opening/closing state for Z-Wave covers (#163368)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-02-19 10:41:52 +01:00
Rob Bierbooms
7914ebe54e Add config flow to InfluxDB integration (#134463)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-02-19 10:33:32 +01:00
Andreas Jakl
3abaa99706 Add charge control to NRGkick integration (new number platform) (#163273)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-02-19 10:31:09 +01:00
karwosts
86d7fdfe1e Allow history_stats to configure state_class: total_increasing (#148637) 2026-02-19 10:16:47 +01:00
epenet
676c42d578 Refactor write_ha_state logic in Tuya (#163431) 2026-02-19 10:13:54 +01:00
Manu
39909b7493 Bump pythonkuma to 0.5.0 (#163430) 2026-02-19 09:57:31 +01:00
Manu
6aef9a99e6 Deprecate action call without config entry in DuckDNS integration (#163269) 2026-02-19 08:43:46 +01:00
Joost Lekkerkerker
ff036f38a0 Add integration_type hub to sharkiq (#163392) 2026-02-19 08:31:40 +01:00
On Freund
53e3b4caf0 Bump py-nymta to 0.4.0 (#163418) 2026-02-19 08:30:49 +01:00
Kamil Breguła
dbdc030b74 Enable strict typing for 10 components (#163420)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-19 08:30:24 +01:00
Kamil Breguła
ee0b24f808 Add sensor showing total size of AWS S3 backups (#162513)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-19 08:29:40 +01:00
Joost Lekkerkerker
c0fd8ff342 Add integration_type hub to smappee (#163397) 2026-02-19 08:15:25 +01:00
Joost Lekkerkerker
84d2ec484d Add integration_type device to slimproto (#163396) 2026-02-19 08:14:47 +01:00
Joost Lekkerkerker
844b20e2fc Add integration_type hub to sleepiq (#163395) 2026-02-19 08:14:05 +01:00
Joost Lekkerkerker
2bd07e6626 Add integration_type hub to sensorpush_cloud (#163390) 2026-02-19 08:09:49 +01:00
johanzander
b91c07b2af Fix midnight bounce suppression for Growatt today sensors (#163106)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-19 09:07:52 +02:00
rhcp011235
37f0f1869f Add sleep health metrics to SleepIQ integration (#163403)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 01:02:43 +01:00
Manu
2fcbd77c95 Don't set last notification timestamp when sending message failed (#163251) 2026-02-19 00:48:01 +01:00
Josef Zweck
b398197c07 Debug logging for config_entries (#163378) 2026-02-19 00:46:06 +01:00
Joost Lekkerkerker
cd5775ca35 Add integration_type service to simplepush (#163394) 2026-02-19 00:37:17 +01:00
Christian Lackas
fafa193549 Add LED light support for WiredPushButton (HmIPW-WRC2/WRC6) (#161841)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-19 00:36:29 +01:00
elgris
ca4d537529 Control datetime on SwitchBot Meter Pro CO2 (#161808) 2026-02-19 00:32:23 +01:00
torben-iometer
e9be363f29 add support for multi tariff meter data in iometer (#161767)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-19 00:23:46 +01:00
Joshua Leaper
0f874f7f03 Add Config Flow for Ness Alarm (#162414)
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-19 00:16:08 +01:00
Brett Adams
14b147b3f7 Mark Splunk dependency-transparency quality scale rule as done (#163355)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-02-19 00:11:10 +01:00
Brett Adams
8a1909e5d8 Bump hass-splunk to 0.1.4 (#163413) 2026-02-18 22:51:31 +00:00
Noah Husby
1fd873869f Bump aiostreammagic to 2.13.0 (#163408) 2026-02-18 22:49:18 +00:00
Robert Resch
3b7b3454d8 Simplify ecovacs unload and register teardown before initialize (#163350) 2026-02-18 23:32:39 +01:00
Josef Zweck
c7276621eb Add metadata validation for missing backup files in OneDrive backup agent (#163072) 2026-02-18 23:32:23 +01:00
Klaas Schoute
6be1e4065f Add Powerfox Local integration (#163302) 2026-02-18 23:27:47 +01:00
Artur Pragacz
ba547c6bdb Add channel muting switches to Onkyo (#162605)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-18 23:26:57 +01:00
mettolen
be25603b76 Refactor optimistic update and delayed refresh for Liebherr integration (#163121) 2026-02-18 23:11:47 +01:00
Joost Lekkerkerker
2e0f727981 Add integration_type hub to senz (#163391) 2026-02-18 23:11:29 +01:00
Joost Lekkerkerker
122bc32f30 Add integration_type device to sensorpush (#163389) 2026-02-18 23:11:01 +01:00
Brett Adams
723825b579 Mark runtime-data quality as exempt in Splunk (#163359)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:06:49 +01:00
rhcp011235
5f6b446195 Migrate SleepIQ sensors to entity descriptions (#163213)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 23:03:53 +01:00
Joost Lekkerkerker
f59f14fe40 Add integration_type device to sensorpro (#163386) 2026-02-18 21:49:12 +01:00
Joost Lekkerkerker
ab9b13302c Add integration_type hub to smarttub (#163399) 2026-02-18 21:47:19 +01:00
Joost Lekkerkerker
f74fdd7605 Add integration_type service to smhi (#163400) 2026-02-18 21:46:18 +01:00
Erwin Douna
f7628b87c8 Add ConfigEntryAuthFailed to Proxmox (#163407) 2026-02-18 21:43:04 +01:00
Karl Beecken
3e31fbfee0 Deduplicate strings in Teltonika integration (#163410) 2026-02-18 21:42:34 +01:00
Norbert Rittel
477797271a Replace "the" with "a" in vacuum action descriptions (#163409) 2026-02-18 21:41:00 +01:00
Andrew Jackson
9f2677ddd8 Add Mastodon mute/unmute actions (#163366) 2026-02-18 19:50:25 +01:00
Manu
558a49cb66 Fix data update in WebhookFlowHandler to preserve existing entry data (#163372) 2026-02-18 19:48:37 +01:00
Stefan Agner
a9b64a15e6 Redact Thread dataset and format them as readable dicts in log messages (#163385)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 19:41:36 +01:00
Andrew Jackson
0a734b7426 Improve Transmission error handling (#163388) 2026-02-18 19:41:28 +01:00
Steve Easley
8df41dc73f Bump Kaleidescape integration dependancy to v1.1.1 (#163384) 2026-02-18 19:41:17 +01:00
Glenn de Haan
e9039cec24 Add HDFury number platform (#163381)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-18 19:22:57 +01:00
Ludovic BOUÉ
d7ef65e562 Add test for vacuum clean_area action and update supported features 2026-02-18 17:48:16 +00:00
Ludovic BOUÉ
e765c1652c Add support for cleaning specific segments in Matter vacuum 2026-02-18 17:45:05 +00:00
Joost Lekkerkerker
15cb102c39 Bump pySmartThings to 3.5.3 (#163375)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-02-18 18:28:57 +01:00
Anthony Hou
30314ec88e Fix 0°C when the temperature is unavailable in HKO API (#162052) 2026-02-18 17:16:00 +00:00
rhcp011235
428aa31749 Update asyncsleepiq to 1.7.0 (#163214) 2026-02-18 16:44:02 +00:00
Joost Lekkerkerker
0170d56893 Add fixture to SmartThings (#163374) 2026-02-18 17:30:41 +01:00
epenet
eb7d973252 Ignore None keys in meteo_france extra state attributes (#163297) 2026-02-18 17:18:27 +01:00
epenet
e3c98dcd09 Use shorthand attributes in wirelesstag (#161214) 2026-02-18 17:14:06 +01:00
epenet
9c71aea622 Refactor extra_state_attributes in xiaomi_aqara (#163299) 2026-02-18 17:12:06 +01:00
epenet
21978917b9 Mark siren/stt/todo method type hints as mandatory (#163265) 2026-02-18 17:10:11 +01:00
puddly
3b6a5b2c79 Fix uses of reconfigure and re-configure in ZHA (#163377) 2026-02-18 11:05:05 -05:00
Ivan Dlugos
68792f02d4 Fix XMLParsedAsHTMLWarning in scrape integration (#159433)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-02-18 17:00:49 +01:00
Josef Zweck
bfea04b482 Mark onedrive for business as platinum (#163376) 2026-02-18 16:53:07 +01:00
Erwin Douna
dc553f20e6 Ecovacs controller pattern optimization (#160895)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Robert Resch <robert@resch.dev>
2026-02-18 16:49:34 +01:00
Manu
5631170900 Fix spelling of reconfigure in strings (#163370) 2026-02-18 16:36:31 +01:00
David Recordon
60d4b050ac Fix Control4 HVAC action mapping for multi-stage and idle states (#163222) 2026-02-18 16:35:57 +01:00
Josef Zweck
c5e261495f Add diagnostics to onedrive for business (#163336) 2026-02-18 16:35:32 +01:00
Erwin Douna
d1a1183b9a OAuth2.0 token request error handling (#153167)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-02-18 15:36:53 +01:00
Manu
4dcfd5fb91 Reconfiguration support for webhook flow helper (#151729) 2026-02-18 15:31:48 +01:00
Jochen Friedrich
680f7fac1c Fix MySensors battery sensors attachment to correct gateway (#151167)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-02-18 14:29:47 +01:00
Artur Pragacz
7a41ce1fd8 Add clean_area action to vacuum (#149315)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-02-18 14:13:08 +01:00
Erwin Douna
937b4866c3 Proxmox polish strings & tests (#163361) 2026-02-18 14:10:16 +01:00
Artur Pragacz
151e075e28 Do not send empty snapshots in analytics (#163351) 2026-02-18 13:45:45 +01:00
Erwin Douna
8094cfc404 Add coordinator to Proxmox (#161146) 2026-02-18 13:37:53 +01:00
Allen Porter
b26483e09e Fix remote calendar event handling of events within the same update period (#163186)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-18 12:52:35 +01:00
Brett Adams
728de32d75 Add missing data_description for reauth_confirm token in Splunk (#163356)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 12:43:44 +01:00
MoonDevLT
8de1e3d27b Change lunatone config entry title to only include the URL (#162855) 2026-02-18 12:27:25 +01:00
Tom Matheussen
cabf3b7ab9 Set last_reported timestamp for Satel Integra entities (#163352) 2026-02-18 12:04:30 +01:00
theobld-ww
f0e22cca56 Reconfiguration flow Watts Vision + and platinium level (#163346) 2026-02-18 11:55:27 +01:00
Karl Beecken
294a3e5360 add teltonika integration (#157539)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-02-18 11:18:50 +01:00
Nic Eggert
fdd753e70c Add support for voltage sensors to eGauge integration (#163206) 2026-02-18 08:44:01 +01:00
epenet
392fc7ff91 Use shorthand attributes in osramlightify (#163296) 2026-02-18 08:35:28 +01:00
Allen Porter
d777c1c542 Bump pyrainbird to 6.0.5 (#163333) 2026-02-18 08:19:38 +01:00
dependabot[bot]
fa71fd3992 Bump actions/stale from 10.1.1 to 10.2.0 (#163223) 2026-02-18 07:46:11 +01:00
Jamie Magee
19f6340546 Bump victron-ble-ha-parser to 0.4.10 (#163310) 2026-02-17 15:57:56 -05:00
Allen Porter
479cb7f1e1 Allow Gemini CLI and Anti-gravity SKILL discovery (#163194) 2026-02-17 21:50:38 +01:00
Manu
d50d914928 Update quality scale of Namecheap DynamicDNS integration to platinum 🏆️ (#161682) 2026-02-17 20:02:23 +00:00
Abílio Costa
551a71104e Bump Idasen Desk dependency (#163309) 2026-02-17 19:41:27 +00:00
Åke Strandberg
65cf61571a Add Miele dishwasher program code (#163308) 2026-02-17 19:36:58 +00:00
Simone Chemelli
58ac3d2f45 Type fixture in Fritz tests (#163271) 2026-02-17 18:32:35 +01:00
christian9712
654e132440 ADS Light Color Temperature Support (#153913)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 17:26:19 +01:00
hbludworth
4af60ef3b9 Show progress indicator during backup stage of Core/App update (#162683) 2026-02-17 17:24:06 +01:00
Josef Zweck
2fc9ded6b7 Add sensors to onedrive_for_business (#163135) 2026-02-17 17:15:49 +01:00
karwosts
9f551f3d5b Improve derivative units and auto-device_class (#157369) 2026-02-17 08:08:59 -08:00
epenet
0b8312d942 Use shorthand attributes in serial (#163287) 2026-02-17 17:05:48 +01:00
epenet
413e297022 Use shorthand attributes in tank_utility (#163288)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-02-17 17:05:04 +01:00
epenet
f7752686df Use shorthand attributes in sony_projector (#163293) 2026-02-17 16:01:42 +00:00
epenet
1313960893 Use shorthand attributes in skybeacon (#163295) 2026-02-17 15:59:53 +00:00
epenet
d298eb033a Use shorthand attributes in vasttrafik (#163285) 2026-02-17 16:58:36 +01:00
epenet
398a6222cd Remove deprecated starline state attribute (#163289) 2026-02-17 16:44:43 +01:00
epenet
7168e2df5a Use shorthand attributes in repetier (#163291) 2026-02-17 16:42:10 +01:00
epenet
3b3c081703 Use shorthand attributes in sigfox (#163286) 2026-02-17 16:41:02 +01:00
epenet
889467e4c2 Use shorthand attributes in openhardwaremonitor (#163284) 2026-02-17 16:35:58 +01:00
epenet
6a3bace824 Use shorthand attributes in hp_ilo (#163282) 2026-02-17 16:35:39 +01:00
epenet
523b527486 Use shorthand attributes in omnilogic (#163283) 2026-02-17 16:19:35 +01:00
epenet
b44900532f Ensure DOMAIN constant is always aliased with _DOMAIN suffix (#163270) 2026-02-17 16:10:11 +01:00
theobld-ww
bd45232972 Translation keys for exceptions Watts Vision + integration (#163231)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-02-17 16:07:53 +01:00
epenet
e7aa0ae398 Add type hints to extra_state_attributes [m-z] (#163281) 2026-02-17 16:00:42 +01:00
epenet
1d41e24653 Add type hints to extra_state_attributes [a-l] (#163279) 2026-02-17 16:00:18 +01:00
Wendelin
049a910494 Fix frontend development PR download cache (#162928) 2026-02-17 15:55:39 +01:00
A. Gideonse
f6f52005fe Add Indevolt integration (#160595)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-17 15:51:55 +01:00
epenet
b23c402d0a Improve haveibeenpwned type hints (#163280) 2026-02-17 15:48:14 +01:00
Sid
91c36fcdf6 Fix dynamic entity creation in eheimdigital (#161155) 2026-02-17 15:47:56 +01:00
epenet
ff2f0ac320 Mark RestoreEntity/RestoreSensor type hints as mandatory (#163272) 2026-02-17 15:34:16 +01:00
Denis Shulyaka
c205785f4f Add quality scale to Anthropic (#162953) 2026-02-17 15:20:57 +01:00
Joost Lekkerkerker
59dad4c935 Add DHCP Discovery for SmartThings (#160314) 2026-02-17 15:15:42 +01:00
epenet
d61f7d8170 Use shorthand attributes in geo_rss_events (#163268) 2026-02-17 14:39:16 +01:00
epenet
b6e7a55cd1 Rename DOMAIN_xxx aliases in tests (#163261) 2026-02-17 14:37:17 +01:00
epenet
163a6805eb Rename DOMAIN_xxx aliases in components (#163260) 2026-02-17 14:35:25 +01:00
epenet
637accbfff Rename DOMAIN_xxx aliases in template (#163259) 2026-02-17 14:34:48 +01:00
epenet
98b8e152e3 Use shorthand attributes in currencylayer (#163267) 2026-02-17 14:25:00 +01:00
Simone Chemelli
d12816d297 Removed more warnings from Fritz tests (#163262) 2026-02-17 14:08:02 +01:00
Tom Matheussen
58e4a42a1b Add coordinator for Satel Integra (#158533)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-02-17 12:55:00 +00:00
epenet
fdad9873e4 Mark weather method type hints as mandatory (#163247) 2026-02-17 13:23:53 +01:00
epenet
0337988be8 Improve type hints in meteoclimatic weather (#163244) 2026-02-17 13:03:17 +01:00
Brett Adams
ba695b5bd9 Add quality scale to Splunk (#162893) 2026-02-17 12:46:46 +01:00
Simone Chemelli
c114ea2666 Fix warning in Fritz switch tests (#163256) 2026-02-17 12:31:31 +01:00
epenet
82148e46f5 Rename DOMAIN aliases in tests (#163254) 2026-02-17 12:15:36 +01:00
epenet
34a78f9251 Rename DOMAIN aliases (#163253) 2026-02-17 12:15:03 +01:00
Willem-Jan van Rootselaar
f1c142b3d3 Refactor BSB-Lan tests (#163245) 2026-02-17 11:40:25 +01:00
Josef Zweck
68c82c2f90 Debug logging for service calls (#163235) 2026-02-17 11:23:54 +01:00
epenet
487e2f8ccc Improve type hints in tomorrowio weather (#163246) 2026-02-17 11:07:48 +01:00
Josef Zweck
6322185206 Bump onedrive-personal-sdk to 0.1.4 (#163238) 2026-02-17 10:56:32 +01:00
epenet
7f65db260f Improve type hints in meteo_france weather (#163243) 2026-02-17 10:55:51 +01:00
epenet
6c50711e2b Improve type hints in ipma weather (#163242) 2026-02-17 10:55:20 +01:00
epenet
f0e7d099e6 Improve type hints in environment_canada weather (#163241) 2026-02-17 10:55:00 +01:00
epenet
6c0fb12189 Improve type hints in ecobee weather (#163240) 2026-02-17 10:54:33 +01:00
Simone Chemelli
8e14dc7b5a Cleanup for 100% coverage of entity for Fritz (#163237) 2026-02-17 10:46:15 +01:00
epenet
219b982ef5 Improve type hints in aemet weather (#163239) 2026-02-17 10:45:44 +01:00
Zoltán Farkasdi
307c6a4ce2 Netatmo doortag binary sensor addition (#160608) 2026-02-17 10:34:23 +01:00
Erik Montnemery
9b1812858b Remove unnecessary set up of other integration from automation tests (#163230) 2026-02-17 10:11:08 +01:00
Simone Chemelli
9c57be215f Add 100% coverage to helpers for Fritz (#162999) 2026-02-17 10:03:57 +01:00
Josef Zweck
cda6236099 Add full debug logs for coordinator failures (#163228) 2026-02-17 09:52:44 +01:00
epenet
e4c7262260 Use unique node_id in matter fixtures (#162779) 2026-02-17 09:31:42 +01:00
theobld-ww
0f648a7f9d Add diagnostics support for Watts Vision integration (#163177)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-17 09:23:44 +01:00
Erik Montnemery
e6b9c2f737 Raise in EntityComponent.async_prepare_reload on configuration error (#101267)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-02-17 07:42:46 +01:00
Andrej Friesen
e0f39e6392 Add Pressure Stall Information (PSI) to Systemmonitor integration (#151946)
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-16 23:48:15 +01:00
Hai-Nam Nguyen
52d645e4bf Hypontech micro invertors support via Hyponcloud (#159442)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-16 23:38:44 +01:00
Brett Adams
e8f2493ed6 Fix common-modules quality scale for advantage_air (#163209)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-16 23:28:25 +01:00
elgris
ba62d95715 Control time display format on SwitchBot Meter Pro CO2 (#163008)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-02-16 22:58:09 +01:00
MarkGodwin
73fa9925c4 Add test coverage for tplink_omada update entities (#162549) 2026-02-16 22:17:56 +01:00
Jordan Rodgers
9ec456d28e Add port link speed sensor to UniFi integration (#162847) 2026-02-16 22:15:50 +01:00
johanzander
4974439850 Add on-grid discharge stop SOC control for Growatt MIN devices (#160634)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:00:55 +01:00
Thomas Rupprecht
5cf37afbf6 Add quality_scale with strict-typing done for SpaceAPI (#163003) 2026-02-16 21:47:48 +01:00
Andrew Jackson
76ebc134f3 Mealie add get shopping list items action (#163090)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-16 21:43:36 +01:00
Allen Porter
667a77502d Store nest media in a .cache subdirectory (#163200)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-16 21:43:27 +01:00
David Recordon
8c146624f9 Add Celsius Temperature Support for Control4 Integration (#163196) 2026-02-16 21:37:59 +01:00
mettolen
2418036798 Saunum integration fix: close client on unload (#163183) 2026-02-16 21:33:10 +01:00
Simone Chemelli
459996b760 Add 100% coverage of sensors for Fritz (#163005) 2026-02-16 21:30:52 +01:00
wollew
eec854386a bump pyvlx to 0.2.30 (#163203) 2026-02-16 21:06:07 +01:00
Manu
47d6e3e938 Refactor HTML5 integration to use aiohttp instead of requests (#163202) 2026-02-16 20:11:04 +01:00
Norbert Rittel
957c6039e9 Fix reboot_gateway action deprecation message in velux (#163201) 2026-02-16 19:43:28 +01:00
Erik Montnemery
c833cfa395 Don't mock out filesystem operations in backup_restore tests (#163172) 2026-02-16 19:11:36 +01:00
theobld-ww
9dc38eda9f Reauthentication flow for Watts Vision + integration (#163141) 2026-02-16 19:00:49 +01:00
Kamil Breguła
e49767d37a GIOS quality scale fixes to platinum (#162510)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 18:59:45 +01:00
wollew
e6c5e72470 add upper and lower shutter of Velux dualrollershutters as entities (#162998)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-16 18:57:45 +01:00
James
66dc566d3a Add zone temperature support to Daikin integration (#152642) 2026-02-16 17:44:38 +00:00
epenet
5bb7699df0 Mark water_heater method type hints as mandatory (#163190) 2026-02-16 18:35:09 +01:00
epenet
168dd36d66 Mark vacuum method type hints as mandatory (#163185) 2026-02-16 18:20:38 +01:00
epenet
66d8a5bc51 Improve type hints in econet water_heater (#163193) 2026-02-16 18:08:46 +01:00
957 changed files with 42275 additions and 6243 deletions

1
.agent/skills Symbolic link
View File

@@ -0,0 +1 @@
../.claude/skills/

1
.gemini/skills Symbolic link
View File

@@ -0,0 +1 @@
../.claude/skills

View File

@@ -27,7 +27,7 @@ jobs:
# - No PRs marked as no-stale
# - No issues (-1)
- name: 60 days stale PRs policy
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
@@ -67,7 +67,7 @@ jobs:
# - No issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: 90 days stale issues
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ steps.token.outputs.token }}
days-before-stale: 90
@@ -97,7 +97,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted
# - No PRs (-1)
- name: Needs more information stale issues policy
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ steps.token.outputs.token }}
only-labels: "needs-more-information"

View File

@@ -49,6 +49,7 @@ homeassistant.components.actiontec.*
homeassistant.components.adax.*
homeassistant.components.adguard.*
homeassistant.components.aftership.*
homeassistant.components.ai_task.*
homeassistant.components.air_quality.*
homeassistant.components.airgradient.*
homeassistant.components.airly.*
@@ -130,6 +131,7 @@ homeassistant.components.bring.*
homeassistant.components.brother.*
homeassistant.components.browser.*
homeassistant.components.bryant_evolution.*
homeassistant.components.bsblan.*
homeassistant.components.bthome.*
homeassistant.components.button.*
homeassistant.components.calendar.*
@@ -209,6 +211,7 @@ homeassistant.components.firefly_iii.*
homeassistant.components.fitbit.*
homeassistant.components.flexit_bacnet.*
homeassistant.components.flux_led.*
homeassistant.components.folder_watcher.*
homeassistant.components.forecast_solar.*
homeassistant.components.fritz.*
homeassistant.components.fritzbox.*
@@ -275,6 +278,7 @@ homeassistant.components.humidifier.*
homeassistant.components.husqvarna_automower.*
homeassistant.components.hydrawise.*
homeassistant.components.hyperion.*
homeassistant.components.hypontech.*
homeassistant.components.ibeacon.*
homeassistant.components.idasen_desk.*
homeassistant.components.image.*
@@ -297,6 +301,7 @@ homeassistant.components.iotty.*
homeassistant.components.ipp.*
homeassistant.components.iqvia.*
homeassistant.components.iron_os.*
homeassistant.components.isal.*
homeassistant.components.islamic_prayer_times.*
homeassistant.components.isy994.*
homeassistant.components.jellyfin.*
@@ -307,6 +312,7 @@ homeassistant.components.knocki.*
homeassistant.components.knx.*
homeassistant.components.kraken.*
homeassistant.components.kulersky.*
homeassistant.components.labs.*
homeassistant.components.lacrosse.*
homeassistant.components.lacrosse_view.*
homeassistant.components.lamarzocco.*
@@ -366,6 +372,7 @@ homeassistant.components.my.*
homeassistant.components.mysensors.*
homeassistant.components.myuplink.*
homeassistant.components.nam.*
homeassistant.components.namecheapdns.*
homeassistant.components.nasweb.*
homeassistant.components.neato.*
homeassistant.components.nest.*
@@ -401,6 +408,7 @@ homeassistant.components.opnsense.*
homeassistant.components.opower.*
homeassistant.components.oralb.*
homeassistant.components.otbr.*
homeassistant.components.otp.*
homeassistant.components.overkiz.*
homeassistant.components.overseerr.*
homeassistant.components.p1_monitor.*
@@ -417,6 +425,7 @@ homeassistant.components.plugwise.*
homeassistant.components.pooldose.*
homeassistant.components.portainer.*
homeassistant.components.powerfox.*
homeassistant.components.powerfox_local.*
homeassistant.components.powerwall.*
homeassistant.components.private_ble_device.*
homeassistant.components.prometheus.*
@@ -435,10 +444,12 @@ homeassistant.components.radarr.*
homeassistant.components.radio_browser.*
homeassistant.components.rainforest_raven.*
homeassistant.components.rainmachine.*
homeassistant.components.random.*
homeassistant.components.raspberry_pi.*
homeassistant.components.rdw.*
homeassistant.components.recollect_waste.*
homeassistant.components.recorder.*
homeassistant.components.recovery_mode.*
homeassistant.components.redgtech.*
homeassistant.components.remember_the_milk.*
homeassistant.components.remote.*
@@ -470,6 +481,7 @@ homeassistant.components.schlage.*
homeassistant.components.scrape.*
homeassistant.components.script.*
homeassistant.components.search.*
homeassistant.components.season.*
homeassistant.components.select.*
homeassistant.components.sensibo.*
homeassistant.components.sensirion_ble.*
@@ -496,6 +508,7 @@ homeassistant.components.smtp.*
homeassistant.components.snooz.*
homeassistant.components.solarlog.*
homeassistant.components.sonarr.*
homeassistant.components.spaceapi.*
homeassistant.components.speedtestdotnet.*
homeassistant.components.spotify.*
homeassistant.components.sql.*
@@ -520,6 +533,7 @@ homeassistant.components.synology_dsm.*
homeassistant.components.system_health.*
homeassistant.components.system_log.*
homeassistant.components.systemmonitor.*
homeassistant.components.systemnexa2.*
homeassistant.components.tag.*
homeassistant.components.tailscale.*
homeassistant.components.tailwind.*
@@ -562,6 +576,7 @@ homeassistant.components.update.*
homeassistant.components.uptime.*
homeassistant.components.uptime_kuma.*
homeassistant.components.uptimerobot.*
homeassistant.components.usage_prediction.*
homeassistant.components.usb.*
homeassistant.components.uvc.*
homeassistant.components.vacuum.*
@@ -580,6 +595,7 @@ homeassistant.components.water_heater.*
homeassistant.components.watts.*
homeassistant.components.watttime.*
homeassistant.components.weather.*
homeassistant.components.web_rtc.*
homeassistant.components.webhook.*
homeassistant.components.webostv.*
homeassistant.components.websocket_api.*

20
CODEOWNERS generated
View File

@@ -753,6 +753,8 @@ build.json @home-assistant/supervisor
/tests/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan
/homeassistant/components/hyperion/ @dermotduffy
/tests/components/hyperion/ @dermotduffy
/homeassistant/components/hypontech/ @jcisio
/tests/components/hypontech/ @jcisio
/homeassistant/components/ialarm/ @RyuzakiKK
/tests/components/ialarm/ @RyuzakiKK
/homeassistant/components/iammeter/ @lewei50
@@ -786,10 +788,12 @@ build.json @home-assistant/supervisor
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh
/tests/components/incomfort/ @jbouwh
/homeassistant/components/indevolt/ @xirtnl
/tests/components/indevolt/ @xirtnl
/homeassistant/components/inels/ @epdevlab
/tests/components/inels/ @epdevlab
/homeassistant/components/influxdb/ @mdegat01
/tests/components/influxdb/ @mdegat01
/homeassistant/components/influxdb/ @mdegat01 @Robbie1221
/tests/components/influxdb/ @mdegat01 @Robbie1221
/homeassistant/components/inkbird/ @bdraco
/tests/components/inkbird/ @bdraco
/homeassistant/components/input_boolean/ @home-assistant/core
@@ -1094,8 +1098,8 @@ build.json @home-assistant/supervisor
/tests/components/nasweb/ @nasWebio
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
/tests/components/nederlandse_spoorwegen/ @YarmoM @heindrichpaul
/homeassistant/components/ness_alarm/ @nickw444
/tests/components/ness_alarm/ @nickw444
/homeassistant/components/ness_alarm/ @nickw444 @poshy163
/tests/components/ness_alarm/ @nickw444 @poshy163
/homeassistant/components/nest/ @allenporter
/tests/components/nest/ @allenporter
/homeassistant/components/netatmo/ @cgtobi
@@ -1279,6 +1283,8 @@ build.json @home-assistant/supervisor
/tests/components/portainer/ @erwindouna
/homeassistant/components/powerfox/ @klaasnicolaas
/tests/components/powerfox/ @klaasnicolaas
/homeassistant/components/powerfox_local/ @klaasnicolaas
/tests/components/powerfox_local/ @klaasnicolaas
/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson
/homeassistant/components/prana/ @prana-dev-official
@@ -1642,6 +1648,8 @@ build.json @home-assistant/supervisor
/tests/components/system_bridge/ @timmo001
/homeassistant/components/systemmonitor/ @gjohansson-ST
/tests/components/systemmonitor/ @gjohansson-ST
/homeassistant/components/systemnexa2/ @konsulten @slangstrom
/tests/components/systemnexa2/ @konsulten @slangstrom
/homeassistant/components/tado/ @erwindouna
/tests/components/tado/ @erwindouna
/homeassistant/components/tag/ @home-assistant/core
@@ -1667,6 +1675,8 @@ build.json @home-assistant/supervisor
/tests/components/telegram_bot/ @hanwg
/homeassistant/components/tellduslive/ @fredrike
/tests/components/tellduslive/ @fredrike
/homeassistant/components/teltonika/ @karlbeecken
/tests/components/teltonika/ @karlbeecken
/homeassistant/components/template/ @Petro31 @home-assistant/core
/tests/components/template/ @Petro31 @home-assistant/core
/homeassistant/components/tesla_fleet/ @Bre77
@@ -1733,6 +1743,8 @@ build.json @home-assistant/supervisor
/tests/components/trafikverket_train/ @gjohansson-ST
/homeassistant/components/trafikverket_weatherstation/ @gjohansson-ST
/tests/components/trafikverket_weatherstation/ @gjohansson-ST
/homeassistant/components/trane/ @bdraco
/tests/components/trane/ @bdraco
/homeassistant/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp
/tests/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp
/homeassistant/components/trend/ @jpbede

View File

@@ -0,0 +1,5 @@
{
"domain": "american_standard",
"name": "American Standard",
"integrations": ["nexia", "trane"]
}

View File

@@ -0,0 +1,5 @@
{
"domain": "powerfox",
"name": "Powerfox",
"integrations": ["powerfox", "powerfox_local"]
}

View File

@@ -0,0 +1,5 @@
{
"domain": "trane",
"name": "Trane",
"integrations": ["nexia", "trane"]
}

View File

@@ -7,7 +7,7 @@ import logging
from accuweather import AccuWeather
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -72,7 +72,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry)
ent_reg = er.async_get(hass)
for day in range(5):
unique_id = f"{location_key}-ozone-{day}"
if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id):
if entity_id := ent_reg.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, unique_id):
_LOGGER.debug("Removing ozone sensor entity %s", entity_id)
ent_reg.async_remove(entity_id)

View File

@@ -9,9 +9,13 @@ import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA,
ColorMode,
LightEntity,
filter_supported_color_modes,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
@@ -24,13 +28,20 @@ from .entity import AdsEntity
from .hub import AdsHub
CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness"
CONF_ADS_VAR_COLOR_TEMP_KELVIN = "adsvar_color_temp_kelvin"
CONF_MIN_COLOR_TEMP_KELVIN = "min_color_temp_kelvin"
CONF_MAX_COLOR_TEMP_KELVIN = "max_color_temp_kelvin"
STATE_KEY_BRIGHTNESS = "brightness"
STATE_KEY_COLOR_TEMP_KELVIN = "color_temp_kelvin"
DEFAULT_NAME = "ADS Light"
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string,
vol.Optional(CONF_ADS_VAR_COLOR_TEMP_KELVIN): cv.string,
vol.Optional(CONF_MIN_COLOR_TEMP_KELVIN): cv.positive_int,
vol.Optional(CONF_MAX_COLOR_TEMP_KELVIN): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
@@ -47,9 +58,24 @@ def setup_platform(
ads_var_enable: str = config[CONF_ADS_VAR]
ads_var_brightness: str | None = config.get(CONF_ADS_VAR_BRIGHTNESS)
ads_var_color_temp_kelvin: str | None = config.get(CONF_ADS_VAR_COLOR_TEMP_KELVIN)
min_color_temp_kelvin: int | None = config.get(CONF_MIN_COLOR_TEMP_KELVIN)
max_color_temp_kelvin: int | None = config.get(CONF_MAX_COLOR_TEMP_KELVIN)
name: str = config[CONF_NAME]
add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, name)])
add_entities(
[
AdsLight(
ads_hub,
ads_var_enable,
ads_var_brightness,
ads_var_color_temp_kelvin,
min_color_temp_kelvin,
max_color_temp_kelvin,
name,
)
]
)
class AdsLight(AdsEntity, LightEntity):
@@ -60,18 +86,40 @@ class AdsLight(AdsEntity, LightEntity):
ads_hub: AdsHub,
ads_var_enable: str,
ads_var_brightness: str | None,
ads_var_color_temp_kelvin: str | None,
min_color_temp_kelvin: int | None,
max_color_temp_kelvin: int | None,
name: str,
) -> None:
"""Initialize AdsLight entity."""
super().__init__(ads_hub, name, ads_var_enable)
self._state_dict[STATE_KEY_BRIGHTNESS] = None
self._state_dict[STATE_KEY_COLOR_TEMP_KELVIN] = None
self._ads_var_brightness = ads_var_brightness
self._ads_var_color_temp_kelvin = ads_var_color_temp_kelvin
# Determine supported color modes
color_modes = {ColorMode.ONOFF}
if ads_var_brightness is not None:
self._attr_color_mode = ColorMode.BRIGHTNESS
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
else:
self._attr_color_mode = ColorMode.ONOFF
self._attr_supported_color_modes = {ColorMode.ONOFF}
color_modes.add(ColorMode.BRIGHTNESS)
if ads_var_color_temp_kelvin is not None:
color_modes.add(ColorMode.COLOR_TEMP)
self._attr_supported_color_modes = filter_supported_color_modes(color_modes)
self._attr_color_mode = next(iter(self._attr_supported_color_modes))
# Set color temperature range (static config values take precedence over defaults)
if ads_var_color_temp_kelvin is not None:
self._attr_min_color_temp_kelvin = (
min_color_temp_kelvin
if min_color_temp_kelvin is not None
else DEFAULT_MIN_KELVIN
)
self._attr_max_color_temp_kelvin = (
max_color_temp_kelvin
if max_color_temp_kelvin is not None
else DEFAULT_MAX_KELVIN
)
async def async_added_to_hass(self) -> None:
"""Register device notification."""
@@ -84,11 +132,23 @@ class AdsLight(AdsEntity, LightEntity):
STATE_KEY_BRIGHTNESS,
)
if self._ads_var_color_temp_kelvin is not None:
await self.async_initialize_device(
self._ads_var_color_temp_kelvin,
pyads.PLCTYPE_UINT,
STATE_KEY_COLOR_TEMP_KELVIN,
)
@property
def brightness(self) -> int | None:
"""Return the brightness of the light (0..255)."""
return self._state_dict[STATE_KEY_BRIGHTNESS]
@property
def color_temp_kelvin(self) -> int | None:
"""Return the color temperature in Kelvin."""
return self._state_dict[STATE_KEY_COLOR_TEMP_KELVIN]
@property
def is_on(self) -> bool:
"""Return True if the entity is on."""
@@ -97,6 +157,8 @@ class AdsLight(AdsEntity, LightEntity):
def turn_on(self, **kwargs: Any) -> None:
"""Turn the light on or set a specific dimmer value."""
brightness = kwargs.get(ATTR_BRIGHTNESS)
color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
self._ads_hub.write_by_name(self._ads_var, True, pyads.PLCTYPE_BOOL)
if self._ads_var_brightness is not None and brightness is not None:
@@ -104,6 +166,11 @@ class AdsLight(AdsEntity, LightEntity):
self._ads_var_brightness, brightness, pyads.PLCTYPE_UINT
)
if self._ads_var_color_temp_kelvin is not None and color_temp is not None:
self._ads_hub.write_by_name(
self._ads_var_color_temp_kelvin, color_temp, pyads.PLCTYPE_UINT
)
def turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
self._ads_hub.write_by_name(self._ads_var, False, pyads.PLCTYPE_BOOL)

View File

@@ -1,26 +1,17 @@
"""Advantage Air climate integration."""
from datetime import timedelta
import logging
from advantage_air import advantage_air
from advantage_air import ApiError, advantage_air
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ADVANTAGE_AIR_RETRY, DOMAIN
from .models import AdvantageAirData
from .coordinator import AdvantageAirCoordinator, AdvantageAirDataConfigEntry
from .services import async_setup_services
type AdvantageAirDataConfigEntry = ConfigEntry[AdvantageAirData]
ADVANTAGE_AIR_SYNC_INTERVAL = 15
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.CLIMATE,
@@ -32,9 +23,6 @@ PLATFORMS = [
Platform.UPDATE,
]
_LOGGER = logging.getLogger(__name__)
REQUEST_REFRESH_DELAY = 0.5
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -57,27 +45,10 @@ async def async_setup_entry(
retry=ADVANTAGE_AIR_RETRY,
)
async def async_get():
try:
return await api.async_get()
except ApiError as err:
raise UpdateFailed(err) from err
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name="Advantage Air",
update_method=async_get,
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
),
)
coordinator = AdvantageAirCoordinator(hass, entry, api)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = AdvantageAirData(coordinator, api)
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -11,8 +11,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .coordinator import AdvantageAirCoordinator
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
from .models import AdvantageAirData
PARALLEL_UPDATES = 0
@@ -24,19 +24,23 @@ async def async_setup_entry(
) -> None:
"""Set up AdvantageAir Binary Sensor platform."""
instance = config_entry.runtime_data
coordinator = config_entry.runtime_data
entities: list[BinarySensorEntity] = []
if aircons := instance.coordinator.data.get("aircons"):
if aircons := coordinator.data.get("aircons"):
for ac_key, ac_device in aircons.items():
entities.append(AdvantageAirFilter(instance, ac_key))
entities.append(AdvantageAirFilter(coordinator, ac_key))
for zone_key, zone in ac_device["zones"].items():
# Only add motion sensor when motion is enabled
if zone["motionConfig"] >= 2:
entities.append(AdvantageAirZoneMotion(instance, ac_key, zone_key))
entities.append(
AdvantageAirZoneMotion(coordinator, ac_key, zone_key)
)
# Only add MyZone if it is available
if zone["type"] != 0:
entities.append(AdvantageAirZoneMyZone(instance, ac_key, zone_key))
entities.append(
AdvantageAirZoneMyZone(coordinator, ac_key, zone_key)
)
async_add_entities(entities)
@@ -47,9 +51,9 @@ class AdvantageAirFilter(AdvantageAirAcEntity, BinarySensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_name = "Filter"
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
def __init__(self, coordinator: AdvantageAirCoordinator, ac_key: str) -> None:
"""Initialize an Advantage Air Filter sensor."""
super().__init__(instance, ac_key)
super().__init__(coordinator, ac_key)
self._attr_unique_id += "-filter"
@property
@@ -63,9 +67,11 @@ class AdvantageAirZoneMotion(AdvantageAirZoneEntity, BinarySensorEntity):
_attr_device_class = BinarySensorDeviceClass.MOTION
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize an Advantage Air Zone Motion sensor."""
super().__init__(instance, ac_key, zone_key)
super().__init__(coordinator, ac_key, zone_key)
self._attr_name = f"{self._zone['name']} motion"
self._attr_unique_id += "-motion"
@@ -81,9 +87,11 @@ class AdvantageAirZoneMyZone(AdvantageAirZoneEntity, BinarySensorEntity):
_attr_entity_registry_enabled_default = False
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize an Advantage Air Zone MyZone sensor."""
super().__init__(instance, ac_key, zone_key)
super().__init__(coordinator, ac_key, zone_key)
self._attr_name = f"{self._zone['name']} myZone"
self._attr_unique_id += "-myzone"

View File

@@ -31,8 +31,8 @@ from .const import (
ADVANTAGE_AIR_STATE_ON,
ADVANTAGE_AIR_STATE_OPEN,
)
from .coordinator import AdvantageAirCoordinator
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
from .models import AdvantageAirData
ADVANTAGE_AIR_HVAC_MODES = {
"heat": HVACMode.HEAT,
@@ -90,16 +90,16 @@ async def async_setup_entry(
) -> None:
"""Set up AdvantageAir climate platform."""
instance = config_entry.runtime_data
coordinator = config_entry.runtime_data
entities: list[ClimateEntity] = []
if aircons := instance.coordinator.data.get("aircons"):
if aircons := coordinator.data.get("aircons"):
for ac_key, ac_device in aircons.items():
entities.append(AdvantageAirAC(instance, ac_key))
entities.append(AdvantageAirAC(coordinator, ac_key))
for zone_key, zone in ac_device["zones"].items():
# Only add zone climate control when zone is in temperature control
if zone["type"] > 0:
entities.append(AdvantageAirZone(instance, ac_key, zone_key))
entities.append(AdvantageAirZone(coordinator, ac_key, zone_key))
async_add_entities(entities)
@@ -114,9 +114,9 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
_attr_name = None
_support_preset = ClimateEntityFeature(0)
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
def __init__(self, coordinator: AdvantageAirCoordinator, ac_key: str) -> None:
"""Initialize an AdvantageAir AC unit."""
super().__init__(instance, ac_key)
super().__init__(coordinator, ac_key)
self._attr_preset_modes = [ADVANTAGE_AIR_MYZONE]
@@ -282,9 +282,11 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity):
_attr_max_temp = 32
_attr_min_temp = 16
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize an AdvantageAir Zone control."""
super().__init__(instance, ac_key, zone_key)
super().__init__(coordinator, ac_key, zone_key)
self._attr_name = self._zone["name"]
@property

View File

@@ -0,0 +1,59 @@
"""Coordinator for the Advantage Air integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from advantage_air import ApiError, advantage_air
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
ADVANTAGE_AIR_SYNC_INTERVAL = 15
REQUEST_REFRESH_DELAY = 0.5
_LOGGER = logging.getLogger(__name__)
type AdvantageAirDataConfigEntry = ConfigEntry[AdvantageAirCoordinator]
class AdvantageAirCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Advantage Air coordinator."""
config_entry: AdvantageAirDataConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: AdvantageAirDataConfigEntry,
api: advantage_air,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name="Advantage Air",
update_interval=timedelta(seconds=ADVANTAGE_AIR_SYNC_INTERVAL),
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
),
)
self.api = api
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from the API."""
try:
return await self.api.async_get()
except ApiError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
) from err

View File

@@ -13,8 +13,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import ADVANTAGE_AIR_STATE_CLOSE, ADVANTAGE_AIR_STATE_OPEN
from .coordinator import AdvantageAirCoordinator
from .entity import AdvantageAirThingEntity, AdvantageAirZoneEntity
from .models import AdvantageAirData
PARALLEL_UPDATES = 0
@@ -26,24 +26,24 @@ async def async_setup_entry(
) -> None:
"""Set up AdvantageAir cover platform."""
instance = config_entry.runtime_data
coordinator = config_entry.runtime_data
entities: list[CoverEntity] = []
if aircons := instance.coordinator.data.get("aircons"):
if aircons := coordinator.data.get("aircons"):
for ac_key, ac_device in aircons.items():
for zone_key, zone in ac_device["zones"].items():
# Only add zone vent controls when zone in vent control mode.
if zone["type"] == 0:
entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key))
if things := instance.coordinator.data.get("myThings"):
entities.append(AdvantageAirZoneVent(coordinator, ac_key, zone_key))
if things := coordinator.data.get("myThings"):
for thing in things["things"].values():
if thing["channelDipState"] in [1, 2]: # 1 = "Blind", 2 = "Blind 2"
entities.append(
AdvantageAirThingCover(instance, thing, CoverDeviceClass.BLIND)
AdvantageAirThingCover(coordinator, thing, CoverDeviceClass.BLIND)
)
elif thing["channelDipState"] in [3, 10]: # 3 & 10 = "Garage door"
entities.append(
AdvantageAirThingCover(instance, thing, CoverDeviceClass.GARAGE)
AdvantageAirThingCover(coordinator, thing, CoverDeviceClass.GARAGE)
)
async_add_entities(entities)
@@ -58,9 +58,11 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity):
| CoverEntityFeature.SET_POSITION
)
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize an Advantage Air Zone Vent."""
super().__init__(instance, ac_key, zone_key)
super().__init__(coordinator, ac_key, zone_key)
self._attr_name = self._zone["name"]
@property
@@ -106,12 +108,12 @@ class AdvantageAirThingCover(AdvantageAirThingEntity, CoverEntity):
def __init__(
self,
instance: AdvantageAirData,
coordinator: AdvantageAirCoordinator,
thing: dict[str, Any],
device_class: CoverDeviceClass,
) -> None:
"""Initialize an Advantage Air Things Cover."""
super().__init__(instance, thing)
super().__init__(coordinator, thing)
self._attr_device_class = device_class
@property

View File

@@ -27,7 +27,7 @@ async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: AdvantageAirDataConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data = config_entry.runtime_data.coordinator.data
data = config_entry.runtime_data.data
# Return only the relevant children
return {

View File

@@ -9,17 +9,17 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .models import AdvantageAirData
from .coordinator import AdvantageAirCoordinator
class AdvantageAirEntity(CoordinatorEntity):
class AdvantageAirEntity(CoordinatorEntity[AdvantageAirCoordinator]):
"""Parent class for Advantage Air Entities."""
_attr_has_entity_name = True
def __init__(self, instance: AdvantageAirData) -> None:
def __init__(self, coordinator: AdvantageAirCoordinator) -> None:
"""Initialize common aspects of an Advantage Air entity."""
super().__init__(instance.coordinator)
super().__init__(coordinator)
self._attr_unique_id: str = self.coordinator.data["system"]["rid"]
def update_handle_factory(self, func, *keys):
@@ -41,9 +41,9 @@ class AdvantageAirEntity(CoordinatorEntity):
class AdvantageAirAcEntity(AdvantageAirEntity):
"""Parent class for Advantage Air AC Entities."""
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
def __init__(self, coordinator: AdvantageAirCoordinator, ac_key: str) -> None:
"""Initialize common aspects of an Advantage Air ac entity."""
super().__init__(instance)
super().__init__(coordinator)
self.ac_key: str = ac_key
self._attr_unique_id += f"-{ac_key}"
@@ -56,7 +56,7 @@ class AdvantageAirAcEntity(AdvantageAirEntity):
name=self.coordinator.data["aircons"][self.ac_key]["info"]["name"],
)
self.async_update_ac = self.update_handle_factory(
instance.api.aircon.async_update_ac, self.ac_key
coordinator.api.aircon.async_update_ac, self.ac_key
)
@property
@@ -73,14 +73,16 @@ class AdvantageAirAcEntity(AdvantageAirEntity):
class AdvantageAirZoneEntity(AdvantageAirAcEntity):
"""Parent class for Advantage Air Zone Entities."""
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize common aspects of an Advantage Air zone entity."""
super().__init__(instance, ac_key)
super().__init__(coordinator, ac_key)
self.zone_key: str = zone_key
self._attr_unique_id += f"-{zone_key}"
self.async_update_zone = self.update_handle_factory(
instance.api.aircon.async_update_zone, self.ac_key, self.zone_key
coordinator.api.aircon.async_update_zone, self.ac_key, self.zone_key
)
@property
@@ -93,9 +95,11 @@ class AdvantageAirThingEntity(AdvantageAirEntity):
_attr_name = None
def __init__(self, instance: AdvantageAirData, thing: dict[str, Any]) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, thing: dict[str, Any]
) -> None:
"""Initialize common aspects of an Advantage Air Things entity."""
super().__init__(instance)
super().__init__(coordinator)
self._id = thing["id"]
self._attr_unique_id += f"-{self._id}"
@@ -108,7 +112,7 @@ class AdvantageAirThingEntity(AdvantageAirEntity):
name=thing["name"],
)
self.async_update_value = self.update_handle_factory(
instance.api.things.async_update_value, self._id
coordinator.api.things.async_update_value, self._id
)
@property
@@ -117,7 +121,7 @@ class AdvantageAirThingEntity(AdvantageAirEntity):
return self.coordinator.data["myThings"]["things"][self._id]
@property
def is_on(self):
def is_on(self) -> bool:
"""Return if the thing is considered on."""
return self._data["value"] > 0

View File

@@ -9,8 +9,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN
from .coordinator import AdvantageAirCoordinator
from .entity import AdvantageAirEntity, AdvantageAirThingEntity
from .models import AdvantageAirData
async def async_setup_entry(
@@ -20,21 +20,21 @@ async def async_setup_entry(
) -> None:
"""Set up AdvantageAir light platform."""
instance = config_entry.runtime_data
coordinator = config_entry.runtime_data
entities: list[LightEntity] = []
if my_lights := instance.coordinator.data.get("myLights"):
if my_lights := coordinator.data.get("myLights"):
for light in my_lights["lights"].values():
if light.get("relay"):
entities.append(AdvantageAirLight(instance, light))
entities.append(AdvantageAirLight(coordinator, light))
else:
entities.append(AdvantageAirLightDimmable(instance, light))
if things := instance.coordinator.data.get("myThings"):
entities.append(AdvantageAirLightDimmable(coordinator, light))
if things := coordinator.data.get("myThings"):
for thing in things["things"].values():
if thing["channelDipState"] == 4: # 4 = "Light (on/off)""
entities.append(AdvantageAirThingLight(instance, thing))
entities.append(AdvantageAirThingLight(coordinator, thing))
elif thing["channelDipState"] == 5: # 5 = "Light (Dimmable)""
entities.append(AdvantageAirThingLightDimmable(instance, thing))
entities.append(AdvantageAirThingLightDimmable(coordinator, thing))
async_add_entities(entities)
@@ -45,9 +45,11 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity):
_attr_supported_color_modes = {ColorMode.ONOFF}
_attr_name = None
def __init__(self, instance: AdvantageAirData, light: dict[str, Any]) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, light: dict[str, Any]
) -> None:
"""Initialize an Advantage Air Light."""
super().__init__(instance)
super().__init__(coordinator)
self._id: str = light["id"]
self._attr_unique_id += f"-{self._id}"
@@ -59,7 +61,7 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity):
name=light["name"],
)
self.async_update_state = self.update_handle_factory(
instance.api.lights.async_update_state, self._id
coordinator.api.lights.async_update_state, self._id
)
@property
@@ -87,11 +89,13 @@ class AdvantageAirLightDimmable(AdvantageAirLight):
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
def __init__(self, instance: AdvantageAirData, light: dict[str, Any]) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, light: dict[str, Any]
) -> None:
"""Initialize an Advantage Air Dimmable Light."""
super().__init__(instance, light)
super().__init__(coordinator, light)
self.async_update_value = self.update_handle_factory(
instance.api.lights.async_update_value, self._id
coordinator.api.lights.async_update_value, self._id
)
@property

View File

@@ -1,17 +0,0 @@
"""The Advantage Air integration models."""
from __future__ import annotations
from dataclasses import dataclass
from advantage_air import advantage_air
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@dataclass
class AdvantageAirData:
"""Data for the Advantage Air integration."""
coordinator: DataUpdateCoordinator
api: advantage_air

View File

@@ -1,16 +1,9 @@
rules:
# Bronze
action-setup:
status: todo
comment: https://developers.home-assistant.io/blog/2025/09/25/entity-services-api-changes/
action-setup: done
appropriate-polling: done
brands: done
common-modules:
status: todo
comment: |
Move coordinator from __init__.py to coordinator.py.
Consider using entity descriptions for binary_sensor and switch.
Consider simplifying climate supported features flow.
common-modules: done
config-flow-test-coverage:
status: todo
comment: |
@@ -33,9 +26,7 @@ rules:
comment: Entities do not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data:
status: done
comment: Consider extending coordinator to access API via coordinator and remove extra dataclass.
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
@@ -92,7 +83,7 @@ rules:
entity-translations: todo
exception-translations:
status: todo
comment: UpdateFailed in the coordinator
comment: HomeAssistantError in entity.py and ServiceValidationError in climate.py
icon-translations: todo
reconfiguration-flow: todo
repair-issues:

View File

@@ -5,8 +5,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .coordinator import AdvantageAirCoordinator
from .entity import AdvantageAirAcEntity
from .models import AdvantageAirData
ADVANTAGE_AIR_INACTIVE = "Inactive"
@@ -18,10 +18,12 @@ async def async_setup_entry(
) -> None:
"""Set up AdvantageAir select platform."""
instance = config_entry.runtime_data
coordinator = config_entry.runtime_data
if aircons := instance.coordinator.data.get("aircons"):
async_add_entities(AdvantageAirMyZone(instance, ac_key) for ac_key in aircons)
if aircons := coordinator.data.get("aircons"):
async_add_entities(
AdvantageAirMyZone(coordinator, ac_key) for ac_key in aircons
)
class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity):
@@ -30,16 +32,16 @@ class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity):
_attr_icon = "mdi:home-thermometer"
_attr_name = "MyZone"
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
def __init__(self, coordinator: AdvantageAirCoordinator, ac_key: str) -> None:
"""Initialize an Advantage Air MyZone control."""
super().__init__(instance, ac_key)
super().__init__(coordinator, ac_key)
self._attr_unique_id += "-myzone"
self._attr_options = [ADVANTAGE_AIR_INACTIVE]
self._number_to_name = {0: ADVANTAGE_AIR_INACTIVE}
self._name_to_number = {ADVANTAGE_AIR_INACTIVE: 0}
if "aircons" in instance.coordinator.data:
for zone in instance.coordinator.data["aircons"][ac_key]["zones"].values():
if "aircons" in coordinator.data:
for zone in coordinator.data["aircons"][ac_key]["zones"].values():
if zone["type"] > 0:
self._name_to_number[zone["name"]] = zone["number"]
self._number_to_name[zone["number"]] = zone["name"]

View File

@@ -16,8 +16,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import ADVANTAGE_AIR_STATE_OPEN
from .coordinator import AdvantageAirCoordinator
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
from .models import AdvantageAirData
ADVANTAGE_AIR_SET_COUNTDOWN_VALUE = "minutes"
ADVANTAGE_AIR_SET_COUNTDOWN_UNIT = "min"
@@ -32,21 +32,23 @@ async def async_setup_entry(
) -> None:
"""Set up AdvantageAir sensor platform."""
instance = config_entry.runtime_data
coordinator = config_entry.runtime_data
entities: list[SensorEntity] = []
if aircons := instance.coordinator.data.get("aircons"):
if aircons := coordinator.data.get("aircons"):
for ac_key, ac_device in aircons.items():
entities.append(AdvantageAirTimeTo(instance, ac_key, "On"))
entities.append(AdvantageAirTimeTo(instance, ac_key, "Off"))
entities.append(AdvantageAirTimeTo(coordinator, ac_key, "On"))
entities.append(AdvantageAirTimeTo(coordinator, ac_key, "Off"))
for zone_key, zone in ac_device["zones"].items():
# Only show damper and temp sensors when zone is in temperature control
if zone["type"] != 0:
entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key))
entities.append(AdvantageAirZoneTemp(instance, ac_key, zone_key))
entities.append(AdvantageAirZoneVent(coordinator, ac_key, zone_key))
entities.append(AdvantageAirZoneTemp(coordinator, ac_key, zone_key))
# Only show wireless signal strength sensors when using wireless sensors
if zone["rssi"] > 0:
entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key))
entities.append(
AdvantageAirZoneSignal(coordinator, ac_key, zone_key)
)
async_add_entities(entities)
@@ -56,9 +58,11 @@ class AdvantageAirTimeTo(AdvantageAirAcEntity, SensorEntity):
_attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(self, instance: AdvantageAirData, ac_key: str, action: str) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, ac_key: str, action: str
) -> None:
"""Initialize the Advantage Air timer control."""
super().__init__(instance, ac_key)
super().__init__(coordinator, ac_key)
self.action = action
self._time_key = f"countDownTo{action}"
self._attr_name = f"Time to {action}"
@@ -89,9 +93,11 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, SensorEntity):
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize an Advantage Air Zone Vent Sensor."""
super().__init__(instance, ac_key, zone_key=zone_key)
super().__init__(coordinator, ac_key, zone_key=zone_key)
self._attr_name = f"{self._zone['name']} vent"
self._attr_unique_id += "-vent"
@@ -117,9 +123,11 @@ class AdvantageAirZoneSignal(AdvantageAirZoneEntity, SensorEntity):
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize an Advantage Air Zone wireless signal sensor."""
super().__init__(instance, ac_key, zone_key)
super().__init__(coordinator, ac_key, zone_key)
self._attr_name = f"{self._zone['name']} signal"
self._attr_unique_id += "-signal"
@@ -151,9 +159,11 @@ class AdvantageAirZoneTemp(AdvantageAirZoneEntity, SensorEntity):
_attr_entity_registry_enabled_default = False
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(self, instance: AdvantageAirData, ac_key: str, zone_key: str) -> None:
def __init__(
self, coordinator: AdvantageAirCoordinator, ac_key: str, zone_key: str
) -> None:
"""Initialize an Advantage Air Zone Temp Sensor."""
super().__init__(instance, ac_key, zone_key)
super().__init__(coordinator, ac_key, zone_key)
self._attr_name = f"{self._zone['name']} temperature"
self._attr_unique_id += "-temp"

View File

@@ -17,6 +17,11 @@
}
}
},
"exceptions": {
"update_failed": {
"message": "An error occurred while updating from the Advantage Air API: {error}"
}
},
"services": {
"set_time_to": {
"description": "Controls timers to turn the system on or off after a set number of minutes.",

View File

@@ -13,8 +13,8 @@ from .const import (
ADVANTAGE_AIR_STATE_OFF,
ADVANTAGE_AIR_STATE_ON,
)
from .coordinator import AdvantageAirCoordinator
from .entity import AdvantageAirAcEntity, AdvantageAirThingEntity
from .models import AdvantageAirData
async def async_setup_entry(
@@ -24,20 +24,20 @@ async def async_setup_entry(
) -> None:
"""Set up AdvantageAir switch platform."""
instance = config_entry.runtime_data
coordinator = config_entry.runtime_data
entities: list[SwitchEntity] = []
if aircons := instance.coordinator.data.get("aircons"):
if aircons := coordinator.data.get("aircons"):
for ac_key, ac_device in aircons.items():
if ac_device["info"]["freshAirStatus"] != "none":
entities.append(AdvantageAirFreshAir(instance, ac_key))
entities.append(AdvantageAirFreshAir(coordinator, ac_key))
if ADVANTAGE_AIR_AUTOFAN_ENABLED in ac_device["info"]:
entities.append(AdvantageAirMyFan(instance, ac_key))
entities.append(AdvantageAirMyFan(coordinator, ac_key))
if ADVANTAGE_AIR_NIGHT_MODE_ENABLED in ac_device["info"]:
entities.append(AdvantageAirNightMode(instance, ac_key))
if things := instance.coordinator.data.get("myThings"):
entities.append(AdvantageAirNightMode(coordinator, ac_key))
if things := coordinator.data.get("myThings"):
entities.extend(
AdvantageAirRelay(instance, thing)
AdvantageAirRelay(coordinator, thing)
for thing in things["things"].values()
if thing["channelDipState"] == 8 # 8 = Other relay
)
@@ -51,9 +51,9 @@ class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity):
_attr_name = "Fresh air"
_attr_device_class = SwitchDeviceClass.SWITCH
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
def __init__(self, coordinator: AdvantageAirCoordinator, ac_key: str) -> None:
"""Initialize an Advantage Air fresh air control."""
super().__init__(instance, ac_key)
super().__init__(coordinator, ac_key)
self._attr_unique_id += "-freshair"
@property
@@ -77,9 +77,9 @@ class AdvantageAirMyFan(AdvantageAirAcEntity, SwitchEntity):
_attr_name = "MyFan"
_attr_device_class = SwitchDeviceClass.SWITCH
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
def __init__(self, coordinator: AdvantageAirCoordinator, ac_key: str) -> None:
"""Initialize an Advantage Air MyFan control."""
super().__init__(instance, ac_key)
super().__init__(coordinator, ac_key)
self._attr_unique_id += "-myfan"
@property
@@ -103,9 +103,9 @@ class AdvantageAirNightMode(AdvantageAirAcEntity, SwitchEntity):
_attr_name = "MySleep$aver"
_attr_device_class = SwitchDeviceClass.SWITCH
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
def __init__(self, coordinator: AdvantageAirCoordinator, ac_key: str) -> None:
"""Initialize an Advantage Air Night Mode control."""
super().__init__(instance, ac_key)
super().__init__(coordinator, ac_key)
self._attr_unique_id += "-nightmode"
@property

View File

@@ -7,8 +7,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import DOMAIN
from .coordinator import AdvantageAirCoordinator
from .entity import AdvantageAirEntity
from .models import AdvantageAirData
async def async_setup_entry(
@@ -18,9 +18,9 @@ async def async_setup_entry(
) -> None:
"""Set up AdvantageAir update platform."""
instance = config_entry.runtime_data
coordinator = config_entry.runtime_data
async_add_entities([AdvantageAirApp(instance)])
async_add_entities([AdvantageAirApp(coordinator)])
class AdvantageAirApp(AdvantageAirEntity, UpdateEntity):
@@ -28,9 +28,9 @@ class AdvantageAirApp(AdvantageAirEntity, UpdateEntity):
_attr_name = "App"
def __init__(self, instance: AdvantageAirData) -> None:
def __init__(self, coordinator: AdvantageAirCoordinator) -> None:
"""Initialize the Advantage Air App."""
super().__init__(instance)
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.coordinator.data["system"]["rid"])},
manufacturer="Advantage Air",

View File

@@ -74,7 +74,7 @@ class AemetWeather(
self._attr_unique_id = unique_id
@property
def condition(self):
def condition(self) -> str | None:
"""Return the current condition."""
cond = self.get_aemet_value([AOD_WEATHER, AOD_CONDITION])
return CONDITIONS_MAP.get(cond)
@@ -90,31 +90,31 @@ class AemetWeather(
return self.get_aemet_forecast(AOD_FORECAST_HOURLY)
@property
def humidity(self):
def humidity(self) -> float | None:
"""Return the humidity."""
return self.get_aemet_value([AOD_WEATHER, AOD_HUMIDITY])
@property
def native_pressure(self):
def native_pressure(self) -> float | None:
"""Return the pressure."""
return self.get_aemet_value([AOD_WEATHER, AOD_PRESSURE])
@property
def native_temperature(self):
def native_temperature(self) -> float | None:
"""Return the temperature."""
return self.get_aemet_value([AOD_WEATHER, AOD_TEMP])
@property
def wind_bearing(self):
def wind_bearing(self) -> float | None:
"""Return the wind bearing."""
return self.get_aemet_value([AOD_WEATHER, AOD_WIND_DIRECTION])
@property
def native_wind_gust_speed(self):
def native_wind_gust_speed(self) -> float | None:
"""Return the wind gust speed in native units."""
return self.get_aemet_value([AOD_WEATHER, AOD_WIND_SPEED_MAX])
@property
def native_wind_speed(self):
def native_wind_speed(self) -> float | None:
"""Return the wind speed."""
return self.get_aemet_value([AOD_WEATHER, AOD_WIND_SPEED])

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_DOMAIN
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -75,9 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirlyConfigEntry) -> boo
# Remove air_quality entities from registry if they exist
ent_reg = er.async_get(hass)
unique_id = f"{coordinator.latitude}-{coordinator.longitude}"
if entity_id := ent_reg.async_get_entity_id(
AIR_QUALITY_PLATFORM, DOMAIN, unique_id
):
if entity_id := ent_reg.async_get_entity_id(AIR_QUALITY_DOMAIN, DOMAIN, unique_id):
_LOGGER.debug("Removing deprecated air_quality entity %s", entity_id)
ent_reg.async_remove(entity_id)

View File

@@ -13,7 +13,7 @@ from homeassistant.components.notify import (
ATTR_DATA,
ATTR_MESSAGE,
ATTR_TITLE,
DOMAIN as DOMAIN_NOTIFY,
DOMAIN as NOTIFY_DOMAIN,
)
from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_ON
from homeassistant.core import Event, EventStateChangedData, HassJob, HomeAssistant
@@ -185,7 +185,7 @@ class AlertEntity(Entity):
for target in self._notifiers:
try:
await self.hass.services.async_call(
DOMAIN_NOTIFY, target, msg_payload, context=self._context
NOTIFY_DOMAIN, target, msg_payload, context=self._context
)
except ServiceNotFound:
LOGGER.error(

View File

@@ -534,6 +534,10 @@ class Analytics:
payload = await _async_snapshot_payload(self._hass)
if not payload:
LOGGER.info("Skipping snapshot submission, no data to send")
return
headers = {
"Content-Type": "application/json",
"User-Agent": f"home-assistant/{HA_VERSION}",

View File

@@ -19,7 +19,6 @@ from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_CHAT_MODEL,
DATA_REPAIR_DEFER_RELOAD,
DEFAULT_CONVERSATION_NAME,
DEPRECATED_MODELS,
DOMAIN,
@@ -34,7 +33,6 @@ type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Anthropic."""
hass.data.setdefault(DOMAIN, {}).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
await async_migrate_integration(hass)
return True
@@ -85,11 +83,6 @@ async def async_update_options(
hass: HomeAssistant, entry: AnthropicConfigEntry
) -> None:
"""Update options."""
defer_reload_entries: set[str] = hass.data.setdefault(DOMAIN, {}).setdefault(
DATA_REPAIR_DEFER_RELOAD, set()
)
if entry.entry_id in defer_reload_entries:
return
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -23,8 +23,6 @@ CONF_WEB_SEARCH_REGION = "region"
CONF_WEB_SEARCH_COUNTRY = "country"
CONF_WEB_SEARCH_TIMEZONE = "timezone"
DATA_REPAIR_DEFER_RELOAD = "repair_defer_reload"
DEFAULT = {
CONF_CHAT_MODEL: "claude-haiku-4-5",
CONF_MAX_TOKENS: 3000,

View File

@@ -0,0 +1,116 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
Integration has no actions.
appropriate-polling:
status: exempt
comment: |
Integration does not poll.
brands: done
common-modules: done
config-flow-test-coverage:
status: todo
comment: |
* Remove integration setup from the config flow init test
* Make `mock_setup_entry` a separate fixture
* Use the mock_config_entry fixture in `test_duplicate_entry`
* `test_duplicate_entry`: Patch `homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.list`
* Fix docstring and name for `test_form_invalid_auth` (does not only test auth)
* In `test_form_invalid_auth`, make sure the test run until CREATE_ENTRY to test that the flow is able to recover
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
Integration has no actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Integration does not subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: todo
comment: |
Reevaluate exceptions for entity services.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates:
status: exempt
comment: |
The API does not limit parallel updates.
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
Service integration, no discovery.
discovery:
status: exempt
comment: |
Service integration, no discovery.
docs-data-update:
status: exempt
comment: |
No data updates.
docs-examples:
status: todo
comment: |
To give examples of how people use the integration
docs-known-limitations: done
docs-supported-devices:
status: todo
comment: |
To write something about what models we support.
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: done
dynamic-devices:
status: exempt
comment: |
Service integration, no devices.
entity-category:
status: exempt
comment: |
No entities with categories.
entity-device-class:
status: exempt
comment: |
No entities with device classes.
entity-disabled-by-default:
status: exempt
comment: |
No entities disabled by default.
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: done
repair-issues: done
stale-devices:
status: exempt
comment: |
Service integration, no devices.
# Platinum
async-dependency: done
inject-websession:
status: done
comment: |
Uses `httpx` session.
strict-typing: done

View File

@@ -12,16 +12,14 @@ from homeassistant.components.repairs import RepairsFlow
from homeassistant.config_entries import ConfigEntryState, ConfigSubentry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
)
from .config_flow import get_model_list
from .const import (
CONF_CHAT_MODEL,
DATA_REPAIR_DEFER_RELOAD,
DEFAULT,
DEPRECATED_MODELS,
DOMAIN,
)
from .const import CONF_CHAT_MODEL, DEFAULT, DEPRECATED_MODELS, DOMAIN
if TYPE_CHECKING:
from . import AnthropicConfigEntry
@@ -33,8 +31,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
_subentry_iter: Iterator[tuple[str, str]] | None
_current_entry_id: str | None
_current_subentry_id: str | None
_reload_pending: set[str]
_pending_updates: dict[str, dict[str, str]]
_model_list_cache: dict[str, list[SelectOptionDict]] | None
def __init__(self) -> None:
"""Initialize the flow."""
@@ -42,33 +39,32 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
self._subentry_iter = None
self._current_entry_id = None
self._current_subentry_id = None
self._reload_pending = set()
self._pending_updates = {}
self._model_list_cache = None
async def async_step_init(
self, user_input: dict[str, str] | None = None
self, user_input: dict[str, str]
) -> data_entry_flow.FlowResult:
"""Handle the first step of a fix flow."""
previous_entry_id: str | None = None
if user_input is not None:
previous_entry_id = self._async_update_current_subentry(user_input)
self._clear_current_target()
"""Handle the steps of a fix flow."""
if user_input.get(CONF_CHAT_MODEL):
self._async_update_current_subentry(user_input)
target = await self._async_next_target()
next_entry_id = target[0].entry_id if target else None
if previous_entry_id and previous_entry_id != next_entry_id:
await self._async_apply_pending_updates(previous_entry_id)
if target is None:
await self._async_apply_all_pending_updates()
return self.async_create_entry(data={})
entry, subentry, model = target
client = entry.runtime_data
model_list = [
model_option
for model_option in await get_model_list(client)
if not model_option["value"].startswith(tuple(DEPRECATED_MODELS))
]
if self._model_list_cache is None:
self._model_list_cache = {}
if entry.entry_id in self._model_list_cache:
model_list = self._model_list_cache[entry.entry_id]
else:
client = entry.runtime_data
model_list = [
model_option
for model_option in await get_model_list(client)
if not model_option["value"].startswith(tuple(DEPRECATED_MODELS))
]
self._model_list_cache[entry.entry_id] = model_list
if "opus" in model:
suggested_model = "claude-opus-4-5"
@@ -124,6 +120,8 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
except StopIteration:
return None
# Verify that the entry/subentry still exists and the model is still
# deprecated. This may have changed since we started the repair flow.
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is None:
continue
@@ -132,9 +130,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
if subentry is None:
continue
model = self._pending_model(entry_id, subentry_id)
if model is None:
model = subentry.data.get(CONF_CHAT_MODEL)
model = subentry.data.get(CONF_CHAT_MODEL)
if not model or not model.startswith(tuple(DEPRECATED_MODELS)):
continue
@@ -142,36 +138,30 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
self._current_subentry_id = subentry_id
return entry, subentry, model
def _async_update_current_subentry(self, user_input: dict[str, str]) -> str | None:
def _async_update_current_subentry(self, user_input: dict[str, str]) -> None:
"""Update the currently selected subentry."""
if not self._current_entry_id or not self._current_subentry_id:
return None
entry = self.hass.config_entries.async_get_entry(self._current_entry_id)
if entry is None:
return None
subentry = entry.subentries.get(self._current_subentry_id)
if subentry is None:
return None
if (
self._current_entry_id is None
or self._current_subentry_id is None
or (
entry := self.hass.config_entries.async_get_entry(
self._current_entry_id
)
)
is None
or (subentry := entry.subentries.get(self._current_subentry_id)) is None
):
raise HomeAssistantError("Subentry not found")
updated_data = {
**subentry.data,
CONF_CHAT_MODEL: user_input[CONF_CHAT_MODEL],
}
if updated_data == subentry.data:
return entry.entry_id
self._queue_pending_update(
entry.entry_id,
subentry.subentry_id,
updated_data[CONF_CHAT_MODEL],
self.hass.config_entries.async_update_subentry(
entry,
subentry,
data=updated_data,
)
return entry.entry_id
def _clear_current_target(self) -> None:
"""Clear current target tracking."""
self._current_entry_id = None
self._current_subentry_id = None
def _format_subentry_type(self, subentry_type: str) -> str:
"""Return a user-friendly subentry type label."""
@@ -181,91 +171,6 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
return "AI task"
return subentry_type
def _queue_pending_update(
self, entry_id: str, subentry_id: str, model: str
) -> None:
"""Store a pending model update for a subentry."""
self._pending_updates.setdefault(entry_id, {})[subentry_id] = model
def _pending_model(self, entry_id: str, subentry_id: str) -> str | None:
"""Return a pending model update if one exists."""
return self._pending_updates.get(entry_id, {}).get(subentry_id)
def _mark_entry_for_reload(self, entry_id: str) -> None:
"""Prevent reload until repairs are complete for the entry."""
self._reload_pending.add(entry_id)
defer_reload_entries: set[str] = self.hass.data.setdefault(
DOMAIN, {}
).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
defer_reload_entries.add(entry_id)
async def _async_reload_entry(self, entry_id: str) -> None:
"""Reload an entry once all repairs are completed."""
if entry_id not in self._reload_pending:
return
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is not None and entry.state is not ConfigEntryState.LOADED:
self._clear_defer_reload(entry_id)
self._reload_pending.discard(entry_id)
return
if entry is not None:
await self.hass.config_entries.async_reload(entry_id)
self._clear_defer_reload(entry_id)
self._reload_pending.discard(entry_id)
def _clear_defer_reload(self, entry_id: str) -> None:
"""Remove entry from the deferred reload set."""
defer_reload_entries: set[str] = self.hass.data.setdefault(
DOMAIN, {}
).setdefault(DATA_REPAIR_DEFER_RELOAD, set())
defer_reload_entries.discard(entry_id)
async def _async_apply_pending_updates(self, entry_id: str) -> None:
"""Apply pending subentry updates for a single entry."""
updates = self._pending_updates.pop(entry_id, None)
if not updates:
return
entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is None or entry.state is not ConfigEntryState.LOADED:
return
changed = False
for subentry_id, model in updates.items():
subentry = entry.subentries.get(subentry_id)
if subentry is None:
continue
updated_data = {
**subentry.data,
CONF_CHAT_MODEL: model,
}
if updated_data == subentry.data:
continue
if not changed:
self._mark_entry_for_reload(entry_id)
changed = True
self.hass.config_entries.async_update_subentry(
entry,
subentry,
data=updated_data,
)
if not changed:
return
await self._async_reload_entry(entry_id)
async def _async_apply_all_pending_updates(self) -> None:
"""Apply all pending updates across entries."""
for entry_id in list(self._pending_updates):
await self._async_apply_pending_updates(entry_id)
async def async_create_fix_flow(
hass: HomeAssistant,

View File

@@ -363,8 +363,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def reload_service_handler(service_call: ServiceCall) -> None:
"""Remove all automations and load new ones from config."""
await async_get_blueprints(hass).async_reset_cache()
if (conf := await component.async_prepare_reload(skip_reset=True)) is None:
return
conf = await component.async_prepare_reload(skip_reset=True)
if automation_id := service_call.data.get(CONF_ID):
await _async_process_single_config(hass, conf, component, automation_id)
else:

View File

@@ -5,11 +5,10 @@ from __future__ import annotations
import logging
from typing import cast
from aiobotocore.client import AioBaseClient as S3Client
from aiobotocore.session import AioSession
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
@@ -21,9 +20,9 @@ from .const import (
DATA_BACKUP_AGENT_LISTENERS,
DOMAIN,
)
from .coordinator import S3ConfigEntry, S3DataUpdateCoordinator
type S3ConfigEntry = ConfigEntry[S3Client]
_PLATFORMS = (Platform.SENSOR,)
_LOGGER = logging.getLogger(__name__)
@@ -64,7 +63,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
translation_key="cannot_connect",
) from err
entry.runtime_data = client
coordinator = S3DataUpdateCoordinator(
hass,
entry=entry,
client=client,
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
def notify_backup_listeners() -> None:
for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []):
@@ -72,11 +77,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
entry.async_on_unload(entry.async_on_state_change(notify_backup_listeners))
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
"""Unload a config entry."""
client = entry.runtime_data
await client.__aexit__(None, None, None)
unload_ok = await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
if not unload_ok:
return False
coordinator = entry.runtime_data
await coordinator.client.__aexit__(None, None, None)
return True

View File

@@ -20,6 +20,7 @@ from homeassistant.core import HomeAssistant, callback
from . import S3ConfigEntry
from .const import CONF_BUCKET, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .helpers import async_list_backups_from_s3
_LOGGER = logging.getLogger(__name__)
CACHE_TTL = 300
@@ -93,7 +94,7 @@ class S3BackupAgent(BackupAgent):
def __init__(self, hass: HomeAssistant, entry: S3ConfigEntry) -> None:
"""Initialize the S3 agent."""
super().__init__()
self._client = entry.runtime_data
self._client = entry.runtime_data.client
self._bucket: str = entry.data[CONF_BUCKET]
self.name = entry.title
self.unique_id = entry.entry_id
@@ -316,35 +317,8 @@ class S3BackupAgent(BackupAgent):
if time() <= self._cache_expiration:
return self._backup_cache
backups = {}
paginator = self._client.get_paginator("list_objects_v2")
metadata_files: list[dict[str, Any]] = []
async for page in paginator.paginate(Bucket=self._bucket):
metadata_files.extend(
obj
for obj in page.get("Contents", [])
if obj["Key"].endswith(".metadata.json")
)
for metadata_file in metadata_files:
try:
# Download and parse metadata file
metadata_response = await self._client.get_object(
Bucket=self._bucket, Key=metadata_file["Key"]
)
metadata_content = await metadata_response["Body"].read()
metadata_json = json.loads(metadata_content)
except (BotoCoreError, json.JSONDecodeError) as err:
_LOGGER.warning(
"Failed to process metadata file %s: %s",
metadata_file["Key"],
err,
)
continue
backup = AgentBackup.from_dict(metadata_json)
backups[backup.backup_id] = backup
self._backup_cache = backups
backups_list = await async_list_backups_from_s3(self._client, self._bucket)
self._backup_cache = {b.backup_id: b for b in backups_list}
self._cache_expiration = time() + CACHE_TTL
return self._backup_cache

View File

@@ -0,0 +1,70 @@
"""DataUpdateCoordinator for AWS S3."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
import logging
from aiobotocore.client import AioBaseClient as S3Client
from botocore.exceptions import BotoCoreError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_BUCKET, DOMAIN
from .helpers import async_list_backups_from_s3
SCAN_INTERVAL = timedelta(hours=6)
type S3ConfigEntry = ConfigEntry[S3DataUpdateCoordinator]
_LOGGER = logging.getLogger(__name__)
@dataclass
class SensorData:
"""Class to represent sensor data."""
all_backups_size: int
class S3DataUpdateCoordinator(DataUpdateCoordinator[SensorData]):
"""Class to manage fetching AWS S3 data from single endpoint."""
config_entry: S3ConfigEntry
client: S3Client
def __init__(
self,
hass: HomeAssistant,
*,
entry: S3ConfigEntry,
client: S3Client,
) -> None:
"""Initialize AWS S3 data updater."""
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self.client = client
self._bucket: str = entry.data[CONF_BUCKET]
async def _async_update_data(self) -> SensorData:
"""Fetch data from AWS S3."""
try:
backups = await async_list_backups_from_s3(self.client, self._bucket)
except BotoCoreError as error:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="error_fetching_data",
) from error
all_backups_size = sum(b.size for b in backups)
return SensorData(
all_backups_size=all_backups_size,
)

View File

@@ -0,0 +1,33 @@
"""Define the AWS S3 entity."""
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_BUCKET, DOMAIN
from .coordinator import S3DataUpdateCoordinator
class S3Entity(CoordinatorEntity[S3DataUpdateCoordinator]):
"""Defines a base AWS S3 entity."""
_attr_has_entity_name = True
def __init__(
self, coordinator: S3DataUpdateCoordinator, description: EntityDescription
) -> None:
"""Initialize an AWS S3 entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}"
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this AWS S3 device."""
return DeviceInfo(
identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)},
name=f"Bucket {self.coordinator.config_entry.data[CONF_BUCKET]}",
manufacturer="AWS",
model="AWS S3",
entry_type=DeviceEntryType.SERVICE,
)

View File

@@ -0,0 +1,57 @@
"""Helpers for the AWS S3 integration."""
from __future__ import annotations
import json
import logging
from typing import Any
from aiobotocore.client import AioBaseClient as S3Client
from botocore.exceptions import BotoCoreError
from homeassistant.components.backup import AgentBackup
_LOGGER = logging.getLogger(__name__)
async def async_list_backups_from_s3(
client: S3Client,
bucket: str,
) -> list[AgentBackup]:
"""List backups from an S3 bucket by reading metadata files."""
paginator = client.get_paginator("list_objects_v2")
metadata_files: list[dict[str, Any]] = []
async for page in paginator.paginate(Bucket=bucket):
metadata_files.extend(
obj
for obj in page.get("Contents", [])
if obj["Key"].endswith(".metadata.json")
)
backups: list[AgentBackup] = []
for metadata_file in metadata_files:
try:
metadata_response = await client.get_object(
Bucket=bucket, Key=metadata_file["Key"]
)
metadata_content = await metadata_response["Body"].read()
metadata_json = json.loads(metadata_content)
except (BotoCoreError, json.JSONDecodeError) as err:
_LOGGER.warning(
"Failed to process metadata file %s: %s",
metadata_file["Key"],
err,
)
continue
try:
backup = AgentBackup.from_dict(metadata_json)
except (KeyError, TypeError, ValueError) as err:
_LOGGER.warning(
"Failed to parse metadata in file %s: %s",
metadata_file["Key"],
err,
)
continue
backups.append(backup)
return backups

View File

@@ -3,9 +3,10 @@
"name": "AWS S3",
"codeowners": ["@tomasbedrich"],
"config_flow": true,
"dependencies": ["backup"],
"documentation": "https://www.home-assistant.io/integrations/aws_s3",
"integration_type": "service",
"iot_class": "cloud_push",
"iot_class": "cloud_polling",
"loggers": ["aiobotocore"],
"quality_scale": "bronze",
"requirements": ["aiobotocore==2.21.1"]

View File

@@ -3,9 +3,7 @@ rules:
action-setup:
status: exempt
comment: Integration does not register custom actions.
appropriate-polling:
status: exempt
comment: This integration does not poll.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
@@ -20,12 +18,8 @@ rules:
entity-event-setup:
status: exempt
comment: Entities of this integration does not explicitly subscribe to events.
entity-unique-id:
status: exempt
comment: This integration does not have entities.
has-entity-name:
status: exempt
comment: This integration does not have entities.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
@@ -40,21 +34,15 @@ rules:
status: exempt
comment: This integration does not have an options flow.
docs-installation-parameters: done
entity-unavailable:
status: exempt
comment: This integration does not have entities.
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates:
status: exempt
comment: This integration does not poll.
parallel-updates: done
reauthentication-flow: todo
test-coverage: done
# Gold
devices:
status: exempt
comment: This integration does not have entities.
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
@@ -62,15 +50,11 @@ rules:
discovery:
status: exempt
comment: S3 is a cloud service that is not discovered on the network.
docs-data-update:
status: exempt
comment: This integration does not poll.
docs-data-update: done
docs-examples:
status: exempt
comment: The integration extends core functionality and does not require examples.
docs-known-limitations:
status: exempt
comment: No known limitations.
docs-known-limitations: done
docs-supported-devices:
status: exempt
comment: This integration does not support physical devices.
@@ -81,19 +65,11 @@ rules:
docs-use-cases: done
dynamic-devices:
status: exempt
comment: This integration does not have devices.
entity-category:
status: exempt
comment: This integration does not have entities.
entity-device-class:
status: exempt
comment: This integration does not have entities.
entity-disabled-by-default:
status: exempt
comment: This integration does not have entities.
entity-translations:
status: exempt
comment: This integration does not have entities.
comment: This integration has a fixed set of devices.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations:
status: exempt
@@ -104,7 +80,7 @@ rules:
comment: There are no issues which can be repaired.
stale-devices:
status: exempt
comment: This integration does not have devices.
comment: This is a service type integration with a single device.
# Platinum
async-dependency: done

View File

@@ -0,0 +1,66 @@
"""Support for AWS S3 sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import EntityCategory, UnitOfInformation
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from .coordinator import S3ConfigEntry, SensorData
from .entity import S3Entity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class S3SensorEntityDescription(SensorEntityDescription):
"""Describes an AWS S3 sensor entity."""
value_fn: Callable[[SensorData], StateType]
SENSORS: tuple[S3SensorEntityDescription, ...] = (
S3SensorEntityDescription(
key="backups_size",
translation_key="backups_size",
native_unit_of_measurement=UnitOfInformation.BYTES,
suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES,
suggested_display_precision=0,
device_class=SensorDeviceClass.DATA_SIZE,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.all_backups_size,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: S3ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up AWS S3 sensor based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
S3SensorEntity(coordinator, description) for description in SENSORS
)
class S3SensorEntity(S3Entity, SensorEntity):
"""Defines an AWS S3 sensor entity."""
entity_description: S3SensorEntityDescription
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -27,10 +27,20 @@
}
}
},
"entity": {
"sensor": {
"backups_size": {
"name": "Total size of backups"
}
}
},
"exceptions": {
"cannot_connect": {
"message": "Cannot connect to endpoint"
},
"error_fetching_data": {
"message": "Error fetching data"
},
"invalid_bucket_name": {
"message": "Invalid bucket name"
},

View File

@@ -34,7 +34,7 @@ class BleBoxSwitchEntity(BleBoxEntity[blebox_uniapi.switch.Switch], SwitchEntity
_attr_device_class = SwitchDeviceClass.SWITCH
@property
def is_on(self):
def is_on(self) -> bool | None:
"""Return whether switch is on."""
return self._feature.is_on

View File

@@ -77,7 +77,7 @@ class ShutterContactSensor(SHCEntity, BinarySensorEntity):
)
@property
def is_on(self):
def is_on(self) -> bool:
"""Return the state of the sensor."""
return self._device.state == SHCShutterContact.ShutterContactService.State.OPEN
@@ -93,7 +93,7 @@ class BatterySensor(SHCEntity, BinarySensorEntity):
self._attr_unique_id = f"{device.serial}_battery"
@property
def is_on(self):
def is_on(self) -> bool:
"""Return the state of the sensor."""
return (
self._device.batterylevel != SHCBatteryDevice.BatteryLevelService.State.OK

View File

@@ -10,7 +10,7 @@ import logging
from brother import BrotherSensors
from homeassistant.components.sensor import (
DOMAIN as PLATFORM,
DOMAIN as SENSOR_DOMAIN,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
@@ -314,7 +314,7 @@ async def async_setup_entry(
entity_registry = er.async_get(hass)
old_unique_id = f"{coordinator.brother.serial.lower()}_b/w_counter"
if entity_id := entity_registry.async_get_entity_id(
PLATFORM, DOMAIN, old_unique_id
SENSOR_DOMAIN, DOMAIN, old_unique_id
):
new_unique_id = f"{coordinator.brother.serial.lower()}_bw_counter"
_LOGGER.debug(

View File

@@ -101,16 +101,16 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self.coordinator.data.state.current_temperature is None:
if (current_temp := self.coordinator.data.state.current_temperature) is None:
return None
return self.coordinator.data.state.current_temperature.value
return current_temp.value
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
if self.coordinator.data.state.target_temperature is None:
if (target_temp := self.coordinator.data.state.target_temperature) is None:
return None
return self.coordinator.data.state.target_temperature.value
return target_temp.value
@property
def _hvac_mode_value(self) -> int | str | None:

View File

@@ -1,7 +1,10 @@
"""DataUpdateCoordinator for the BSB-Lan integration."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING
from bsblan import (
BSBLAN,
@@ -14,7 +17,6 @@ from bsblan import (
State,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
@@ -22,6 +24,9 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DOMAIN, LOGGER, SCAN_INTERVAL_FAST, SCAN_INTERVAL_SLOW
if TYPE_CHECKING:
from . import BSBLanConfigEntry
# Filter lists for optimized API calls - only fetch parameters we actually use
# This significantly reduces response time (~0.2s per parameter saved)
STATE_INCLUDE = ["current_temperature", "target_temperature", "hvac_mode"]
@@ -54,12 +59,12 @@ class BSBLanSlowData:
class BSBLanCoordinator[T](DataUpdateCoordinator[T]):
"""Base BSB-Lan coordinator."""
config_entry: ConfigEntry
config_entry: BSBLanConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: BSBLanConfigEntry,
client: BSBLAN,
name: str,
update_interval: timedelta,
@@ -81,7 +86,7 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: BSBLanConfigEntry,
client: BSBLAN,
) -> None:
"""Initialize the BSB-Lan fast coordinator."""
@@ -126,7 +131,7 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: BSBLanConfigEntry,
client: BSBLAN,
) -> None:
"""Initialize the BSB-Lan slow coordinator."""

View File

@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bsblan"],
"requirements": ["python-bsblan==4.2.0"],
"requirements": ["python-bsblan==4.2.1"],
"zeroconf": [
{
"name": "bsb-lan*",

View File

@@ -81,58 +81,57 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
self._attr_available = True
# Set temperature limits based on device capabilities from slow coordinator
dhw_config = (
data.slow_coordinator.data.dhw_config
if data.slow_coordinator.data
else None
)
# For min_temp: Use reduced_setpoint from config data (slow polling)
if (
data.slow_coordinator.data
and data.slow_coordinator.data.dhw_config is not None
and data.slow_coordinator.data.dhw_config.reduced_setpoint is not None
and hasattr(data.slow_coordinator.data.dhw_config.reduced_setpoint, "value")
dhw_config is not None
and dhw_config.reduced_setpoint is not None
and dhw_config.reduced_setpoint.value is not None
):
self._attr_min_temp = float(
data.slow_coordinator.data.dhw_config.reduced_setpoint.value
)
self._attr_min_temp = dhw_config.reduced_setpoint.value
else:
self._attr_min_temp = 10.0 # Default minimum
# For max_temp: Use nominal_setpoint_max from config data (slow polling)
if (
data.slow_coordinator.data
and data.slow_coordinator.data.dhw_config is not None
and data.slow_coordinator.data.dhw_config.nominal_setpoint_max is not None
and hasattr(
data.slow_coordinator.data.dhw_config.nominal_setpoint_max, "value"
)
dhw_config is not None
and dhw_config.nominal_setpoint_max is not None
and dhw_config.nominal_setpoint_max.value is not None
):
self._attr_max_temp = float(
data.slow_coordinator.data.dhw_config.nominal_setpoint_max.value
)
self._attr_max_temp = dhw_config.nominal_setpoint_max.value
else:
self._attr_max_temp = 65.0 # Default maximum
@property
def current_operation(self) -> str | None:
"""Return current operation."""
if self.coordinator.data.dhw.operating_mode is None:
if (operating_mode := self.coordinator.data.dhw.operating_mode) is None:
return None
# The operating_mode.value is an integer (0=Off, 1=On, 2=Eco)
current_mode_value = self.coordinator.data.dhw.operating_mode.value
if isinstance(current_mode_value, int):
return BSBLAN_TO_HA_OPERATION_MODE.get(current_mode_value)
if isinstance(operating_mode.value, int):
return BSBLAN_TO_HA_OPERATION_MODE.get(operating_mode.value)
return None
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self.coordinator.data.dhw.dhw_actual_value_top_temperature is None:
if (
current_temp := self.coordinator.data.dhw.dhw_actual_value_top_temperature
) is None:
return None
return self.coordinator.data.dhw.dhw_actual_value_top_temperature.value
return current_temp.value
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
if self.coordinator.data.dhw.nominal_setpoint is None:
if (target_temp := self.coordinator.data.dhw.nominal_setpoint) is None:
return None
return self.coordinator.data.dhw.nominal_setpoint.value
return target_temp.value
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""

View File

@@ -16,7 +16,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONNECT_TIMEOUT, DOMAIN, STREAM_MAGIC_EXCEPTIONS
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.SELECT, Platform.SWITCH]
PLATFORMS: list[Platform] = [
Platform.MEDIA_PLAYER,
Platform.NUMBER,
Platform.SELECT,
Platform.SWITCH,
]
_LOGGER = logging.getLogger(__name__)

View File

@@ -1,5 +1,10 @@
{
"entity": {
"number": {
"room_correction_intensity": {
"default": "mdi:home-sound-out"
}
},
"select": {
"audio_output": {
"default": "mdi:audio-input-stereo-minijack"

View File

@@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["aiostreammagic"],
"quality_scale": "platinum",
"requirements": ["aiostreammagic==2.12.1"],
"requirements": ["aiostreammagic==2.13.0"],
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
}

View File

@@ -0,0 +1,88 @@
"""Support for Cambridge Audio number entities."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from aiostreammagic import StreamMagicClient
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import CambridgeAudioConfigEntry
from .entity import CambridgeAudioEntity, command
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class CambridgeAudioNumberEntityDescription(NumberEntityDescription):
"""Describes Cambridge Audio number entity."""
exists_fn: Callable[[StreamMagicClient], bool] = lambda _: True
value_fn: Callable[[StreamMagicClient], int]
set_value_fn: Callable[[StreamMagicClient, int], Awaitable[None]]
def room_correction_intensity(client: StreamMagicClient) -> int:
"""Get room correction intensity."""
if TYPE_CHECKING:
assert client.audio.tilt_eq is not None
return client.audio.tilt_eq.intensity
CONTROL_ENTITIES: tuple[CambridgeAudioNumberEntityDescription, ...] = (
CambridgeAudioNumberEntityDescription(
key="room_correction_intensity",
translation_key="room_correction_intensity",
entity_category=EntityCategory.CONFIG,
native_min_value=-15,
native_max_value=15,
native_step=1,
exists_fn=lambda client: client.audio.tilt_eq is not None,
value_fn=room_correction_intensity,
set_value_fn=lambda client, value: client.set_room_correction_intensity(value),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: CambridgeAudioConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Cambridge Audio number entities based on a config entry."""
client = entry.runtime_data
async_add_entities(
CambridgeAudioNumber(entry.runtime_data, description)
for description in CONTROL_ENTITIES
if description.exists_fn(client)
)
class CambridgeAudioNumber(CambridgeAudioEntity, NumberEntity):
"""Defines a Cambridge Audio number entity."""
entity_description: CambridgeAudioNumberEntityDescription
def __init__(
self,
client: StreamMagicClient,
description: CambridgeAudioNumberEntityDescription,
) -> None:
"""Initialize Cambridge Audio number entity."""
super().__init__(client)
self.entity_description = description
self._attr_unique_id = f"{client.info.unit_id}-{description.key}"
@property
def native_value(self) -> int | None:
"""Return the state of the number."""
return self.entity_description.value_fn(self.client)
@command
async def async_set_native_value(self, value: float) -> None:
"""Set the selected value."""
await self.entity_description.set_value_fn(self.client, int(value))

View File

@@ -35,6 +35,11 @@
}
},
"entity": {
"number": {
"room_correction_intensity": {
"name": "Room correction intensity"
}
},
"select": {
"audio_output": {
"name": "Audio output"

View File

@@ -27,7 +27,7 @@ from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from homeassistant.components.media_player import (
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
DOMAIN as DOMAIN_MP,
DOMAIN as MP_DOMAIN,
SERVICE_PLAY_MEDIA,
)
from homeassistant.components.stream import (
@@ -133,7 +133,7 @@ MIN_STREAM_INTERVAL: Final = 0.5 # seconds
CAMERA_SERVICE_SNAPSHOT: VolDictType = {vol.Required(ATTR_FILENAME): cv.template}
CAMERA_SERVICE_PLAY_STREAM: VolDictType = {
vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(DOMAIN_MP),
vol.Required(ATTR_MEDIA_PLAYER): cv.entities_domain(MP_DOMAIN),
vol.Optional(ATTR_FORMAT, default="hls"): vol.In(OUTPUT_FORMATS),
}
@@ -1044,7 +1044,7 @@ async def async_handle_play_stream_service(
url = f"{get_url(hass)}{url}"
await hass.services.async_call(
DOMAIN_MP,
MP_DOMAIN,
SERVICE_PLAY_MEDIA,
{
ATTR_ENTITY_ID: service_call.data[ATTR_MEDIA_PLAYER],

View File

@@ -34,20 +34,33 @@ CONTROL4_CATEGORY = "comfort"
# Control4 variable names
CONTROL4_HVAC_STATE = "HVAC_STATE"
CONTROL4_HVAC_MODE = "HVAC_MODE"
CONTROL4_CURRENT_TEMPERATURE = "TEMPERATURE_F"
CONTROL4_HUMIDITY = "HUMIDITY"
CONTROL4_COOL_SETPOINT = "COOL_SETPOINT_F"
CONTROL4_HEAT_SETPOINT = "HEAT_SETPOINT_F"
CONTROL4_SCALE = "SCALE" # "FAHRENHEIT" or "CELSIUS"
# Temperature variables - Fahrenheit
CONTROL4_CURRENT_TEMPERATURE_F = "TEMPERATURE_F"
CONTROL4_COOL_SETPOINT_F = "COOL_SETPOINT_F"
CONTROL4_HEAT_SETPOINT_F = "HEAT_SETPOINT_F"
# Temperature variables - Celsius
CONTROL4_CURRENT_TEMPERATURE_C = "TEMPERATURE_C"
CONTROL4_COOL_SETPOINT_C = "COOL_SETPOINT_C"
CONTROL4_HEAT_SETPOINT_C = "HEAT_SETPOINT_C"
CONTROL4_FAN_MODE = "FAN_MODE"
CONTROL4_FAN_MODES_LIST = "FAN_MODES_LIST"
VARIABLES_OF_INTEREST = {
CONTROL4_HVAC_STATE,
CONTROL4_HVAC_MODE,
CONTROL4_CURRENT_TEMPERATURE,
CONTROL4_HUMIDITY,
CONTROL4_COOL_SETPOINT,
CONTROL4_HEAT_SETPOINT,
CONTROL4_CURRENT_TEMPERATURE_F,
CONTROL4_CURRENT_TEMPERATURE_C,
CONTROL4_COOL_SETPOINT_F,
CONTROL4_HEAT_SETPOINT_F,
CONTROL4_COOL_SETPOINT_C,
CONTROL4_HEAT_SETPOINT_C,
CONTROL4_SCALE,
CONTROL4_FAN_MODE,
CONTROL4_FAN_MODES_LIST,
}
@@ -62,11 +75,12 @@ C4_TO_HA_HVAC_MODE = {
HA_TO_C4_HVAC_MODE = {v: k for k, v in C4_TO_HA_HVAC_MODE.items()}
# Map the five known Control4 HVAC states to Home Assistant HVAC actions
# Map Control4 HVAC states to Home Assistant HVAC actions
C4_TO_HA_HVAC_ACTION = {
"off": HVACAction.OFF,
"heat": HVACAction.HEATING,
"cool": HVACAction.COOLING,
"idle": HVACAction.IDLE,
"dry": HVACAction.DRYING,
"fan": HVACAction.FAN,
}
@@ -156,7 +170,6 @@ class Control4Climate(Control4Entity, ClimateEntity):
"""Control4 climate entity."""
_attr_has_entity_name = True
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
_attr_translation_key = "thermostat"
_attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL]
@@ -213,13 +226,45 @@ class Control4Climate(Control4Entity, ClimateEntity):
features |= ClimateEntityFeature.FAN_MODE
return features
@property
def temperature_unit(self) -> str:
"""Return the temperature unit based on the thermostat's SCALE setting."""
data = self._thermostat_data
if data is None:
return UnitOfTemperature.CELSIUS # Default per HA conventions
if data.get(CONTROL4_SCALE) == "FAHRENHEIT":
return UnitOfTemperature.FAHRENHEIT
return UnitOfTemperature.CELSIUS
@property
def _cool_setpoint(self) -> float | None:
"""Return the cooling setpoint from the appropriate variable."""
data = self._thermostat_data
if data is None:
return None
if self.temperature_unit == UnitOfTemperature.CELSIUS:
return data.get(CONTROL4_COOL_SETPOINT_C)
return data.get(CONTROL4_COOL_SETPOINT_F)
@property
def _heat_setpoint(self) -> float | None:
"""Return the heating setpoint from the appropriate variable."""
data = self._thermostat_data
if data is None:
return None
if self.temperature_unit == UnitOfTemperature.CELSIUS:
return data.get(CONTROL4_HEAT_SETPOINT_C)
return data.get(CONTROL4_HEAT_SETPOINT_F)
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
data = self._thermostat_data
if data is None:
return None
return data.get(CONTROL4_CURRENT_TEMPERATURE)
if self.temperature_unit == UnitOfTemperature.CELSIUS:
return data.get(CONTROL4_CURRENT_TEMPERATURE_C)
return data.get(CONTROL4_CURRENT_TEMPERATURE_F)
@property
def current_humidity(self) -> int | None:
@@ -248,8 +293,14 @@ class Control4Climate(Control4Entity, ClimateEntity):
c4_state = data.get(CONTROL4_HVAC_STATE)
if c4_state is None:
return None
# Convert state to lowercase for mapping
action = C4_TO_HA_HVAC_ACTION.get(str(c4_state).lower())
# Substring match for multi-stage systems that report
# e.g. "Stage 1 Heat", "Stage 2 Cool"
if action is None:
if "heat" in str(c4_state).lower():
action = HVACAction.HEATING
elif "cool" in str(c4_state).lower():
action = HVACAction.COOLING
if action is None:
_LOGGER.debug("Unknown HVAC state received from Control4: %s", c4_state)
return action
@@ -257,34 +308,25 @@ class Control4Climate(Control4Entity, ClimateEntity):
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
data = self._thermostat_data
if data is None:
return None
hvac_mode = self.hvac_mode
if hvac_mode == HVACMode.COOL:
return data.get(CONTROL4_COOL_SETPOINT)
return self._cool_setpoint
if hvac_mode == HVACMode.HEAT:
return data.get(CONTROL4_HEAT_SETPOINT)
return self._heat_setpoint
return None
@property
def target_temperature_high(self) -> float | None:
"""Return the high target temperature for auto mode."""
data = self._thermostat_data
if data is None:
return None
if self.hvac_mode == HVACMode.HEAT_COOL:
return data.get(CONTROL4_COOL_SETPOINT)
return self._cool_setpoint
return None
@property
def target_temperature_low(self) -> float | None:
"""Return the low target temperature for auto mode."""
data = self._thermostat_data
if data is None:
return None
if self.hvac_mode == HVACMode.HEAT_COOL:
return data.get(CONTROL4_HEAT_SETPOINT)
return self._heat_setpoint
return None
@property
@@ -326,15 +368,27 @@ class Control4Climate(Control4Entity, ClimateEntity):
# Handle temperature range for auto mode
if self.hvac_mode == HVACMode.HEAT_COOL:
if low_temp is not None:
await c4_climate.setHeatSetpointF(low_temp)
if self.temperature_unit == UnitOfTemperature.CELSIUS:
await c4_climate.setHeatSetpointC(low_temp)
else:
await c4_climate.setHeatSetpointF(low_temp)
if high_temp is not None:
await c4_climate.setCoolSetpointF(high_temp)
if self.temperature_unit == UnitOfTemperature.CELSIUS:
await c4_climate.setCoolSetpointC(high_temp)
else:
await c4_climate.setCoolSetpointF(high_temp)
# Handle single temperature setpoint
elif temp is not None:
if self.hvac_mode == HVACMode.COOL:
await c4_climate.setCoolSetpointF(temp)
if self.temperature_unit == UnitOfTemperature.CELSIUS:
await c4_climate.setCoolSetpointC(temp)
else:
await c4_climate.setCoolSetpointF(temp)
elif self.hvac_mode == HVACMode.HEAT:
await c4_climate.setHeatSetpointF(temp)
if self.temperature_unit == UnitOfTemperature.CELSIUS:
await c4_climate.setHeatSetpointC(temp)
else:
await c4_climate.setHeatSetpointF(temp)
await self.coordinator.async_request_refresh()

View File

@@ -189,7 +189,7 @@ class Control4Light(Control4Entity, LightEntity):
return C4Light(self.runtime_data.director, self._idx)
@property
def is_on(self):
def is_on(self) -> bool:
"""Return whether this light is on or off."""
if self._is_dimmer:
for var in CONTROL4_DIMMER_VARS:

View File

@@ -65,33 +65,18 @@ class CurrencylayerSensor(SensorEntity):
_attr_attribution = "Data provided by currencylayer.com"
_attr_icon = "mdi:currency"
def __init__(self, rest, base, quote):
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
"""Initialize the sensor."""
self.rest = rest
self._quote = quote
self._base = base
self._state = None
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._quote
@property
def name(self):
"""Return the name of the sensor."""
return self._base
@property
def native_value(self):
"""Return the state of the sensor."""
return self._state
self._attr_name = base
self._attr_native_unit_of_measurement = quote
self._key = f"{base}{quote}"
def update(self) -> None:
"""Update current date."""
self.rest.update()
if (value := self.rest.data) is not None:
self._state = round(value[f"{self._base}{self._quote}"], 4)
self._attr_native_value = round(value[self._key], 4)
class CurrencylayerData:

View File

@@ -2,9 +2,12 @@
from __future__ import annotations
from collections.abc import Sequence
import logging
from typing import Any
from pydaikin.daikin_base import Appliance
from homeassistant.components.climate import (
ATTR_FAN_MODE,
ATTR_HVAC_MODE,
@@ -21,6 +24,7 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
@@ -29,12 +33,19 @@ from .const import (
ATTR_STATE_OFF,
ATTR_STATE_ON,
ATTR_TARGET_TEMPERATURE,
DOMAIN,
ZONE_NAME_UNCONFIGURED,
)
from .coordinator import DaikinConfigEntry, DaikinCoordinator
from .entity import DaikinEntity
_LOGGER = logging.getLogger(__name__)
type DaikinZone = Sequence[str | int]
DAIKIN_ZONE_TEMP_HEAT = "lztemp_h"
DAIKIN_ZONE_TEMP_COOL = "lztemp_c"
HA_STATE_TO_DAIKIN = {
HVACMode.FAN_ONLY: "fan",
@@ -78,6 +89,70 @@ HA_ATTR_TO_DAIKIN = {
}
DAIKIN_ATTR_ADVANCED = "adv"
ZONE_TEMPERATURE_WINDOW = 2
def _zone_error(
translation_key: str, placeholders: dict[str, str] | None = None
) -> HomeAssistantError:
"""Return a Home Assistant error with Daikin translation info."""
return HomeAssistantError(
translation_domain=DOMAIN,
translation_key=translation_key,
translation_placeholders=placeholders,
)
def _zone_is_configured(zone: DaikinZone) -> bool:
"""Return True if the Daikin zone represents a configured zone."""
if not zone:
return False
return zone[0] != ZONE_NAME_UNCONFIGURED
def _zone_temperature_lists(device: Appliance) -> tuple[list[str], list[str]]:
"""Return the decoded zone temperature lists."""
try:
heating = device.represent(DAIKIN_ZONE_TEMP_HEAT)[1]
cooling = device.represent(DAIKIN_ZONE_TEMP_COOL)[1]
except AttributeError:
return ([], [])
return (list(heating or []), list(cooling or []))
def _supports_zone_temperature_control(device: Appliance) -> bool:
"""Return True if the device exposes zone temperature settings."""
zones = device.zones
if not zones:
return False
heating, cooling = _zone_temperature_lists(device)
return bool(
heating
and cooling
and len(heating) >= len(zones)
and len(cooling) >= len(zones)
)
def _system_target_temperature(device: Appliance) -> float | None:
"""Return the system target temperature when available."""
target = device.target_temperature
if target is None:
return None
try:
return float(target)
except TypeError, ValueError:
return None
def _zone_temperature_from_list(values: list[str], zone_id: int) -> float | None:
"""Return the parsed temperature for a zone from a Daikin list."""
if zone_id >= len(values):
return None
try:
return float(values[zone_id])
except TypeError, ValueError:
return None
async def async_setup_entry(
@@ -86,8 +161,16 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Daikin climate based on config_entry."""
daikin_api = entry.runtime_data
async_add_entities([DaikinClimate(daikin_api)])
coordinator = entry.runtime_data
entities: list[ClimateEntity] = [DaikinClimate(coordinator)]
if _supports_zone_temperature_control(coordinator.device):
zones = coordinator.device.zones or []
entities.extend(
DaikinZoneClimate(coordinator, zone_id)
for zone_id, zone in enumerate(zones)
if _zone_is_configured(zone)
)
async_add_entities(entities)
def format_target_temperature(target_temperature: float) -> str:
@@ -284,3 +367,130 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
{HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVACMode.OFF]}
)
await self.coordinator.async_refresh()
class DaikinZoneClimate(DaikinEntity, ClimateEntity):
"""Representation of a Daikin zone temperature controller."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_has_entity_name = True
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_target_temperature_step = 1
def __init__(self, coordinator: DaikinCoordinator, zone_id: int) -> None:
"""Initialize the zone climate entity."""
super().__init__(coordinator)
self._zone_id = zone_id
self._attr_unique_id = f"{self.device.mac}-zone{zone_id}-temperature"
zone_name = self.device.zones[self._zone_id][0]
self._attr_name = f"{zone_name} temperature"
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the hvac modes (mirrors the main unit)."""
return [self.hvac_mode]
@property
def hvac_mode(self) -> HVACMode:
"""Return the current HVAC mode."""
daikin_mode = self.device.represent(HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE])[1]
return DAIKIN_TO_HA_STATE.get(daikin_mode, HVACMode.HEAT_COOL)
@property
def hvac_action(self) -> HVACAction | None:
"""Return the current HVAC action."""
return HA_STATE_TO_CURRENT_HVAC.get(self.hvac_mode)
@property
def target_temperature(self) -> float | None:
"""Return the zone target temperature for the active mode."""
heating, cooling = _zone_temperature_lists(self.device)
mode = self.hvac_mode
if mode == HVACMode.HEAT:
return _zone_temperature_from_list(heating, self._zone_id)
if mode == HVACMode.COOL:
return _zone_temperature_from_list(cooling, self._zone_id)
return None
@property
def min_temp(self) -> float:
"""Return the minimum selectable temperature."""
target = _system_target_temperature(self.device)
if target is None:
return super().min_temp
return target - ZONE_TEMPERATURE_WINDOW
@property
def max_temp(self) -> float:
"""Return the maximum selectable temperature."""
target = _system_target_temperature(self.device)
if target is None:
return super().max_temp
return target + ZONE_TEMPERATURE_WINDOW
@property
def available(self) -> bool:
"""Return if the entity is available."""
return (
super().available
and _supports_zone_temperature_control(self.device)
and _system_target_temperature(self.device) is not None
)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return additional metadata."""
return {"zone_id": self._zone_id}
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the zone temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="zone_temperature_missing",
)
zones = self.device.zones
if not zones or not _supports_zone_temperature_control(self.device):
raise _zone_error("zone_parameters_unavailable")
try:
zone = zones[self._zone_id]
except (IndexError, TypeError) as err:
raise _zone_error(
"zone_missing",
{
"zone_id": str(self._zone_id),
"max_zone": str(len(zones) - 1),
},
) from err
if not _zone_is_configured(zone):
raise _zone_error("zone_inactive", {"zone_id": str(self._zone_id)})
temperature_value = float(temperature)
target = _system_target_temperature(self.device)
if target is None:
raise _zone_error("zone_parameters_unavailable")
mode = self.hvac_mode
if mode == HVACMode.HEAT:
zone_key = DAIKIN_ZONE_TEMP_HEAT
elif mode == HVACMode.COOL:
zone_key = DAIKIN_ZONE_TEMP_COOL
else:
raise _zone_error("zone_hvac_mode_unsupported")
zone_value = str(round(temperature_value))
try:
await self.device.set_zone(self._zone_id, zone_key, zone_value)
except (AttributeError, KeyError, NotImplementedError, TypeError) as err:
raise _zone_error("zone_set_failed") from err
await self.coordinator.async_request_refresh()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Disallow changing HVAC mode via zone climate."""
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="zone_hvac_read_only",
)

View File

@@ -24,4 +24,6 @@ ATTR_STATE_OFF = "off"
KEY_MAC = "mac"
KEY_IP = "ip"
ZONE_NAME_UNCONFIGURED = "-"
TIMEOUT_SEC = 120

View File

@@ -57,5 +57,28 @@
"name": "Power"
}
}
},
"exceptions": {
"zone_hvac_mode_unsupported": {
"message": "Zone temperature can only be changed when the main climate mode is heat or cool."
},
"zone_hvac_read_only": {
"message": "Zone HVAC mode is controlled by the main climate entity."
},
"zone_inactive": {
"message": "Zone {zone_id} is not active. Enable the zone on your Daikin device first."
},
"zone_missing": {
"message": "Zone {zone_id} does not exist. Available zones are 0-{max_zone}."
},
"zone_parameters_unavailable": {
"message": "This device does not expose the required zone temperature parameters."
},
"zone_set_failed": {
"message": "Failed to set zone temperature. The device may not support this operation."
},
"zone_temperature_missing": {
"message": "Provide a temperature value when adjusting a zone."
}
}
}

View File

@@ -8,6 +8,7 @@ from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import ZONE_NAME_UNCONFIGURED
from .coordinator import DaikinConfigEntry, DaikinCoordinator
from .entity import DaikinEntity
@@ -28,7 +29,7 @@ async def async_setup_entry(
switches.extend(
DaikinZoneSwitch(daikin_api, zone_id)
for zone_id, zone in enumerate(zones)
if zone[0] != "-"
if zone[0] != ZONE_NAME_UNCONFIGURED
)
if daikin_api.device.support_advanced_modes:
# It isn't possible to find out from the API responses if a specific

View File

@@ -59,21 +59,10 @@ class DanfossAir(SwitchEntity):
def __init__(self, data, name, state_command, on_command, off_command):
"""Initialize the switch."""
self._data = data
self._name = name
self._attr_name = name
self._state_command = state_command
self._on_command = on_command
self._off_command = off_command
self._state = None
@property
def name(self):
"""Return the name of the switch."""
return self._name
@property
def is_on(self):
"""Return true if switch is on."""
return self._state
def turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
@@ -89,6 +78,6 @@ class DanfossAir(SwitchEntity):
"""Update the switch's state."""
self._data.update()
self._state = self._data.get_value(self._state_command)
if self._state is None:
self._attr_is_on = self._data.get_value(self._state_command)
if self._attr_is_on is None:
_LOGGER.debug("Could not get data for %s", self._state_command)

View File

@@ -137,7 +137,7 @@ class DecoraWifiLight(LightEntity):
return int(self._switch.brightness * 255 / 100)
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if switch is on."""
return self._switch.power == "ON"

View File

@@ -7,6 +7,7 @@ from typing import Any
from homeassistant.components.vacuum import (
ATTR_CLEANED_AREA,
Segment,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
@@ -14,8 +15,11 @@ from homeassistant.components.vacuum import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import event
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN
SUPPORT_MINIMAL_SERVICES = VacuumEntityFeature.TURN_ON | VacuumEntityFeature.TURN_OFF
SUPPORT_BASIC_SERVICES = (
@@ -45,9 +49,17 @@ SUPPORT_ALL_SERVICES = (
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.MAP
| VacuumEntityFeature.CLEAN_SPOT
| VacuumEntityFeature.CLEAN_AREA
)
FAN_SPEEDS = ["min", "medium", "high", "max"]
DEMO_SEGMENTS = [
Segment(id="living_room", name="Living room"),
Segment(id="kitchen", name="Kitchen"),
Segment(id="bedroom_1", name="Master bedroom", group="Bedrooms"),
Segment(id="bedroom_2", name="Guest bedroom", group="Bedrooms"),
Segment(id="bathroom", name="Bathroom"),
]
DEMO_VACUUM_COMPLETE = "Demo vacuum 0 ground floor"
DEMO_VACUUM_MOST = "Demo vacuum 1 first floor"
DEMO_VACUUM_BASIC = "Demo vacuum 2 second floor"
@@ -63,11 +75,11 @@ async def async_setup_entry(
"""Set up the Demo config entry."""
async_add_entities(
[
StateDemoVacuum(DEMO_VACUUM_COMPLETE, SUPPORT_ALL_SERVICES),
StateDemoVacuum(DEMO_VACUUM_MOST, SUPPORT_MOST_SERVICES),
StateDemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES),
StateDemoVacuum(DEMO_VACUUM_MINIMAL, SUPPORT_MINIMAL_SERVICES),
StateDemoVacuum(DEMO_VACUUM_NONE, VacuumEntityFeature(0)),
StateDemoVacuum("vacuum_1", DEMO_VACUUM_COMPLETE, SUPPORT_ALL_SERVICES),
StateDemoVacuum("vacuum_2", DEMO_VACUUM_MOST, SUPPORT_MOST_SERVICES),
StateDemoVacuum("vacuum_3", DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES),
StateDemoVacuum("vacuum_4", DEMO_VACUUM_MINIMAL, SUPPORT_MINIMAL_SERVICES),
StateDemoVacuum("vacuum_5", DEMO_VACUUM_NONE, VacuumEntityFeature(0)),
]
)
@@ -75,13 +87,21 @@ async def async_setup_entry(
class StateDemoVacuum(StateVacuumEntity):
"""Representation of a demo vacuum supporting states."""
_attr_has_entity_name = True
_attr_name = None
_attr_should_poll = False
_attr_translation_key = "model_s"
def __init__(self, name: str, supported_features: VacuumEntityFeature) -> None:
def __init__(
self, unique_id: str, name: str, supported_features: VacuumEntityFeature
) -> None:
"""Initialize the vacuum."""
self._attr_name = name
self._attr_unique_id = unique_id
self._attr_supported_features = supported_features
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=name,
)
self._attr_activity = VacuumActivity.DOCKED
self._fan_speed = FAN_SPEEDS[1]
self._cleaned_area: float = 0
@@ -163,6 +183,16 @@ class StateDemoVacuum(StateVacuumEntity):
self._attr_activity = VacuumActivity.IDLE
self.async_write_ha_state()
async def async_get_segments(self) -> list[Segment]:
"""Get the list of segments."""
return DEMO_SEGMENTS
async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
"""Clean the specified segments."""
self._attr_activity = VacuumActivity.CLEANING
self._cleaned_area += len(segment_ids) * 0.7
self.async_write_ha_state()
def __set_state_to_dock(self, _: datetime) -> None:
self._attr_activity = VacuumActivity.DOCKED
self.schedule_update_ha_state()

View File

@@ -10,13 +10,16 @@ import voluptuous as vol
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
DEVICE_CLASS_UNITS,
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
RestoreSensor,
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
CONF_SOURCE,
@@ -83,6 +86,17 @@ UNIT_TIME = {
UnitOfTime.DAYS: 24 * 60 * 60,
}
DERIVED_CLASS = {
SensorDeviceClass.ENERGY: SensorDeviceClass.POWER,
SensorDeviceClass.ENERGY_STORAGE: SensorDeviceClass.POWER,
SensorDeviceClass.DATA_SIZE: SensorDeviceClass.DATA_RATE,
SensorDeviceClass.DISTANCE: SensorDeviceClass.SPEED,
SensorDeviceClass.WATER: SensorDeviceClass.VOLUME_FLOW_RATE,
SensorDeviceClass.GAS: SensorDeviceClass.VOLUME_FLOW_RATE,
SensorDeviceClass.VOLUME: SensorDeviceClass.VOLUME_FLOW_RATE,
SensorDeviceClass.VOLUME_STORAGE: SensorDeviceClass.VOLUME_FLOW_RATE,
}
DEFAULT_ROUND = 3
DEFAULT_TIME_WINDOW = 0
@@ -203,10 +217,11 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self._attr_name = name if name is not None else f"{source_entity} derivative"
self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity}
self._unit_template: str | None = None
self._string_unit_prefix: str | None = None
self._string_unit_time: str | None = None
if unit_of_measurement is None:
final_unit_prefix = "" if unit_prefix is None else unit_prefix
self._unit_template = f"{final_unit_prefix}{{}}/{unit_time}"
self._string_unit_prefix = "" if unit_prefix is None else unit_prefix
self._string_unit_time = unit_time
# we postpone the definition of unit_of_measurement to later
self._attr_native_unit_of_measurement = None
else:
@@ -225,12 +240,40 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
)
def _derive_and_set_attributes_from_state(self, source_state: State | None) -> None:
if self._unit_template and source_state:
if not source_state:
return
source_class_raw = source_state.attributes.get(ATTR_DEVICE_CLASS)
source_class: SensorDeviceClass | None = None
if isinstance(source_class_raw, str):
try:
source_class = SensorDeviceClass(source_class_raw)
except ValueError:
source_class = None
if self._string_unit_prefix is not None and self._string_unit_time is not None:
original_unit = self._attr_native_unit_of_measurement
source_unit = source_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._attr_native_unit_of_measurement = self._unit_template.format(
"" if source_unit is None else source_unit
)
if (
(
source_class
in (SensorDeviceClass.ENERGY, SensorDeviceClass.ENERGY_STORAGE)
)
and self._string_unit_time == UnitOfTime.HOURS
and source_unit
and source_unit.endswith("Wh")
):
self._attr_native_unit_of_measurement = (
f"{self._string_unit_prefix}{source_unit[:-1]}"
)
else:
unit_template = (
f"{self._string_unit_prefix}{{}}/{self._string_unit_time}"
)
self._attr_native_unit_of_measurement = unit_template.format(
"" if source_unit is None else source_unit
)
if original_unit != self._attr_native_unit_of_measurement:
_LOGGER.debug(
"%s: Derivative sensor switched UoM from %s to %s, resetting state to 0",
@@ -241,6 +284,16 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self._state_list = []
self._attr_native_value = round(Decimal(0), self._round_digits)
self._attr_device_class = None
if source_class:
derived_class = DERIVED_CLASS.get(source_class)
if (
derived_class
and self._attr_native_unit_of_measurement
in DEVICE_CLASS_UNITS[derived_class]
):
self._attr_device_class = derived_class
def _calc_derivative_from_state_list(self, current_time: datetime) -> Decimal:
def calculate_weight(start: datetime, end: datetime, now: datetime) -> float:
window_start = now - timedelta(seconds=self._time_window)
@@ -309,6 +362,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
except InvalidOperation, TypeError:
self._attr_native_value = None
last_state = await self.async_get_last_state()
if last_state:
self._attr_device_class = last_state.attributes.get(ATTR_DEVICE_CLASS)
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()

View File

@@ -7,17 +7,17 @@ import logging
import voluptuous as vol
from homeassistant.components.device_tracker import (
DOMAIN as DOMAIN_DEVICE_TRACKER,
DOMAIN as DEVICE_TRACKER_DOMAIN,
is_on as device_tracker_is_on,
)
from homeassistant.components.group import get_entity_ids as group_get_entity_ids
from homeassistant.components.light import (
ATTR_PROFILE,
ATTR_TRANSITION,
DOMAIN as DOMAIN_LIGHT,
DOMAIN as LIGHT_DOMAIN,
is_on as light_is_on,
)
from homeassistant.components.person import DOMAIN as DOMAIN_PERSON
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
EVENT_HOMEASSISTANT_START,
@@ -97,13 +97,13 @@ async def activate_automation( # noqa: C901
logger = logging.getLogger(__name__)
if device_group is None:
device_entity_ids = hass.states.async_entity_ids(DOMAIN_DEVICE_TRACKER)
device_entity_ids = hass.states.async_entity_ids(DEVICE_TRACKER_DOMAIN)
else:
device_entity_ids = group_get_entity_ids(
hass, device_group, DOMAIN_DEVICE_TRACKER
hass, device_group, DEVICE_TRACKER_DOMAIN
)
device_entity_ids.extend(
group_get_entity_ids(hass, device_group, DOMAIN_PERSON)
group_get_entity_ids(hass, device_group, PERSON_DOMAIN)
)
if not device_entity_ids:
@@ -112,9 +112,9 @@ async def activate_automation( # noqa: C901
# Get the light IDs from the specified group
if light_group is None:
light_ids = hass.states.async_entity_ids(DOMAIN_LIGHT)
light_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
else:
light_ids = group_get_entity_ids(hass, light_group, DOMAIN_LIGHT)
light_ids = group_get_entity_ids(hass, light_group, LIGHT_DOMAIN)
if not light_ids:
logger.error("No lights found to turn on")
@@ -147,7 +147,7 @@ async def activate_automation( # noqa: C901
if not anyone_home() or light_is_on(hass, light_id):
return
await hass.services.async_call(
DOMAIN_LIGHT,
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: light_id,
@@ -222,7 +222,7 @@ async def activate_automation( # noqa: C901
logger.info("Home coming event for %s. Turning lights on", entity)
hass.async_create_task(
hass.services.async_call(
DOMAIN_LIGHT,
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: light_ids, ATTR_PROFILE: light_profile},
)
@@ -241,7 +241,7 @@ async def activate_automation( # noqa: C901
if now > start_point + index * LIGHT_TRANSITION_TIME:
hass.async_create_task(
hass.services.async_call(
DOMAIN_LIGHT, SERVICE_TURN_ON, {ATTR_ENTITY_ID: light_id}
LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: light_id}
)
)
@@ -273,7 +273,7 @@ async def activate_automation( # noqa: C901
logger.info("Everyone has left but there are lights on. Turning them off")
hass.async_create_task(
hass.services.async_call(
DOMAIN_LIGHT, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: light_ids}
LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: light_ids}
)
)

View File

@@ -8,7 +8,7 @@ from typing import Final
import voluptuous as vol
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.components.zone import DOMAIN as DOMAIN_ZONE, trigger as zone
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN, trigger as zone
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_DOMAIN,
@@ -31,7 +31,7 @@ TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
vol.Required(CONF_ZONE): cv.entity_domain(DOMAIN_ZONE),
vol.Required(CONF_ZONE): cv.entity_domain(ZONE_DOMAIN),
}
)
@@ -83,7 +83,7 @@ async def async_attach_trigger(
event = zone.EVENT_LEAVE
zone_config = {
CONF_PLATFORM: DOMAIN_ZONE,
CONF_PLATFORM: ZONE_DOMAIN,
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
CONF_ZONE: config[CONF_ZONE],
CONF_EVENT: event,
@@ -100,7 +100,7 @@ async def async_get_trigger_capabilities(
"""List trigger capabilities."""
zones = {
ent.entity_id: ent.name
for ent in sorted(hass.states.async_all(DOMAIN_ZONE), key=attrgetter("name"))
for ent in sorted(hass.states.async_all(ZONE_DOMAIN), key=attrgetter("name"))
}
return {
"extra_fields": vol.Schema(

View File

@@ -2,6 +2,7 @@
"config": {
"abort": {
"cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]",
"reconfigure_successful": "**Reconfiguration was successful**\n\nGo to the [webhook service of Dialogflow]({dialogflow_url}) and update the webhook with following settings:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details.",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
},
@@ -9,6 +10,10 @@
"default": "To send events to Home Assistant, you will need to set up the [webhook service of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details."
},
"step": {
"reconfigure": {
"description": "Are you sure you want to reconfigure Dialogflow?",
"title": "Reconfigure Dialogflow webhook"
},
"user": {
"description": "Are you sure you want to set up Dialogflow?",
"title": "Set up the Dialogflow webhook"

View File

@@ -117,7 +117,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity):
self._attr_assumed_state = self._is_recorded
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device specific state attributes."""
if self._is_standby:
return {}

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
import random
from typing import Any
import discogs_client
import voluptuous as vol
@@ -118,7 +119,7 @@ class DiscogsSensor(SensorEntity):
self._attr_name = f"{name} {description.name}"
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the device state attributes of the sensor."""
if self._attr_native_value is None or self._attrs is None:
return None

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
import re
from typing import Any
import voluptuous as vol
@@ -138,6 +139,6 @@ class DovadoSensor(SensorEntity):
self._attr_native_value = self._compute_state()
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return {k: v for k, v in self._data.state.items() if k not in ["date", "time"]}

View File

@@ -11,8 +11,7 @@ ATTR_FILENAME = "filename"
ATTR_SUBDIR = "subdir"
ATTR_URL = "url"
ATTR_OVERWRITE = "overwrite"
CONF_DOWNLOAD_DIR = "download_dir"
ATTR_HEADERS = "headers"
DOWNLOAD_FAILED_EVENT = "download_failed"
DOWNLOAD_COMPLETED_EVENT = "download_completed"

View File

@@ -19,6 +19,7 @@ from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
from .const import (
_LOGGER,
ATTR_FILENAME,
ATTR_HEADERS,
ATTR_OVERWRITE,
ATTR_SUBDIR,
ATTR_URL,
@@ -39,6 +40,7 @@ def download_file(service: ServiceCall) -> None:
subdir: str | None = service.data.get(ATTR_SUBDIR)
target_filename: str | None = service.data.get(ATTR_FILENAME)
overwrite: bool = service.data[ATTR_OVERWRITE]
headers: dict[str, str] = service.data[ATTR_HEADERS]
if subdir:
# Check the path
@@ -62,7 +64,7 @@ def download_file(service: ServiceCall) -> None:
final_path = None
filename = target_filename
try:
req = requests.get(url, stream=True, timeout=10)
req = requests.get(url, stream=True, headers=headers, timeout=10)
if req.status_code != HTTPStatus.OK:
_LOGGER.warning(
@@ -162,6 +164,9 @@ def async_setup_services(hass: HomeAssistant) -> None:
vol.Optional(ATTR_SUBDIR): cv.string,
vol.Required(ATTR_URL): cv.url,
vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean,
vol.Optional(ATTR_HEADERS, default=dict): vol.Schema(
{cv.string: cv.string}
),
}
),
)

View File

@@ -17,3 +17,9 @@ download_file:
default: false
selector:
boolean:
headers:
default: {}
example:
Accept: application/json
selector:
object:

View File

@@ -28,6 +28,10 @@
"description": "Custom name for the downloaded file.",
"name": "Filename"
},
"headers": {
"description": "Additional custom HTTP headers.",
"name": "Headers"
},
"overwrite": {
"description": "Overwrite file if it exists.",
"name": "Overwrite"

View File

@@ -38,3 +38,18 @@ def deprecate_yaml_issue(hass: HomeAssistant, *, import_success: bool) -> None:
"url": "/config/integrations/dashboard/add?domain=duckdns"
},
)
def action_called_without_config_entry(hass: HomeAssistant) -> None:
"""Deprecate the use of action without config entry."""
async_create_issue(
hass,
DOMAIN,
"deprecated_call_without_config_entry",
breaks_in_ha_version="2026.9.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_call_without_config_entry",
)

View File

@@ -15,6 +15,7 @@ from homeassistant.helpers.selector import ConfigEntrySelector
from .const import ATTR_CONFIG_ENTRY, ATTR_TXT, DOMAIN, SERVICE_SET_TXT
from .coordinator import DuckDnsConfigEntry
from .helpers import update_duckdns
from .issue import action_called_without_config_entry
SERVICE_TXT_SCHEMA = vol.Schema(
{
@@ -42,6 +43,7 @@ def get_config_entry(
"""Return config entry or raise if not found or not loaded."""
if entry_id is None:
action_called_without_config_entry(hass)
if len(entries := hass.config_entries.async_entries(DOMAIN)) != 1:
raise ServiceValidationError(
translation_domain=DOMAIN,

View File

@@ -16,7 +16,7 @@
"data_description": {
"access_token": "[%key:component::duckdns::config::step::user::data_description::access_token%]"
},
"title": "Re-configure {name}"
"title": "Reconfigure {name}"
},
"user": {
"data": {
@@ -46,6 +46,10 @@
}
},
"issues": {
"deprecated_call_without_config_entry": {
"description": "Calling the `duckdns.set_txt` action without specifying a config entry is deprecated.\n\nThe `config_entry_id` field will be required in a future release.\n\nPlease update your automations and scripts to include the `config_entry_id` parameter.",
"title": "Detected deprecated use of action without config entry"
},
"deprecated_yaml_import_issue_error": {
"description": "Configuring Duck DNS using YAML is being removed but there was an error when trying to import the YAML configuration.\n\nEnsure the YAML configuration is correct and restart Home Assistant to try again or remove the Duck DNS YAML configuration from your `configuration.yaml` file and continue to [set up the integration]({url}) manually.",
"title": "The Duck DNS YAML configuration import failed"

View File

@@ -28,7 +28,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from . import EcobeeConfigEntry
from . import EcobeeConfigEntry, EcobeeData
from .const import (
DOMAIN,
ECOBEE_MODEL_TO_NAME,
@@ -64,7 +64,7 @@ class EcobeeWeather(WeatherEntity):
_attr_name = None
_attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
def __init__(self, data, name, index):
def __init__(self, data: EcobeeData, name: str, index: int) -> None:
"""Initialize the Ecobee weather platform."""
self.data = data
self._name = name
@@ -99,7 +99,7 @@ class EcobeeWeather(WeatherEntity):
)
@property
def condition(self):
def condition(self) -> str | None:
"""Return the current condition."""
try:
return ECOBEE_WEATHER_SYMBOL_TO_HASS[self.get_forecast(0, "weatherSymbol")]
@@ -107,7 +107,7 @@ class EcobeeWeather(WeatherEntity):
return None
@property
def native_temperature(self):
def native_temperature(self) -> float | None:
"""Return the temperature."""
try:
return float(self.get_forecast(0, "temperature")) / 10
@@ -115,7 +115,7 @@ class EcobeeWeather(WeatherEntity):
return None
@property
def native_pressure(self):
def native_pressure(self) -> float | None:
"""Return the pressure."""
try:
pressure = self.get_forecast(0, "pressure")
@@ -124,7 +124,7 @@ class EcobeeWeather(WeatherEntity):
return None
@property
def humidity(self):
def humidity(self) -> float | None:
"""Return the humidity."""
try:
return int(self.get_forecast(0, "relativeHumidity"))
@@ -132,7 +132,7 @@ class EcobeeWeather(WeatherEntity):
return None
@property
def native_visibility(self):
def native_visibility(self) -> float | None:
"""Return the visibility."""
try:
return int(self.get_forecast(0, "visibility"))
@@ -140,7 +140,7 @@ class EcobeeWeather(WeatherEntity):
return None
@property
def native_wind_speed(self):
def native_wind_speed(self) -> float | None:
"""Return the wind speed."""
try:
return int(self.get_forecast(0, "windSpeed"))
@@ -148,7 +148,7 @@ class EcobeeWeather(WeatherEntity):
return None
@property
def wind_bearing(self):
def wind_bearing(self) -> float | None:
"""Return the wind direction."""
try:
return int(self.get_forecast(0, "windBearing"))
@@ -156,7 +156,7 @@ class EcobeeWeather(WeatherEntity):
return None
@property
def attribution(self):
def attribution(self) -> str | None:
"""Return the attribution."""
if not self.weather:
return None
@@ -167,7 +167,7 @@ class EcobeeWeather(WeatherEntity):
def _forecast(self) -> list[Forecast] | None:
"""Return the forecast array."""
if "forecasts" not in self.weather:
if not self.weather or "forecasts" not in self.weather:
return None
forecasts: list[Forecast] = []

View File

@@ -74,6 +74,6 @@ class EcoNetBinarySensor(EcoNetEntity, BinarySensorEntity):
)
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return getattr(self._econet, self.entity_description.key)

View File

@@ -136,12 +136,12 @@ class EcoNetWaterHeater(EcoNetEntity[WaterHeater], WaterHeaterEntity):
return self.water_heater.set_point
@property
def min_temp(self):
def min_temp(self) -> float:
"""Return the minimum temperature."""
return self.water_heater.set_point_limits[0]
@property
def max_temp(self):
def max_temp(self) -> float:
"""Return the maximum temperature."""
return self.water_heater.set_point_limits[1]

View File

@@ -38,12 +38,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: EcovacsConfigEntry) -> bool:
"""Set up this integration using UI."""
controller = EcovacsController(hass, entry.data)
entry.async_on_unload(controller.teardown)
await controller.initialize()
async def on_unload() -> None:
await controller.teardown()
entry.async_on_unload(on_unload)
entry.runtime_data = controller
async def _async_wait_connect(device: VacBot) -> None:

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from functools import partial
import logging
@@ -80,11 +81,22 @@ class EcovacsController:
try:
devices = await self._api_client.get_devices()
credentials = await self._authenticator.authenticate()
for device_info in devices.mqtt:
device = Device(device_info, self._authenticator)
if devices.mqtt:
mqtt = await self._get_mqtt_client()
await device.initialize(mqtt)
self._devices.append(device)
mqtt_devices = [
Device(info, self._authenticator) for info in devices.mqtt
]
async with asyncio.TaskGroup() as tg:
async def _init(device: Device) -> None:
"""Initialize MQTT device."""
await device.initialize(mqtt)
self._devices.append(device)
for device in mqtt_devices:
tg.create_task(_init(device))
for device_config in devices.xmpp:
bot = VacBot(
credentials.user_id,

View File

@@ -53,25 +53,9 @@ class SmartPlugSwitch(SwitchEntity):
def __init__(self, smartplug, name):
"""Initialize the switch."""
self.smartplug = smartplug
self._name = name
self._state = False
self._attr_name = name
self._attr_is_on = False
self._info = None
self._mac = None
@property
def unique_id(self):
"""Return the device's MAC address."""
return self._mac
@property
def name(self):
"""Return the name of the Smart Plug, if any."""
return self._name
@property
def is_on(self):
"""Return true if switch is on."""
return self._state
def turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
@@ -85,6 +69,6 @@ class SmartPlugSwitch(SwitchEntity):
"""Update edimax switch."""
if not self._info:
self._info = self.smartplug.info
self._mac = self._info["mac"]
self._attr_unique_id = self._info["mac"]
self._state = self.smartplug.state == "ON"
self._attr_is_on = self.smartplug.state == "ON"

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from egauge_async.json.models import RegisterType
from egauge_async.json.models import RegisterInfo, RegisterType
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -13,7 +13,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfEnergy, UnitOfPower
from homeassistant.const import UnitOfElectricPotential, UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -27,6 +27,7 @@ class EgaugeSensorEntityDescription(SensorEntityDescription):
native_value_fn: Callable[[EgaugeData, str], float]
available_fn: Callable[[EgaugeData, str], bool]
supported_fn: Callable[[RegisterInfo], bool]
SENSORS: tuple[EgaugeSensorEntityDescription, ...] = (
@@ -37,6 +38,7 @@ SENSORS: tuple[EgaugeSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
native_value_fn=lambda data, register: data.measurements[register],
available_fn=lambda data, register: register in data.measurements,
supported_fn=lambda register_info: register_info.type == RegisterType.POWER,
),
EgaugeSensorEntityDescription(
key="energy",
@@ -46,6 +48,16 @@ SENSORS: tuple[EgaugeSensorEntityDescription, ...] = (
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
native_value_fn=lambda data, register: data.counters[register],
available_fn=lambda data, register: register in data.counters,
supported_fn=lambda register_info: register_info.type == RegisterType.POWER,
),
EgaugeSensorEntityDescription(
key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
native_value_fn=lambda data, register: data.measurements[register],
available_fn=lambda data, register: register in data.measurements,
supported_fn=lambda register_info: register_info.type == RegisterType.VOLTAGE,
),
)
@@ -61,7 +73,7 @@ async def async_setup_entry(
EgaugeSensor(coordinator, register_name, sensor)
for sensor in SENSORS
for register_name, register_info in coordinator.data.register_info.items()
if register_info.type == RegisterType.POWER
if sensor.supported_fn(register_info)
)

View File

@@ -53,6 +53,7 @@ class EheimDigitalUpdateCoordinator(
main_device_added_event=self.main_device_added_event,
)
self.known_devices: set[str] = set()
self.incomplete_devices: set[str] = set()
self.platform_callbacks: set[AsyncSetupDeviceEntitiesCallback] = set()
def add_platform_callback(
@@ -70,11 +71,26 @@ class EheimDigitalUpdateCoordinator(
This function is called from the library whenever a new device is added.
"""
if device_address not in self.known_devices:
if self.hub.devices[device_address].is_missing_data:
self.incomplete_devices.add(device_address)
return
if (
device_address not in self.known_devices
or device_address in self.incomplete_devices
):
for platform_callback in self.platform_callbacks:
platform_callback({device_address: self.hub.devices[device_address]})
if device_address in self.incomplete_devices:
self.incomplete_devices.remove(device_address)
async def _async_receive_callback(self) -> None:
if any(self.incomplete_devices):
for device_address in self.incomplete_devices.copy():
if not self.hub.devices[device_address].is_missing_data:
await self._async_device_found(
device_address, EheimDeviceType.VERSION_UNDEFINED
)
self.async_set_updated_data(self.hub.devices)
async def _async_setup(self) -> None:

View File

@@ -16,8 +16,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "PCA 301"
def setup_platform(
hass: HomeAssistant,
@@ -54,26 +52,9 @@ class SmartPlugSwitch(SwitchEntity):
def __init__(self, pca, device_id):
"""Initialize the switch."""
self._device_id = device_id
self._name = "PCA 301"
self._state = None
self._available = True
self._attr_name = "PCA 301"
self._pca = pca
@property
def name(self):
"""Return the name of the Smart Plug, if any."""
return self._name
@property
def available(self) -> bool:
"""Return if switch is available."""
return self._available
@property
def is_on(self):
"""Return true if switch is on."""
return self._state
def turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self._pca.turn_on(self._device_id)
@@ -85,10 +66,10 @@ class SmartPlugSwitch(SwitchEntity):
def update(self) -> None:
"""Update the PCA switch's state."""
try:
self._state = self._pca.get_state(self._device_id)
self._available = True
self._attr_is_on = self._pca.get_state(self._device_id)
self._attr_available = True
except OSError as ex:
if self._available:
if self._attr_available:
_LOGGER.warning("Could not read state for %s: %s", self.name, ex)
self._available = False
self._attr_available = False

View File

@@ -173,6 +173,9 @@ class GasSourceType(TypedDict):
stat_energy_from: str
# Instantaneous flow rate: m³/h, L/min, etc.
stat_rate: NotRequired[str]
# statistic_id of costs ($) incurred from the gas meter
# If set to None and entity_energy_price or number_energy_price are configured,
# an EnergyCostSensor will be automatically created
@@ -190,6 +193,9 @@ class WaterSourceType(TypedDict):
stat_energy_from: str
# Instantaneous flow rate: L/min, gal/min, m³/h, etc.
stat_rate: NotRequired[str]
# statistic_id of costs ($) incurred from the water meter
# If set to None and entity_energy_price or number_energy_price are configured,
# an EnergyCostSensor will be automatically created
@@ -440,6 +446,7 @@ GAS_SOURCE_SCHEMA = vol.Schema(
{
vol.Required("type"): "gas",
vol.Required("stat_energy_from"): str,
vol.Optional("stat_rate"): str,
vol.Optional("stat_cost"): vol.Any(str, None),
# entity_energy_from was removed in HA Core 2022.10
vol.Remove("entity_energy_from"): vol.Any(str, None),
@@ -451,6 +458,7 @@ WATER_SOURCE_SCHEMA = vol.Schema(
{
vol.Required("type"): "water",
vol.Required("stat_energy_from"): str,
vol.Optional("stat_rate"): str,
vol.Optional("stat_cost"): vol.Any(str, None),
vol.Optional("entity_energy_price"): vol.Any(str, None),
vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),

View File

@@ -44,6 +44,10 @@
"description": "[%key:component::energy::issues::entity_unexpected_unit_energy_price::description%]",
"title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]"
},
"entity_unexpected_unit_volume_flow_rate": {
"description": "The following entities do not have an expected unit of measurement (either of {flow_rate_units}):",
"title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]"
},
"entity_unexpected_unit_water": {
"description": "The following entities do not have the expected unit of measurement (either of {water_units}):",
"title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]"

View File

@@ -14,6 +14,7 @@ from homeassistant.const import (
UnitOfEnergy,
UnitOfPower,
UnitOfVolume,
UnitOfVolumeFlowRate,
)
from homeassistant.core import HomeAssistant, callback, valid_entity_id
@@ -28,6 +29,11 @@ POWER_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.POWER,)
POWER_USAGE_UNITS: dict[str, tuple[UnitOfPower, ...]] = {
sensor.SensorDeviceClass.POWER: tuple(UnitOfPower)
}
VOLUME_FLOW_RATE_DEVICE_CLASSES = (sensor.SensorDeviceClass.VOLUME_FLOW_RATE,)
VOLUME_FLOW_RATE_UNITS: dict[str, tuple[UnitOfVolumeFlowRate, ...]] = {
sensor.SensorDeviceClass.VOLUME_FLOW_RATE: tuple(UnitOfVolumeFlowRate)
}
VOLUME_FLOW_RATE_UNIT_ERROR = "entity_unexpected_unit_volume_flow_rate"
ENERGY_PRICE_UNITS = tuple(
f"/{unit}" for units in ENERGY_USAGE_UNITS.values() for unit in units
@@ -109,6 +115,12 @@ def _get_placeholders(hass: HomeAssistant, issue_type: str) -> dict[str, str] |
return {
"price_units": ", ".join(f"{currency}{unit}" for unit in WATER_PRICE_UNITS),
}
if issue_type == VOLUME_FLOW_RATE_UNIT_ERROR:
return {
"flow_rate_units": ", ".join(
VOLUME_FLOW_RATE_UNITS[sensor.SensorDeviceClass.VOLUME_FLOW_RATE]
),
}
return None
@@ -590,6 +602,21 @@ def _validate_gas_source(
)
)
if stat_rate := source.get("stat_rate"):
wanted_statistics_metadata.add(stat_rate)
validate_calls.append(
functools.partial(
_async_validate_power_stat,
hass,
statistics_metadata,
stat_rate,
VOLUME_FLOW_RATE_DEVICE_CLASSES,
VOLUME_FLOW_RATE_UNITS,
VOLUME_FLOW_RATE_UNIT_ERROR,
source_result,
)
)
def _validate_water_source(
hass: HomeAssistant,
@@ -650,6 +677,21 @@ def _validate_water_source(
)
)
if stat_rate := source.get("stat_rate"):
wanted_statistics_metadata.add(stat_rate)
validate_calls.append(
functools.partial(
_async_validate_power_stat,
hass,
statistics_metadata,
stat_rate,
VOLUME_FLOW_RATE_DEVICE_CLASSES,
VOLUME_FLOW_RATE_UNITS,
VOLUME_FLOW_RATE_UNIT_ERROR,
source_result,
)
)
async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
"""Validate the energy configuration."""

View File

@@ -1,10 +1,12 @@
"""Support for EnOcean devices."""
from serial import SerialException
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_DEVICE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
@@ -42,7 +44,10 @@ async def async_setup_entry(
hass: HomeAssistant, config_entry: EnOceanConfigEntry
) -> bool:
"""Set up an EnOcean dongle for the given entry."""
usb_dongle = EnOceanDongle(hass, config_entry.data[CONF_DEVICE])
try:
usb_dongle = EnOceanDongle(hass, config_entry.data[CONF_DEVICE])
except SerialException as err:
raise ConfigEntryNotReady(f"Failed to set up EnOcean dongle: {err}") from err
await usb_dongle.async_setup()
config_entry.runtime_data = usb_dongle

View File

@@ -322,7 +322,7 @@ class ECAlertSensorEntity(ECBaseSensorEntity[ECWeather]):
"""Environment Canada sensor for alerts."""
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the extra state attributes."""
value = self.entity_description.value_fn(self._ec_data)
if not value:

View File

@@ -123,7 +123,7 @@ class ECWeatherEntity(
self._attr_device_info = coordinator.device_info
@property
def native_temperature(self):
def native_temperature(self) -> float | None:
"""Return the temperature."""
if (
temperature := self.ec_data.conditions.get("temperature", {}).get("value")
@@ -138,42 +138,42 @@ class ECWeatherEntity(
return None
@property
def humidity(self):
def humidity(self) -> float | None:
"""Return the humidity."""
if self.ec_data.conditions.get("humidity", {}).get("value"):
return float(self.ec_data.conditions["humidity"]["value"])
return None
@property
def native_wind_speed(self):
def native_wind_speed(self) -> float | None:
"""Return the wind speed."""
if self.ec_data.conditions.get("wind_speed", {}).get("value"):
return float(self.ec_data.conditions["wind_speed"]["value"])
return None
@property
def wind_bearing(self):
def wind_bearing(self) -> float | None:
"""Return the wind bearing."""
if self.ec_data.conditions.get("wind_bearing", {}).get("value"):
return float(self.ec_data.conditions["wind_bearing"]["value"])
return None
@property
def native_pressure(self):
def native_pressure(self) -> float | None:
"""Return the pressure."""
if self.ec_data.conditions.get("pressure", {}).get("value"):
return float(self.ec_data.conditions["pressure"]["value"])
return None
@property
def native_visibility(self):
def native_visibility(self) -> float | None:
"""Return the visibility."""
if self.ec_data.conditions.get("visibility", {}).get("value"):
return float(self.ec_data.conditions["visibility"]["value"])
return None
@property
def condition(self):
def condition(self) -> str | None:
"""Return the weather condition."""
icon_code = None
@@ -186,7 +186,7 @@ class ECWeatherEntity(
if icon_code:
return icon_code_to_condition(int(icon_code))
return ""
return None
@callback
def _async_forecast_daily(self) -> list[Forecast] | None:
@@ -261,7 +261,7 @@ def get_forecast(ec_data, hourly) -> list[Forecast] | None:
return forecast_array
def icon_code_to_condition(icon_code):
def icon_code_to_condition(icon_code: int) -> str | None:
"""Return the condition corresponding to an icon code."""
for condition, codes in ICON_CONDITION_MAP.items():
if icon_code in codes:

View File

@@ -116,7 +116,7 @@ class EnvisalinkBinarySensor(EnvisalinkEntity, BinarySensorEntity):
return attr
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if sensor is on."""
return self._info["status"]["open"]

View File

@@ -89,7 +89,7 @@ class EnvisalinkSensor(EnvisalinkEntity, SensorEntity):
return self._info["status"]["alpha"]
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return self._info["status"]

View File

@@ -77,7 +77,7 @@ class EnvisalinkSwitch(EnvisalinkEntity, SwitchEntity):
)
@property
def is_on(self):
def is_on(self) -> bool:
"""Return the boolean response if the zone is bypassed."""
return self._info["bypassed"]

View File

@@ -46,12 +46,12 @@ class EufyHomeLight(LightEntity):
self._temp = None
self._brightness = None
self._hs = None
self._state = None
self._name = device["name"]
self._address = device["address"]
self._code = device["code"]
self._attr_name = device["name"]
self._type = device["type"]
self._bulb = lakeside.bulb(self._address, self._code, self._type)
self._bulb = lakeside.bulb(
(device_address := device["address"]), device["code"], self._type
)
self._attr_unique_id = device_address
self._colormode = False
if self._type == "T1011":
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
@@ -72,22 +72,7 @@ class EufyHomeLight(LightEntity):
self._hs = color_util.color_RGB_to_hs(*self._bulb.colors)
else:
self._colormode = False
self._state = self._bulb.power
@property
def unique_id(self):
"""Return the ID of this light."""
return self._address
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def is_on(self):
"""Return true if device is on."""
return self._state
self._attr_is_on = self._bulb.power
@property
def brightness(self):

View File

@@ -30,33 +30,17 @@ class EufyHomeSwitch(SwitchEntity):
def __init__(self, device):
"""Initialize the light."""
self._state = None
self._name = device["name"]
self._address = device["address"]
self._code = device["code"]
self._type = device["type"]
self._switch = lakeside.switch(self._address, self._code, self._type)
self._attr_name = device["name"]
self._attr_unique_id = device["address"]
self._switch = lakeside.switch(
device["address"], device["code"], device["type"]
)
self._switch.connect()
def update(self) -> None:
"""Synchronise state from the switch."""
self._switch.update()
self._state = self._switch.power
@property
def unique_id(self):
"""Return the ID of this light."""
return self._address
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def is_on(self):
"""Return true if device is on."""
return self._state
self._attr_is_on = self._switch.power
def turn_on(self, **kwargs: Any) -> None:
"""Turn the specified switch on."""

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