mirror of
https://github.com/home-assistant/core.git
synced 2026-02-23 10:41:19 +01:00
Compare commits
246 Commits
homewizard
...
matter_cle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d97424a22 | ||
|
|
76114c9ded | ||
|
|
0334dad2f8 | ||
|
|
2dd65172b0 | ||
|
|
4532fd379e | ||
|
|
578b2b3d43 | ||
|
|
9bb2f56fbe | ||
|
|
a7d209f1f5 | ||
|
|
83d73dce5c | ||
|
|
d84f81daf2 | ||
|
|
79d4f5c8cf | ||
|
|
e9e1abb604 | ||
|
|
9a97541253 | ||
|
|
fd39f3c431 | ||
|
|
2cc4a77746 | ||
|
|
e8885de8c2 | ||
|
|
03d9c2cf7b | ||
|
|
7f3583587d | ||
|
|
e009440bf9 | ||
|
|
43dccf15ba | ||
|
|
c647ab1877 | ||
|
|
6b395b2703 | ||
|
|
882a44a1c2 | ||
|
|
3c9a505fc3 | ||
|
|
b2679ddc42 | ||
|
|
2055082993 | ||
|
|
6f49f9a12a | ||
|
|
36c560b7bf | ||
|
|
05abe7efe0 | ||
|
|
865ec96429 | ||
|
|
e6dbed0a87 | ||
|
|
a3fd2f692e | ||
|
|
eb7e00346d | ||
|
|
77159e612e | ||
|
|
05f9e25f29 | ||
|
|
7fa51117a9 | ||
|
|
9e87fa75f8 | ||
|
|
0188f2ffec | ||
|
|
c144aec03e | ||
|
|
1cb44aef64 | ||
|
|
900f2300ad | ||
|
|
b075fba594 | ||
|
|
c2ba97fb79 | ||
|
|
d0a373aecc | ||
|
|
758225edad | ||
|
|
8ab1a527a4 | ||
|
|
c7582b2f25 | ||
|
|
91b8a67ce2 | ||
|
|
2b13ff98da | ||
|
|
fd2d9c2ee2 | ||
|
|
61b5466dcc | ||
|
|
bc4af64bea | ||
|
|
3323f84c22 | ||
|
|
b1f48a5886 | ||
|
|
a14b1db886 | ||
|
|
9de89b923e | ||
|
|
21cf5dc321 | ||
|
|
fe32582233 | ||
|
|
6ebf19c4ba | ||
|
|
5794189f8d | ||
|
|
c336e58afc | ||
|
|
cdad602af0 | ||
|
|
520046cd82 | ||
|
|
e0b2ff0b2a | ||
|
|
6164198bde | ||
|
|
dd41b4cefd | ||
|
|
ccb8d6af44 | ||
|
|
6e8c064474 | ||
|
|
7079eda8d9 | ||
|
|
4e3832758b | ||
|
|
773c3c4f07 | ||
|
|
b73beba152 | ||
|
|
82589b613d | ||
|
|
c9b5f5f2c1 | ||
|
|
725b45db7f | ||
|
|
b194741a13 | ||
|
|
4615b4d104 | ||
|
|
2c7d9cb62e | ||
|
|
e229ba591a | ||
|
|
7914ebe54e | ||
|
|
3abaa99706 | ||
|
|
86d7fdfe1e | ||
|
|
676c42d578 | ||
|
|
39909b7493 | ||
|
|
6aef9a99e6 | ||
|
|
ff036f38a0 | ||
|
|
53e3b4caf0 | ||
|
|
dbdc030b74 | ||
|
|
ee0b24f808 | ||
|
|
c0fd8ff342 | ||
|
|
84d2ec484d | ||
|
|
844b20e2fc | ||
|
|
2bd07e6626 | ||
|
|
b91c07b2af | ||
|
|
37f0f1869f | ||
|
|
2fcbd77c95 | ||
|
|
b398197c07 | ||
|
|
cd5775ca35 | ||
|
|
fafa193549 | ||
|
|
ca4d537529 | ||
|
|
e9be363f29 | ||
|
|
0f874f7f03 | ||
|
|
14b147b3f7 | ||
|
|
8a1909e5d8 | ||
|
|
1fd873869f | ||
|
|
3b7b3454d8 | ||
|
|
c7276621eb | ||
|
|
6be1e4065f | ||
|
|
ba547c6bdb | ||
|
|
be25603b76 | ||
|
|
2e0f727981 | ||
|
|
122bc32f30 | ||
|
|
723825b579 | ||
|
|
5f6b446195 | ||
|
|
f59f14fe40 | ||
|
|
ab9b13302c | ||
|
|
f74fdd7605 | ||
|
|
f7628b87c8 | ||
|
|
3e31fbfee0 | ||
|
|
477797271a | ||
|
|
9f2677ddd8 | ||
|
|
558a49cb66 | ||
|
|
a9b64a15e6 | ||
|
|
0a734b7426 | ||
|
|
8df41dc73f | ||
|
|
e9039cec24 | ||
|
|
d7ef65e562 | ||
|
|
e765c1652c | ||
|
|
15cb102c39 | ||
|
|
30314ec88e | ||
|
|
428aa31749 | ||
|
|
0170d56893 | ||
|
|
eb7d973252 | ||
|
|
e3c98dcd09 | ||
|
|
9c71aea622 | ||
|
|
21978917b9 | ||
|
|
3b6a5b2c79 | ||
|
|
68792f02d4 | ||
|
|
bfea04b482 | ||
|
|
dc553f20e6 | ||
|
|
5631170900 | ||
|
|
60d4b050ac | ||
|
|
c5e261495f | ||
|
|
d1a1183b9a | ||
|
|
4dcfd5fb91 | ||
|
|
680f7fac1c | ||
|
|
7a41ce1fd8 | ||
|
|
937b4866c3 | ||
|
|
151e075e28 | ||
|
|
8094cfc404 | ||
|
|
b26483e09e | ||
|
|
728de32d75 | ||
|
|
8de1e3d27b | ||
|
|
cabf3b7ab9 | ||
|
|
f0e22cca56 | ||
|
|
294a3e5360 | ||
|
|
fdd753e70c | ||
|
|
392fc7ff91 | ||
|
|
d777c1c542 | ||
|
|
fa71fd3992 | ||
|
|
19f6340546 | ||
|
|
479cb7f1e1 | ||
|
|
d50d914928 | ||
|
|
551a71104e | ||
|
|
65cf61571a | ||
|
|
58ac3d2f45 | ||
|
|
654e132440 | ||
|
|
4af60ef3b9 | ||
|
|
2fc9ded6b7 | ||
|
|
9f551f3d5b | ||
|
|
0b8312d942 | ||
|
|
413e297022 | ||
|
|
f7752686df | ||
|
|
1313960893 | ||
|
|
d298eb033a | ||
|
|
398a6222cd | ||
|
|
7168e2df5a | ||
|
|
3b3c081703 | ||
|
|
889467e4c2 | ||
|
|
6a3bace824 | ||
|
|
523b527486 | ||
|
|
b44900532f | ||
|
|
bd45232972 | ||
|
|
e7aa0ae398 | ||
|
|
1d41e24653 | ||
|
|
049a910494 | ||
|
|
f6f52005fe | ||
|
|
b23c402d0a | ||
|
|
91c36fcdf6 | ||
|
|
ff2f0ac320 | ||
|
|
c205785f4f | ||
|
|
59dad4c935 | ||
|
|
d61f7d8170 | ||
|
|
b6e7a55cd1 | ||
|
|
163a6805eb | ||
|
|
637accbfff | ||
|
|
98b8e152e3 | ||
|
|
d12816d297 | ||
|
|
58e4a42a1b | ||
|
|
fdad9873e4 | ||
|
|
0337988be8 | ||
|
|
ba695b5bd9 | ||
|
|
c114ea2666 | ||
|
|
82148e46f5 | ||
|
|
34a78f9251 | ||
|
|
f1c142b3d3 | ||
|
|
68c82c2f90 | ||
|
|
487e2f8ccc | ||
|
|
6322185206 | ||
|
|
7f65db260f | ||
|
|
6c50711e2b | ||
|
|
f0e7d099e6 | ||
|
|
6c0fb12189 | ||
|
|
8e14dc7b5a | ||
|
|
219b982ef5 | ||
|
|
307c6a4ce2 | ||
|
|
9b1812858b | ||
|
|
9c57be215f | ||
|
|
cda6236099 | ||
|
|
e4c7262260 | ||
|
|
0f648a7f9d | ||
|
|
e6b9c2f737 | ||
|
|
e0f39e6392 | ||
|
|
52d645e4bf | ||
|
|
e8f2493ed6 | ||
|
|
ba62d95715 | ||
|
|
73fa9925c4 | ||
|
|
9ec456d28e | ||
|
|
4974439850 | ||
|
|
5cf37afbf6 | ||
|
|
76ebc134f3 | ||
|
|
667a77502d | ||
|
|
8c146624f9 | ||
|
|
2418036798 | ||
|
|
459996b760 | ||
|
|
eec854386a | ||
|
|
47d6e3e938 | ||
|
|
957c6039e9 | ||
|
|
c833cfa395 | ||
|
|
9dc38eda9f | ||
|
|
e49767d37a | ||
|
|
e6c5e72470 | ||
|
|
66dc566d3a | ||
|
|
5bb7699df0 | ||
|
|
168dd36d66 | ||
|
|
66d8a5bc51 |
1
.agent/skills
Symbolic link
1
.agent/skills
Symbolic link
@@ -0,0 +1 @@
|
||||
../.claude/skills/
|
||||
1
.gemini/skills
Symbolic link
1
.gemini/skills
Symbolic link
@@ -0,0 +1 @@
|
||||
../.claude/skills
|
||||
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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
20
CODEOWNERS
generated
@@ -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
|
||||
|
||||
5
homeassistant/brands/american_standard.json
Normal file
5
homeassistant/brands/american_standard.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "american_standard",
|
||||
"name": "American Standard",
|
||||
"integrations": ["nexia", "trane"]
|
||||
}
|
||||
5
homeassistant/brands/powerfox.json
Normal file
5
homeassistant/brands/powerfox.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "powerfox",
|
||||
"name": "Powerfox",
|
||||
"integrations": ["powerfox", "powerfox_local"]
|
||||
}
|
||||
5
homeassistant/brands/trane.json
Normal file
5
homeassistant/brands/trane.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "trane",
|
||||
"name": "Trane",
|
||||
"integrations": ["nexia", "trane"]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
59
homeassistant/components/advantage_air/coordinator.py
Normal file
59
homeassistant/components/advantage_air/coordinator.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
116
homeassistant/components/anthropic/quality_scale.yaml
Normal file
116
homeassistant/components/anthropic/quality_scale.yaml
Normal 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
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
70
homeassistant/components/aws_s3/coordinator.py
Normal file
70
homeassistant/components/aws_s3/coordinator.py
Normal 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,
|
||||
)
|
||||
33
homeassistant/components/aws_s3/entity.py
Normal file
33
homeassistant/components/aws_s3/entity.py
Normal 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,
|
||||
)
|
||||
57
homeassistant/components/aws_s3/helpers.py
Normal file
57
homeassistant/components/aws_s3/helpers.py
Normal 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
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
66
homeassistant/components/aws_s3/sensor.py
Normal file
66
homeassistant/components/aws_s3/sensor.py
Normal 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)
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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*",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"entity": {
|
||||
"number": {
|
||||
"room_correction_intensity": {
|
||||
"default": "mdi:home-sound-out"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"audio_output": {
|
||||
"default": "mdi:audio-input-stereo-minijack"
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
|
||||
88
homeassistant/components/cambridge_audio/number.py
Normal file
88
homeassistant/components/cambridge_audio/number.py
Normal 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))
|
||||
@@ -35,6 +35,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"number": {
|
||||
"room_correction_intensity": {
|
||||
"name": "Room correction intensity"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"audio_output": {
|
||||
"name": "Audio output"
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -24,4 +24,6 @@ ATTR_STATE_OFF = "off"
|
||||
KEY_MAC = "mac"
|
||||
KEY_IP = "ip"
|
||||
|
||||
ZONE_NAME_UNCONFIGURED = "-"
|
||||
|
||||
TIMEOUT_SEC = 120
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -17,3 +17,9 @@ download_file:
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
headers:
|
||||
default: {}
|
||||
example:
|
||||
Accept: application/json
|
||||
selector:
|
||||
object:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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%]"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user