latest changes
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
2024.1.5
|
||||
2025.11.2
|
||||
+415
-343
@@ -30,113 +30,6 @@
|
||||
entity_id: switch.on_off_plug_in_unit_27
|
||||
domain: switch
|
||||
mode: single
|
||||
- id: '1654177411866'
|
||||
alias: arm_blink-system
|
||||
description: ''
|
||||
trigger:
|
||||
- platform: zone
|
||||
entity_id: device_tracker.mi_9t
|
||||
zone: zone.home
|
||||
event: leave
|
||||
- platform: zone
|
||||
entity_id: device_tracker.poco_x3_pro
|
||||
zone: zone.home
|
||||
event: leave
|
||||
condition: []
|
||||
action:
|
||||
- device_id: 6836ccb6a93416b9743169b0a1567027
|
||||
domain: alarm_control_panel
|
||||
entity_id: alarm_control_panel.blink_blink_kameras
|
||||
type: arm_away
|
||||
mode: single
|
||||
- id: '1675426194975'
|
||||
alias: Henry starten
|
||||
description: Starten AEG Saugroboter im Standard-Modus
|
||||
trigger:
|
||||
- device_id: 3a7d8ce472e3d48d7530661e0a2b73dd
|
||||
domain: deconz
|
||||
platform: device
|
||||
type: remote_button_short_release
|
||||
subtype: left
|
||||
condition: []
|
||||
action:
|
||||
- device_id: 26a7579f7cc57661a35064dfb47cff05
|
||||
domain: vacuum
|
||||
entity_id: vacuum.henry
|
||||
type: clean
|
||||
mode: single
|
||||
- id: '1675426263026'
|
||||
alias: Henry zur Basis
|
||||
description: Sendet AEG Saugroboter zur Basis zurück
|
||||
trigger:
|
||||
- device_id: 3a7d8ce472e3d48d7530661e0a2b73dd
|
||||
domain: deconz
|
||||
platform: device
|
||||
type: remote_button_short_release
|
||||
subtype: right
|
||||
condition: []
|
||||
action:
|
||||
- device_id: 26a7579f7cc57661a35064dfb47cff05
|
||||
domain: vacuum
|
||||
entity_id: vacuum.henry
|
||||
type: dock
|
||||
mode: single
|
||||
- id: '1679989643189'
|
||||
alias: info_battery_3QJH
|
||||
description: ''
|
||||
trigger:
|
||||
- type: bat_low
|
||||
platform: device
|
||||
device_id: 6ceb8b6e12fe699960c86a2733e3625d
|
||||
entity_id: binary_sensor.blink_carport_battery
|
||||
domain: binary_sensor
|
||||
condition: []
|
||||
action:
|
||||
- service: notify.mobile_app_phone_1
|
||||
data:
|
||||
message: Battery Blink Carport is low
|
||||
title: Attention
|
||||
- service: notify.hass_synchat
|
||||
data:
|
||||
message: Battery Blink Carport is low
|
||||
title: Attention
|
||||
mode: single
|
||||
- id: '1679989681294'
|
||||
alias: info_battery_3QLG
|
||||
description: ''
|
||||
trigger:
|
||||
- type: bat_low
|
||||
platform: device
|
||||
device_id: de1028940093c9c8648b3f8de481d3a8
|
||||
entity_id: binary_sensor.blink_grundstuck_battery
|
||||
domain: binary_sensor
|
||||
condition: []
|
||||
action:
|
||||
- service: notify.mobile_app_phone_1
|
||||
data:
|
||||
message: Battery Blink Terrasse is low
|
||||
title: Attention
|
||||
- service: notify.hass_synchat
|
||||
data:
|
||||
message: Battery Blink Terrasse is low
|
||||
title: Attention
|
||||
mode: single
|
||||
- id: '1679989779284'
|
||||
alias: blink_2h_newimage
|
||||
description: Triggers every 2 hours to refresh the picture
|
||||
trigger:
|
||||
- platform: time_pattern
|
||||
hours: /2
|
||||
condition: []
|
||||
action:
|
||||
- service: blink.trigger_camera
|
||||
data: {}
|
||||
target:
|
||||
entity_id:
|
||||
- camera.blink_carport
|
||||
- camera.blink_grundstuck
|
||||
- camera.blink_terrasse
|
||||
mode: single
|
||||
- id: '1679990307886'
|
||||
alias: ring_30min_Wohnen_new_snapshot
|
||||
description: ''
|
||||
@@ -152,16 +45,20 @@
|
||||
entity_id: camera.flur
|
||||
mode: single
|
||||
- id: '1697198876699'
|
||||
alias: Open_Window_Recognition_GWC
|
||||
alias: OpenWindow_GWC
|
||||
description: Fenster öffen schaltet thermostat aus
|
||||
trigger:
|
||||
triggers:
|
||||
- type: opened
|
||||
platform: device
|
||||
device_id: 3d9bbd20d917477dba1d762a3d8e57d0
|
||||
entity_id: 63d4831aedc671a64c86af2bcefdd983
|
||||
domain: binary_sensor
|
||||
condition: []
|
||||
action:
|
||||
trigger: device
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 30
|
||||
conditions: []
|
||||
actions:
|
||||
- device_id: a152eac711fe7d8599834c5016e8ec79
|
||||
domain: climate
|
||||
entity_id: 234b20c3d47cc9b257eb39806a8df372
|
||||
@@ -169,227 +66,34 @@
|
||||
hvac_mode: 'off'
|
||||
mode: single
|
||||
- id: '1697198957606'
|
||||
alias: Heizen_GWC
|
||||
alias: CloseWindow_GWC
|
||||
description: Schaltet Thermostat auf heizen, wenn Fenster schließt
|
||||
trigger:
|
||||
triggers:
|
||||
- type: not_opened
|
||||
platform: device
|
||||
device_id: 3d9bbd20d917477dba1d762a3d8e57d0
|
||||
entity_id: 63d4831aedc671a64c86af2bcefdd983
|
||||
domain: binary_sensor
|
||||
condition: []
|
||||
action:
|
||||
trigger: device
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 30
|
||||
conditions: []
|
||||
actions:
|
||||
- device_id: a152eac711fe7d8599834c5016e8ec79
|
||||
domain: climate
|
||||
entity_id: 234b20c3d47cc9b257eb39806a8df372
|
||||
type: set_hvac_mode
|
||||
hvac_mode: heat
|
||||
mode: single
|
||||
- id: '1697219751213'
|
||||
alias: OpenWindow_EZ
|
||||
description: ''
|
||||
trigger:
|
||||
- type: opened
|
||||
platform: device
|
||||
device_id: 938becb0cb270ff5ba696295c9725265
|
||||
entity_id: 037cd57a6b208ae6111efdd18c4b646f
|
||||
domain: binary_sensor
|
||||
condition: []
|
||||
action:
|
||||
- device_id: 4310fd4e6889633f88fc76c2c040b2a2
|
||||
domain: climate
|
||||
entity_id: 36ed9ff3613857c403d338429d180187
|
||||
type: set_hvac_mode
|
||||
hvac_mode: 'off'
|
||||
mode: single
|
||||
- id: '1697219805799'
|
||||
alias: CloseWindow_EZ
|
||||
description: ''
|
||||
trigger:
|
||||
- type: not_opened
|
||||
platform: device
|
||||
device_id: 938becb0cb270ff5ba696295c9725265
|
||||
entity_id: 037cd57a6b208ae6111efdd18c4b646f
|
||||
domain: binary_sensor
|
||||
condition: []
|
||||
action:
|
||||
- device_id: 4310fd4e6889633f88fc76c2c040b2a2
|
||||
domain: climate
|
||||
entity_id: 36ed9ff3613857c403d338429d180187
|
||||
type: set_hvac_mode
|
||||
hvac_mode: heat
|
||||
mode: single
|
||||
- id: '1697219876457'
|
||||
alias: OpenWindow_K
|
||||
description: ''
|
||||
trigger:
|
||||
- type: opened
|
||||
platform: device
|
||||
device_id: 925011b73a5e493591e69fb225b7cfd7
|
||||
entity_id: 3cd23ba868711451231a06df2c8b2a70
|
||||
domain: binary_sensor
|
||||
condition: []
|
||||
action:
|
||||
- device_id: d1b6e8d6cb808a38ac2b0e02c8263748
|
||||
domain: climate
|
||||
entity_id: f377ead71021311ed369a790fc19115c
|
||||
type: set_hvac_mode
|
||||
hvac_mode: 'off'
|
||||
mode: single
|
||||
- id: '1697219906412'
|
||||
alias: CloseWindow_K
|
||||
description: ''
|
||||
trigger:
|
||||
- type: not_opened
|
||||
platform: device
|
||||
device_id: 925011b73a5e493591e69fb225b7cfd7
|
||||
entity_id: 3cd23ba868711451231a06df2c8b2a70
|
||||
domain: binary_sensor
|
||||
condition: []
|
||||
action:
|
||||
- device_id: d1b6e8d6cb808a38ac2b0e02c8263748
|
||||
domain: climate
|
||||
entity_id: f377ead71021311ed369a790fc19115c
|
||||
type: set_hvac_mode
|
||||
hvac_mode: heat
|
||||
mode: single
|
||||
- id: '1697219964028'
|
||||
alias: OpenWindow_WZ
|
||||
description: ''
|
||||
trigger:
|
||||
- type: opened
|
||||
platform: device
|
||||
device_id: 8579bd30fee6b1e2f4fdd308b10f1294
|
||||
entity_id: b44da533c2091c7e686b2981886e8520
|
||||
domain: binary_sensor
|
||||
condition: []
|
||||
action:
|
||||
- device_id: 4e5bc694a9c4d1cb2c9acd8da493aa33
|
||||
domain: climate
|
||||
entity_id: 7d001a74f7572603920f3ee67fd4f05a
|
||||
type: set_hvac_mode
|
||||
hvac_mode: 'off'
|
||||
mode: single
|
||||
- id: '1697219992895'
|
||||
alias: CloseWindow_WZ
|
||||
description: ''
|
||||
trigger:
|
||||
- type: not_opened
|
||||
platform: device
|
||||
device_id: 8579bd30fee6b1e2f4fdd308b10f1294
|
||||
entity_id: b44da533c2091c7e686b2981886e8520
|
||||
domain: binary_sensor
|
||||
condition: []
|
||||
action:
|
||||
- device_id: 4e5bc694a9c4d1cb2c9acd8da493aa33
|
||||
domain: climate
|
||||
entity_id: 7d001a74f7572603920f3ee67fd4f05a
|
||||
type: set_hvac_mode
|
||||
hvac_mode: heat
|
||||
mode: single
|
||||
- id: '1699360571026'
|
||||
alias: Wohnraum_aufheizen
|
||||
description: Thermostaten im Wohnbereich auf "On" werktags um 16:00 Uhr
|
||||
trigger:
|
||||
- platform: time
|
||||
at: '16:00:00'
|
||||
condition:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.workday_sensor
|
||||
state: 'on'
|
||||
action:
|
||||
- service: climate.set_temperature
|
||||
data:
|
||||
temperature: 25
|
||||
target:
|
||||
device_id:
|
||||
- 4310fd4e6889633f88fc76c2c040b2a2
|
||||
- 4e5bc694a9c4d1cb2c9acd8da493aa33
|
||||
mode: single
|
||||
- id: '1699360994119'
|
||||
alias: Wohnraum_normal
|
||||
description: Thermostaten im Wohnbereich auf 23°C werktags ab 18:00 Uhr
|
||||
trigger:
|
||||
- platform: time
|
||||
at: '18:00:00'
|
||||
condition:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.workday_sensor
|
||||
state: 'on'
|
||||
action:
|
||||
- service: climate.set_temperature
|
||||
data:
|
||||
temperature: 22
|
||||
target:
|
||||
device_id:
|
||||
- 4310fd4e6889633f88fc76c2c040b2a2
|
||||
- 4e5bc694a9c4d1cb2c9acd8da493aa33
|
||||
mode: single
|
||||
- id: '1700655324441'
|
||||
alias: Ring_Lights_3min
|
||||
description: Wenns klingelt, schalte Eingangsbeleuchtung für 3 Minuten ein, dann
|
||||
wieder aus.
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- binary_sensor.haustur_ding
|
||||
from: 'off'
|
||||
to: 'on'
|
||||
condition:
|
||||
- condition: sun
|
||||
before: sunrise
|
||||
after: sunset
|
||||
action:
|
||||
- service: switch.turn_on
|
||||
data: {}
|
||||
target:
|
||||
entity_id: switch.on_off_plug_in_unit_33
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 3
|
||||
seconds: 0
|
||||
milliseconds: 0
|
||||
- service: switch.turn_off
|
||||
data: {}
|
||||
target:
|
||||
entity_id: switch.on_off_plug_in_unit_33
|
||||
mode: single
|
||||
- id: '1700655937621'
|
||||
alias: Ring_Motion_LightsOn_1m
|
||||
description: Wenn Bewegung festgestellt, schalte Eingangsbeleuchtung ein für 1 Minute
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id:
|
||||
- binary_sensor.haustur_motion
|
||||
from: 'off'
|
||||
to: 'on'
|
||||
condition:
|
||||
- condition: sun
|
||||
before: sunrise
|
||||
after: sunset
|
||||
action:
|
||||
- service: switch.turn_on
|
||||
data: {}
|
||||
target:
|
||||
entity_id: switch.on_off_plug_in_unit_33
|
||||
- delay:
|
||||
hours: 0
|
||||
minutes: 1
|
||||
seconds: 0
|
||||
milliseconds: 0
|
||||
- service: switch.turn_off
|
||||
data: {}
|
||||
target:
|
||||
entity_id: switch.on_off_plug_in_unit_33
|
||||
mode: single
|
||||
- id: '1702998213164'
|
||||
alias: Bodensensor_Batterie_schwach
|
||||
description: ''
|
||||
trigger:
|
||||
- type: battery_level
|
||||
platform: device
|
||||
device_id: 08b4e2ecbf199bc85cc9a2d1f40cabcb
|
||||
entity_id: 1bcc278d48917c5a802e27f2eeffa5d9
|
||||
device_id: c98f516ed093e6f4027ef95659173f06
|
||||
entity_id: fcad955888a5219fe0bed96490dd2f74
|
||||
domain: sensor
|
||||
below: 10
|
||||
condition: []
|
||||
@@ -416,43 +120,30 @@
|
||||
type: set_hvac_mode
|
||||
hvac_mode: 'off'
|
||||
mode: single
|
||||
- id: '1703424624861'
|
||||
alias: CloseWindow_HWR
|
||||
description: ''
|
||||
trigger:
|
||||
- type: not_opened
|
||||
platform: device
|
||||
device_id: b5cf85bb338ae5f4c153bd9e50f4108d
|
||||
entity_id: e514fa88f6aed2c3edce0b8e759aabeb
|
||||
domain: binary_sensor
|
||||
condition: []
|
||||
action:
|
||||
- device_id: cddc98ceb2cb41339a3f275bc4252103
|
||||
domain: climate
|
||||
entity_id: c9f5569dab1fbf4342cc5c9234393a13
|
||||
type: set_hvac_mode
|
||||
hvac_mode: heat
|
||||
mode: single
|
||||
- id: '1704809927574'
|
||||
alias: info_battery_haustür
|
||||
description: ''
|
||||
trigger:
|
||||
triggers:
|
||||
- type: battery_level
|
||||
platform: device
|
||||
device_id: 1130260339712c9b3286464e2b148876
|
||||
entity_id: 625d0c9d0634087e813593e30368e45f
|
||||
domain: sensor
|
||||
below: 5
|
||||
condition: []
|
||||
action:
|
||||
- service: notify.mobile_app_phone_1
|
||||
data:
|
||||
message: Battery Ring Haustür is low
|
||||
title: Attention
|
||||
- service: notify.hass_synchat
|
||||
data:
|
||||
trigger: device
|
||||
- type: battery_level
|
||||
device_id: 9e8fab1237714c677c6d747fe81e5639
|
||||
entity_id: eb89c5a51befafbb6ab287111ff3f0d9
|
||||
domain: sensor
|
||||
trigger: device
|
||||
below: 10
|
||||
conditions: []
|
||||
actions:
|
||||
- action: notify.mobile_app_m2102j20sg
|
||||
data: {}
|
||||
- data:
|
||||
message: Battery Ring Haustür is low
|
||||
title: Attention
|
||||
action: notify.hass_synchat
|
||||
mode: single
|
||||
- id: '1704809998635'
|
||||
alias: info_battery_garten
|
||||
@@ -475,3 +166,384 @@
|
||||
message: Battery Ring Garten is low
|
||||
title: Attention
|
||||
mode: single
|
||||
- id: '1707337272134'
|
||||
alias: window_reminder
|
||||
description: Erinnerung an geöffnetes Fenster im Gäste-WC
|
||||
triggers:
|
||||
- type: opened
|
||||
device_id: 3d9bbd20d917477dba1d762a3d8e57d0
|
||||
entity_id: 63d4831aedc671a64c86af2bcefdd983
|
||||
domain: binary_sensor
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 30
|
||||
seconds: 0
|
||||
trigger: device
|
||||
- type: opened
|
||||
device_id: 2b540425d60dc7fced5b0fbb75ac8509
|
||||
entity_id: 4dea675e02547c2de3e5e6b18cb590c4
|
||||
domain: binary_sensor
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 30
|
||||
seconds: 0
|
||||
trigger: device
|
||||
- type: opened
|
||||
device_id: b11d9ab16282543c5702070e8869e646
|
||||
entity_id: e7c2634ca474b4c35ee2fe207369f279
|
||||
domain: binary_sensor
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 30
|
||||
seconds: 0
|
||||
trigger: device
|
||||
- type: opened
|
||||
device_id: 247a36af48712de701c6903728d9fbb8
|
||||
entity_id: 28af81ebf4fd7f2a03efd34aa6312c78
|
||||
domain: binary_sensor
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 30
|
||||
seconds: 0
|
||||
trigger: device
|
||||
- type: opened
|
||||
device_id: 938becb0cb270ff5ba696295c9725265
|
||||
entity_id: 037cd57a6b208ae6111efdd18c4b646f
|
||||
domain: binary_sensor
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 30
|
||||
seconds: 0
|
||||
trigger: device
|
||||
- type: opened
|
||||
device_id: 925011b73a5e493591e69fb225b7cfd7
|
||||
entity_id: 3cd23ba868711451231a06df2c8b2a70
|
||||
domain: binary_sensor
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 30
|
||||
seconds: 0
|
||||
trigger: device
|
||||
- type: opened
|
||||
device_id: b5cf85bb338ae5f4c153bd9e50f4108d
|
||||
entity_id: e514fa88f6aed2c3edce0b8e759aabeb
|
||||
domain: binary_sensor
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 30
|
||||
seconds: 0
|
||||
trigger: device
|
||||
- type: opened
|
||||
device_id: 8579bd30fee6b1e2f4fdd308b10f1294
|
||||
entity_id: b44da533c2091c7e686b2981886e8520
|
||||
domain: binary_sensor
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 30
|
||||
seconds: 0
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- metadata: {}
|
||||
data:
|
||||
message: Fenster {{ trigger.to_state.attributes.friendly_name }} geöffnet! Bitte
|
||||
schließen.
|
||||
action: notify.mobile_app_m2102j20sg
|
||||
- metadata: {}
|
||||
data:
|
||||
message: Fenster {{ trigger.to_state.attributes.friendly_name }} geöffnet! Bitte
|
||||
schließen.
|
||||
action: notify.mobile_app_lenovo_tb_j606f
|
||||
- action: notify.mobile_app_a142
|
||||
metadata: {}
|
||||
data:
|
||||
message: Fenster {{ trigger.to_state.attributes.friendly_name }} geöffnet! Bitte
|
||||
schließen.
|
||||
mode: single
|
||||
- id: '1715684534015'
|
||||
alias: Markise_schliessen_bei_Wind
|
||||
description: ''
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id:
|
||||
- sensor.pws_windgeschwindigkeit
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 5
|
||||
seconds: 0
|
||||
above: 15
|
||||
condition: []
|
||||
action:
|
||||
- device_id: 34ea13c8acad7a8e8acf3033646ea099
|
||||
domain: cover
|
||||
entity_id: 9eeea6ecf5a2610a111cd817db7a4d2a
|
||||
type: close
|
||||
mode: single
|
||||
- id: '1716390459792'
|
||||
alias: wasser-melder
|
||||
description: Meldet sofort wenn einer der Wassersensoren triggert
|
||||
triggers:
|
||||
- type: moist
|
||||
device_id: bda8699b590633126547aa5e5cc81f0c
|
||||
entity_id: 202cf9218a1c53587c485326ec3e0ceb
|
||||
domain: binary_sensor
|
||||
trigger: device
|
||||
- type: moist
|
||||
device_id: 538ec130076138519272e5cce4b52d41
|
||||
entity_id: b676e03dc1305baba571998ee659000d
|
||||
domain: binary_sensor
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- metadata: {}
|
||||
data:
|
||||
message: ACHTUNG! {{ trigger.to_state.attributes.friendly_name }} meldet Wasser!
|
||||
action: notify.mobile_app_lenovo_tb_j606f
|
||||
- action: notify.mobile_app_nothing3apro
|
||||
metadata: {}
|
||||
data:
|
||||
message: ACHTUNG! {{ trigger.to_state.attributes.friendly_name }} meldet Wasser!
|
||||
- action: notify.mobile_app_a142
|
||||
metadata: {}
|
||||
data:
|
||||
message: ACHTUNG! {{ trigger.to_state.attributes.friendly_name }} meldet Wasser!
|
||||
mode: single
|
||||
- id: '1736319858082'
|
||||
alias: "\U0001F525 Advanced Heating Control V5"
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: panhans/advanced_heating_control.yaml
|
||||
input:
|
||||
input_trvs:
|
||||
- climate.t_esszimmer
|
||||
- climate.t_wohnzimmer
|
||||
- climate.t_kuche
|
||||
input_temperature_sensor: sensor.temperature_38
|
||||
input_temperature_eco_static: 16
|
||||
input_min_instead_of_off: true
|
||||
input_persons:
|
||||
- person.gorden
|
||||
- person.indra
|
||||
input_schedulers:
|
||||
- schedule.heating
|
||||
input_away_offset: 8
|
||||
input_away_scheduler_mode: true
|
||||
input_windows:
|
||||
- binary_sensor.fk_esszimmer
|
||||
- binary_sensor.fk_wohnzimmer
|
||||
- binary_sensor.openclose_17
|
||||
input_liming_protection: true
|
||||
input_mode_outside_temperature: sensor.sensor1_soil_temperature
|
||||
input_mode_outside_temperature_threshold: 18
|
||||
input_generic_calibration_offset: 7
|
||||
input_frost_protection_temp: 7
|
||||
input_temperature_comfort_static: 24
|
||||
input_people_entering_home_duration:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 30
|
||||
input_people_leaving_home_duration:
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 30
|
||||
input_temperature_comfort: input_number.comforttemp
|
||||
input_temperature_eco: input_number.ecotemp
|
||||
- id: '1736353137059'
|
||||
alias: "\U0001F525 Advanced Heating Control Oben"
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: panhans/advanced_heating_control.yaml
|
||||
input:
|
||||
input_trvs:
|
||||
- climate.t_buro
|
||||
- climate.t_gaste1
|
||||
input_temperature_sensor: sensor.office_room_temperature
|
||||
input_temperature_comfort_static: 23
|
||||
input_min_instead_of_off: true
|
||||
input_persons:
|
||||
- person.gorden
|
||||
input_schedulers:
|
||||
- schedule.homeoffice
|
||||
input_away_offset: 8
|
||||
input_windows:
|
||||
- binary_sensor.fk_buro
|
||||
input_liming_protection: true
|
||||
input_frost_protection_temp: 7
|
||||
- id: '1749638886388'
|
||||
alias: gäste-wc_licht
|
||||
description: Schaltet bei Bewegung das Licht an
|
||||
triggers:
|
||||
- type: motion
|
||||
device_id: 6e9d01f467927d61f45768160a892e45
|
||||
entity_id: fb26eefedea1e06a200b9f9ec17a7f3d
|
||||
domain: binary_sensor
|
||||
trigger: device
|
||||
conditions: []
|
||||
actions:
|
||||
- type: turn_on
|
||||
device_id: 8d6bbf2015aa51bbf1e064cac4aef609
|
||||
entity_id: b7f728e28b4c342d1921a47409822ed9
|
||||
domain: light
|
||||
brightness_pct: 50
|
||||
mode: single
|
||||
- id: '1749638989528'
|
||||
alias: gäste-wc_licht_aus
|
||||
description: ''
|
||||
triggers:
|
||||
- type: no_motion
|
||||
device_id: 6e9d01f467927d61f45768160a892e45
|
||||
entity_id: fb26eefedea1e06a200b9f9ec17a7f3d
|
||||
domain: binary_sensor
|
||||
trigger: device
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 2
|
||||
seconds: 10
|
||||
conditions: []
|
||||
actions:
|
||||
- type: turn_off
|
||||
device_id: 8d6bbf2015aa51bbf1e064cac4aef609
|
||||
entity_id: b7f728e28b4c342d1921a47409822ed9
|
||||
domain: light
|
||||
mode: single
|
||||
- id: '1758515986313'
|
||||
alias: Licht Eingang an
|
||||
description: Schaltet bei Bewegung das Licht an
|
||||
triggers:
|
||||
- type: motion
|
||||
device_id: ab5a4270111ddad1ee8bb693143416bd
|
||||
entity_id: d67ad6df4a99dd98ac122f72dbd9cfa0
|
||||
domain: binary_sensor
|
||||
trigger: device
|
||||
conditions:
|
||||
- condition: sun
|
||||
before: sunrise
|
||||
after: sunset
|
||||
actions:
|
||||
- type: turn_on
|
||||
device_id: 294f5f31177faceb618c0ffd1e24ba7d
|
||||
entity_id: f0e686c16165f9db196292b778fee428
|
||||
domain: switch
|
||||
mode: single
|
||||
- id: '1758516096352'
|
||||
alias: eingang_licht_aus
|
||||
description: ''
|
||||
triggers:
|
||||
- type: no_motion
|
||||
device_id: ab5a4270111ddad1ee8bb693143416bd
|
||||
entity_id: d67ad6df4a99dd98ac122f72dbd9cfa0
|
||||
domain: binary_sensor
|
||||
trigger: device
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 1
|
||||
seconds: 0
|
||||
conditions: []
|
||||
actions:
|
||||
- type: turn_off
|
||||
device_id: 294f5f31177faceb618c0ffd1e24ba7d
|
||||
entity_id: f0e686c16165f9db196292b778fee428
|
||||
domain: switch
|
||||
mode: single
|
||||
- id: '1762262741326'
|
||||
alias: remove_orphan_deconz
|
||||
description: ''
|
||||
triggers:
|
||||
- trigger: homeassistant
|
||||
event: start
|
||||
conditions: []
|
||||
actions:
|
||||
- action: deconz.remove_orphaned_entries
|
||||
metadata: {}
|
||||
data: {}
|
||||
mode: single
|
||||
- id: '1763064329677'
|
||||
alias: GastWC_Licht
|
||||
description: ''
|
||||
triggers:
|
||||
- entity_id:
|
||||
- binary_sensor.presence_75
|
||||
from:
|
||||
- 'off'
|
||||
to:
|
||||
- 'on'
|
||||
id: 'on'
|
||||
trigger: state
|
||||
- entity_id:
|
||||
- binary_sensor.presence_75
|
||||
from:
|
||||
- 'on'
|
||||
to:
|
||||
- 'off'
|
||||
id: 'off'
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 3
|
||||
seconds: 0
|
||||
trigger: state
|
||||
conditions:
|
||||
- type: is_illuminance
|
||||
condition: device
|
||||
device_id: 6e9d01f467927d61f45768160a892e45
|
||||
entity_id: 60270718debf85162065975626b4f984
|
||||
domain: sensor
|
||||
below: 20
|
||||
actions:
|
||||
- if:
|
||||
- condition: trigger
|
||||
id: 'on'
|
||||
then:
|
||||
- data:
|
||||
brightness_pct: 50
|
||||
action: light.turn_on
|
||||
target:
|
||||
entity_id: light.l_gastewc
|
||||
else:
|
||||
- action: light.turn_off
|
||||
data: {}
|
||||
target:
|
||||
entity_id: light.l_gastewc
|
||||
- id: '1763064702979'
|
||||
alias: Eingang_Licht
|
||||
description: ''
|
||||
triggers:
|
||||
- entity_id:
|
||||
- binary_sensor.presence_74
|
||||
from:
|
||||
- 'off'
|
||||
to:
|
||||
- 'on'
|
||||
id: 'on'
|
||||
trigger: state
|
||||
- entity_id:
|
||||
- binary_sensor.presence_74
|
||||
from:
|
||||
- 'on'
|
||||
to:
|
||||
- 'off'
|
||||
id: 'off'
|
||||
for:
|
||||
hours: 0
|
||||
minutes: 3
|
||||
seconds: 0
|
||||
trigger: state
|
||||
conditions:
|
||||
- condition: sun
|
||||
before: sunrise
|
||||
after: sunset
|
||||
actions:
|
||||
- if:
|
||||
- condition: trigger
|
||||
id: 'on'
|
||||
then:
|
||||
- data:
|
||||
brightness_pct: 50
|
||||
action: light.turn_on
|
||||
target:
|
||||
entity_id: light.p_eingang
|
||||
else:
|
||||
- action: light.turn_off
|
||||
data: {}
|
||||
target:
|
||||
entity_id: light.p_eingang
|
||||
mode: single
|
||||
|
||||
+11
-1
@@ -3,8 +3,14 @@ default_config:
|
||||
|
||||
homeassistant:
|
||||
internal_url: "http://192.168.0.40:8123"
|
||||
external_url: "http://homeassistant.realm.local"
|
||||
external_url: "https://ha.solusar.de"
|
||||
country: DE
|
||||
|
||||
auth_mfa_modules:
|
||||
- type: totp
|
||||
name: "Authenticator app"
|
||||
- type: notify
|
||||
message: "I almost forget, to get into my clubhouse, you need to say {}"
|
||||
|
||||
auth_providers:
|
||||
- type: trusted_networks
|
||||
@@ -28,6 +34,10 @@ homeassistant:
|
||||
allowlist_external_dirs:
|
||||
- /tmp
|
||||
|
||||
packages:
|
||||
mtec: !include mtec.yaml
|
||||
mtec_db: !include mtec_sql_postgre.yaml
|
||||
|
||||
# Text to speech
|
||||
tts:
|
||||
- platform: google_translate
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
"""The better_thermostat component."""
|
||||
|
||||
import logging
|
||||
from asyncio import Lock
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, Config
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import (
|
||||
CONF_FIX_CALIBRATION,
|
||||
from .utils.const import (
|
||||
CONF_CALIBRATION_MODE,
|
||||
CONF_HEATER,
|
||||
CONF_NO_SYSTEM_MODE_OFF,
|
||||
CONF_WINDOW_TIMEOUT,
|
||||
CONF_WINDOW_TIMEOUT_AFTER,
|
||||
CalibrationMode,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -19,15 +22,20 @@ DOMAIN = "better_thermostat"
|
||||
PLATFORMS = [Platform.CLIMATE]
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
config_entry_update_listener_lock = Lock()
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Config):
|
||||
"""Set up this integration using YAML is not supported."""
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up this integration using YAML."""
|
||||
if DOMAIN in config:
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass.data[DOMAIN] = {}
|
||||
"""Set up entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {}
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
@@ -35,14 +43,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
await async_unload_entry(hass, entry)
|
||||
await async_setup_entry(hass, entry)
|
||||
async with config_entry_update_listener_lock:
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
||||
|
||||
@@ -57,7 +66,7 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry):
|
||||
if config_entry.version == 1:
|
||||
new = {**config_entry.data}
|
||||
for trv in new[CONF_HEATER]:
|
||||
trv["advanced"].update({CONF_FIX_CALIBRATION: False})
|
||||
trv["advanced"].update({CalibrationMode.AGGRESIVE_CALIBRATION: False})
|
||||
config_entry.version = 2
|
||||
hass.config_entries.async_update_entry(config_entry, data=new)
|
||||
|
||||
@@ -71,12 +80,14 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry):
|
||||
new = {**config_entry.data}
|
||||
for trv in new[CONF_HEATER]:
|
||||
if (
|
||||
CONF_FIX_CALIBRATION in trv["advanced"]
|
||||
and trv["advanced"][CONF_FIX_CALIBRATION]
|
||||
CalibrationMode.AGGRESIVE_CALIBRATION in trv["advanced"]
|
||||
and trv["advanced"][CalibrationMode.AGGRESIVE_CALIBRATION]
|
||||
):
|
||||
trv["advanced"].update({CONF_CALIBRATION_MODE: CONF_FIX_CALIBRATION})
|
||||
trv["advanced"].update(
|
||||
{CONF_CALIBRATION_MODE: CalibrationMode.AGGRESIVE_CALIBRATION}
|
||||
)
|
||||
else:
|
||||
trv["advanced"].update({CONF_CALIBRATION_MODE: "default"})
|
||||
trv["advanced"].update({CONF_CALIBRATION_MODE: CalibrationMode.DEFAULT})
|
||||
config_entry.version = 4
|
||||
hass.config_entries.async_update_entry(config_entry, data=new)
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ async def get_current_offset(self, entity_id):
|
||||
return float(str(self.hass.states.get(entity_id).attributes.get("offset", 0)))
|
||||
|
||||
|
||||
async def get_offset_steps(self, entity_id):
|
||||
"""Get offset steps."""
|
||||
async def get_offset_step(self, entity_id):
|
||||
"""Get offset step."""
|
||||
return float(1.0)
|
||||
|
||||
|
||||
|
||||
@@ -21,14 +21,14 @@ async def get_info(self, entity_id):
|
||||
async def init(self, entity_id):
|
||||
if (
|
||||
self.real_trvs[entity_id]["local_temperature_calibration_entity"] is None
|
||||
and self.real_trvs[entity_id]["calibration"] == 0
|
||||
and self.real_trvs[entity_id]["calibration"] != 1
|
||||
):
|
||||
self.real_trvs[entity_id][
|
||||
"local_temperature_calibration_entity"
|
||||
] = await find_local_calibration_entity(self, entity_id)
|
||||
self.real_trvs[entity_id]["local_temperature_calibration_entity"] = (
|
||||
await find_local_calibration_entity(self, entity_id)
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"better_thermostat %s: uses local calibration entity %s",
|
||||
self.name,
|
||||
self.device_name,
|
||||
self.real_trvs[entity_id]["local_temperature_calibration_entity"],
|
||||
)
|
||||
# Wait for the entity to be available
|
||||
@@ -39,7 +39,7 @@ async def init(self, entity_id):
|
||||
).state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None):
|
||||
_LOGGER.info(
|
||||
"better_thermostat %s: waiting for TRV/climate entity with id '%s' to become fully available...",
|
||||
self.name,
|
||||
self.device_name,
|
||||
self.real_trvs[entity_id]["local_temperature_calibration_entity"],
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
@@ -64,8 +64,8 @@ async def get_current_offset(self, entity_id):
|
||||
return None
|
||||
|
||||
|
||||
async def get_offset_steps(self, entity_id):
|
||||
"""Get offset steps."""
|
||||
async def get_offset_step(self, entity_id):
|
||||
"""Get offset step."""
|
||||
if self.real_trvs[entity_id]["local_temperature_calibration_entity"] is not None:
|
||||
return float(
|
||||
str(
|
||||
@@ -119,13 +119,17 @@ async def set_temperature(self, entity_id, temperature):
|
||||
|
||||
async def set_hvac_mode(self, entity_id, hvac_mode):
|
||||
"""Set new target hvac mode."""
|
||||
await self.hass.services.async_call(
|
||||
"climate",
|
||||
"set_hvac_mode",
|
||||
{"entity_id": entity_id, "hvac_mode": hvac_mode},
|
||||
blocking=True,
|
||||
context=self.context,
|
||||
)
|
||||
_LOGGER.debug("better_thermostat %s: set_hvac_mode %s", self.device_name, hvac_mode)
|
||||
try:
|
||||
await self.hass.services.async_call(
|
||||
"climate",
|
||||
"set_hvac_mode",
|
||||
{"entity_id": entity_id, "hvac_mode": hvac_mode},
|
||||
blocking=True,
|
||||
context=self.context,
|
||||
)
|
||||
except TypeError:
|
||||
_LOGGER.debug("TypeError in set_hvac_mode")
|
||||
|
||||
|
||||
async def set_offset(self, entity_id, offset):
|
||||
|
||||
@@ -29,14 +29,14 @@ async def get_info(self, entity_id):
|
||||
async def init(self, entity_id):
|
||||
if (
|
||||
self.real_trvs[entity_id]["local_temperature_calibration_entity"] is None
|
||||
and self.real_trvs[entity_id]["calibration"] == 0
|
||||
and self.real_trvs[entity_id]["calibration"] != 1
|
||||
):
|
||||
self.real_trvs[entity_id][
|
||||
"local_temperature_calibration_entity"
|
||||
] = await find_local_calibration_entity(self, entity_id)
|
||||
self.real_trvs[entity_id]["local_temperature_calibration_entity"] = (
|
||||
await find_local_calibration_entity(self, entity_id)
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"better_thermostat %s: uses local calibration entity %s",
|
||||
self.name,
|
||||
self.device_name,
|
||||
self.real_trvs[entity_id]["local_temperature_calibration_entity"],
|
||||
)
|
||||
# Wait for the entity to be available
|
||||
@@ -47,7 +47,7 @@ async def init(self, entity_id):
|
||||
).state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None):
|
||||
_LOGGER.info(
|
||||
"better_thermostat %s: waiting for TRV/climate entity with id '%s' to become fully available...",
|
||||
self.name,
|
||||
self.device_name,
|
||||
self.real_trvs[entity_id]["local_temperature_calibration_entity"],
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
@@ -90,8 +90,8 @@ async def get_current_offset(self, entity_id):
|
||||
)
|
||||
|
||||
|
||||
async def get_offset_steps(self, entity_id):
|
||||
"""Get offset steps."""
|
||||
async def get_offset_step(self, entity_id):
|
||||
"""Get offset step."""
|
||||
return float(
|
||||
str(
|
||||
self.hass.states.get(
|
||||
@@ -159,7 +159,7 @@ async def set_offset(self, entity_id, offset):
|
||||
async def set_valve(self, entity_id, valve):
|
||||
"""Set new target valve."""
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: TO TRV {entity_id} set_valve: {valve}"
|
||||
f"better_thermostat {self.device_name}: TO TRV {entity_id} set_valve: {valve}"
|
||||
)
|
||||
await self.hass.services.async_call(
|
||||
"number",
|
||||
|
||||
@@ -33,8 +33,8 @@ async def get_current_offset(self, entity_id):
|
||||
)
|
||||
|
||||
|
||||
async def get_offset_steps(self, entity_id):
|
||||
"""Get offset steps."""
|
||||
async def get_offset_step(self, entity_id):
|
||||
"""Get offset step."""
|
||||
return float(0.01)
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,18 +10,18 @@ from homeassistant.components.climate.const import HVACMode
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
|
||||
from .utils.bridge import load_adapter
|
||||
from .adapters.delegate import load_adapter
|
||||
|
||||
from .utils.helpers import get_device_model, get_trv_intigration
|
||||
|
||||
from .const import (
|
||||
from .utils.const import (
|
||||
CONF_COOLER,
|
||||
CONF_PROTECT_OVERHEATING,
|
||||
CONF_CALIBRATION,
|
||||
CONF_CHILD_LOCK,
|
||||
CONF_HEAT_AUTO_SWAPPED,
|
||||
CONF_HEATER,
|
||||
CONF_HOMATICIP,
|
||||
CONF_HOMEMATICIP,
|
||||
CONF_HUMIDITY,
|
||||
CONF_MODEL,
|
||||
CONF_NO_SYSTEM_MODE_OFF,
|
||||
@@ -35,6 +35,7 @@ from .const import (
|
||||
CONF_WINDOW_TIMEOUT_AFTER,
|
||||
CONF_CALIBRATION_MODE,
|
||||
CONF_TOLERANCE,
|
||||
CONF_TARGET_TEMP_STEP,
|
||||
CalibrationMode,
|
||||
CalibrationType,
|
||||
)
|
||||
@@ -65,6 +66,21 @@ CALIBRATION_TYPE_ALL_SELECTOR = selector.SelectSelector(
|
||||
selector.SelectOptionDict(
|
||||
value=CalibrationType.LOCAL_BASED, label="Offset Based"
|
||||
),
|
||||
selector.SelectOptionDict(value=CalibrationType.HYBRID, label="Hybrid"),
|
||||
],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
)
|
||||
|
||||
TEMP_STEP_SELECTOR = selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[
|
||||
selector.SelectOptionDict(value="0.0", label="Auto"),
|
||||
selector.SelectOptionDict(value="0.1", label="0.1 °C"),
|
||||
selector.SelectOptionDict(value="0.2", label="0.2 °C"),
|
||||
selector.SelectOptionDict(value="0.25", label="0.25 °C"),
|
||||
selector.SelectOptionDict(value="0.5", label="0.5 °C"),
|
||||
selector.SelectOptionDict(value="1.0", label="1 °C"),
|
||||
],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
@@ -75,7 +91,7 @@ CALIBRATION_MODE_SELECTOR = selector.SelectSelector(
|
||||
options=[
|
||||
selector.SelectOptionDict(value=CalibrationMode.DEFAULT, label="Normal"),
|
||||
selector.SelectOptionDict(
|
||||
value=CalibrationMode.FIX_CALIBRATION, label="Agressive"
|
||||
value=CalibrationMode.AGGRESIVE_CALIBRATION, label="Agressive"
|
||||
),
|
||||
selector.SelectOptionDict(
|
||||
value=CalibrationMode.HEATING_POWER_CALIBRATION, label="AI Time Based"
|
||||
@@ -95,13 +111,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the config flow."""
|
||||
self.name = ""
|
||||
self.device_name = ""
|
||||
self.data = None
|
||||
self.model = None
|
||||
self.heater_entity_id = None
|
||||
self.trv_bundle = []
|
||||
self.integration = None
|
||||
self.i = 0
|
||||
super().__init__()
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
@@ -119,7 +136,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
if self.data is not None:
|
||||
_LOGGER.debug("Confirm: %s", self.data[CONF_HEATER])
|
||||
await self.async_set_unique_id(self.data["name"])
|
||||
unique_trv_string = "_".join([x["trv"] for x in self.data[CONF_HEATER]])
|
||||
await self.async_set_unique_id(
|
||||
f"{self.data['name']}_{unique_trv_string}"
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=self.data["name"], data=self.data)
|
||||
if confirm_type is not None:
|
||||
errors["base"] = confirm_type
|
||||
@@ -226,7 +247,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
] = bool
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_HOMATICIP, default=user_input.get(CONF_HOMATICIP, homematic)
|
||||
CONF_HOMEMATICIP, default=user_input.get(CONF_HOMEMATICIP, homematic)
|
||||
)
|
||||
] = bool
|
||||
|
||||
@@ -289,7 +310,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"trv": trv,
|
||||
"integration": _intigration,
|
||||
"model": await get_device_model(self, trv),
|
||||
"adapter": load_adapter(self, _intigration, trv),
|
||||
"adapter": await load_adapter(self, _intigration, trv),
|
||||
}
|
||||
)
|
||||
self.data[CONF_MODEL] = "/".join([x["model"] for x in self.trv_bundle])
|
||||
@@ -354,6 +375,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
vol.Optional(
|
||||
CONF_TOLERANCE, default=user_input.get(CONF_TOLERANCE, 0.0)
|
||||
): vol.All(vol.Coerce(float), vol.Range(min=0)),
|
||||
vol.Optional(
|
||||
CONF_TARGET_TEMP_STEP,
|
||||
default=str(user_input.get(CONF_TARGET_TEMP_STEP, "0.0")),
|
||||
): TEMP_STEP_SELECTOR,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
@@ -366,13 +391,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.options = dict(config_entry.options)
|
||||
self.i = 0
|
||||
self.trv_bundle = []
|
||||
self.name = ""
|
||||
self.device_name = ""
|
||||
self._last_step = False
|
||||
self.updated_config = {}
|
||||
super().__init__()
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
@@ -412,9 +436,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
fields = OrderedDict()
|
||||
|
||||
_default_calibration = "target_temp_based"
|
||||
self.name = user_input.get(CONF_NAME, "-")
|
||||
self.device_name = user_input.get(CONF_NAME, "-")
|
||||
|
||||
_adapter = load_adapter(
|
||||
_adapter = await load_adapter(
|
||||
self, _trv_config.get("integration"), _trv_config.get("trv")
|
||||
)
|
||||
if _adapter is not None:
|
||||
@@ -499,8 +523,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
] = bool
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_HOMATICIP,
|
||||
default=_trv_config["advanced"].get(CONF_HOMATICIP, homematic),
|
||||
CONF_HOMEMATICIP,
|
||||
default=_trv_config["advanced"].get(CONF_HOMEMATICIP, homematic),
|
||||
)
|
||||
] = bool
|
||||
|
||||
@@ -556,6 +580,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
self.updated_config[CONF_TOLERANCE] = float(
|
||||
user_input.get(CONF_TOLERANCE, 0.0)
|
||||
)
|
||||
self.updated_config[CONF_TARGET_TEMP_STEP] = float(
|
||||
user_input.get(CONF_TARGET_TEMP_STEP, "0.0")
|
||||
)
|
||||
|
||||
for trv in self.updated_config[CONF_HEATER]:
|
||||
trv["adapter"] = None
|
||||
@@ -683,6 +710,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
CONF_TOLERANCE, default=self.config_entry.data.get(CONF_TOLERANCE, 0.0)
|
||||
)
|
||||
] = vol.All(vol.Coerce(float), vol.Range(min=0))
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_TARGET_TEMP_STEP,
|
||||
default=str(self.config_entry.data.get(CONF_TARGET_TEMP_STEP, 0.0)),
|
||||
)
|
||||
] = TEMP_STEP_SELECTOR
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=vol.Schema(fields), last_step=False
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
""""""
|
||||
import json
|
||||
from enum import IntEnum
|
||||
from homeassistant.backports.enum import StrEnum
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
make_entity_service_schema,
|
||||
)
|
||||
from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEFAULT_NAME = "Better Thermostat"
|
||||
VERSION = "master"
|
||||
try:
|
||||
with open("custom_components/better_thermostat/manifest.json") as manifest_file:
|
||||
manifest = json.load(manifest_file)
|
||||
VERSION = manifest["version"]
|
||||
except (FileNotFoundError, KeyError, json.JSONDecodeError) as e:
|
||||
_LOGGER.error("better_thermostat %s: could not read version from manifest file.", e)
|
||||
|
||||
|
||||
CONF_HEATER = "thermostat"
|
||||
CONF_COOLER = "cooler"
|
||||
CONF_SENSOR = "temperature_sensor"
|
||||
CONF_HUMIDITY = "humidity_sensor"
|
||||
CONF_SENSOR_WINDOW = "window_sensors"
|
||||
CONF_TARGET_TEMP = "target_temp"
|
||||
CONF_WEATHER = "weather"
|
||||
CONF_OFF_TEMPERATURE = "off_temperature"
|
||||
CONF_WINDOW_TIMEOUT = "window_off_delay"
|
||||
CONF_WINDOW_TIMEOUT_AFTER = "window_off_delay_after"
|
||||
CONF_OUTDOOR_SENSOR = "outdoor_sensor"
|
||||
CONF_VALVE_MAINTENANCE = "valve_maintenance"
|
||||
CONF_MIN_TEMP = "min_temp"
|
||||
CONF_MAX_TEMP = "max_temp"
|
||||
CONF_PRECISION = "precision"
|
||||
CONF_CALIBRATION = "calibration"
|
||||
CONF_CHILD_LOCK = "child_lock"
|
||||
CONF_PROTECT_OVERHEATING = "protect_overheating"
|
||||
CONF_CALIBRATION_MODE = "calibration_mode"
|
||||
CONF_FIX_CALIBRATION = "fix_calibration"
|
||||
CONF_HEATING_POWER_CALIBRATION = "heating_power_calibration"
|
||||
CONF_NO_CALIBRATION = "no_calibration"
|
||||
CONF_HEAT_AUTO_SWAPPED = "heat_auto_swapped"
|
||||
CONF_MODEL = "model"
|
||||
CONF_HOMATICIP = "homaticip"
|
||||
CONF_INTEGRATION = "integration"
|
||||
CONF_NO_SYSTEM_MODE_OFF = "no_off_system_mode"
|
||||
CONF_TOLERANCE = "tolerance"
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
|
||||
|
||||
ATTR_STATE_WINDOW_OPEN = "window_open"
|
||||
ATTR_STATE_CALL_FOR_HEAT = "call_for_heat"
|
||||
ATTR_STATE_LAST_CHANGE = "last_change"
|
||||
ATTR_STATE_SAVED_TEMPERATURE = "saved_temperature"
|
||||
ATTR_VALVE_POSITION = "valve_position"
|
||||
ATTR_STATE_HUMIDIY = "humidity"
|
||||
ATTR_STATE_MAIN_MODE = "main_mode"
|
||||
ATTR_STATE_HEATING_POWER = "heating_power"
|
||||
ATTR_STATE_HEATING_STATS = "heating_stats"
|
||||
ATTR_STATE_ERRORS = "errors"
|
||||
ATTR_STATE_BATTERIES = "batteries"
|
||||
|
||||
SERVICE_RESTORE_SAVED_TARGET_TEMPERATURE = "restore_saved_target_temperature"
|
||||
SERVICE_SET_TEMP_TARGET_TEMPERATURE = "set_temp_target_temperature"
|
||||
SERVICE_RESET_HEATING_POWER = "reset_heating_power"
|
||||
|
||||
BETTERTHERMOSTAT_SET_TEMPERATURE_SCHEMA = vol.All(
|
||||
cv.has_at_least_one_key(ATTR_TEMPERATURE),
|
||||
make_entity_service_schema(
|
||||
{vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float)}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class BetterThermostatEntityFeature(IntEnum):
|
||||
"""Supported features of the climate entity."""
|
||||
|
||||
TARGET_TEMPERATURE = 1
|
||||
TARGET_TEMPERATURE_RANGE = 2
|
||||
|
||||
|
||||
class CalibrationType(StrEnum):
|
||||
"""Calibration type"""
|
||||
|
||||
TARGET_TEMP_BASED = "target_temp_based"
|
||||
LOCAL_BASED = "local_calibration_based"
|
||||
|
||||
|
||||
class CalibrationMode(StrEnum):
|
||||
"""Calibration mode."""
|
||||
|
||||
DEFAULT = "default"
|
||||
FIX_CALIBRATION = "fix_calibration"
|
||||
HEATING_POWER_CALIBRATION = "heating_power_calibration"
|
||||
NO_CALIBRATION = "no_calibration"
|
||||
@@ -11,11 +11,7 @@ from homeassistant.components.homeassistant.triggers import (
|
||||
from . import DOMAIN
|
||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
HVAC_MODES,
|
||||
)
|
||||
from homeassistant.components.climate.const import HVAC_MODES
|
||||
from homeassistant.const import (
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
@@ -32,7 +28,7 @@ from homeassistant.const import (
|
||||
async def async_get_triggers(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> list[dict[str, str]]:
|
||||
"""List device triggers for Climate devices."""
|
||||
"""List device triggers for Better Thermostat devices."""
|
||||
registry = entity_registry.async_get(hass)
|
||||
triggers = []
|
||||
|
||||
@@ -42,8 +38,9 @@ async def async_get_triggers(
|
||||
continue
|
||||
|
||||
state = hass.states.get(entry.entity_id)
|
||||
if not state:
|
||||
continue
|
||||
|
||||
# Add triggers for each entity that belongs to this integration
|
||||
base_trigger = {
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DEVICE_ID: device_id,
|
||||
@@ -51,13 +48,26 @@ async def async_get_triggers(
|
||||
CONF_ENTITY_ID: entry.entity_id,
|
||||
}
|
||||
|
||||
triggers.append({**base_trigger, CONF_TYPE: "hvac_mode_changed"})
|
||||
|
||||
if state and ATTR_CURRENT_TEMPERATURE in state.attributes:
|
||||
triggers.append({**base_trigger, CONF_TYPE: "current_temperature_changed"})
|
||||
|
||||
if state and ATTR_CURRENT_HUMIDITY in state.attributes:
|
||||
triggers.append({**base_trigger, CONF_TYPE: "current_humidity_changed"})
|
||||
# Add standard climate triggers
|
||||
triggers.extend(
|
||||
[
|
||||
{
|
||||
**base_trigger,
|
||||
CONF_TYPE: "hvac_mode_changed",
|
||||
"metadata": {"secondary": False},
|
||||
},
|
||||
{
|
||||
**base_trigger,
|
||||
CONF_TYPE: "current_temperature_changed",
|
||||
"metadata": {"secondary": False},
|
||||
},
|
||||
{
|
||||
**base_trigger,
|
||||
CONF_TYPE: "current_humidity_changed",
|
||||
"metadata": {"secondary": True},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
return triggers
|
||||
|
||||
@@ -93,13 +103,13 @@ async def async_attach_trigger(
|
||||
}
|
||||
|
||||
if trigger_type == "current_temperature_changed":
|
||||
numeric_state_config[
|
||||
numeric_state_trigger.CONF_VALUE_TEMPLATE
|
||||
] = "{{ state.attributes.current_temperature }}"
|
||||
numeric_state_config[numeric_state_trigger.CONF_VALUE_TEMPLATE] = (
|
||||
"{{ state.attributes.current_temperature }}"
|
||||
)
|
||||
else:
|
||||
numeric_state_config[
|
||||
numeric_state_trigger.CONF_VALUE_TEMPLATE
|
||||
] = "{{ state.attributes.current_humidity }}"
|
||||
numeric_state_config[numeric_state_trigger.CONF_VALUE_TEMPLATE] = (
|
||||
"{{ state.attributes.current_humidity }}"
|
||||
)
|
||||
|
||||
if CONF_ABOVE in config:
|
||||
numeric_state_config[CONF_ABOVE] = config[CONF_ABOVE]
|
||||
@@ -122,31 +132,35 @@ async def async_get_trigger_capabilities(
|
||||
"""List trigger capabilities."""
|
||||
trigger_type = config[CONF_TYPE]
|
||||
|
||||
if trigger_type == "hvac_action_changed":
|
||||
return {}
|
||||
|
||||
if trigger_type == "hvac_mode_changed":
|
||||
return {
|
||||
"extra_fields": vol.Schema(
|
||||
{vol.Optional(CONF_FOR): cv.positive_time_period_dict}
|
||||
{
|
||||
vol.Required(state_trigger.CONF_TO): vol.In(HVAC_MODES),
|
||||
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if trigger_type == "current_temperature_changed":
|
||||
unit_of_measurement = hass.config.units.temperature_unit
|
||||
else:
|
||||
unit_of_measurement = PERCENTAGE
|
||||
|
||||
return {
|
||||
"extra_fields": vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_ABOVE, description={"suffix": unit_of_measurement}
|
||||
): vol.Coerce(float),
|
||||
vol.Optional(
|
||||
CONF_BELOW, description={"suffix": unit_of_measurement}
|
||||
): vol.Coerce(float),
|
||||
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
|
||||
}
|
||||
# Temperature and humidity triggers use the same schema
|
||||
if trigger_type in ["current_temperature_changed", "current_humidity_changed"]:
|
||||
unit = (
|
||||
hass.config.units.temperature_unit
|
||||
if trigger_type == "current_temperature_changed"
|
||||
else PERCENTAGE
|
||||
)
|
||||
}
|
||||
return {
|
||||
"extra_fields": vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ABOVE, description={"suffix": unit}): vol.Coerce(
|
||||
float
|
||||
),
|
||||
vol.Optional(CONF_BELOW, description={"suffix": unit}): vol.Coerce(
|
||||
float
|
||||
),
|
||||
vol.Optional(CONF_FOR): cv.positive_time_period_dict,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Diagnostics support for Brother."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .utils.bridge import load_adapter
|
||||
|
||||
from .const import CONF_HEATER, CONF_SENSOR, CONF_SENSOR_WINDOW
|
||||
from .utils.const import CONF_HEATER, CONF_SENSOR, CONF_SENSOR_WINDOW
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
@@ -18,8 +17,9 @@ async def async_get_config_entry_diagnostics(
|
||||
trv = hass.states.get(trv_id["trv"])
|
||||
if trv is None:
|
||||
continue
|
||||
_adapter_name = load_adapter(hass, trv_id["integration"], trv_id["trv"], True)
|
||||
trv_id["adapter"] = _adapter_name
|
||||
trv_id["adapter"] = trv_id["integration"]
|
||||
if trv_id["adapter"] is None:
|
||||
trv_id["adapter"] = "unknown"
|
||||
trvs[trv_id["trv"]] = {
|
||||
"name": trv.name,
|
||||
"state": trv.state,
|
||||
|
||||
@@ -29,13 +29,13 @@ async def trigger_cooler_change(self, event):
|
||||
|
||||
if None in (new_state, old_state, new_state.attributes):
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: Cooler {entity_id} update contained not all necessary data for processing, skipping"
|
||||
f"better_thermostat {self.device_name}: Cooler {entity_id} update contained not all necessary data for processing, skipping"
|
||||
)
|
||||
return
|
||||
|
||||
if not isinstance(new_state, State) or not isinstance(old_state, State):
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: Cooler {entity_id} update contained not a State, skipping"
|
||||
f"better_thermostat {self.device_name}: Cooler {entity_id} update contained not a State, skipping"
|
||||
)
|
||||
return
|
||||
# set context HACK TO FIND OUT IF AN EVENT WAS SEND BY BT
|
||||
@@ -44,7 +44,9 @@ async def trigger_cooler_change(self, event):
|
||||
if self.context == event.context:
|
||||
return
|
||||
|
||||
_LOGGER.debug(f"better_thermostat {self.name}: Cooler {entity_id} update received")
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.device_name}: Cooler {entity_id} update received"
|
||||
)
|
||||
|
||||
_main_key = "temperature"
|
||||
if "temperature" not in old_state.attributes:
|
||||
@@ -52,12 +54,12 @@ async def trigger_cooler_change(self, event):
|
||||
|
||||
_old_cooling_setpoint = convert_to_float(
|
||||
str(old_state.attributes.get(_main_key, None)),
|
||||
self.name,
|
||||
self.device_name,
|
||||
"trigger_cooler_change()",
|
||||
)
|
||||
_new_cooling_setpoint = convert_to_float(
|
||||
str(new_state.attributes.get(_main_key, None)),
|
||||
self.name,
|
||||
self.device_name,
|
||||
"trigger_cooler_change()",
|
||||
)
|
||||
if (
|
||||
@@ -66,14 +68,14 @@ async def trigger_cooler_change(self, event):
|
||||
and self.bt_hvac_mode is not HVACMode.OFF
|
||||
):
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: trigger_cooler_change / _old_cooling_setpoint: {_old_cooling_setpoint} - _new_cooling_setpoint: {_new_cooling_setpoint}"
|
||||
f"better_thermostat {self.device_name}: trigger_cooler_change / _old_cooling_setpoint: {_old_cooling_setpoint} - _new_cooling_setpoint: {_new_cooling_setpoint}"
|
||||
)
|
||||
if (
|
||||
_new_cooling_setpoint < self.bt_min_temp
|
||||
or self.bt_max_temp < _new_cooling_setpoint
|
||||
):
|
||||
_LOGGER.warning(
|
||||
f"better_thermostat {self.name}: New Cooler {entity_id} setpoint outside of range, overwriting it"
|
||||
f"better_thermostat {self.device_name}: New Cooler {entity_id} setpoint outside of range, overwriting it"
|
||||
)
|
||||
|
||||
if _new_cooling_setpoint < self.bt_min_temp:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import logging
|
||||
|
||||
from custom_components.better_thermostat.const import CONF_HOMATICIP
|
||||
from custom_components.better_thermostat.utils.const import CONF_HOMEMATICIP
|
||||
from ..utils.helpers import convert_to_float
|
||||
from datetime import datetime
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import callback
|
||||
@@ -33,18 +34,36 @@ async def trigger_temperature_change(self, event):
|
||||
return
|
||||
|
||||
_incoming_temperature = convert_to_float(
|
||||
str(new_state.state), self.name, "external_temperature"
|
||||
str(new_state.state), self.device_name, "external_temperature"
|
||||
)
|
||||
|
||||
_time_diff = 5
|
||||
_time_diff = 60
|
||||
|
||||
try:
|
||||
for trv in self.all_trvs:
|
||||
if trv["advanced"][CONF_HOMATICIP]:
|
||||
if trv["advanced"][CONF_HOMEMATICIP]:
|
||||
_time_diff = 600
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if _incoming_temperature is None or _incoming_temperature < -50:
|
||||
# raise a ha repair notication
|
||||
_LOGGER.error(
|
||||
"better_thermostat %s: external_temperature is not a valid number: %s",
|
||||
self.device_name,
|
||||
new_state.state,
|
||||
)
|
||||
ir.async_create_issue(
|
||||
hass=self.hass,
|
||||
issue_id=f"missing_entity_{self.device_name}",
|
||||
issue_title=f"better_thermostat {self.device_name} has invalid external_temperature value",
|
||||
issue_severity="error",
|
||||
issue_description=f"better_thermostat {self.device_name} has invalid external_temperature: {new_state.state}",
|
||||
issue_category="config",
|
||||
issue_suggested_action="Please check the external_temperature sensor",
|
||||
)
|
||||
return
|
||||
|
||||
if (
|
||||
_incoming_temperature != self.cur_temp
|
||||
and (datetime.now() - self.last_external_sensor_change).total_seconds()
|
||||
@@ -52,7 +71,7 @@ async def trigger_temperature_change(self, event):
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"better_thermostat %s: external_temperature changed from %s to %s",
|
||||
self.name,
|
||||
self.device_name,
|
||||
self.cur_temp,
|
||||
_incoming_temperature,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Union
|
||||
from custom_components.better_thermostat.const import CONF_HOMATICIP
|
||||
from custom_components.better_thermostat.utils.const import CONF_HOMEMATICIP
|
||||
|
||||
from homeassistant.components.climate.const import (
|
||||
HVACMode,
|
||||
@@ -11,14 +10,21 @@ from homeassistant.components.climate.const import (
|
||||
)
|
||||
from homeassistant.core import State, callback
|
||||
from homeassistant.components.group.util import find_state_attributes
|
||||
from ..utils.helpers import (
|
||||
calculate_local_setpoint_delta,
|
||||
calculate_setpoint_override,
|
||||
from custom_components.better_thermostat.utils.helpers import (
|
||||
convert_to_float,
|
||||
mode_remap,
|
||||
round_to_half_degree,
|
||||
)
|
||||
from custom_components.better_thermostat.utils.bridge import get_current_offset
|
||||
from custom_components.better_thermostat.adapters.delegate import get_current_offset
|
||||
|
||||
from custom_components.better_thermostat.utils.const import (
|
||||
CalibrationType,
|
||||
CalibrationMode,
|
||||
)
|
||||
|
||||
from custom_components.better_thermostat.calibration import (
|
||||
calculate_calibration_local,
|
||||
calculate_calibration_setpoint,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,6 +36,8 @@ async def trigger_trv_change(self, event):
|
||||
return
|
||||
if self.control_queue_task is None:
|
||||
return
|
||||
if self.bt_target_temp is None or self.cur_temp is None or self.tolerance is None:
|
||||
return
|
||||
asyncio.create_task(update_hvac_action(self))
|
||||
_main_change = False
|
||||
old_state = event.data.get("old_state")
|
||||
@@ -38,13 +46,13 @@ async def trigger_trv_change(self, event):
|
||||
|
||||
if None in (new_state, old_state, new_state.attributes):
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: TRV {entity_id} update contained not all necessary data for processing, skipping"
|
||||
f"better_thermostat {self.device_name}: TRV {entity_id} update contained not all necessary data for processing, skipping"
|
||||
)
|
||||
return
|
||||
|
||||
if not isinstance(new_state, State) or not isinstance(old_state, State):
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: TRV {entity_id} update contained not a State, skipping"
|
||||
f"better_thermostat {self.device_name}: TRV {entity_id} update contained not a State, skipping"
|
||||
)
|
||||
return
|
||||
# set context HACK TO FIND OUT IF AN EVENT WAS SEND BY BT
|
||||
@@ -53,21 +61,21 @@ async def trigger_trv_change(self, event):
|
||||
if self.context == event.context:
|
||||
return
|
||||
|
||||
_LOGGER.debug(f"better_thermostat {self.name}: TRV {entity_id} update received")
|
||||
# _LOGGER.debug(f"better_thermostat {self.device_name}: TRV {entity_id} update received")
|
||||
|
||||
_org_trv_state = self.hass.states.get(entity_id)
|
||||
child_lock = self.real_trvs[entity_id]["advanced"].get("child_lock")
|
||||
|
||||
_new_current_temp = convert_to_float(
|
||||
str(_org_trv_state.attributes.get("current_temperature", None)),
|
||||
self.name,
|
||||
self.device_name,
|
||||
"TRV_current_temp",
|
||||
)
|
||||
|
||||
_time_diff = 5
|
||||
try:
|
||||
for trv in self.all_trvs:
|
||||
if trv["advanced"][CONF_HOMATICIP]:
|
||||
if trv["advanced"][CONF_HOMEMATICIP]:
|
||||
_time_diff = 600
|
||||
except KeyError:
|
||||
pass
|
||||
@@ -79,14 +87,14 @@ async def trigger_trv_change(self, event):
|
||||
> _time_diff
|
||||
or (
|
||||
self.real_trvs[entity_id]["calibration_received"] is False
|
||||
and self.real_trvs[entity_id]["calibration"] == 0
|
||||
and self.real_trvs[entity_id]["calibration"] != 1
|
||||
)
|
||||
)
|
||||
):
|
||||
_old_temp = self.real_trvs[entity_id]["current_temperature"]
|
||||
self.real_trvs[entity_id]["current_temperature"] = _new_current_temp
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: TRV {entity_id} sends new internal temperature from {_old_temp} to {_new_current_temp}"
|
||||
f"better_thermostat {self.device_name}: TRV {entity_id} sends new internal temperature from {_old_temp} to {_new_current_temp}"
|
||||
)
|
||||
self.last_internal_sensor_change = datetime.now()
|
||||
_main_change = True
|
||||
@@ -95,15 +103,13 @@ async def trigger_trv_change(self, event):
|
||||
if self.real_trvs[entity_id]["calibration_received"] is False:
|
||||
self.real_trvs[entity_id]["calibration_received"] = True
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: calibration accepted by TRV {entity_id}"
|
||||
f"better_thermostat {self.device_name}: calibration accepted by TRV {entity_id}"
|
||||
)
|
||||
_main_change = False
|
||||
self.old_internal_temp = self.real_trvs[entity_id]["current_temperature"]
|
||||
self.old_external_temp = self.cur_temp
|
||||
if self.real_trvs[entity_id]["calibration"] == 0:
|
||||
self.real_trvs[entity_id][
|
||||
"last_calibration"
|
||||
] = await get_current_offset(self, entity_id)
|
||||
self.real_trvs[entity_id]["last_calibration"] = (
|
||||
await get_current_offset(self, entity_id)
|
||||
)
|
||||
|
||||
if self.ignore_states:
|
||||
return
|
||||
@@ -112,7 +118,7 @@ async def trigger_trv_change(self, event):
|
||||
mapped_state = convert_inbound_states(self, entity_id, _org_trv_state)
|
||||
except TypeError:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: remapping TRV {entity_id} state failed, skipping"
|
||||
f"better_thermostat {self.device_name}: remapping TRV {entity_id} state failed, skipping"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -123,7 +129,7 @@ async def trigger_trv_change(self, event):
|
||||
):
|
||||
_old = self.real_trvs[entity_id]["hvac_mode"]
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: TRV {entity_id} decoded TRV mode changed from {_old} to {_org_trv_state.state} - converted {new_state.state}"
|
||||
f"better_thermostat {self.device_name}: TRV {entity_id} decoded TRV mode changed from {_old} to {_org_trv_state.state} - converted {new_state.state}"
|
||||
)
|
||||
self.real_trvs[entity_id]["hvac_mode"] = _org_trv_state.state
|
||||
_main_change = True
|
||||
@@ -136,16 +142,16 @@ async def trigger_trv_change(self, event):
|
||||
|
||||
_main_key = "temperature"
|
||||
if "temperature" not in old_state.attributes:
|
||||
_main_key = "target_temp_high"
|
||||
_main_key = "target_temp_low"
|
||||
|
||||
_old_heating_setpoint = convert_to_float(
|
||||
str(old_state.attributes.get(_main_key, None)),
|
||||
self.name,
|
||||
self.device_name,
|
||||
"trigger_trv_change()",
|
||||
)
|
||||
_new_heating_setpoint = convert_to_float(
|
||||
str(new_state.attributes.get(_main_key, None)),
|
||||
self.name,
|
||||
self.device_name,
|
||||
"trigger_trv_change()",
|
||||
)
|
||||
if (
|
||||
@@ -154,14 +160,14 @@ async def trigger_trv_change(self, event):
|
||||
and self.bt_hvac_mode is not HVACMode.OFF
|
||||
):
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: trigger_trv_change / _old_heating_setpoint: {_old_heating_setpoint} - _new_heating_setpoint: {_new_heating_setpoint} - _last_temperature: {self.real_trvs[entity_id]['last_temperature']}"
|
||||
f"better_thermostat {self.device_name}: trigger_trv_change test / _old_heating_setpoint: {_old_heating_setpoint} - _new_heating_setpoint: {_new_heating_setpoint} - _last_temperature: {self.real_trvs[entity_id]['last_temperature']}"
|
||||
)
|
||||
if (
|
||||
_new_heating_setpoint < self.bt_min_temp
|
||||
or self.bt_max_temp < _new_heating_setpoint
|
||||
):
|
||||
_LOGGER.warning(
|
||||
f"better_thermostat {self.name}: New TRV {entity_id} setpoint outside of range, overwriting it"
|
||||
f"better_thermostat {self.device_name}: New TRV {entity_id} setpoint outside of range, overwriting it"
|
||||
)
|
||||
|
||||
if _new_heating_setpoint < self.bt_min_temp:
|
||||
@@ -180,7 +186,7 @@ async def trigger_trv_change(self, event):
|
||||
and self.window_open is False
|
||||
):
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: TRV {entity_id} decoded TRV target temp changed from {self.bt_target_temp} to {_new_heating_setpoint}"
|
||||
f"better_thermostat {self.device_name}: TRV {entity_id} decoded TRV target temp changed from {self.bt_target_temp} to {_new_heating_setpoint}"
|
||||
)
|
||||
self.bt_target_temp = _new_heating_setpoint
|
||||
if self.cooler_entity_id is not None:
|
||||
@@ -210,6 +216,9 @@ async def trigger_trv_change(self, event):
|
||||
|
||||
|
||||
async def update_hvac_action(self):
|
||||
self.bt_target_temp = 5.0 if self.bt_target_temp is None else self.bt_target_temp
|
||||
self.cur_temp = 5.0 if self.cur_temp is None else self.cur_temp
|
||||
self.tolerance = 0.0 if self.tolerance is None else self.tolerance
|
||||
"""Update the hvac action."""
|
||||
if self.startup_running or self.control_queue_task is None:
|
||||
return
|
||||
@@ -218,7 +227,7 @@ async def update_hvac_action(self):
|
||||
|
||||
# i don't know why this is here just for hometicip / wtom - 2023-08-23
|
||||
# for trv in self.all_trvs:
|
||||
# if trv["advanced"][CONF_HOMATICIP]:
|
||||
# if trv["advanced"][CONF_HOMEMATICIP]:
|
||||
# entity_id = trv["trv"]
|
||||
# state = self.hass.states.get(entity_id)
|
||||
# if state is None:
|
||||
@@ -290,7 +299,7 @@ def convert_inbound_states(self, entity_id, state: State) -> str:
|
||||
return remapped_state
|
||||
|
||||
|
||||
def convert_outbound_states(self, entity_id, hvac_mode) -> Union[dict, None]:
|
||||
def convert_outbound_states(self, entity_id, hvac_mode) -> dict | None:
|
||||
"""Creates the new outbound thermostat state.
|
||||
Parameters
|
||||
----------
|
||||
@@ -314,90 +323,60 @@ def convert_outbound_states(self, entity_id, hvac_mode) -> Union[dict, None]:
|
||||
_new_heating_setpoint = None
|
||||
|
||||
try:
|
||||
_calibration_type = self.real_trvs[entity_id].get("calibration", 1)
|
||||
_calibration_type = self.real_trvs[entity_id]["advanced"].get("calibration")
|
||||
_calibration_mode = self.real_trvs[entity_id]["advanced"].get(
|
||||
"calibration_mode"
|
||||
)
|
||||
|
||||
if _calibration_type is None:
|
||||
_LOGGER.warning(
|
||||
"better_thermostat %s: no calibration type found in device config, talking to the TRV using fallback mode",
|
||||
self.name,
|
||||
self.device_name,
|
||||
)
|
||||
_new_heating_setpoint = self.bt_target_temp
|
||||
_new_local_calibration = round_to_half_degree(
|
||||
calculate_local_setpoint_delta(self, entity_id)
|
||||
)
|
||||
_new_local_calibration = calculate_calibration_local(self, entity_id)
|
||||
|
||||
if _new_local_calibration is None:
|
||||
return None
|
||||
|
||||
else:
|
||||
if _calibration_type == 0:
|
||||
_round_calibration = self.real_trvs[entity_id]["advanced"].get(
|
||||
"calibration_round"
|
||||
)
|
||||
|
||||
if _round_calibration is not None and (
|
||||
(
|
||||
isinstance(_round_calibration, str)
|
||||
and _round_calibration.lower() == "true"
|
||||
)
|
||||
or _round_calibration is True
|
||||
):
|
||||
_new_local_calibration = round_to_half_degree(
|
||||
calculate_local_setpoint_delta(self, entity_id)
|
||||
)
|
||||
else:
|
||||
_new_local_calibration = calculate_local_setpoint_delta(
|
||||
self, entity_id
|
||||
)
|
||||
if _calibration_type == CalibrationType.LOCAL_BASED:
|
||||
_new_local_calibration = calculate_calibration_local(self, entity_id)
|
||||
|
||||
_new_heating_setpoint = self.bt_target_temp
|
||||
|
||||
elif _calibration_type == 1:
|
||||
_round_calibration = self.real_trvs[entity_id]["advanced"].get(
|
||||
"calibration_round"
|
||||
)
|
||||
|
||||
if _round_calibration is not None and (
|
||||
(
|
||||
isinstance(_round_calibration, str)
|
||||
and _round_calibration.lower() == "true"
|
||||
)
|
||||
or _round_calibration is True
|
||||
):
|
||||
_new_heating_setpoint = round_to_half_degree(
|
||||
calculate_setpoint_override(self, entity_id)
|
||||
)
|
||||
elif _calibration_type == CalibrationType.TARGET_TEMP_BASED:
|
||||
if _calibration_mode == CalibrationMode.NO_CALIBRATION:
|
||||
_new_heating_setpoint = self.bt_target_temp
|
||||
else:
|
||||
_new_heating_setpoint = calculate_setpoint_override(self, entity_id)
|
||||
_new_heating_setpoint = calculate_calibration_setpoint(
|
||||
self, entity_id
|
||||
)
|
||||
|
||||
_system_modes = self.real_trvs[entity_id]["hvac_modes"]
|
||||
_has_system_mode = False
|
||||
if _system_modes is not None:
|
||||
_has_system_mode = True
|
||||
_has_system_mode = _system_modes is not None
|
||||
|
||||
# Handling different devices with or without system mode reported or contained in the device config
|
||||
|
||||
hvac_mode = mode_remap(self, entity_id, str(hvac_mode), False)
|
||||
|
||||
if _has_system_mode is False:
|
||||
if not _has_system_mode:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: device config expects no system mode, while the device has one. Device system mode will be ignored"
|
||||
f"better_thermostat {self.device_name}: device config expects no system mode, while the device has one. Device system mode will be ignored"
|
||||
)
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
_new_heating_setpoint = self.real_trvs[entity_id]["min_temp"]
|
||||
hvac_mode = None
|
||||
if (
|
||||
if hvac_mode == HVACMode.OFF and (
|
||||
HVACMode.OFF not in _system_modes
|
||||
or self.real_trvs[entity_id]["advanced"].get(
|
||||
"no_off_system_mode", False
|
||||
)
|
||||
is True
|
||||
or self.real_trvs[entity_id]["advanced"].get("no_off_system_mode")
|
||||
):
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: sending 5°C to the TRV because this device has no system mode off and heater should be off"
|
||||
)
|
||||
_new_heating_setpoint = self.real_trvs[entity_id]["min_temp"]
|
||||
hvac_mode = None
|
||||
_min_temp = self.real_trvs[entity_id]["min_temp"]
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.device_name}: sending {_min_temp}°C to the TRV because this device has no system mode off and heater should be off"
|
||||
)
|
||||
_new_heating_setpoint = _min_temp
|
||||
hvac_mode = None
|
||||
|
||||
return {
|
||||
"temperature": _new_heating_setpoint,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from custom_components.better_thermostat import DOMAIN
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import STATE_OFF
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,7 +39,7 @@ async def trigger_window_change(self, event) -> None:
|
||||
if new_state == "unknown":
|
||||
_LOGGER.warning(
|
||||
"better_thermostat %s: Window sensor state is unknown, assuming window is open",
|
||||
self.name,
|
||||
self.device_name,
|
||||
)
|
||||
|
||||
# window was opened, disable heating power calculation for this period
|
||||
@@ -47,45 +49,65 @@ async def trigger_window_change(self, event) -> None:
|
||||
new_window_open = False
|
||||
else:
|
||||
_LOGGER.error(
|
||||
f"better_thermostat {self.name}: New window sensor state '{new_state}' not recognized"
|
||||
f"better_thermostat {self.device_name}: New window sensor state '{new_state}' not recognized"
|
||||
)
|
||||
ir.async_create_issue(
|
||||
hass=self.hass,
|
||||
domain=DOMAIN,
|
||||
issue_id=f"missing_entity_{self.device_name}",
|
||||
issue_title=f"better_thermostat {self.device_name} has invalid window sensor state",
|
||||
issue_severity="error",
|
||||
issue_description=f"better_thermostat {self.device_name} has invalid window sensor state: {new_state}",
|
||||
issue_category="config",
|
||||
issue_suggested_action="Please check the window sensor",
|
||||
)
|
||||
return
|
||||
|
||||
# make sure to skip events which do not change the saved window state:
|
||||
if new_window_open == old_window_open:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: Window state did not change, skipping event"
|
||||
f"better_thermostat {self.device_name}: Window state did not change, skipping event"
|
||||
)
|
||||
return
|
||||
await self.window_queue_task.put(new_window_open)
|
||||
|
||||
|
||||
async def window_queue(self):
|
||||
while True:
|
||||
window_event_to_process = await self.window_queue_task.get()
|
||||
if window_event_to_process is not None:
|
||||
if window_event_to_process:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: Window opened, waiting {self.window_delay} seconds before continuing"
|
||||
)
|
||||
await asyncio.sleep(self.window_delay)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: Window closed, waiting {self.window_delay_after} seconds before continuing"
|
||||
)
|
||||
await asyncio.sleep(self.window_delay_after)
|
||||
# remap off on to true false
|
||||
current_window_state = True
|
||||
if self.hass.states.get(self.window_id).state == STATE_OFF:
|
||||
current_window_state = False
|
||||
# make sure the current state is the suggested change state to prevent a false positive:
|
||||
if current_window_state == window_event_to_process:
|
||||
self.window_open = window_event_to_process
|
||||
self.async_write_ha_state()
|
||||
if not self.control_queue_task.empty():
|
||||
empty_queue(self.control_queue_task)
|
||||
await self.control_queue_task.put(self)
|
||||
self.window_queue_task.task_done()
|
||||
try:
|
||||
while True:
|
||||
window_event_to_process = await self.window_queue_task.get()
|
||||
try:
|
||||
if window_event_to_process is not None:
|
||||
if window_event_to_process:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.device_name}: Window opened, waiting {self.window_delay} seconds before continuing"
|
||||
)
|
||||
await asyncio.sleep(self.window_delay)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.device_name}: Window closed, waiting {self.window_delay_after} seconds before continuing"
|
||||
)
|
||||
await asyncio.sleep(self.window_delay_after)
|
||||
# remap off on to true false
|
||||
current_window_state = True
|
||||
if self.hass.states.get(self.window_id).state == STATE_OFF:
|
||||
current_window_state = False
|
||||
# make sure the current state is the suggested change state to prevent a false positive:
|
||||
if current_window_state == window_event_to_process:
|
||||
self.window_open = window_event_to_process
|
||||
self.async_write_ha_state()
|
||||
if not self.control_queue_task.empty():
|
||||
empty_queue(self.control_queue_task)
|
||||
await self.control_queue_task.put(self)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
finally:
|
||||
self.window_queue_task.task_done()
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.device_name}: Window queue task cancelled"
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def empty_queue(q: asyncio.Queue):
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"domain": "better_thermostat",
|
||||
"name": "Better Thermostat",
|
||||
"after_dependencies": [
|
||||
"climate"
|
||||
"climate",
|
||||
"zwave_js"
|
||||
],
|
||||
"codeowners": [
|
||||
"@kartoffeltoby"
|
||||
@@ -16,5 +17,5 @@
|
||||
"iot_class": "local_push",
|
||||
"issue_tracker": "https://github.com/KartoffelToby/better_thermostat/issues",
|
||||
"requirements": [],
|
||||
"version": "1.4.0"
|
||||
"version": "1.7.0"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
def fix_local_calibration(self, entity_id, offset):
|
||||
if (self.cur_temp - 0.5) <= self.bt_target_temp:
|
||||
offset -= 2.5
|
||||
elif (self.cur_temp + 0.10) >= self.bt_target_temp:
|
||||
_cur_external_temp = self.cur_temp
|
||||
_target_temp = self.bt_target_temp
|
||||
|
||||
if (_cur_external_temp + 0.1) >= _target_temp:
|
||||
offset = round(offset + 0.5, 1)
|
||||
elif (_cur_external_temp + 0.5) >= _target_temp:
|
||||
offset -= 2.5
|
||||
|
||||
return offset
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
def fix_local_calibration(self, entity_id, offset):
|
||||
if (self.cur_temp - 0.5) <= self.bt_target_temp:
|
||||
offset -= 2.5
|
||||
elif (self.cur_temp + 0.10) >= self.bt_target_temp:
|
||||
_cur_external_temp = self.cur_temp
|
||||
_target_temp = self.bt_target_temp
|
||||
|
||||
if (_cur_external_temp + 0.1) >= _target_temp:
|
||||
offset = round(offset + 0.5, 1)
|
||||
elif (_cur_external_temp + 0.5) >= _target_temp:
|
||||
offset -= 2.5
|
||||
|
||||
return offset
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ async def override_set_hvac_mode(self, entity_id, hvac_mode):
|
||||
model = self.real_trvs[entity_id]["model"]
|
||||
if model == "TV02-Zigbee" and hvac_mode != HVACMode.OFF:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: TRV {entity_id} device quirk hvac trv02-zigbee active"
|
||||
f"better_thermostat {self.device_name}: TRV {entity_id} device quirk hvac trv02-zigbee active"
|
||||
)
|
||||
await self.hass.services.async_call(
|
||||
"climate",
|
||||
@@ -66,7 +66,7 @@ async def override_set_temperature(self, entity_id, temperature):
|
||||
model = self.real_trvs[entity_id]["model"]
|
||||
if model == "TV02-Zigbee":
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: TRV {entity_id} device quirk trv02-zigbee active"
|
||||
f"better_thermostat {self.device_name}: TRV {entity_id} device quirk trv02-zigbee active"
|
||||
)
|
||||
await self.hass.services.async_call(
|
||||
"climate",
|
||||
|
||||
@@ -1,52 +1,43 @@
|
||||
save_current_target_temperature:
|
||||
name: Save current Temperature
|
||||
description: Save the current target temperature for later restore.
|
||||
name: Save target temperature
|
||||
description: Save the current target temperature for later use.
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
integration: better_thermostat
|
||||
device:
|
||||
integration: better_thermostat
|
||||
|
||||
restore_saved_target_temperature:
|
||||
name: Restore temperature
|
||||
description: Restore the saved target temperature.
|
||||
name: Restore target temperature
|
||||
description: Restore the previously saved target temperature.
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
integration: better_thermostat
|
||||
device:
|
||||
integration: better_thermostat
|
||||
|
||||
reset_heating_power:
|
||||
name: Reset heating power
|
||||
description: Reset heating power to default value.
|
||||
description: Reset the heating power to its default value.
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
integration: better_thermostat
|
||||
device:
|
||||
integration: better_thermostat
|
||||
|
||||
set_temp_target_temperature:
|
||||
name: Set eco temperature
|
||||
description: Set the target temperature to a temporay like night mode, and save the old one.
|
||||
name: Set temporary (ECO) target temperature
|
||||
description: Set a temporary target temperature and save the current temperature.
|
||||
fields:
|
||||
temperature:
|
||||
name: Target temperature
|
||||
description: The target temperature to set.
|
||||
example: 18.5
|
||||
name: Temperature
|
||||
description: New target temperature to set
|
||||
required: true
|
||||
advanced: true
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 35
|
||||
step: 0.5
|
||||
mode: box
|
||||
unit_of_measurement: °C
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
integration: better_thermostat
|
||||
device:
|
||||
integration: better_thermostat
|
||||
|
||||
@@ -2,38 +2,38 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Setup your Better Thermostat to integrate with Home Assistant\n**If you need more info: https://better-thermostat.org/configuration#first-step** ",
|
||||
"description": "Setup your Better Thermostat to integrate with Home Assistant\n**If you need more info: https://better-thermostat.org/configuration#first-step**",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat": "The real thermostat",
|
||||
"cooler": "The cooling device (optional) | Preview BETA",
|
||||
"cooler": "The cooling device (optional)",
|
||||
"temperature_sensor": "Temperature sensor",
|
||||
"humidity_sensor": "Humidity sensor",
|
||||
"window_sensors": "Window sensor",
|
||||
"off_temperature": "The outdoor temperature when the thermostat turn off",
|
||||
"tolerance": "Tolerance, to prevent the thermostat from turning on and off too often.",
|
||||
"window_off_delay": "Delay before the thermostat turn off when the window is opened",
|
||||
"window_off_delay_after": "Delay before the thermostat turn on when the window is closed",
|
||||
"outdoor_sensor": "If you have an outdoor sensor, you can use it to get the outdoor temperature",
|
||||
"weather": "Your weather entity to get the outdoor temperature"
|
||||
"off_temperature": "The outdoor temperature when the thermostat should turn off",
|
||||
"tolerance": "Tolerance, to prevent the thermostat from turning on and off too often",
|
||||
"window_off_delay": "Delay before the thermostat should turn off when the window is opened",
|
||||
"window_off_delay_after": "Delay before the thermostat should turn on when the window is closed",
|
||||
"outdoor_sensor": "Outdoor temperature sensor",
|
||||
"weather": "Weather entity to get the outdoor temperature"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"description": "Advanced configuration {trv}\n***Info about calibration types: https://better-thermostat.org/configuration#second-step***",
|
||||
"data": {
|
||||
"protect_overheating": "Overheating protection?",
|
||||
"heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it",
|
||||
"heat_auto_swapped": "If 'auto' means 'heat' for your TRV and you want to swap it",
|
||||
"child_lock": "Ignore all inputs on the TRV like a child lock",
|
||||
"homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle",
|
||||
"homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle",
|
||||
"valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one",
|
||||
"calibration": "Calibration Type",
|
||||
"calibration": "Calibration type",
|
||||
"calibration_mode": "Calibration mode",
|
||||
"no_off_system_mode": "If your TRV can't handle the off mode, you can enable this to use target temperature 5°C instead"
|
||||
"no_off_system_mode": "If your TRV doesn't support the 'off' mode, enable this to use target temperature 5°C instead"
|
||||
},
|
||||
"data_description": {
|
||||
"protect_overheating": "Some TRVs doest close the valve completly when the temperature is reached. Or the radiator have a lot of rest heat. This can cause overheating. This option can prevent this.",
|
||||
"calibration_mode": "The kind how the calibration should be calculated\n***Normal***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.",
|
||||
"calibration": "How the calibration should be applied on the TRV (Target temp or offset)\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the offset."
|
||||
"protect_overheating": "Some TRVs don't close the valve completely when the temperature is reached. Or the radiator has a lot of rest heat. This can cause overheating. This option can prevent this.",
|
||||
"calibration_mode": "How the calibration is calculated\n***Normal***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.",
|
||||
"calibration": "How the calibration is applied on the TRV\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the temperature offset."
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
@@ -42,14 +42,14 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"failed": "something went wrong.",
|
||||
"failed": "Something went wrong.",
|
||||
"no_name": "Please enter a name.",
|
||||
"no_off_mode": "You device is very special and has no off mode :(\nBetter Thermostat will use the minimal target temp instead.",
|
||||
"no_off_mode": "Your device is special and has no off mode :(\nBetter Thermostat will use the minimal target temp instead.",
|
||||
"no_outside_temp": "You have no outside temperature sensor. Better Thermostat will use the weather entity instead."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single Thermostat for each real is allowed.",
|
||||
"no_devices_found": "No thermostat entity found, make sure you have a climate entity in your home assistant"
|
||||
"single_instance_allowed": "Only a single Better Thermostat is allowed for each real thermostat.",
|
||||
"no_devices_found": "No thermostat entity found, make sure you have a climate entity in your Home Assistant"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -62,17 +62,17 @@
|
||||
"temperature_sensor": "Temperature Sensor",
|
||||
"humidity_sensor": "Humidity sensor",
|
||||
"window_sensors": "Window Sensor",
|
||||
"off_temperature": "The outdoor temperature when the thermostat turn off",
|
||||
"tolerance": "Tolerance, to prevent the thermostat from turning on and off too often.",
|
||||
"window_off_delay": "Delay before the thermostat turn off when the window is opened",
|
||||
"window_off_delay_after": "Delay before the thermostat turn on when the window is closed",
|
||||
"outdoor_sensor": "If you have an outdoor sensor, you can use it to get the outdoor temperature",
|
||||
"off_temperature": "The outdoor temperature when the thermostat should turn off",
|
||||
"tolerance": "Tolerance, to prevent the thermostat from turning on and off too often",
|
||||
"window_off_delay": "Delay before the thermostat should turn off when the window is opened",
|
||||
"window_off_delay_after": "Delay before the thermostat should turn on when the window is closed",
|
||||
"outdoor_sensor": "Outdoor temperature sensor",
|
||||
"weather": "Weather entity to get the outdoor temperature",
|
||||
"valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one",
|
||||
"calibration": "The sort of calibration https://better-thermostat.org/configuration#second-step",
|
||||
"weather": "Your weather entity to get the outdoor temperature",
|
||||
"heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it",
|
||||
"child_lock": "Ignore all inputs on the TRV like a child lock",
|
||||
"homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle"
|
||||
"homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -81,17 +81,16 @@
|
||||
"protect_overheating": "Overheating protection?",
|
||||
"heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it",
|
||||
"child_lock": "Ignore all inputs on the TRV like a child lock",
|
||||
"homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle",
|
||||
"homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle",
|
||||
"valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one",
|
||||
"calibration": "The sort of calibration you want to use",
|
||||
"calibration": "Calibration type",
|
||||
"calibration_mode": "Calibration mode",
|
||||
"no_off_system_mode": "If your TRV can't handle the off mode, you can enable this to use target temperature 5°C instead"
|
||||
"no_off_system_mode": "If your TRV doesn't support the 'off' mode, enable this to use target temperature 5°C instead"
|
||||
},
|
||||
"data_description": {
|
||||
"protect_overheating": "Some TRVs doest close the valve completly when the temperature is reached. Or the radiator have a lot of rest heat. This can cause overheating. This option can prevent this.",
|
||||
"calibration_mode": "The kind how the calibration should be calculated\n***Normal***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.",
|
||||
"calibration": "How the calibration should be applied on the TRV (Target temp or offset)\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the offset."
|
||||
}
|
||||
"protect_overheating": "Some TRVs don't close the valve completely when the temperature is reached. Or the radiator has a lot of rest heat. This can cause overheating. This option can prevent this.",
|
||||
"calibration_mode": "How the calibration is calculated\n***Normal***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.",
|
||||
"calibration": "How the calibration is applied on the TRV\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the temperature offset." }
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -102,7 +101,7 @@
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "The related entity {entity} is missing",
|
||||
"description": "The reason for this is that the entity ({entity}) is not available in your Home Assistant.\n\nYou can fix this by checking if the batterie of the device is full or reconnect it to HA. Make sure that the entity is back in HA before you continue."
|
||||
"description": "The reason for this is that the entity ({entity}) is not available in your Home Assistant.\n\nYou can fix this by checking if the battery of the device is full or reconnecting it to HA. Make sure that the entity is back in HA before you continue."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +122,7 @@
|
||||
},
|
||||
"set_temp_target_temperature": {
|
||||
"name": "Set eco temperature",
|
||||
"description": "Set the target temperature to a temporay like night mode, and save the old one."
|
||||
"description": "Set the target temperature to a temporary like night mode, and save the old one."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,30 +7,33 @@
|
||||
"data": {
|
||||
"name": "Navn",
|
||||
"thermostat": "Den rigtige termostat",
|
||||
"cooler": "Køleenhed (valgfri)",
|
||||
"temperature_sensor": "Temperatur måler",
|
||||
"humidity_sensor": "Fugtighedssensor",
|
||||
"window_sensors": "Vinduessensor",
|
||||
"off_temperature": "Udetemperaturen, når termostaten slukker",
|
||||
"tolerance": "Tolerancen for at forhindre, at termostaten tænder og slukker for ofte.",
|
||||
"window_off_delay": "Forsinkelse, før termostaten slukker, når vinduet er åbent, og tændt, når vinduet er lukket",
|
||||
"window_off_delay_after": "Forsinkelse, før termostaten tænder, når vinduet er lukket",
|
||||
"outdoor_sensor": "Hvis du har en udeføler, kan du bruge den til at få udetemperaturen",
|
||||
"weather": "Din vejrentitet for at få udendørstemperaturen"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"description": "Avanceret konfiguration\n\n**Info om kalibreringstyper: https://better-thermostat.org/configuration#second-step** ",
|
||||
"description": "Avanceret konfiguration {trv}\n***Info om kalibreringstyper: https://better-thermostat.org/configuration#second-step*** ",
|
||||
"data": {
|
||||
"protect_overheating": "Overophedningsbeskyttelse?",
|
||||
"heat_auto_swapped": "Hvis auto betyder varme for din TRV og du vil bytte det",
|
||||
"child_lock": "Ignorer alle input på TRV som en børnesikring",
|
||||
"homaticip": "Hvis du bruger HomaticIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus",
|
||||
"homematicip": "Hvis du bruger HomematicIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus",
|
||||
"valve_maintenance": "Hvis din termostat ikke har nogen egen vedligeholdelsestilstand, kan du bruge denne",
|
||||
"calibration": "Den slags kalibrering du vil bruge",
|
||||
"no_off_system_mode": "Hvis din TRV ikke kan håndtere den slukkede tilstand, kan du aktivere denne for at bruge måltemperatur 5°C i stedet",
|
||||
"calibration_mode": "Kalibreringstilstand",
|
||||
"protect_overheating": "Overophedningsbeskyttelse?"
|
||||
"no_off_system_mode": "Hvis din TRV ikke kan håndtere den slukkede tilstand, kan du aktivere denne for at bruge måltemperatur 5°C i stedet"
|
||||
},
|
||||
"data_description": {
|
||||
"protect_overheating": "Nogle TRV'er lukker ikke ventilen helt, når temperaturen er nået. Eller radiatoren har meget hvilevarme. Dette kan forårsage overophedning. Denne mulighed kan forhindre dette.",
|
||||
"calibration_mode": "Den slags, hvordan kalibreringen skal beregnes\n***Normal***: I denne tilstand er TRV's interne temperaturføler fastgjort af den eksterne temperaturføler.\n***Aggressiv***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler, men indstillet meget lavere/højere for at få et hurtigere løft.\n***AI-tidsbaseret***: I denne tilstand er TRV's interne temperatursensor fastsat af den eksterne temperatursensor, men værdien beregnes af en brugerdefineret algoritme for at forbedre TRV's interne algoritme.",
|
||||
"calibration_mode": "Hvordan kalibreringen skal beregnes\n***Normal***: I denne tilstand er TRV's interne temperaturføler fastgjort af den eksterne temperaturføler.\n***Aggressiv***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler, men indstillet meget lavere/højere for at få et hurtigere løft.\n***AI-tidsbaseret***: I denne tilstand er TRV's interne temperatursensor fastsat af den eksterne temperatursensor, men værdien beregnes af en brugerdefineret algoritme for at forbedre TRV's interne algoritme.",
|
||||
"calibration": "Hvordan kalibreringen skal anvendes på TRV (Target temp or offset)\n***Måltemperaturbaseret***: Anvend kalibreringen til måltemperaturen.\n***Offset-baseret***: Anvend kalibreringen til offset."
|
||||
}
|
||||
},
|
||||
@@ -40,10 +43,10 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"no_outside_temp": "Du har ingen udetemperaturføler. Bedre termostat vil bruge vejret i stedet.",
|
||||
"failed": "noget gik galt.",
|
||||
"failed": "Noget gik galt.",
|
||||
"no_name": "Indtast venligst et navn.",
|
||||
"no_off_mode": "Din enhed er meget speciel og har ingen slukket tilstand :(\nDet virker alligevel, men du skal oprette en automatisering, der passer til dine specialer baseret på enhedsbegivenhederne"
|
||||
"no_off_mode": "Din enhed er meget speciel og har ingen slukket tilstand :(\nDet virker alligevel, men du skal oprette en automatisering, der passer til dine specialer baseret på enhedsbegivenhederne",
|
||||
"no_outside_temp": "Du har ingen udetemperaturføler. Better Thermostat vil bruge vejret i stedet."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Kun en enkelt termostat for hver virkelige er tilladt.",
|
||||
@@ -55,37 +58,39 @@
|
||||
"user": {
|
||||
"description": "Opdater dine Better Thermostat indstillinger",
|
||||
"data": {
|
||||
"name": "Navn",
|
||||
"thermostat": "Den rigtige termostat",
|
||||
"temperature_sensor": "Temperatur måler",
|
||||
"humidity_sensor": "Fugtighedssensor",
|
||||
"window_sensors": "Vinduessensor",
|
||||
"off_temperature": "Udendørstemperaturen, når termostaten skal slukker",
|
||||
"tolerance": "Tolerancen for at forhindre, at termostaten tænder og slukker for ofte.",
|
||||
"window_off_delay": "Forsinkelse, før termostaten slukker, når vinduet er åbent, og tændt, når vinduet er lukket",
|
||||
"window_off_delay_after": "Forsinkelse, før termostaten tænder, når vinduet er lukket",
|
||||
"outdoor_sensor": "Har du en udendørsføler, kan du bruge den til at få udetemperaturen",
|
||||
"valve_maintenance": "Hvis din termostat ikke har nogen egen vedligeholdelsestilstand, kan du bruge denne",
|
||||
"calibration": "Den slags kalibrering https://better-thermostat.org/configuration#second-step",
|
||||
"weather": "Din vejrentitet for at få udendørstemperaturen",
|
||||
"heat_auto_swapped": "Hvis auto betyder varme til din TRV, og du ønsker at bytte den",
|
||||
"child_lock": "Ignorer alle input på TRV som en børnesikring",
|
||||
"homaticip": "Hvis du bruger HomaticIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus",
|
||||
"name": "Navn",
|
||||
"thermostat": "Den rigtige termostat"
|
||||
"homaticip": "Hvis du bruger HomematicIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"description": "Avanceret konfiguration\n\n**Info om kalibreringstyper: https://better-thermostat.org/configuration#second-step** ",
|
||||
"description": "Avanceret konfiguration {trv}\n***Info om kalibreringstyper: https://better-thermostat.org/configuration#second-step*** ",
|
||||
"data": {
|
||||
"protect_overheating": "Overophedningsbeskyttelse?",
|
||||
"heat_auto_swapped": "Hvis auto betyder varme for din TRV og du vil bytte det",
|
||||
"child_lock": "Ignorer alle input på TRV som en børnesikring",
|
||||
"homaticip": "Hvis du bruger HomaticIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus",
|
||||
"homematicip": "Hvis du bruger HomematicIP, bør du aktivere dette for at bremse anmodningerne for at forhindre arbejdscyklus",
|
||||
"valve_maintenance": "Hvis din termostat ikke har nogen egen vedligeholdelsestilstand, kan du bruge denne",
|
||||
"calibration": "Den slags kalibrering du vil bruge",
|
||||
"protect_overheating": "Overophedningsbeskyttelse?",
|
||||
"calibration_mode": "Kalibreringstilstand",
|
||||
"no_off_system_mode": "Hvis din TRV ikke kan håndtere den slukkede tilstand, kan du aktivere denne for at bruge måltemperatur 5°C i stedet"
|
||||
},
|
||||
"data_description": {
|
||||
"protect_overheating": "Nogle TRV'er lukker ikke ventilen helt, når temperaturen er nået. Eller radiatoren har meget hvilevarme. Dette kan forårsage overophedning. Denne mulighed kan forhindre dette.",
|
||||
"calibration_mode": "Den slags, hvordan kalibreringen skal beregnes\n***Normal***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler.\n***Aggressiv***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler, men indstillet meget lavere/højere for at få et hurtigere løft.\n***AI-tidsbaseret***: I denne tilstand er TRV's interne temperatursensor fastsat af den eksterne temperatursensor, men værdien beregnes af en brugerdefineret algoritme for at forbedre TRV's interne algoritme.",
|
||||
"calibration_mode": "Hvordan kalibreringen skal beregnes\n***Normal***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler.\n***Aggressiv***: I denne tilstand er TRV's interne temperaturføler fikseret af den eksterne temperaturføler, men indstillet meget lavere/højere for at få et hurtigere løft.\n***AI-tidsbaseret***: I denne tilstand er TRV's interne temperatursensor fastsat af den eksterne temperatursensor, men værdien beregnes af en brugerdefineret algoritme for at forbedre TRV's interne algoritme.",
|
||||
"calibration": "Hvordan kalibreringen skal anvendes på TRV (Target temp or offset)\n***Måltemperaturbaseret***: Anvend kalibreringen til måltemperaturen.\n***Offset-baseret***: Anvend kalibreringen til offset."
|
||||
}
|
||||
}
|
||||
@@ -103,5 +108,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"save_current_target_temperature": {
|
||||
"name": "Gem nuværende temperatur",
|
||||
"description": "Gem den nuværende måltemperatur til senere gendannelse."
|
||||
},
|
||||
"restore_saved_target_temperature": {
|
||||
"name": "Gendan temperatur",
|
||||
"description": "Gendan den gemte måltemperatur."
|
||||
},
|
||||
"reset_heating_power": {
|
||||
"name": "Nulstil opvarmningskraft",
|
||||
"description": "Nulstil opvarmningskraft til standardværdi."
|
||||
},
|
||||
"set_temp_target_temperature": {
|
||||
"name": "Indstil eco-temperatur",
|
||||
"description": "Indstil måltemperaturen til en midlertidig som nattilstand, og gem den gamle."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Einrichtung von Better Thermostat mit Home Assistant\n**Für mehr Informationen: https://better-thermostat.org/configuration#first-step** ",
|
||||
"description": "Einrichtung von Better Thermostat mit Home Assistant\n**Für mehr Informationen: https://better-thermostat.org/configuration#first-step**",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat": "Das reale Thermostat",
|
||||
"cooler": "Klimmagerät AC (optional) | Preview BETA",
|
||||
"cooler": "Klimagerät AC (optional)",
|
||||
"temperature_sensor": "Externer Temperatursensor",
|
||||
"humidity_sensor": "Luftfeuchtigkeitssensor",
|
||||
"window_sensors": "Fenstersensor(en)",
|
||||
@@ -25,7 +25,7 @@
|
||||
"protect_overheating": "Überhitzung verhindern?",
|
||||
"heat_auto_swapped": "Tauscht die Modi auto und heat, falls diese bei dem realen Thermostat vertauscht sind.",
|
||||
"child_lock": "Ignoriere alle manuellen Einstellungen am realen Thermostat (Kindersicherung).",
|
||||
"homaticip": "Wenn du HomaticIP nutzt, solltest du diese Option aktivieren, um die Funk-Übertragung zu reduzieren.",
|
||||
"homematicip": "Wenn du HomematicIP nutzt, solltest du diese Option aktivieren, um die Funk-Übertragung zu reduzieren.",
|
||||
"valve_maintenance": "Soll BT die Wartung des Thermostats übernehmen?",
|
||||
"calibration": "Kalibrierungstyp",
|
||||
"calibration_mode": "Kalibrierungsmodus",
|
||||
@@ -71,7 +71,7 @@
|
||||
"weather": "Die Wetter-Entität für die Außentemperatur.",
|
||||
"valve_maintenance": "Wenn Ihr Thermostat keinen eigenen Wartungsmodus hat, können Sie diesen verwenden.",
|
||||
"child_lock": "Ignorieren Sie alle Eingaben am TRV wie eine Kindersicherung.",
|
||||
"homaticip": "Wenn Sie HomaticIP verwenden, sollten Sie dies aktivieren, um die Anfragen zu verlangsamen und den Duty Cycle zu verhindern.",
|
||||
"homematicip": "Wenn Sie HomematicIP verwenden, sollten Sie dies aktivieren, um die Anfragen zu verlangsamen und den Duty Cycle zu verhindern.",
|
||||
"heat_auto_swapped": "Wenn das Auto Wärme für Ihr TRV bedeutet und Sie es austauschen möchten.",
|
||||
"calibration": "Die Art der Kalibrierung https://better-thermostat.org/configuration#second-step"
|
||||
}
|
||||
@@ -82,7 +82,7 @@
|
||||
"protect_overheating": "Überhitzung verhindern?",
|
||||
"heat_auto_swapped": "Tauscht die Modi auto und heat, falls diese bei dem realen Thermostat vertauscht sind.",
|
||||
"child_lock": "Ignoriere alle manuellen Einstellungen am realen Thermostat (Kindersicherung).",
|
||||
"homaticip": "Wenn du HomaticIP nutzt, solltest du diese Option aktivieren, um die Funk-Übertragung zu reduzieren.",
|
||||
"homematicip": "Wenn du HomematicIP nutzt, solltest du diese Option aktivieren, um die Funk-Übertragung zu reduzieren.",
|
||||
"valve_maintenance": "Soll BT die Wartung des Thermostats übernehmen?",
|
||||
"calibration": "Kalibrierungstyp",
|
||||
"calibration_mode": "Kalibrierungsmodus",
|
||||
@@ -127,4 +127,4 @@
|
||||
"description": "Speichert eine ECO Temperatur, die für den ECO Modus genutzt wird."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Setup your Better Thermostat to integrate with Home Assistant\n**If you need more info: https://better-thermostat.org/configuration#first-step** ",
|
||||
"description": "Setup your Better Thermostat to integrate with Home Assistant\n**If you need more info: https://better-thermostat.org/configuration#first-step**",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"thermostat": "The real thermostat",
|
||||
"cooler": "The cooling device (optional) | Preview BETA",
|
||||
"cooler": "The cooling device (optional)",
|
||||
"temperature_sensor": "Temperature sensor",
|
||||
"humidity_sensor": "Humidity sensor",
|
||||
"window_sensors": "Window sensor",
|
||||
"off_temperature": "The outdoor temperature when the thermostat turn off",
|
||||
"off_temperature": "The outdoor temperature when the thermostat turns off",
|
||||
"tolerance": "Tolerance, to prevent the thermostat from turning on and off too often.",
|
||||
"window_off_delay": "Delay before the thermostat turn off when the window is opened",
|
||||
"window_off_delay_after": "Delay before the thermostat turn on when the window is closed",
|
||||
"window_off_delay": "Delay before the thermostat turns off when the window is opened",
|
||||
"window_off_delay_after": "Delay before the thermostat turns on when the window is closed",
|
||||
"outdoor_sensor": "If you have an outdoor sensor, you can use it to get the outdoor temperature",
|
||||
"weather": "Your weather entity to get the outdoor temperature"
|
||||
}
|
||||
@@ -25,15 +25,15 @@
|
||||
"protect_overheating": "Overheating protection?",
|
||||
"heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it",
|
||||
"child_lock": "Ignore all inputs on the TRV like a child lock",
|
||||
"homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle",
|
||||
"homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle",
|
||||
"valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one",
|
||||
"calibration": "Calibration Type",
|
||||
"calibration_mode": "Calibration mode",
|
||||
"no_off_system_mode": "If your TRV can't handle the off mode, you can enable this to use target temperature 5°C instead"
|
||||
},
|
||||
"data_description": {
|
||||
"protect_overheating": "Some TRVs doest close the valve completly when the temperature is reached. Or the radiator have a lot of rest heat. This can cause overheating. This option can prevent this.",
|
||||
"calibration_mode": "The kind how the calibration should be calculated\n***Normal***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.",
|
||||
"protect_overheating": "Some TRVs don't close the valve completely when the temperature is reached. Or the radiator has a lot of rest heat. This can cause overheating. This option can prevent this.",
|
||||
"calibration_mode": "The kind how the calibration should be calculated\n***Normal***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor, but a custom algorithm calculates the value to improve the TRV internal algorithm.",
|
||||
"calibration": "How the calibration should be applied on the TRV (Target temp or offset)\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the offset."
|
||||
}
|
||||
},
|
||||
@@ -43,9 +43,9 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"failed": "something went wrong.",
|
||||
"failed": "Something went wrong.",
|
||||
"no_name": "Please enter a name.",
|
||||
"no_off_mode": "You device is very special and has no off mode :(\nBetter Thermostat will use the minimal target temp instead.",
|
||||
"no_off_mode": "Your device is very special and has no off mode :(\nBetter Thermostat will use the minimal target temp instead.",
|
||||
"no_outside_temp": "You have no outside temperature sensor. Better Thermostat will use the weather entity instead."
|
||||
},
|
||||
"abort": {
|
||||
@@ -63,17 +63,17 @@
|
||||
"temperature_sensor": "Temperature Sensor",
|
||||
"humidity_sensor": "Humidity sensor",
|
||||
"window_sensors": "Window Sensor",
|
||||
"off_temperature": "The outdoor temperature when the thermostat turn off",
|
||||
"off_temperature": "The outdoor temperature when the thermostat turns off",
|
||||
"tolerance": "Tolerance, to prevent the thermostat from turning on and off too often.",
|
||||
"window_off_delay": "Delay before the thermostat turn off when the window is opened",
|
||||
"window_off_delay_after": "Delay before the thermostat turn on when the window is closed",
|
||||
"window_off_delay": "Delay before the thermostat turns off when the window is opened",
|
||||
"window_off_delay_after": "Delay before the thermostat turns on when the window is closed",
|
||||
"outdoor_sensor": "If you have an outdoor sensor, you can use it to get the outdoor temperature",
|
||||
"valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one",
|
||||
"calibration": "The sort of calibration https://better-thermostat.org/configuration#second-step",
|
||||
"weather": "Your weather entity to get the outdoor temperature",
|
||||
"heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it",
|
||||
"child_lock": "Ignore all inputs on the TRV like a child lock",
|
||||
"homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle"
|
||||
"homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -82,15 +82,15 @@
|
||||
"protect_overheating": "Overheating protection?",
|
||||
"heat_auto_swapped": "If the auto means heat for your TRV and you want to swap it",
|
||||
"child_lock": "Ignore all inputs on the TRV like a child lock",
|
||||
"homaticip": "If you use HomaticIP, you should enable this to slow down the requests to prevent the duty cycle",
|
||||
"homematicip": "If you use HomematicIP, you should enable this to slow down the requests to prevent the duty cycle",
|
||||
"valve_maintenance": "If your thermostat has no own maintenance mode, you can use this one",
|
||||
"calibration": "The sort of calibration you want to use",
|
||||
"calibration_mode": "Calibration mode",
|
||||
"no_off_system_mode": "If your TRV can't handle the off mode, you can enable this to use target temperature 5°C instead"
|
||||
},
|
||||
"data_description": {
|
||||
"protect_overheating": "Some TRVs doest close the valve completly when the temperature is reached. Or the radiator have a lot of rest heat. This can cause overheating. This option can prevent this.",
|
||||
"calibration_mode": "The kind how the calibration should be calculated\n***Normal***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor, but the value is calculated by a custom algorithm to improve the TRV internal algorithm.",
|
||||
"protect_overheating": "Some TRVs don't close the valve completely when the temperature is reached. Or the radiator has a lot of rest heat. This can cause overheating. This option can prevent this.",
|
||||
"calibration_mode": "The kind how the calibration should be calculated\n***Normal***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor.\n***Aggresive***: In this mode, the TRV internal temperature sensor is fixed by the external temperature sensor but set much lower/higher to get a quicker boost.\n***AI Time Based***: In this mode the TRV internal temperature sensor is fixed by the external temperature sensor, but a custom algorithm calculates the value to improve the TRV internal algorithm.",
|
||||
"calibration": "How the calibration should be applied on the TRV (Target temp or offset)\n***Target Temperature Based***: Apply the calibration to the target temperature.\n***Offset Based***: Apply the calibration to the offset."
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "The related entity {entity} is missing",
|
||||
"description": "The reason for this is that the entity ({entity}) is not available in your Home Assistant.\n\nYou can fix this by checking if the batterie of the device is full or reconnect it to HA. Make sure that the entity is back in HA before you continue."
|
||||
"description": "The reason for this is that the entity ({entity}) is not available in your Home Assistant.\n\nYou can fix this by checking if the battery of the device is full or reconnecting it to HA. Make sure that the entity is back in HA before you continue."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@
|
||||
},
|
||||
"set_temp_target_temperature": {
|
||||
"name": "Set eco temperature",
|
||||
"description": "Set the target temperature to a temporay like night mode, and save the old one."
|
||||
"description": "Set the target temperature to a temporary like night mode, and save the old one."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,105 +3,128 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Configuration de Better Thermostat pour l'intégrer à Home Assistant\n**If you need more info: https://better-thermostat.org/configuration#first-step** ",
|
||||
"description": "Configurez Better Thermostat pour l'intégrer à Home Assistant\n**Si vous avez besoin de plus d'informations : https://better-thermostat.org/configuration#first-step**",
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"thermostat": "Le véritable thermostat",
|
||||
"thermostat": "Le vrai thermostat",
|
||||
"cooler": "Le dispositif de refroidissement (optionnel)",
|
||||
"temperature_sensor": "Capteur de température",
|
||||
"humidity_sensor": "Capteur d'humidité",
|
||||
"window_sensors": "Capteur de fenêtre",
|
||||
"off_temperature": "La température extérieure lorsque le thermostat s'éteint",
|
||||
"window_off_delay": "Délai avant que le thermostat ne s'éteigne lorsque la fenêtre est ouverte et ne s'allume lorsque la fenêtre est fermée",
|
||||
"off_temperature": "La température extérieure à laquelle le thermostat s'éteint",
|
||||
"tolerance": "Tolérance, pour éviter que le thermostat ne s'allume et s'éteigne trop souvent.",
|
||||
"window_off_delay": "Délai avant que le thermostat ne s'éteigne lorsque la fenêtre est ouverte",
|
||||
"window_off_delay_after": "Délai avant que le thermostat ne s'allume lorsque la fenêtre est fermée",
|
||||
"outdoor_sensor": "Si vous avez un capteur extérieur, vous pouvez l'utiliser pour obtenir la température extérieure",
|
||||
"weather": "Votre entité météo pour obtenir la température extérieure"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"description": "Configuration avancée\n\n**Informations sur les types d'étalonnage : https://better-thermostat.org/configuration#second-step**",
|
||||
"description": "Configuration avancée {trv}\n***Informations sur les types de calibrage : https://better-thermostat.org/configuration#second-step***",
|
||||
"data": {
|
||||
"heat_auto_swapped": "Si auto signifie chauffer pour votre TRV et que vous voulez l'inverser (pour les utilisateurs de Google Home)",
|
||||
"child_lock": "Ignorer toutes les entrées sur le TRV comme un vérouillage enfant",
|
||||
"homaticip": "Si vous utilisez HomaticIP, vous devez l'activer pour ralentir les requêtes afin d'éviter le cycle de service",
|
||||
"valve_maintenance": "Si votre thermostat n'a pas de mode de maintenance intégré, vous pouvez utiliser celui-ci",
|
||||
"calibration": "The sort of calibration",
|
||||
"no_off_system_mode": "Si votre TRV ne peut pas gérer le mode arrêt, vous pouvez l'activer pour utiliser la température cible de 5 °C à la place",
|
||||
"calibration_mode": "Mode d'étalonnage",
|
||||
"protect_overheating": "Protection contre la surchauffe ?"
|
||||
"protect_overheating": "Protection contre la surchauffe ?",
|
||||
"heat_auto_swapped": "Si le mode automatique signifie chauffage pour votre TRV et que vous voulez le permuter",
|
||||
"child_lock": "Ignorer toutes les entrées sur le TRV comme une sécurité enfant",
|
||||
"homematicip": "Si vous utilisez HomematicIP, vous devriez l'activer pour ralentir les demandes et éviter le cycle de fonctionnement excessif",
|
||||
"valve_maintenance": "Si votre thermostat n'a pas de mode maintenance propre, vous pouvez utiliser celui-ci",
|
||||
"calibration": "Type de calibrage",
|
||||
"calibration_mode": "Mode de calibrage",
|
||||
"no_off_system_mode": "Si votre TRV ne peut pas gérer le mode hors tension, vous pouvez activer ceci pour utiliser une température cible de 5°C à la place"
|
||||
},
|
||||
"data_description": {
|
||||
"protect_overheating": "Certaines VTR ferment complètement la vanne lorsque la température est atteinte. Ou le radiateur a beaucoup de chaleur résiduelle. Cela peut provoquer une surchauffe. Cette option peut empêcher cela.",
|
||||
"calibration_mode": "Le type de calcul de l'étalonnage\n***Normal*** : Dans ce mode, le capteur de température interne TRV est fixé par le capteur de température externe.\n***Agressif*** : dans ce mode, le capteur de température interne TRV est fixé par le capteur de température externe mais réglé beaucoup plus bas/plus haut pour obtenir une accélération plus rapide.\n*** AI Time Based *** : Dans ce mode, le capteur de température interne TRV est fixé par le capteur de température externe, mais la valeur est calculée par un algorithme personnalisé pour améliorer l'algorithme interne TRV.",
|
||||
"calibration": "Comment l'étalonnage doit être appliqué sur la VTR (température cible ou décalage)\n***Basé sur la température cible*** : Appliquez l'étalonnage à la température cible.\n***Basé sur le décalage*** : Appliquez l'étalonnage au décalage."
|
||||
"protect_overheating": "Certains TRVs ne ferment pas complètement la vanne lorsque la température est atteinte. Ou le radiateur a beaucoup de chaleur résiduelle. Cela peut provoquer une surchauffe. Cette option peut prévenir cela.",
|
||||
"calibration_mode": "Le type de calcul du calibrage\n***Normal*** : Dans ce mode, le capteur de température interne du TRV est fixé par le capteur de température externe.\n***Aggressif*** : Dans ce mode, le capteur de température interne du TRV est fixé par le capteur de température externe mais réglé beaucoup plus bas/haut pour obtenir un boost plus rapide.\n***AI Time Based*** : Dans ce mode, le capteur de température interne du TRV est fixé par le capteur de température externe, mais la valeur est calculée par un algorithme personnalisé pour améliorer l'algorithme interne du TRV.",
|
||||
"calibration": "Comment le calibrage doit être appliqué sur le TRV (Température cible ou décalage)\n***Basé sur la température cible*** : Appliquer le calibrage à la température cible.\n***Basé sur le décalage*** : Appliquer le calibrage au décalage."
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Confirmer l'ajout d'un meilleur thermostat",
|
||||
"description": "Vous êtes sur le point d'ajouter \"{name}\" à Home Assistant.\nAvec {trv} comme vrai thermostat"
|
||||
"title": "Confirmer l'ajout d'un Better Thermostat",
|
||||
"description": "Vous êtes sur le point d'ajouter `{name}` à Home Assistant.\nAvec {trv} en tant que vrai thermostat"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"no_name": "Veuillez entrer un nom.",
|
||||
"failed": "quelque chose s'est mal passé.",
|
||||
"no_outside_temp": "Vous devez définir un capteur de température extérieure ou une entité météorologique.",
|
||||
"no_off_mode": "You device is very special and has no off mode :(\nIt work anyway, but you have to create a automation to fit your specials based on the device events"
|
||||
"no_name": "Veuillez entrer un nom.",
|
||||
"no_off_mode": "Votre appareil est très spécial et n'a pas de mode hors tension :(\nBetter Thermostat utilisera plutôt la température cible minimale.",
|
||||
"no_outside_temp": "Vous n'avez pas de capteur de température extérieure. Better Thermostat utilisera plutôt l'entité météo."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Un seul et unique thermostat est autorisé pour chaque thermostat réel",
|
||||
"no_devices_found": "Aucune entité thermostat n'a été trouvée, assurez-vous d'avoir une entité climat dans votre home assistant"
|
||||
"single_instance_allowed": "Seule une instance de thermostat pour chaque vrai thermostat est autorisée.",
|
||||
"no_devices_found": "Aucune entité de thermostat trouvée, assurez-vous d'avoir une entité de climatisation dans votre Home Assistant"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Mettez à jour vos paramètres de Better Thermostat",
|
||||
"description": "Mettez à jour les paramètres de Better Thermostat",
|
||||
"data": {
|
||||
"name": "Nom",
|
||||
"thermostat": "Le vrai thermostat",
|
||||
"temperature_sensor": "Capteur de température",
|
||||
"humidity_sensor": "Capteur d'humidité",
|
||||
"window_sensors": "Capteur de fenêtre",
|
||||
"off_temperature": "La température extérieure lorsque le thermostat s'éteint",
|
||||
"window_off_delay": "Délai avant que le thermostat ne s'éteigne lorsque la fenêtre est ouverte et ne s'allume lorsque la fenêtre est fermée",
|
||||
"off_temperature": "La température extérieure à laquelle le thermostat s'éteint",
|
||||
"tolerance": "Tolérance, pour éviter que le thermostat ne s'allume et s'éteigne trop souvent.",
|
||||
"window_off_delay": "Délai avant que le thermostat ne s'éteigne lorsque la fenêtre est ouverte",
|
||||
"window_off_delay_after": "Délai avant que le thermostat ne s'allume lorsque la fenêtre est fermée",
|
||||
"outdoor_sensor": "Si vous avez un capteur extérieur, vous pouvez l'utiliser pour obtenir la température extérieure",
|
||||
"valve_maintenance": "Si votre thermostat n'a pas de mode de maintenance intégré, vous pouvez utiliser celui-ci",
|
||||
"calibration": "Le type d'étalonnage https://better-thermostat.org/configuration#second-step",
|
||||
"valve_maintenance": "Si votre thermostat n'a pas de mode maintenance propre, vous pouvez utiliser celui-ci",
|
||||
"calibration": "Le type de calibrage https://better-thermostat.org/configuration#second-step",
|
||||
"weather": "Votre entité météo pour obtenir la température extérieure",
|
||||
"heat_auto_swapped": "Si auto signifie chauffer pour votre TRV et que vous voulez l'inverser",
|
||||
"child_lock": "Ignorer toutes les entrées sur le TRV comme un vérouillage enfant",
|
||||
"homaticip": "Si vous utilisez HomaticIP, vous devez l'activer pour ralentir les requêtes afin d'éviter le cycle de service",
|
||||
"name": "Nom",
|
||||
"thermostat": "Le vrai thermostat"
|
||||
"heat_auto_swapped": "Si le mode automatique signifie chauffage pour votre TRV et que vous voulez le permuter",
|
||||
"child_lock": "Ignorer toutes les entrées sur le TRV comme une sécurité enfant",
|
||||
"homematicip": "Si vous utilisez HomematicIP, vous devriez l'activer pour ralentir les demandes et éviter le cycle de fonctionnement excessif"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"description": "Configuration avancée {trv}\n\n***Informations sur les types d'étalonnage : https://better-thermostat.org/configuration#second-step***",
|
||||
"description": "Configuration avancée {trv}\n***Informations sur les types de calibrage : https://better-thermostat.org/configuration#second-step***",
|
||||
"data": {
|
||||
"heat_auto_swapped": "Si auto signifie chauffer pour votre TRV et que vous voulez l'inverser",
|
||||
"child_lock": "Ignorer toutes les entrées sur le TRV comme un vérouillage enfant",
|
||||
"homaticip": "Si vous utilisez HomaticIP, vous devez l'activer pour ralentir les requêtes afin d'éviter le cycle de service",
|
||||
"valve_maintenance": "Si votre thermostat n'a pas de mode de maintenance intégré, vous pouvez utiliser celui-ci",
|
||||
"calibration": "The sort of calibration",
|
||||
"protect_overheating": "Protection contre la surchauffe ?",
|
||||
"calibration_mode": "Mode d'étalonnage",
|
||||
"no_off_system_mode": "Si votre TRV ne peut pas gérer le mode arrêt, vous pouvez l'activer pour utiliser la température cible de 5 °C à la place"
|
||||
"heat_auto_swapped": "Si le mode automatique signifie chauffage pour votre TRV et que vous voulez le permuter",
|
||||
"child_lock": "Ignorer toutes les entrées sur le TRV comme une sécurité enfant",
|
||||
"homematicip": "Si vous utilisez HomematicIP, vous devriez l'activer pour ralentir les demandes et éviter le cycle de fonctionnement excessif",
|
||||
"valve_maintenance": "Si votre thermostat n'a pas de mode maintenance propre, vous pouvez utiliser celui-ci",
|
||||
"calibration": "Le type de calibrage que vous souhaitez utiliser",
|
||||
"calibration_mode": "Mode de calibrage",
|
||||
"no_off_system_mode": "Si votre TRV ne peut pas gérer le mode hors tension, vous pouvez activer ceci pour utiliser une température cible de 5°C à la place"
|
||||
},
|
||||
"data_description": {
|
||||
"protect_overheating": "Certaines VTR ferment complètement la vanne lorsque la température est atteinte. Ou le radiateur a beaucoup de chaleur résiduelle. Cela peut provoquer une surchauffe. Cette option peut empêcher cela.",
|
||||
"calibration_mode": "Le type de calcul de l'étalonnage\n***Normal*** : Dans ce mode, le capteur de température interne TRV est fixé par le capteur de température externe.\n***Agressif*** : dans ce mode, le capteur de température interne TRV est fixé par le capteur de température externe mais réglé beaucoup plus bas/plus haut pour obtenir une accélération plus rapide.\n*** AI Time Based *** : Dans ce mode, le capteur de température interne TRV est fixé par le capteur de température externe, mais la valeur est calculée par un algorithme personnalisé pour améliorer l'algorithme interne TRV.",
|
||||
"calibration": "Comment l'étalonnage doit être appliqué sur la VTR (température cible ou décalage)\n***Basé sur la température cible*** : Appliquez l'étalonnage à la température cible.\n***Basé sur le décalage*** : Appliquez l'étalonnage au décalage."
|
||||
"protect_overheating": "Certains TRVs ne ferment pas complètement la vanne lorsque la température est atteinte. Ou le radiateur a beaucoup de chaleur résiduelle. Cela peut provoquer une surchauffe. Cette option peut prévenir cela.",
|
||||
"calibration_mode": "Le type de calcul du calibrage\n***Normal*** : Dans ce mode, le capteur de température interne du TRV est fixé par le capteur de température externe.\n***Aggressif*** : Dans ce mode, le capteur de température interne du TRV est fixé par le capteur de température externe mais réglé beaucoup plus bas/haut pour obtenir un boost plus rapide.\n***AI Time Based*** : Dans ce mode, le capteur de température interne du TRV est fixé par le capteur de température externe, mais la valeur est calculée par un algorithme personnalisé pour améliorer l'algorithme interne du TRV.",
|
||||
"calibration": "Comment le calibrage doit être appliqué sur le TRV (Température cible ou décalage)\n***Basé sur la température cible*** : Appliquer le calibrage à la température cible.\n***Basé sur le décalage*** : Appliquer le calibrage au décalage."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"missing_entity": {
|
||||
"title": "BT : {name} - l'entité associée est manquante",
|
||||
"title": "BT: {name} - l'entité associée est manquante",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "L'entité associée est {entity} manquante",
|
||||
"description": "La raison en est que l'entité ({entity}) n'est pas disponible dans votre Home Assistant.\n\nVous pouvez résoudre ce problème en vérifiant si la batterie de l'appareil est pleine ou en le reconnectant à HA. Assurez-vous que l'entité est de retour dans HA avant de continuer."
|
||||
"title": "L'entité associée {entity} est manquante",
|
||||
"description": "La raison en est que l'entité ({entity}) n'est pas disponible dans votre Home Assistant.\n\nVous pouvez résoudre ce problème en vérifiant si la batterie du dispositif est pleine ou en le reconnectant à HA. Assurez-vous que l'entité est de retour dans HA avant de continuer."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"save_current_target_temperature": {
|
||||
"name": "Enregistrer la température actuelle",
|
||||
"description": "Enregistrez la température cible actuelle pour une restauration ultérieure."
|
||||
},
|
||||
"restore_saved_target_temperature": {
|
||||
"name": "Restaurer la température",
|
||||
"description": "Restaurez la température cible enregistrée."
|
||||
},
|
||||
"reset_heating_power": {
|
||||
"name": "Réinitialiser la puissance de chauffage",
|
||||
"description": "Réinitialisez la puissance de chauffage à la valeur par défaut."
|
||||
},
|
||||
"set_temp_target_temperature": {
|
||||
"name": "Définir la température éco",
|
||||
"description": "Définissez la température cible sur une valeur temporaire comme le mode nuit, et enregistrez l'ancienne."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Skonfiguruj swój Better Thermostat do integracji z Home Assistant\n**Więcej informacji znajdziesz na: https://better-thermostat.org/configuration#first-step** ",
|
||||
"description": "Skonfiguruj swój Better Thermostat do integracji z Home Assistant\n**Więcej informacji znajdziesz na: https://better-thermostat.org/configuration#first-step**",
|
||||
"data": {
|
||||
"name": "Nazwa",
|
||||
"thermostat": "Twój termostat",
|
||||
@@ -17,11 +17,11 @@
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"description": "Zaawansowana konfiguracja\n\n**Informacja o typach kalibracji: https://better-thermostat.org/configuration#second-step** ",
|
||||
"description": "Zaawansowana konfiguracja {trv}\n***Informacja o typach kalibracji: https://better-thermostat.org/configuration#second-step***",
|
||||
"data": {
|
||||
"heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić",
|
||||
"child_lock": "Ignoruj wszystkie wejścia w TRV jak np. Blokada dziecięca",
|
||||
"homaticip": "Jeżeli używasz HomaticIP, powinieneś włączyć tę opcję, żeby spowolnić żądania cyklu pracy",
|
||||
"homematicip": "Jeżeli używasz HomematicIP, powinieneś włączyć tę opcję, żeby spowolnić żądania cyklu pracy",
|
||||
"valve_maintenance": "Jeżeli Twój termostat nie ma trybu konserwacji, możesz użyć tej opcji.",
|
||||
"calibration": "Rodzaj kalibracji, której chcesz użyć",
|
||||
"no_off_system_mode": "Jeśli Twój TRV nie obsługuje trybu wyłączenia, możesz go włączyć, aby zamiast tego używać temperatury docelowej 5°C",
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"no_outside_temp": "Nie masz czujnika temperatury zewnętrznej. Lepszy termostat zamiast tego użyje jednostki pogodowej.",
|
||||
"no_outside_temp": "Nie masz czujnika temperatury zewnętrznej. Better Thermostat zamiast tego użyje jednostki pogodowej.",
|
||||
"failed": "coś poszło nie tak.",
|
||||
"no_name": "Proszę podać nazwę.",
|
||||
"no_off_mode": "Twoje urządzenie jest inne i nie ma trybu wyłączenia :(\nTo będzie działać, ale musisz stworzyć automatyzację, aby ustawić funkcje pod swoje urządzenie"
|
||||
@@ -66,17 +66,17 @@
|
||||
"weather": "Twoja jednostka pogodowa, aby uzyskać temperaturę zewnętrzną",
|
||||
"heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić",
|
||||
"child_lock": "Ignoruj wszystkie wejścia w TRV jak np. Blokada dziecięca",
|
||||
"homaticip": "Jeżeli używasz HomaticIP, powinienieś włączyć tę opcję żeby spowolnić żądania",
|
||||
"homematicip": "Jeżeli używasz HomematicIP, powinienieś włączyć tę opcję żeby spowolnić żądania",
|
||||
"name": "Nazwa",
|
||||
"thermostat": "Prawdziwy termostat"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"description": "Zaawansowana konfiguracja**Informacja o typach kalibracji: https://better-thermostat.org/configuration#second-step** ",
|
||||
"description": "Zaawansowana konfiguracja {trv}\n***Informacja o typach kalibracji: https://better-thermostat.org/configuration#second-step***",
|
||||
"data": {
|
||||
"heat_auto_swapped": "Jeżeli tryb auto oznacza grzanie dla Twojego TRV i chcesz to zmienić",
|
||||
"child_lock": "Ignoruj wszystkie wejścia w TRV jak np. Blokada dziecięca",
|
||||
"homaticip": "Jeżeli używasz HomaticIP, powinieneś włączyć tę opcję, żeby spowolnić żądania spowolnienia cyklu pracy",
|
||||
"homematicip": "Jeżeli używasz HomematicIP, powinieneś włączyć tę opcję, żeby spowolnić żądania spowolnienia cyklu pracy",
|
||||
"valve_maintenance": "Jeżeli Twój termostat nie ma trybu konserwacji, możesz użyć tej opcji.",
|
||||
"calibration": "Rodzaj kalibracji, której chcesz użyć",
|
||||
"protect_overheating": "Zabezpieczenie przed przegrzaniem?",
|
||||
@@ -104,4 +104,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Nastavenie lepšieho termostatu na integráciu s aplikáciou Home Assistant\n**Ak potrebujete viac informácií: https://better-thermostat.org/configuration#first-step** ",
|
||||
"description": "Nastavenie lepšieho termostatu na integráciu s aplikáciou Home Assistant\n**Ak potrebujete viac informácií: https://better-thermostat.org/configuration#first-step**",
|
||||
"data": {
|
||||
"name": "Názov",
|
||||
"thermostat": "Skutočný termostat",
|
||||
"cooler": "Chladiace zariadenie (voliteľné)",
|
||||
"temperature_sensor": "Snímač teploty",
|
||||
"humidity_sensor": "Snímač vlhkosti",
|
||||
"window_sensors": "Snímač okna",
|
||||
@@ -24,7 +25,7 @@
|
||||
"protect_overheating": "Ochrana proti prehriatiu?",
|
||||
"heat_auto_swapped": "Ak auto znamená teplo pre váš TRV a chcete ho vymeniť",
|
||||
"child_lock": "Ignorovať všetky vstupy na TRV ako detský zámok",
|
||||
"homaticip": "Ak používate HomaticIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu",
|
||||
"homematicip": "Ak používate HomematicIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu",
|
||||
"valve_maintenance": "Ak váš termostat nemá vlastný režim údržby, môžete použiť tento režim",
|
||||
"calibration": "Typ kalibrácie",
|
||||
"calibration_mode": "Režim kalibrácie",
|
||||
@@ -72,7 +73,7 @@
|
||||
"weather": "Váš meteorologický subjekt na zistenie vonkajšej teploty",
|
||||
"heat_auto_swapped": "Ak auto znamená teplo pre váš TRV a chcete ho vymeniť",
|
||||
"child_lock": "Ignorovať všetky vstupy na TRV ako detský zámok",
|
||||
"homaticip": "Ak používate HomaticIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu"
|
||||
"homematicip": "Ak používate HomematicIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu"
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
@@ -81,7 +82,7 @@
|
||||
"protect_overheating": "Ochrana proti prehriatiu?",
|
||||
"heat_auto_swapped": "Ak auto znamená teplo pre váš TRV a chcete ho vymeniť",
|
||||
"child_lock": "Ignorovať všetky vstupy na TRV ako detský zámok",
|
||||
"homaticip": "Ak používate HomaticIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu",
|
||||
"homematicip": "Ak používate HomematicIP, mali by ste túto funkciu povoliť, aby ste spomalili požiadavky a zabránili tak pracovnému cyklu",
|
||||
"valve_maintenance": "Ak váš termostat nemá vlastný režim údržby, môžete použiť tento režim",
|
||||
"calibration": "Druh kalibrácie, ktorý chcete použiť",
|
||||
"calibration_mode": "Režim kalibrácie",
|
||||
@@ -126,4 +127,4 @@
|
||||
"description": "Nastavte cieľovú teplotu na dočasný, napríklad nočný režim, a uložte starú teplotu."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
from importlib import import_module
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_adapter(self, integration, entity_id, get_name=False):
|
||||
"""Load adapter."""
|
||||
if get_name:
|
||||
self.name = "-"
|
||||
|
||||
if integration == "generic_thermostat":
|
||||
integration = "generic"
|
||||
|
||||
try:
|
||||
self.adapter = import_module(
|
||||
"custom_components.better_thermostat.adapters." + integration,
|
||||
package="better_thermostat",
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"better_thermostat %s: uses adapter %s for trv %s",
|
||||
self.name,
|
||||
integration,
|
||||
entity_id,
|
||||
)
|
||||
except Exception:
|
||||
self.adapter = import_module(
|
||||
"custom_components.better_thermostat.adapters.generic",
|
||||
package="better_thermostat",
|
||||
)
|
||||
_LOGGER.info(
|
||||
"better_thermostat %s: intigration: %s isn't native supported, feel free to open an issue, fallback adapter %s",
|
||||
self.name,
|
||||
integration,
|
||||
"generic",
|
||||
)
|
||||
pass
|
||||
|
||||
if get_name:
|
||||
return integration
|
||||
return self.adapter
|
||||
|
||||
|
||||
async def init(self, entity_id):
|
||||
"""Init adapter."""
|
||||
return await self.real_trvs[entity_id]["adapter"].init(self, entity_id)
|
||||
|
||||
|
||||
async def get_info(self, entity_id):
|
||||
return await self.real_trvs[entity_id]["adapter"].get_info(self, entity_id)
|
||||
|
||||
|
||||
async def get_current_offset(self, entity_id):
|
||||
"""Get current offset."""
|
||||
return await self.real_trvs[entity_id]["adapter"].get_current_offset(
|
||||
self, entity_id
|
||||
)
|
||||
|
||||
|
||||
async def get_offset_steps(self, entity_id):
|
||||
"""get offset setps."""
|
||||
return await self.real_trvs[entity_id]["adapter"].get_offset_steps(self, entity_id)
|
||||
|
||||
|
||||
async def get_min_offset(self, entity_id):
|
||||
"""Get min offset."""
|
||||
return await self.real_trvs[entity_id]["adapter"].get_min_offset(self, entity_id)
|
||||
|
||||
|
||||
async def get_max_offset(self, entity_id):
|
||||
"""Get max offset."""
|
||||
return await self.real_trvs[entity_id]["adapter"].get_max_offset(self, entity_id)
|
||||
|
||||
|
||||
async def set_temperature(self, entity_id, temperature):
|
||||
"""Set new target temperature."""
|
||||
return await self.real_trvs[entity_id]["adapter"].set_temperature(
|
||||
self, entity_id, temperature
|
||||
)
|
||||
|
||||
|
||||
async def set_hvac_mode(self, entity_id, hvac_mode):
|
||||
"""Set new target hvac mode."""
|
||||
return await self.real_trvs[entity_id]["adapter"].set_hvac_mode(
|
||||
self, entity_id, hvac_mode
|
||||
)
|
||||
|
||||
|
||||
async def set_offset(self, entity_id, offset):
|
||||
"""Set new target offset."""
|
||||
return await self.real_trvs[entity_id]["adapter"].set_offset(
|
||||
self, entity_id, offset
|
||||
)
|
||||
|
||||
|
||||
async def set_valve(self, entity_id, valve):
|
||||
"""Set new target valve."""
|
||||
return await self.real_trvs[entity_id]["adapter"].set_valve(self, entity_id, valve)
|
||||
@@ -1,26 +1,44 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from custom_components.better_thermostat.utils.model_quirks import (
|
||||
from homeassistant.components.climate.const import HVACMode
|
||||
|
||||
from custom_components.better_thermostat.model_fixes.model_quirks import (
|
||||
override_set_hvac_mode,
|
||||
override_set_temperature,
|
||||
)
|
||||
|
||||
from .bridge import (
|
||||
from custom_components.better_thermostat.adapters.delegate import (
|
||||
set_offset,
|
||||
get_current_offset,
|
||||
get_offset_steps,
|
||||
set_temperature,
|
||||
set_hvac_mode,
|
||||
)
|
||||
from ..events.trv import convert_outbound_states, update_hvac_action
|
||||
from homeassistant.components.climate.const import HVACMode
|
||||
|
||||
from .helpers import convert_to_float, calibration_round
|
||||
from custom_components.better_thermostat.events.trv import (
|
||||
convert_outbound_states,
|
||||
update_hvac_action,
|
||||
)
|
||||
|
||||
from custom_components.better_thermostat.utils.helpers import convert_to_float
|
||||
|
||||
from custom_components.better_thermostat.utils.const import CalibrationMode
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TaskManager:
|
||||
def __init__(self):
|
||||
self.tasks = set()
|
||||
|
||||
def create_task(self, coro):
|
||||
task = asyncio.create_task(coro)
|
||||
self.tasks.add(task)
|
||||
task.add_done_callback(self.tasks.discard)
|
||||
return task
|
||||
|
||||
|
||||
async def control_queue(self):
|
||||
"""The accutal control loop.
|
||||
Parameters
|
||||
@@ -32,6 +50,9 @@ async def control_queue(self):
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if not hasattr(self, "task_manager"):
|
||||
self.task_manager = TaskManager()
|
||||
|
||||
while True:
|
||||
if self.ignore_states or self.startup_running:
|
||||
await asyncio.sleep(1)
|
||||
@@ -49,7 +70,7 @@ async def control_queue(self):
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"better_thermostat %s: ERROR controlling: %s",
|
||||
self.name,
|
||||
self.device_name,
|
||||
trv,
|
||||
)
|
||||
result = False
|
||||
@@ -80,13 +101,18 @@ async def control_trv(self, heater_entity_id=None):
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if not hasattr(self, "task_manager"):
|
||||
self.task_manager = TaskManager()
|
||||
|
||||
async with self._temp_lock:
|
||||
self.real_trvs[heater_entity_id]["ignore_trv_states"] = True
|
||||
await update_hvac_action(self)
|
||||
await self.calculate_heating_power()
|
||||
_trv = self.hass.states.get(heater_entity_id)
|
||||
_current_set_temperature = convert_to_float(
|
||||
str(_trv.attributes.get("temperature", None)), self.name, "controlling()"
|
||||
str(_trv.attributes.get("temperature", None)),
|
||||
self.device_name,
|
||||
"controlling()",
|
||||
)
|
||||
|
||||
_remapped_states = convert_outbound_states(
|
||||
@@ -94,7 +120,7 @@ async def control_trv(self, heater_entity_id=None):
|
||||
)
|
||||
if not isinstance(_remapped_states, dict):
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: ERROR {heater_entity_id} {_remapped_states}"
|
||||
f"better_thermostat {self.device_name}: ERROR {heater_entity_id} {_remapped_states}"
|
||||
)
|
||||
await asyncio.sleep(10)
|
||||
self.ignore_states = False
|
||||
@@ -103,6 +129,9 @@ async def control_trv(self, heater_entity_id=None):
|
||||
|
||||
_temperature = _remapped_states.get("temperature", None)
|
||||
_calibration = _remapped_states.get("local_temperature_calibration", None)
|
||||
_calibration_mode = self.real_trvs[heater_entity_id]["advanced"].get(
|
||||
"calibration_mode", CalibrationMode.DEFAULT
|
||||
)
|
||||
|
||||
_new_hvac_mode = handle_window_open(self, _remapped_states)
|
||||
|
||||
@@ -131,7 +160,7 @@ async def control_trv(self, heater_entity_id=None):
|
||||
context=self.context,
|
||||
)
|
||||
elif (
|
||||
self.cur_temp < self.bt_target_cooltemp - self.tolerance
|
||||
self.cur_temp <= (self.bt_target_cooltemp - self.tolerance)
|
||||
and _new_hvac_mode is not HVACMode.OFF
|
||||
):
|
||||
await self.hass.services.async_call(
|
||||
@@ -186,7 +215,7 @@ async def control_trv(self, heater_entity_id=None):
|
||||
if _no_off_system_mode is True and _new_hvac_mode == HVACMode.OFF:
|
||||
_min_temp = self.real_trvs[heater_entity_id]["min_temp"]
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: sending {_min_temp}°C to the TRV because this device has no system mode off and heater should be off"
|
||||
f"better_thermostat {self.device_name}: sending {_min_temp}°C to the TRV because this device has no system mode off and heater should be off"
|
||||
)
|
||||
_temperature = _min_temp
|
||||
|
||||
@@ -200,7 +229,7 @@ async def control_trv(self, heater_entity_id=None):
|
||||
)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: TO TRV set_hvac_mode: {heater_entity_id} from: {_trv.state} to: {_new_hvac_mode}"
|
||||
f"better_thermostat {self.device_name}: TO TRV set_hvac_mode: {heater_entity_id} from: {_trv.state} to: {_new_hvac_mode}"
|
||||
)
|
||||
self.real_trvs[heater_entity_id]["last_hvac_mode"] = _new_hvac_mode
|
||||
_tvr_has_quirk = await override_set_hvac_mode(
|
||||
@@ -210,55 +239,54 @@ async def control_trv(self, heater_entity_id=None):
|
||||
await set_hvac_mode(self, heater_entity_id, _new_hvac_mode)
|
||||
if self.real_trvs[heater_entity_id]["system_mode_received"] is True:
|
||||
self.real_trvs[heater_entity_id]["system_mode_received"] = False
|
||||
asyncio.create_task(check_system_mode(self, heater_entity_id))
|
||||
self.task_manager.create_task(check_system_mode(self, heater_entity_id))
|
||||
|
||||
# set new calibration offset
|
||||
if _calibration is not None and _new_hvac_mode != HVACMode.OFF:
|
||||
old_calibration = await get_current_offset(self, heater_entity_id)
|
||||
step_calibration = await get_offset_steps(self, heater_entity_id)
|
||||
if old_calibration is None or step_calibration is None:
|
||||
if (
|
||||
_calibration is not None
|
||||
and _new_hvac_mode != HVACMode.OFF
|
||||
and _calibration_mode != CalibrationMode.NO_CALIBRATION
|
||||
):
|
||||
_current_calibration_s = await get_current_offset(self, heater_entity_id)
|
||||
|
||||
if _current_calibration_s is None:
|
||||
_LOGGER.error(
|
||||
"better_thermostat %s: calibration fatal error %s",
|
||||
self.name,
|
||||
self.device_name,
|
||||
heater_entity_id,
|
||||
)
|
||||
# this should not be before, set_hvac_mode (because if it fails, the new hvac mode will never be sent)
|
||||
self.ignore_states = False
|
||||
self.real_trvs[heater_entity_id]["ignore_trv_states"] = False
|
||||
return True
|
||||
current_calibration = convert_to_float(
|
||||
str(old_calibration), self.name, "controlling()"
|
||||
)
|
||||
if step_calibration.is_integer():
|
||||
_calibration = calibration_round(
|
||||
float(str(format(float(_calibration), ".1f")))
|
||||
)
|
||||
else:
|
||||
_calibration = float(str(format(float(_calibration), ".1f")))
|
||||
|
||||
old = self.real_trvs[heater_entity_id].get(
|
||||
"last_calibration", current_calibration
|
||||
_current_calibration = convert_to_float(
|
||||
str(_current_calibration_s), self.device_name, "controlling()"
|
||||
)
|
||||
|
||||
_calibration = float(str(_calibration))
|
||||
|
||||
_old_calibration = self.real_trvs[heater_entity_id].get(
|
||||
"last_calibration", _current_calibration
|
||||
)
|
||||
|
||||
if self.real_trvs[heater_entity_id][
|
||||
"calibration_received"
|
||||
] is True and float(old) != float(_calibration):
|
||||
] is True and float(_old_calibration) != float(_calibration):
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: TO TRV set_local_temperature_calibration: {heater_entity_id} from: {old} to: {_calibration}"
|
||||
f"better_thermostat {self.device_name}: TO TRV set_local_temperature_calibration: {heater_entity_id} from: {_old_calibration} to: {_calibration}"
|
||||
)
|
||||
await set_offset(self, heater_entity_id, _calibration)
|
||||
self.real_trvs[heater_entity_id]["calibration_received"] = False
|
||||
|
||||
# set new target temperature
|
||||
if (
|
||||
_temperature is not None
|
||||
and _new_hvac_mode != HVACMode.OFF
|
||||
or _no_off_system_mode
|
||||
if _temperature is not None and (
|
||||
_new_hvac_mode != HVACMode.OFF or _no_off_system_mode
|
||||
):
|
||||
if _temperature != _current_set_temperature:
|
||||
old = self.real_trvs[heater_entity_id].get("last_temperature", "?")
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: TO TRV set_temperature: {heater_entity_id} from: {old} to: {_temperature}"
|
||||
f"better_thermostat {self.device_name}: TO TRV set_temperature: {heater_entity_id} from: {old} to: {_temperature}"
|
||||
)
|
||||
self.real_trvs[heater_entity_id]["last_temperature"] = _temperature
|
||||
_tvr_has_quirk = await override_set_temperature(
|
||||
@@ -268,7 +296,7 @@ async def control_trv(self, heater_entity_id=None):
|
||||
await set_temperature(self, heater_entity_id, _temperature)
|
||||
if self.real_trvs[heater_entity_id]["target_temp_received"] is True:
|
||||
self.real_trvs[heater_entity_id]["target_temp_received"] = False
|
||||
asyncio.create_task(
|
||||
self.task_manager.create_task(
|
||||
check_target_temperature(self, heater_entity_id)
|
||||
)
|
||||
|
||||
@@ -288,13 +316,13 @@ def handle_window_open(self, _remapped_states):
|
||||
_hvac_mode_send = HVACMode.OFF
|
||||
self.last_window_state = True
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: control_trv: window is open or status of window is unknown, setting window open"
|
||||
f"better_thermostat {self.device_name}: control_trv: window is open or status of window is unknown, setting window open"
|
||||
)
|
||||
elif self.window_open is False and self.last_window_state is True:
|
||||
_hvac_mode_send = self.last_main_hvac_mode
|
||||
self.last_window_state = False
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: control_trv: window is closed, setting window closed restoring mode: {_hvac_mode_send}"
|
||||
f"better_thermostat {self.device_name}: control_trv: window is closed, setting window closed restoring mode: {_hvac_mode_send}"
|
||||
)
|
||||
|
||||
# Force off on window open
|
||||
@@ -311,7 +339,7 @@ async def check_system_mode(self, heater_entity_id=None):
|
||||
while _real_trv["hvac_mode"] != _real_trv["last_hvac_mode"]:
|
||||
if _timeout > 360:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: {heater_entity_id} the real TRV did not respond to the system mode change"
|
||||
f"better_thermostat {self.device_name}: {heater_entity_id} the real TRV did not respond to the system mode change"
|
||||
)
|
||||
_timeout = 0
|
||||
break
|
||||
@@ -333,12 +361,12 @@ async def check_target_temperature(self, heater_entity_id=None):
|
||||
"temperature", None
|
||||
)
|
||||
),
|
||||
self.name,
|
||||
self.device_name,
|
||||
"check_target_temperature()",
|
||||
)
|
||||
if _timeout == 0:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: {heater_entity_id} / check_target_temp / _last: {_real_trv['last_temperature']} - _current: {_current_set_temperature}"
|
||||
f"better_thermostat {self.device_name}: {heater_entity_id} / check_target_temp / _last: {_real_trv['last_temperature']} - _current: {_current_set_temperature}"
|
||||
)
|
||||
if (
|
||||
_current_set_temperature is None
|
||||
@@ -346,9 +374,9 @@ async def check_target_temperature(self, heater_entity_id=None):
|
||||
):
|
||||
_timeout = 0
|
||||
break
|
||||
if _timeout > 120:
|
||||
if _timeout > 360:
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: {heater_entity_id} the real TRV did not respond to the target temperature change"
|
||||
f"better_thermostat {self.device_name}: {heater_entity_id} the real TRV did not respond to the target temperature change"
|
||||
)
|
||||
_timeout = 0
|
||||
break
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
"""Helper functions for the Better Thermostat component."""
|
||||
|
||||
import re
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
from enum import Enum
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.entity_registry import async_entries_for_config_entry
|
||||
|
||||
from homeassistant.components.climate.const import HVACMode, HVACAction
|
||||
from homeassistant.components.climate.const import HVACMode
|
||||
|
||||
from custom_components.better_thermostat.utils.model_quirks import (
|
||||
fix_local_calibration,
|
||||
fix_target_temperature_calibration,
|
||||
)
|
||||
|
||||
|
||||
from ..const import (
|
||||
CONF_HEAT_AUTO_SWAPPED,
|
||||
CONF_HEATING_POWER_CALIBRATION,
|
||||
CONF_FIX_CALIBRATION,
|
||||
CONF_PROTECT_OVERHEATING,
|
||||
)
|
||||
from custom_components.better_thermostat.utils.const import CONF_HEAT_AUTO_SWAPPED
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -56,239 +47,80 @@ def mode_remap(self, entity_id, hvac_mode: str, inbound: bool = False) -> str:
|
||||
)
|
||||
|
||||
if _heat_auto_swapped:
|
||||
if hvac_mode == HVACMode.HEAT and inbound is False:
|
||||
if hvac_mode == HVACMode.HEAT and not inbound:
|
||||
return HVACMode.AUTO
|
||||
elif hvac_mode == HVACMode.AUTO and inbound is True:
|
||||
if hvac_mode == HVACMode.AUTO and inbound:
|
||||
return HVACMode.HEAT
|
||||
else:
|
||||
return hvac_mode
|
||||
else:
|
||||
if hvac_mode != HVACMode.AUTO:
|
||||
return hvac_mode
|
||||
else:
|
||||
_LOGGER.error(
|
||||
f"better_thermostat {self.name}: {entity_id} HVAC mode {hvac_mode} is not supported by this device, is it possible that you forgot to set the heat auto swapped option?"
|
||||
)
|
||||
return HVACMode.OFF
|
||||
return hvac_mode
|
||||
|
||||
trv_modes = self.real_trvs[entity_id]["hvac_modes"]
|
||||
if HVACMode.HEAT not in trv_modes and HVACMode.HEAT_COOL in trv_modes:
|
||||
# entity only supports HEAT_COOL, but not HEAT - need to translate
|
||||
if not inbound and hvac_mode == HVACMode.HEAT:
|
||||
return HVACMode.HEAT_COOL
|
||||
if inbound and hvac_mode == HVACMode.HEAT_COOL:
|
||||
return HVACMode.HEAT
|
||||
|
||||
def calculate_local_setpoint_delta(self, entity_id) -> Union[float, None]:
|
||||
"""Calculate local delta to adjust the setpoint of the TRV based on the air temperature of the external sensor.
|
||||
if hvac_mode != HVACMode.AUTO:
|
||||
return hvac_mode
|
||||
|
||||
This calibration is for devices with local calibration option, it syncs the current temperature of the TRV to the target temperature of
|
||||
the external sensor.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
self :
|
||||
self instance of better_thermostat
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
new local calibration delta
|
||||
"""
|
||||
_context = "calculate_local_setpoint_delta()"
|
||||
|
||||
if None in (self.cur_temp, self.bt_target_temp, self.old_internal_temp):
|
||||
return None
|
||||
|
||||
# check if we need to calculate
|
||||
if (
|
||||
self.real_trvs[entity_id]["current_temperature"] == self.old_internal_temp
|
||||
and self.cur_temp == self.old_external_temp
|
||||
):
|
||||
return None
|
||||
|
||||
_cur_trv_temp = convert_to_float(
|
||||
str(self.real_trvs[entity_id]["current_temperature"]), self.name, _context
|
||||
_LOGGER.error(
|
||||
f"better_thermostat {self.device_name}: {entity_id} HVAC mode {
|
||||
hvac_mode} is not supported by this device, is it possible that you forgot to set the heat auto swapped option?"
|
||||
)
|
||||
|
||||
_calibration_delta = float(
|
||||
str(format(float(abs(_cur_trv_temp - self.cur_temp)), ".1f"))
|
||||
)
|
||||
|
||||
if _calibration_delta <= 0.5:
|
||||
return None
|
||||
|
||||
self.old_internal_temp = self.real_trvs[entity_id]["current_temperature"]
|
||||
self.old_external_temp = self.cur_temp
|
||||
|
||||
_current_trv_calibration = round_to_half_degree(
|
||||
convert_to_float(
|
||||
str(self.real_trvs[entity_id]["last_calibration"]), self.name, _context
|
||||
)
|
||||
)
|
||||
|
||||
if None in (_current_trv_calibration, self.cur_temp, _cur_trv_temp):
|
||||
_LOGGER.warning(
|
||||
f"better thermostat {self.name}: {entity_id} Could not calculate local setpoint delta in {_context}:"
|
||||
f" current_trv_calibration: {_current_trv_calibration}, current_trv_temp: {_cur_trv_temp}, cur_temp: {self.cur_temp}"
|
||||
)
|
||||
return None
|
||||
|
||||
_new_local_calibration = (self.cur_temp - _cur_trv_temp) + _current_trv_calibration
|
||||
|
||||
_calibration_mode = self.real_trvs[entity_id]["advanced"].get(
|
||||
"calibration_mode", "default"
|
||||
)
|
||||
if _calibration_mode == CONF_FIX_CALIBRATION:
|
||||
# _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp))
|
||||
# if _temp_diff > 0.30 and _new_local_calibration > -2.5:
|
||||
# _new_local_calibration -= 2.5
|
||||
if self.attr_hvac_action == HVACAction.HEATING:
|
||||
if _new_local_calibration > -2.5:
|
||||
_new_local_calibration -= 2.5
|
||||
|
||||
elif _calibration_mode == CONF_HEATING_POWER_CALIBRATION:
|
||||
# _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp))
|
||||
# if _temp_diff > 0.0:
|
||||
# valve_position = heating_power_valve_position(self, entity_id)
|
||||
# _new_local_calibration = _current_trv_calibration - (
|
||||
# (self.real_trvs[entity_id]["local_calibration_min"] + _cur_trv_temp)
|
||||
# * valve_position
|
||||
# )
|
||||
if self.attr_hvac_action == HVACAction.HEATING:
|
||||
# if _temp_diff > 0.0:
|
||||
valve_position = heating_power_valve_position(self, entity_id)
|
||||
_new_local_calibration = _current_trv_calibration - (
|
||||
(self.real_trvs[entity_id]["local_calibration_min"] + _cur_trv_temp)
|
||||
* valve_position
|
||||
)
|
||||
elif self.attr_hvac_action == HVACAction.IDLE:
|
||||
if _new_local_calibration < 0.0:
|
||||
_new_local_calibration += self.tolerance
|
||||
|
||||
_new_local_calibration = fix_local_calibration(
|
||||
self, entity_id, _new_local_calibration
|
||||
)
|
||||
|
||||
_overheating_protection = self.real_trvs[entity_id]["advanced"].get(
|
||||
CONF_PROTECT_OVERHEATING, False
|
||||
)
|
||||
|
||||
if _overheating_protection is True:
|
||||
if self.cur_temp >= self.bt_target_temp:
|
||||
_new_local_calibration += (self.cur_temp - self.bt_target_temp) * 10.0
|
||||
|
||||
_new_local_calibration = round_down_to_half_degree(_new_local_calibration)
|
||||
|
||||
if _new_local_calibration > float(
|
||||
self.real_trvs[entity_id]["local_calibration_max"]
|
||||
):
|
||||
_new_local_calibration = float(
|
||||
self.real_trvs[entity_id]["local_calibration_max"]
|
||||
)
|
||||
elif _new_local_calibration < float(
|
||||
self.real_trvs[entity_id]["local_calibration_min"]
|
||||
):
|
||||
_new_local_calibration = float(
|
||||
self.real_trvs[entity_id]["local_calibration_min"]
|
||||
)
|
||||
|
||||
_new_local_calibration = convert_to_float(
|
||||
str(_new_local_calibration), self.name, _context
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"better_thermostat %s: %s - output calib: %s",
|
||||
self.name,
|
||||
entity_id,
|
||||
_new_local_calibration,
|
||||
)
|
||||
|
||||
return convert_to_float(str(_new_local_calibration), self.name, _context)
|
||||
|
||||
|
||||
def calculate_setpoint_override(self, entity_id) -> Union[float, None]:
|
||||
"""Calculate new setpoint for the TRV based on its own temperature measurement and the air temperature of the external sensor.
|
||||
|
||||
This calibration is for devices with no local calibration option, it syncs the target temperature of the TRV to a new target
|
||||
temperature based on the current temperature of the external sensor.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
self :
|
||||
self instance of better_thermostat
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
new target temp with calibration
|
||||
"""
|
||||
if None in (self.cur_temp, self.bt_target_temp):
|
||||
return None
|
||||
|
||||
_cur_trv_temp = self.hass.states.get(entity_id).attributes["current_temperature"]
|
||||
if None in (self.bt_target_temp, self.cur_temp, _cur_trv_temp):
|
||||
return None
|
||||
|
||||
_calibrated_setpoint = (self.bt_target_temp - self.cur_temp) + _cur_trv_temp
|
||||
|
||||
_calibration_mode = self.real_trvs[entity_id]["advanced"].get(
|
||||
"calibration_mode", "default"
|
||||
)
|
||||
if _calibration_mode == CONF_FIX_CALIBRATION:
|
||||
# _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp))
|
||||
if self.attr_hvac_action == HVACAction.HEATING:
|
||||
if _calibrated_setpoint - _cur_trv_temp < 2.5:
|
||||
_calibrated_setpoint += 2.5
|
||||
elif self.attr_hvac_action == HVACAction.IDLE:
|
||||
if _calibrated_setpoint - _cur_trv_temp > 0.0:
|
||||
_calibrated_setpoint -= self.tolerance
|
||||
|
||||
elif _calibration_mode == CONF_HEATING_POWER_CALIBRATION:
|
||||
# _temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp))
|
||||
if self.attr_hvac_action == HVACAction.HEATING:
|
||||
# if _temp_diff > 0.0:
|
||||
valve_position = heating_power_valve_position(self, entity_id)
|
||||
_calibrated_setpoint = _cur_trv_temp + (
|
||||
(self.real_trvs[entity_id]["max_temp"] - _cur_trv_temp) * valve_position
|
||||
)
|
||||
elif self.attr_hvac_action == HVACAction.IDLE:
|
||||
if _calibrated_setpoint - _cur_trv_temp > 0.0:
|
||||
_calibrated_setpoint -= self.tolerance
|
||||
|
||||
_calibrated_setpoint = fix_target_temperature_calibration(
|
||||
self, entity_id, _calibrated_setpoint
|
||||
)
|
||||
|
||||
_overheating_protection = self.real_trvs[entity_id]["advanced"].get(
|
||||
CONF_PROTECT_OVERHEATING, False
|
||||
)
|
||||
|
||||
if _overheating_protection is True:
|
||||
if self.cur_temp >= self.bt_target_temp:
|
||||
_calibrated_setpoint -= (self.cur_temp - self.bt_target_temp) * 10.0
|
||||
|
||||
_calibrated_setpoint = round_down_to_half_degree(_calibrated_setpoint)
|
||||
|
||||
# check if new setpoint is inside the TRV's range, else set to min or max
|
||||
if _calibrated_setpoint < self.real_trvs[entity_id]["min_temp"]:
|
||||
_calibrated_setpoint = self.real_trvs[entity_id]["min_temp"]
|
||||
if _calibrated_setpoint > self.real_trvs[entity_id]["max_temp"]:
|
||||
_calibrated_setpoint = self.real_trvs[entity_id]["max_temp"]
|
||||
|
||||
return _calibrated_setpoint
|
||||
return HVACMode.OFF
|
||||
|
||||
|
||||
def heating_power_valve_position(self, entity_id):
|
||||
_temp_diff = float(float(self.bt_target_temp) - float(self.cur_temp))
|
||||
valve_pos = (_temp_diff / self.heating_power) / 100
|
||||
|
||||
a = 0.019
|
||||
b = 0.946
|
||||
valve_pos = a * (_temp_diff / self.heating_power) ** b
|
||||
|
||||
if valve_pos < 0.0:
|
||||
valve_pos = 0.0
|
||||
if valve_pos > 1.0:
|
||||
valve_pos = 1.0
|
||||
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: {entity_id} / heating_power_valve_position - temp diff: {round(_temp_diff, 1)} - heating power: {round(self.heating_power, 4)} - expected valve position: {round(valve_pos * 100)}%"
|
||||
f"better_thermostat {self.device_name}: {entity_id} / heating_power_valve_position - temp diff: {round(
|
||||
_temp_diff, 1)} - heating power: {round(self.heating_power, 4)} - expected valve position: {round(valve_pos * 100)}%"
|
||||
)
|
||||
return valve_pos
|
||||
|
||||
# Example values for different heating_power and temp_diff:
|
||||
# With heating_power of 0.02:
|
||||
# | temp_diff | valve_pos |
|
||||
# |-----------|------------|
|
||||
# | 0.1 | 0.0871 |
|
||||
# | 0.2 | 0.1678 |
|
||||
# | 0.3 | 0.2462 |
|
||||
# | 0.4 | 0.3232 |
|
||||
# | 0.5 | 0.3992 |
|
||||
|
||||
# With heating_power of 0.01:
|
||||
# | temp_diff | valve_pos |
|
||||
# |-----------|------------|
|
||||
# | 0.1 | 0.1678 |
|
||||
# | 0.2 | 0.3232 |
|
||||
# | 0.3 | 0.4744 |
|
||||
# | 0.4 | 0.6227 |
|
||||
# | 0.5 | 0.7691 |
|
||||
|
||||
# With heating_power of 0.005:
|
||||
# | temp_diff | valve_pos |
|
||||
# |-----------|------------|
|
||||
# | 0.1 | 0.3232 |
|
||||
# | 0.2 | 0.6227 |
|
||||
# | 0.3 | 0.9139 |
|
||||
# | 0.4 | 1.0000 |
|
||||
# | 0.5 | 1.0000 |
|
||||
|
||||
|
||||
def convert_to_float(
|
||||
value: Union[str, int, float], instance_name: str, context: str
|
||||
) -> Union[float, None]:
|
||||
value: str | float, instance_name: str, context: str
|
||||
) -> float | None:
|
||||
"""Convert value to float or print error message.
|
||||
|
||||
Parameters
|
||||
@@ -307,115 +139,53 @@ def convert_to_float(
|
||||
None
|
||||
If error occurred and cannot convert the value.
|
||||
"""
|
||||
if isinstance(value, float):
|
||||
return round(value, 1)
|
||||
elif value is None or value == "None":
|
||||
if value is None or value == "None":
|
||||
return None
|
||||
try:
|
||||
return round_by_step(float(value), 0.1)
|
||||
except (ValueError, TypeError, AttributeError, KeyError):
|
||||
_LOGGER.debug(
|
||||
f"better thermostat {instance_name}: Could not convert '{
|
||||
value}' to float in {context}"
|
||||
)
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
return round(float(str(format(float(value), ".1f"))), 1)
|
||||
except (ValueError, TypeError, AttributeError, KeyError):
|
||||
_LOGGER.debug(
|
||||
f"better thermostat {instance_name}: Could not convert '{value}' to float in {context}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def calibration_round(value: Union[int, float, None]) -> Union[float, int, None]:
|
||||
"""Round the calibration value to the nearest 0.5.
|
||||
class rounding(Enum):
|
||||
# rounding functions that avoid errors due to using floats
|
||||
|
||||
def up(x: float) -> float:
|
||||
return math.ceil(x - 0.0001)
|
||||
|
||||
def down(x: float) -> float:
|
||||
return math.floor(x + 0.0001)
|
||||
|
||||
def nearest(x: float) -> float:
|
||||
return round(x - 0.0001)
|
||||
|
||||
|
||||
def round_by_step(
|
||||
value: float | None, step: float | None, f_rounding: rounding = rounding.nearest
|
||||
) -> float | None:
|
||||
"""Round the value based on the allowed decimal 'step' size.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : float
|
||||
the value to round
|
||||
step : float
|
||||
size of one step
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
the rounded value
|
||||
"""
|
||||
if value is None:
|
||||
|
||||
if value is None or step is None:
|
||||
return None
|
||||
split = str(float(str(value))).split(".", 1)
|
||||
decimale = int(split[1])
|
||||
if decimale > 8:
|
||||
return float(str(split[0])) + 1.0
|
||||
else:
|
||||
return float(str(split[0]))
|
||||
|
||||
|
||||
def round_down_to_half_degree(
|
||||
value: Union[int, float, None]
|
||||
) -> Union[float, int, None]:
|
||||
"""Round the value down to the nearest 0.5.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : float
|
||||
the value to round
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
the rounded value
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
split = str(float(str(value))).split(".", 1)
|
||||
decimale = int(split[1])
|
||||
if decimale >= 5:
|
||||
if float(split[0]) > 0:
|
||||
return float(str(split[0])) + 0.5
|
||||
else:
|
||||
return float(str(split[0])) - 0.5
|
||||
else:
|
||||
return float(str(split[0]))
|
||||
|
||||
|
||||
def round_to_half_degree(value: Union[int, float, None]) -> Union[float, int, None]:
|
||||
"""Rounds numbers to the nearest n.5/n.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : int, float
|
||||
input value
|
||||
|
||||
Returns
|
||||
-------
|
||||
float, int
|
||||
either an int, if input was an int, or a float rounded to n.5/n.0
|
||||
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
elif isinstance(value, float):
|
||||
return round(value * 2) / 2
|
||||
elif isinstance(value, int):
|
||||
return value
|
||||
|
||||
|
||||
def round_to_hundredth_degree(
|
||||
value: Union[int, float, None]
|
||||
) -> Union[float, int, None]:
|
||||
"""Rounds numbers to the nearest n.nn0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : int, float
|
||||
input value
|
||||
|
||||
Returns
|
||||
-------
|
||||
float, int
|
||||
either an int, if input was an int, or a float rounded to n.nn0
|
||||
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
elif isinstance(value, float):
|
||||
return round(value * 100) / 100
|
||||
elif isinstance(value, int):
|
||||
return value
|
||||
# convert to integer number of steps for rounding, then convert back to decimal
|
||||
return f_rounding(value / step) * step
|
||||
|
||||
|
||||
def check_float(potential_float):
|
||||
@@ -499,7 +269,8 @@ async def find_valve_entity(self, entity_id):
|
||||
if entity.device_id == reg_entity.device_id:
|
||||
if "_valve_position" in uid or "_position" in uid:
|
||||
_LOGGER.debug(
|
||||
f"better thermostat: Found valve position entity {entity.entity_id} for {entity_id}"
|
||||
f"better thermostat: Found valve position entity {
|
||||
entity.entity_id} for {entity_id}"
|
||||
)
|
||||
return entity.entity_id
|
||||
|
||||
@@ -561,7 +332,8 @@ async def find_local_calibration_entity(self, entity_id):
|
||||
if entity.device_id == reg_entity.device_id:
|
||||
if "temperature_calibration" in uid or "temperature_offset" in uid:
|
||||
_LOGGER.debug(
|
||||
f"better thermostat: Found local calibration entity {entity.entity_id} for {entity_id}"
|
||||
f"better thermostat: Found local calibration entity {
|
||||
entity.entity_id} for {entity_id}"
|
||||
)
|
||||
return entity.entity_id
|
||||
|
||||
@@ -635,12 +407,13 @@ async def get_device_model(self, entity_id):
|
||||
entry = entity_reg.async_get(entity_id)
|
||||
dev_reg = dr.async_get(self.hass)
|
||||
device = dev_reg.async_get(entry.device_id)
|
||||
_LOGGER.debug(f"better_thermostat {self.name}: found device:")
|
||||
_LOGGER.debug(f"better_thermostat {self.device_name}: found device:")
|
||||
_LOGGER.debug(device)
|
||||
try:
|
||||
# Z2M reports the device name as a long string with the actual model name in braces, we need to extract it
|
||||
return re.search("\\((.+?)\\)", device.model).group(1)
|
||||
except AttributeError:
|
||||
matches = re.findall(r"\((.+?)\)", device.model)
|
||||
return matches[-1]
|
||||
except IndexError:
|
||||
# Other climate integrations might report the model name plainly, need more infos on this
|
||||
return device.model
|
||||
except (
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
from importlib import import_module
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_model_quirks(self, model, entity_id):
|
||||
"""Load model."""
|
||||
|
||||
# remove / from model
|
||||
model = model.replace("/", "_")
|
||||
|
||||
try:
|
||||
self.model_quirks = import_module(
|
||||
"custom_components.better_thermostat.model_fixes." + model,
|
||||
package="better_thermostat",
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"better_thermostat %s: uses quirks fixes for model %s for trv %s",
|
||||
self.name,
|
||||
model,
|
||||
entity_id,
|
||||
)
|
||||
except Exception:
|
||||
self.model_quirks = import_module(
|
||||
"custom_components.better_thermostat.model_fixes.default",
|
||||
package="better_thermostat",
|
||||
)
|
||||
pass
|
||||
|
||||
return self.model_quirks
|
||||
|
||||
|
||||
def fix_local_calibration(self, entity_id, offset):
|
||||
return self.real_trvs[entity_id]["model_quirks"].fix_local_calibration(
|
||||
self, entity_id, offset
|
||||
)
|
||||
|
||||
|
||||
def fix_target_temperature_calibration(self, entity_id, temperature):
|
||||
return self.real_trvs[entity_id]["model_quirks"].fix_target_temperature_calibration(
|
||||
self, entity_id, temperature
|
||||
)
|
||||
|
||||
|
||||
async def override_set_hvac_mode(self, entity_id, hvac_mode):
|
||||
return await self.real_trvs[entity_id]["model_quirks"].override_set_hvac_mode(
|
||||
self, entity_id, hvac_mode
|
||||
)
|
||||
|
||||
|
||||
async def override_set_temperature(self, entity_id, temperature):
|
||||
return await self.real_trvs[entity_id]["model_quirks"].override_set_temperature(
|
||||
self, entity_id, temperature
|
||||
)
|
||||
@@ -24,7 +24,7 @@ async def check_entity(self, entity) -> bool:
|
||||
"unavailable",
|
||||
):
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: {entity} is unavailable. with state {state}"
|
||||
f"better_thermostat {self.device_name}: {entity} is unavailable. with state {state}"
|
||||
)
|
||||
return False
|
||||
if entity in self.devices_errors:
|
||||
@@ -66,7 +66,10 @@ async def check_all_entities(self) -> bool:
|
||||
learn_more_url="https://better-thermostat.org/qanda/missing_entity",
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="missing_entity",
|
||||
translation_placeholders={"entity": str(name), "name": str(self.name)},
|
||||
translation_placeholders={
|
||||
"entity": str(name),
|
||||
"name": str(self.device_name),
|
||||
},
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -17,7 +17,7 @@ from statistics import median
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_weather(self) -> bool:
|
||||
async def check_weather(self) -> bool:
|
||||
"""check weather predictions or ambient air temperature if available
|
||||
|
||||
Parameters
|
||||
@@ -37,7 +37,7 @@ def check_weather(self) -> bool:
|
||||
self.call_for_heat = True
|
||||
|
||||
if self.weather_entity is not None:
|
||||
_call_for_heat_weather = check_weather_prediction(self)
|
||||
_call_for_heat_weather = await check_weather_prediction(self)
|
||||
self.call_for_heat = _call_for_heat_weather
|
||||
|
||||
if self.outdoor_sensor is not None:
|
||||
@@ -45,7 +45,7 @@ def check_weather(self) -> bool:
|
||||
# TODO: add condition if heating period (oct-mar) then set it to true?
|
||||
_LOGGER.warning(
|
||||
"better_thermostat %s: no outdoor sensor data found. fallback to heat",
|
||||
self.name,
|
||||
self.device_name,
|
||||
)
|
||||
_call_for_heat_outdoor = True
|
||||
else:
|
||||
@@ -63,7 +63,7 @@ def check_weather(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def check_weather_prediction(self) -> bool:
|
||||
async def check_weather_prediction(self) -> bool:
|
||||
"""Checks configured weather entity for next two days of temperature predictions.
|
||||
|
||||
Returns
|
||||
@@ -74,17 +74,26 @@ def check_weather_prediction(self) -> bool:
|
||||
if not successful
|
||||
"""
|
||||
if self.weather_entity is None:
|
||||
_LOGGER.warning(f"better_thermostat {self.name}: weather entity not available.")
|
||||
_LOGGER.warning(
|
||||
f"better_thermostat {self.device_name}: weather entity not available."
|
||||
)
|
||||
return None
|
||||
|
||||
if self.off_temperature is None or not isinstance(self.off_temperature, float):
|
||||
_LOGGER.warning(
|
||||
f"better_thermostat {self.name}: off_temperature not set or not a float."
|
||||
f"better_thermostat {self.device_name}: off_temperature not set or not a float."
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
forecast = self.hass.states.get(self.weather_entity).attributes.get("forecast")
|
||||
forecasts = await self.hass.services.async_call(
|
||||
"weather",
|
||||
"get_forecasts",
|
||||
{"type": "daily", "entity_id": self.weather_entity},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
forecast = forecasts.get(self.weather_entity).get("forecast")
|
||||
if len(forecast) > 0:
|
||||
cur_outside_temp = convert_to_float(
|
||||
str(
|
||||
@@ -92,7 +101,7 @@ def check_weather_prediction(self) -> bool:
|
||||
"temperature"
|
||||
)
|
||||
),
|
||||
self.name,
|
||||
self.device_name,
|
||||
"check_weather_prediction()",
|
||||
)
|
||||
max_forecast_temp = int(
|
||||
@@ -100,12 +109,12 @@ def check_weather_prediction(self) -> bool:
|
||||
(
|
||||
convert_to_float(
|
||||
str(forecast[0]["temperature"]),
|
||||
self.name,
|
||||
self.device_name,
|
||||
"check_weather_prediction()",
|
||||
)
|
||||
+ convert_to_float(
|
||||
str(forecast[1]["temperature"]),
|
||||
self.name,
|
||||
self.device_name,
|
||||
"check_weather_prediction()",
|
||||
)
|
||||
)
|
||||
@@ -119,7 +128,9 @@ def check_weather_prediction(self) -> bool:
|
||||
else:
|
||||
raise TypeError
|
||||
except TypeError:
|
||||
_LOGGER.warning(f"better_thermostat {self.name}: no weather entity data found.")
|
||||
_LOGGER.warning(
|
||||
f"better_thermostat {self.device_name}: no weather entity data found."
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -138,13 +149,13 @@ async def check_ambient_air_temperature(self):
|
||||
|
||||
if self.off_temperature is None or not isinstance(self.off_temperature, float):
|
||||
_LOGGER.warning(
|
||||
f"better_thermostat {self.name}: off_temperature not set or not a float."
|
||||
f"better_thermostat {self.device_name}: off_temperature not set or not a float."
|
||||
)
|
||||
return None
|
||||
|
||||
self.last_avg_outdoor_temp = convert_to_float(
|
||||
self.hass.states.get(self.outdoor_sensor).state,
|
||||
self.name,
|
||||
self.device_name,
|
||||
"check_ambient_air_temperature()",
|
||||
)
|
||||
if "recorder" in self.hass.config.components:
|
||||
@@ -170,13 +181,15 @@ async def check_ambient_air_temperature(self):
|
||||
)
|
||||
|
||||
for item in history_list.get(lower_entity_id):
|
||||
# filter out all None, NaN and "unknown" states
|
||||
# filter out all None, NaN, "unknown" and "unavailable" states.
|
||||
# only keep real values
|
||||
with suppress(ValueError):
|
||||
if item.state != "unknown":
|
||||
if item.state not in ("unknown", "unavailable"):
|
||||
_temp_history.add_measurement(
|
||||
convert_to_float(
|
||||
item.state, self.name, "check_ambient_air_temperature()"
|
||||
item.state,
|
||||
self.device_name,
|
||||
"check_ambient_air_temperature()",
|
||||
),
|
||||
datetime.fromtimestamp(item.last_updated.timestamp()),
|
||||
)
|
||||
@@ -188,7 +201,7 @@ async def check_ambient_air_temperature(self):
|
||||
avg_temp = self.last_avg_outdoor_temp
|
||||
|
||||
_LOGGER.debug(
|
||||
f"better_thermostat {self.name}: avg outdoor temp: {avg_temp}, threshold is {self.off_temperature}"
|
||||
f"better_thermostat {self.device_name}: avg outdoor temp: {avg_temp}, threshold is {self.off_temperature}"
|
||||
)
|
||||
|
||||
if avg_temp is not None:
|
||||
|
||||
@@ -1,130 +1,119 @@
|
||||
"""electrolux status integration."""
|
||||
from pyelectroluxconnect import Session
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import Config
|
||||
from homeassistant.const import (
|
||||
CONF_LANGUAGE,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
CONF_COUNTRY_CODE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .pyelectroluxconnect_util import pyelectroluxconnect_util
|
||||
from .api import Appliance, Appliances, ElectroluxLibraryEntity
|
||||
from .const import CONF_PASSWORD, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, CONF_REGION, DEFAULT_REGION
|
||||
from .const import CONF_LANGUAGE, DEFAULT_LANGUAGE
|
||||
from .const import CONF_USERNAME
|
||||
from .const import DOMAIN
|
||||
from .const import PLATFORMS
|
||||
from .const import languages
|
||||
from .const import (
|
||||
CONF_RENEW_INTERVAL,
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_COUNTRY_CODE,
|
||||
DEFAULT_WEBSOCKET_RENEWAL_DELAY,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
languages,
|
||||
)
|
||||
from .coordinator import ElectroluxCoordinator
|
||||
from .util import get_electrolux_session
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
async def async_setup(hass: HomeAssistant, config: Config):
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up this integration using YAML is not supported."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
if hass.data.get(DOMAIN) is None:
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
if entry.options.get(CONF_SCAN_INTERVAL):
|
||||
update_interval = timedelta(seconds=entry.options[CONF_SCAN_INTERVAL])
|
||||
else:
|
||||
update_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
|
||||
renew_interval = DEFAULT_WEBSOCKET_RENEWAL_DELAY
|
||||
if entry.options.get(CONF_RENEW_INTERVAL):
|
||||
renew_interval = entry.options[CONF_RENEW_INTERVAL]
|
||||
|
||||
username = entry.data.get(CONF_USERNAME)
|
||||
password = entry.data.get(CONF_PASSWORD)
|
||||
region = entry.data.get(CONF_REGION, DEFAULT_REGION)
|
||||
language = languages.get(entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE),"eng")
|
||||
country_code = entry.data.get(CONF_COUNTRY_CODE, DEFAULT_COUNTRY_CODE)
|
||||
language = languages.get(entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE), "eng")
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
client = pyelectroluxconnect_util.get_session(username, password, region, language)
|
||||
client = get_electrolux_session(username, password, country_code, session, language)
|
||||
coordinator = ElectroluxCoordinator(
|
||||
hass,
|
||||
client=client,
|
||||
renew_interval=renew_interval,
|
||||
username=username,
|
||||
)
|
||||
|
||||
coordinator = ElectroluxStatusDataUpdateCoordinator(hass, client=client, update_interval=update_interval)
|
||||
await coordinator.get_stored_token()
|
||||
if not await coordinator.async_login():
|
||||
raise ConfigEntryAuthFailed
|
||||
raise ConfigEntryAuthFailed("Electrolux wrong credentials")
|
||||
|
||||
# Bug ?
|
||||
if coordinator.config_entry is None:
|
||||
coordinator.config_entry = entry
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
# Initialize entities
|
||||
_LOGGER.debug("async_setup_entry setup_entities")
|
||||
await coordinator.setup_entities()
|
||||
_LOGGER.debug("async_setup_entry listen_websocket")
|
||||
coordinator.listen_websocket()
|
||||
# _LOGGER.debug("async_setup_entry launch_websocket_renewal_task")
|
||||
# await coordinator.launch_websocket_renewal_task()
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.api.close)
|
||||
)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
_LOGGER.debug("async_setup_entry async_config_entry_first_refresh")
|
||||
# Fill in the values for first time
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
_LOGGER.debug("async_setup_entry extend PLATFORMS")
|
||||
coordinator.platforms.extend(PLATFORMS)
|
||||
|
||||
# Call async_setup_entry in entity files
|
||||
_LOGGER.debug("async_setup_entry async_forward_entry_setups")
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.add_update_listener(async_reload_entry)
|
||||
_LOGGER.debug("async_setup_entry OVER")
|
||||
return True
|
||||
|
||||
|
||||
class ElectroluxStatusDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching data from the API."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: Session, update_interval: timedelta) -> None:
|
||||
"""Initialize."""
|
||||
self.api = client
|
||||
self.platforms = []
|
||||
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
||||
|
||||
async def async_login(self) -> bool:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self.api.login)
|
||||
except Exception as ex:
|
||||
_LOGGER.error("Could not log in to ElectroluxStatus, %s", ex)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _async_update_data(self):
|
||||
"""Update data via library."""
|
||||
await self.async_login()
|
||||
found_appliances = {}
|
||||
try:
|
||||
appliances_json = await self.hass.async_add_executor_job(self.api.getAppliances)
|
||||
for appliance in appliances_json:
|
||||
connection_state = await self.hass.async_add_executor_job(self.api.getApplianceConnectionState, appliance)
|
||||
appliance_state = await self.hass.async_add_executor_job(self.api.getApplianceState, appliance)
|
||||
appliance_profile = await self.hass.async_add_executor_job(self.api.getApplianceProfile, appliance)
|
||||
appliance_name = appliances_json[appliance]['alias'] or appliance
|
||||
appliance_model = appliances_json[appliance]['model'] or appliances_json[appliance]['pnc']
|
||||
app = Appliance(appliance_name, appliance, appliances_json[appliance]['brand'], appliance_model)
|
||||
app.setup(ElectroluxLibraryEntity(appliance_name, connection_state, appliance_state, appliance_profile))
|
||||
found_appliances[appliance_name] = app
|
||||
return {
|
||||
"appliances": Appliances(found_appliances)
|
||||
}
|
||||
except Exception as exception:
|
||||
_LOGGER.exception(exception)
|
||||
raise UpdateFailed() from exception
|
||||
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Update listener."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Handle removal of an entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
unloaded = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, platform)
|
||||
for platform in PLATFORMS
|
||||
if platform in coordinator.platforms
|
||||
]
|
||||
)
|
||||
)
|
||||
if unloaded:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unloaded
|
||||
coordinator: ElectroluxCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
await coordinator.close_websocket()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Reload config entry."""
|
||||
_LOGGER.debug("Electrolux async_reload_entry %s", entry)
|
||||
await async_unload_entry(hass, entry)
|
||||
await async_setup_entry(hass, entry)
|
||||
|
||||
@@ -1,330 +1,701 @@
|
||||
"""API for Electrolux Status."""
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from pyelectroluxocp.apiModels import ApplianceInfoResponse, ApplienceStatusResponse
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.button import ButtonDeviceClass
|
||||
from homeassistant.components.number import NumberDeviceClass
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
from homeassistant.const import Platform, UnitOfTemperature
|
||||
|
||||
from .const import BINARY_SENSOR, SENSOR, BUTTON, icon_mapping
|
||||
from .const import sensors, sensors_binary
|
||||
from .binary_sensor import ElectroluxBinarySensor
|
||||
from .button import ElectroluxButton
|
||||
from .catalog_core import CATALOG_BASE, CATALOG_MODEL
|
||||
from .const import (
|
||||
BINARY_SENSOR,
|
||||
BUTTON,
|
||||
ATTRIBUTES_BLACKLIST,
|
||||
NUMBER,
|
||||
PLATFORMS,
|
||||
RENAME_RULES,
|
||||
SELECT,
|
||||
SENSOR,
|
||||
STATIC_ATTRIBUTES,
|
||||
SWITCH, ATTRIBUTES_WHITELIST,
|
||||
)
|
||||
from .entity import ElectroluxEntity
|
||||
from .model import ElectroluxDevice
|
||||
from .number import ElectroluxNumber
|
||||
from .select import ElectroluxSelect
|
||||
from .sensor import ElectroluxSensor
|
||||
from .switch import ElectroluxSwitch
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
HEADERS = {"Content-type": "application/json; charset=UTF-8"}
|
||||
|
||||
def deep_merge_dicts(dict1, dict2):
|
||||
"""
|
||||
Recursively merge two dictionaries.
|
||||
"""
|
||||
result = dict1.copy()
|
||||
for key, value in dict2.items():
|
||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||
result[key] = deep_merge_dicts(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
class ElectroluxLibraryEntity:
|
||||
def __init__(self, name, status, last_states, appliance_profile):
|
||||
"""Electrolux Library Entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
status: str,
|
||||
state: ApplienceStatusResponse,
|
||||
appliance_info: ApplianceInfoResponse,
|
||||
capabilities: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initaliaze the entity."""
|
||||
self.name = name
|
||||
self.status: dict = status
|
||||
self.states = last_states
|
||||
self.profile = appliance_profile
|
||||
self.status = status
|
||||
self.state = state
|
||||
self.appliance_info = appliance_info
|
||||
self.capabilities = capabilities
|
||||
|
||||
@property
|
||||
def reported_state(self) -> dict[str, Any]:
|
||||
"""Return the reported state of the appliance."""
|
||||
return self.state.get("properties", {}).get("reported")
|
||||
|
||||
def get_name(self):
|
||||
"""Get entity name."""
|
||||
return self.name
|
||||
|
||||
def get_value(self, attr_name, field=None, source=None):
|
||||
if attr_name in ["TargetMicrowavePower"]:
|
||||
return self.fix_microwave_power(attr_name, field, source)
|
||||
if attr_name in ["LinkQualityIndicator"]:
|
||||
return self.num_to_dbm(attr_name, field, source)
|
||||
if attr_name in ['StartTime', 'TimeToEnd', 'RunningTime', 'DryingTime', 'ApplianceTotalWorkingTime',
|
||||
"FCTotalWashingTime"]:
|
||||
return self.time_to_end_in_minutes(attr_name, field, source)
|
||||
if attr_name in self.status:
|
||||
return self.status.get(attr_name)
|
||||
if attr_name in [self.states[k].get("name") for k in self.states]:
|
||||
val = self.get_from_states(attr_name, field, source)
|
||||
if field == "container":
|
||||
if val["1"]["name"] == "Coefficient" and val["3"]["name"] == "Exponent":
|
||||
return val["1"]["numberValue"] * (10 ** val["3"]["numberValue"])
|
||||
def get_value(self, attr_name) -> Any:
|
||||
"""Return value by attribute."""
|
||||
if "/" in attr_name:
|
||||
source, attr = attr_name.split("/")
|
||||
return self.reported_state.get(source, {}).get(attr, None)
|
||||
return self.reported_state.get(attr_name, None)
|
||||
|
||||
def get_sensor_name(self, attr_name: str) -> str:
|
||||
"""Get the name of the sensor."""
|
||||
sensor = attr_name
|
||||
for truncate_rule in RENAME_RULES:
|
||||
sensor = re.sub(truncate_rule, "", sensor)
|
||||
sensor = sensor[0].upper() + sensor[1:]
|
||||
sensor = sensor.replace("_", " ")
|
||||
sensor = sensor.replace("/", " ")
|
||||
group = ""
|
||||
words = []
|
||||
for i, char in enumerate(sensor):
|
||||
if group == "":
|
||||
group = char
|
||||
else:
|
||||
return val
|
||||
if attr_name in [self.states[st]["container"][cr].get("name") for st in self.states for cr in
|
||||
self.states[st].get("container", [])]:
|
||||
return self.get_from_states(attr_name, field, source)
|
||||
return None
|
||||
if char == " " and len(group) > 0:
|
||||
words.append(group)
|
||||
group = ""
|
||||
continue
|
||||
|
||||
def time_to_end_in_minutes(self, attr_name, field, source):
|
||||
seconds = self.get_from_states(attr_name, field, source)
|
||||
if seconds is not None:
|
||||
if seconds == -1:
|
||||
return -1
|
||||
return int(math.ceil((int(seconds) / 60)))
|
||||
return None
|
||||
|
||||
def fix_microwave_power(self, attr_name, field, source):
|
||||
microwave_power = self.get_from_states(attr_name, field, source)
|
||||
if microwave_power is not None:
|
||||
if microwave_power == 65535:
|
||||
return 0
|
||||
return microwave_power
|
||||
return None
|
||||
|
||||
def num_to_dbm(self, attr_name, field, source):
|
||||
number_from_0_to_5 = self.get_from_states(attr_name, field, source)
|
||||
if number_from_0_to_5 is not None:
|
||||
if int(number_from_0_to_5) == 0:
|
||||
return -110
|
||||
if int(number_from_0_to_5) == 1:
|
||||
return -80
|
||||
if int(number_from_0_to_5) == 2:
|
||||
return -70
|
||||
if int(number_from_0_to_5) == 3:
|
||||
return -60
|
||||
if int(number_from_0_to_5) == 4:
|
||||
return -55
|
||||
if int(number_from_0_to_5) == 5:
|
||||
return -20
|
||||
return None
|
||||
|
||||
def get_from_states(self, attr_name, field, source):
|
||||
for k in self.states:
|
||||
if attr_name == self.states[k].get("name") and source == self.states[k].get("source"):
|
||||
return self._get_states(self.states[k], field) if field else self._get_states(self.states[k])
|
||||
attr_val = None
|
||||
for c in self.states[k].get("container", []):
|
||||
if attr_name == self.states[k]["container"][c].get("name"):
|
||||
attr_val = self._get_states(self.states[k]["container"][c], field) if field else self._get_states(
|
||||
self.states[k]["container"][c])
|
||||
if attr_val is not None:
|
||||
return attr_val
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_states(states, field=None):
|
||||
if field:
|
||||
if field in states.keys():
|
||||
return states.get(field)
|
||||
if field == "string":
|
||||
if "valueTransl" in states.keys():
|
||||
return states.get("valueTransl").strip(" :.")
|
||||
if "valTransl" in states.keys():
|
||||
return states.get("valTransl").strip(" :.")
|
||||
if "stringValue" in states.keys():
|
||||
return states.get("stringValue").strip(" :.")
|
||||
return ""
|
||||
else:
|
||||
if "valueTransl" in states.keys():
|
||||
return states.get("valueTransl").strip(" :.")
|
||||
if "valTransl" in states.keys():
|
||||
return states.get("valTransl").strip(" :.")
|
||||
if "stringValue" in states.keys():
|
||||
return states.get("stringValue").strip(" :.")
|
||||
if "numberValue" in states.keys():
|
||||
return states.get("numberValue")
|
||||
|
||||
def get_sensor_name(self, attr_name, source):
|
||||
for k in self.states:
|
||||
if attr_name == self.states[k].get("name") and source == self.states[k].get("source"):
|
||||
if "nameTransl" in self.states[k].keys():
|
||||
return self.states[k].get("nameTransl").strip(" :.")
|
||||
if (
|
||||
(char.isupper() or char.isdigit())
|
||||
and (sensor[i - 1].isupper() or sensor[i - 1].isdigit())
|
||||
and (
|
||||
(i == len(sensor) - 1)
|
||||
or (sensor[i + 1].isupper() or sensor[i + 1].isdigit())
|
||||
)
|
||||
):
|
||||
group += char
|
||||
elif (char.isupper() or char.isdigit()) and sensor[i - 1].islower():
|
||||
if re.match("^[A-Z0-9]+$", group):
|
||||
words.append(group)
|
||||
else:
|
||||
words.append(group.lower())
|
||||
group = char
|
||||
else:
|
||||
return self.states[k].get("name").strip(" :.")
|
||||
if "container" in self.states[k]:
|
||||
for c in self.states[k].get("container", []):
|
||||
if attr_name == self.states[k]["container"][c].get("name"):
|
||||
if "nameTransl" in self.states[k]["container"][c].keys():
|
||||
return self.states[k]["container"][c].get("nameTransl").strip(" :.")
|
||||
else:
|
||||
return self.states[k]["container"][c].get("name").strip(" :.")
|
||||
group += char
|
||||
if len(group) > 0:
|
||||
if re.match("^[A-Z0-9]+$", group):
|
||||
words.append(group)
|
||||
else:
|
||||
words.append(group.lower())
|
||||
return " ".join(words).lower().capitalize()
|
||||
|
||||
# def get_sensor_name_old(self, attr_name: str, container: str | None = None):
|
||||
# """Convert sensor format.
|
||||
|
||||
# ex: "fCMiscellaneousState/detergentExtradosage" to "Detergent extradosage".
|
||||
# """
|
||||
# attr_name = attr_name.rpartition("/")[-1] or attr_name
|
||||
# attr_name = attr_name[0].upper() + attr_name[1:]
|
||||
# attr_name = " ".join(re.findall("[A-Z][^A-Z]*", attr_name))
|
||||
# return attr_name.capitalize()
|
||||
|
||||
def get_entity_name(self, attr_name: str) -> str:
|
||||
"""Extract Entity Name.
|
||||
|
||||
ex: Convert format "fCMiscellaneousState/EWX1493A_detergentExtradosage" to "XdetergentExtradosage"
|
||||
"""
|
||||
for truncate_rule in RENAME_RULES:
|
||||
attr_name = re.sub(truncate_rule, "", attr_name)
|
||||
|
||||
return attr_name.rpartition("/")[-1] or attr_name
|
||||
|
||||
def get_entity_attr(self, attr_name: str) -> str:
|
||||
"""Extract Entity attr in raw format.
|
||||
|
||||
ex: Convert format "fCMiscellaneousState/EWX1493A_detergentExtradosage" to "EWX1493A_detergentExtradosage"
|
||||
"""
|
||||
return attr_name.rpartition("/")[-1] or attr_name
|
||||
|
||||
def get_category(self, attr_name: str) -> str:
|
||||
"""Extract category.
|
||||
|
||||
ex: "fCMiscellaneousState/detergentExtradosage" to "fCMiscellaneousState".
|
||||
or "" if none
|
||||
"""
|
||||
return attr_name.rpartition("/")[0]
|
||||
|
||||
def get_capability(self, attr_name: str) -> dict[str, Any] | None:
|
||||
"""Retrieve the capability from self.capabilities using the attribute name.
|
||||
|
||||
May contain slashes for nested keys.
|
||||
"""
|
||||
if self.capabilities.get(attr_name, None):
|
||||
return self.capabilities.get(attr_name)
|
||||
|
||||
keys = attr_name.split("/")
|
||||
result = self.capabilities
|
||||
|
||||
for key in keys:
|
||||
result = result.get(key)
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return result
|
||||
|
||||
def get_entity_unit(self, attr_name: str):
|
||||
"""Get entity unit type."""
|
||||
capability_def: dict[str, Any] | None = self.get_capability(attr_name)
|
||||
if not capability_def:
|
||||
return None
|
||||
# Type : string, int, number, boolean (other values ignored)
|
||||
type_units = capability_def.get("type", None)
|
||||
if not type_units:
|
||||
return None
|
||||
if type_units == "temperature":
|
||||
return UnitOfTemperature.CELSIUS
|
||||
return None
|
||||
|
||||
def value_exists(self, attr_name, source):
|
||||
_container_attr = []
|
||||
for k in self.states:
|
||||
for c in self.states[k].get("container", []):
|
||||
_container_attr.append(self.states[k]["container"][c].get("name"))
|
||||
return (attr_name in self.status) or \
|
||||
(attr_name in [self.states[k].get("name") for k in self.states if
|
||||
self.states[k].get("source") == source]) or \
|
||||
(attr_name in [self.profile[k].get("name") for k in self.profile if
|
||||
self.profile[k].get("source") == source]) or \
|
||||
(attr_name in _container_attr)
|
||||
def get_entity_device_class(self, attr_name: str):
|
||||
"""Get entity device class."""
|
||||
capability_def: dict[str, Any] | None = self.get_capability(attr_name)
|
||||
if not capability_def:
|
||||
return None
|
||||
# Type : string, int, number, boolean (other values ignored)
|
||||
type_class = capability_def.get("type", None)
|
||||
if not type_class:
|
||||
return None
|
||||
if type_class == "temperature":
|
||||
if capability_def.get("access", None) == "readwrite":
|
||||
return NumberDeviceClass.TEMPERATURE
|
||||
return SensorDeviceClass.TEMPERATURE
|
||||
return None
|
||||
|
||||
def sources_list(self):
|
||||
return list(
|
||||
{self.states[k].get("source") for k in self.states if self.states[k].get("source") not in ["NIU", "APL"]}
|
||||
def get_entity_type(self, attr_name: str) -> Platform | None:
|
||||
"""Get entity type."""
|
||||
|
||||
capability_def: dict[str, Any] | None = self.get_capability(attr_name)
|
||||
if not capability_def:
|
||||
return None
|
||||
|
||||
# Type : string, int, number, boolean (other values ignored)
|
||||
type_object = capability_def.get("type", None)
|
||||
if not type_object:
|
||||
return None
|
||||
|
||||
# Access : read, readwrite (other values ignored)
|
||||
access = capability_def.get("access", None)
|
||||
if not access:
|
||||
return None
|
||||
|
||||
# Exception (Electrolux bug)
|
||||
if (
|
||||
type_object == "boolean"
|
||||
and access == "readwrite"
|
||||
and capability_def.get("values", None) is not None
|
||||
):
|
||||
return SWITCH
|
||||
|
||||
# List of values ? if values is defined and has at least 1 entry
|
||||
values: dict[str, Any] | None = capability_def.get("values", None)
|
||||
if (
|
||||
values
|
||||
and access == "readwrite"
|
||||
and isinstance(values, dict)
|
||||
and len(values) > 0
|
||||
):
|
||||
if (
|
||||
type_object not in ["number", "temperature"]
|
||||
or capability_def.get("min", None) is None
|
||||
):
|
||||
return SELECT
|
||||
|
||||
match type_object:
|
||||
case "boolean":
|
||||
if access == "read":
|
||||
return BINARY_SENSOR
|
||||
if access == "readwrite":
|
||||
return SWITCH
|
||||
case "temperature":
|
||||
if access == "read":
|
||||
return SENSOR
|
||||
if access == "readwrite":
|
||||
return NUMBER
|
||||
case "alert":
|
||||
return SENSOR
|
||||
case _:
|
||||
if (
|
||||
self.get_entity_name(attr_name) == "executeCommand"
|
||||
and access == "read"
|
||||
): # FIX for https://github.com/albaintor/homeassistant_electrolux_status/issues/74
|
||||
return BUTTON
|
||||
if access == "write":
|
||||
return BUTTON
|
||||
if access == "constant":
|
||||
return SENSOR
|
||||
if access == "read" and type_object in [
|
||||
"number",
|
||||
"int",
|
||||
"boolean",
|
||||
"string",
|
||||
]:
|
||||
return SENSOR
|
||||
if type_object in ("int", "number"):
|
||||
return NUMBER
|
||||
_LOGGER.debug(
|
||||
"Electrolux unable to determine type for %s. Type: %s Access: %s",
|
||||
attr_name,
|
||||
type_object,
|
||||
access,
|
||||
)
|
||||
|
||||
def commands_list(self, source):
|
||||
commands = list(self.profile[k].get("steps") for k in self.profile if
|
||||
self.profile[k].get("source") == source and self.profile[k].get("name") == "ExecuteCommand")
|
||||
if len(commands) > 0:
|
||||
return commands[0]
|
||||
else:
|
||||
return {}
|
||||
|
||||
def get_command_name(self, command_desc):
|
||||
if "transl" in command_desc:
|
||||
return command_desc["transl"]
|
||||
elif "key" in command_desc:
|
||||
return command_desc["key"]
|
||||
return None
|
||||
|
||||
def get_suffix(self, attr_name, source):
|
||||
res = list({self.states[k].get("source") for k in self.states if self.states[k].get("name") == attr_name})
|
||||
if len(res) == 1:
|
||||
return ""
|
||||
else:
|
||||
if source in res:
|
||||
return f" ({source})"
|
||||
return ""
|
||||
def sources_list(self) -> list[str] | None:
|
||||
"""List the capability types."""
|
||||
if self.capabilities is None:
|
||||
_LOGGER.warning("Electrolux capabilities list is empty")
|
||||
return None
|
||||
|
||||
# dont load these entities by as they are not useful
|
||||
# we do load some of these directly via STATIC_ATTRIBUTES as
|
||||
# one or another are useful, but not all child values are
|
||||
|
||||
class ApplianceEntity:
|
||||
entity_type = None
|
||||
def keep_source(source: str) -> bool:
|
||||
for ignored_pattern in ATTRIBUTES_BLACKLIST:
|
||||
if re.match(ignored_pattern, source):
|
||||
for whitelist_pattern in ATTRIBUTES_WHITELIST:
|
||||
if re.match(whitelist_pattern, source):
|
||||
return True
|
||||
_LOGGER.debug("Exclude source %s from list", source)
|
||||
return False
|
||||
return True
|
||||
|
||||
def __init__(self, name, attr, device_class=None, entity_category=None, field=None, source=None) -> None:
|
||||
self.attr = attr
|
||||
self.name = name
|
||||
self.device_class = device_class
|
||||
self.entity_category = entity_category
|
||||
self.field = field
|
||||
self.source = source
|
||||
self.val_to_send = None
|
||||
self.icon = None
|
||||
self._state = None
|
||||
sources = [
|
||||
key
|
||||
for key in list(self.capabilities.keys())
|
||||
if keep_source(key)
|
||||
]
|
||||
|
||||
def setup(self, data: ElectroluxLibraryEntity):
|
||||
self._state = data.get_value(self.attr, self.field, self.source)
|
||||
return self
|
||||
for key, value in self.capabilities.items():
|
||||
if not keep_source(key):
|
||||
continue
|
||||
|
||||
def clear_state(self):
|
||||
self._state = None
|
||||
if isinstance(value, dict):
|
||||
for sub_key, sub_value in value.items():
|
||||
if (
|
||||
isinstance(sub_value, dict)
|
||||
and "access" in sub_value
|
||||
and "type" in sub_value
|
||||
):
|
||||
sources.append(f"{key}/{sub_key}")
|
||||
elif "access" in value and "type" in value:
|
||||
sources.append(key)
|
||||
|
||||
return sources
|
||||
|
||||
class ApplianceSensor(ApplianceEntity):
|
||||
entity_type = SENSOR
|
||||
|
||||
def __init__(self, name, attr, unit=None, device_class=None, entity_category=None, field=None, source=None) -> None:
|
||||
super().__init__(name, attr, device_class, entity_category, field, source)
|
||||
self.unit = unit
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
|
||||
class ApplianceBinary(ApplianceEntity):
|
||||
entity_type = BINARY_SENSOR
|
||||
|
||||
def __init__(self, name, attr, device_class=None, entity_category=None, field=None, invert=False,
|
||||
source=None) -> None:
|
||||
super().__init__(name, attr, device_class, entity_category, field, source)
|
||||
self.invert = invert
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
state = self._state in [1, 'enabled', True, 'Connected', 'connect']
|
||||
return not state if self.invert else state
|
||||
|
||||
|
||||
class ApplianceButton(ApplianceEntity):
|
||||
entity_type = BUTTON
|
||||
|
||||
def __init__(self, name, attr, unit=None, device_class=None, entity_category=None, source=None, val_to_send=None,
|
||||
icon=None) -> None:
|
||||
super().__init__(name, attr, device_class, entity_category, None, source)
|
||||
self.val_to_send = val_to_send
|
||||
self.icon = icon
|
||||
|
||||
def setup(self, data: ElectroluxLibraryEntity):
|
||||
return self
|
||||
# def sources_list_old(self):
|
||||
# _LOGGER.warning(self.capabilities)
|
||||
# return [
|
||||
# key
|
||||
# for key in list(self.capabilities.keys())
|
||||
# if not key.startswith("applianceCareAndMaintenance")
|
||||
# ]
|
||||
|
||||
|
||||
class Appliance:
|
||||
"""Define the Appliance Class."""
|
||||
|
||||
brand: str
|
||||
device: str
|
||||
entities: []
|
||||
entities: list[ElectroluxEntity]
|
||||
coordinator: Any
|
||||
|
||||
def __init__(self, name, pnc_id, brand, model) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: Any,
|
||||
name: str,
|
||||
pnc_id: str,
|
||||
brand: str,
|
||||
model: str,
|
||||
state: ApplienceStatusResponse,
|
||||
) -> None:
|
||||
"""Initiate the appliance."""
|
||||
self.own_capabilties = False
|
||||
self.data = None
|
||||
self.coordinator = coordinator
|
||||
self.model = model
|
||||
self.pnc_id = pnc_id
|
||||
self.name = name
|
||||
self.brand = brand
|
||||
self.state: ApplienceStatusResponse = state
|
||||
|
||||
def get_entity(self, entity_type, entity_attr, entity_source, val_to_send):
|
||||
return next(
|
||||
entity
|
||||
for entity in self.entities
|
||||
if
|
||||
entity.attr == entity_attr and entity.entity_type == entity_type and entity.source == entity_source and entity.val_to_send == val_to_send
|
||||
@property
|
||||
def reported_state(self) -> dict[str, Any]:
|
||||
"""Return the reported state of the appliance."""
|
||||
return self.state.get("properties", {}).get("reported", {})
|
||||
|
||||
@property
|
||||
def appliance_type(self) -> dict[str, Any]:
|
||||
"""Return the reported type of the appliance.
|
||||
|
||||
CR: Refridgerator
|
||||
WM: Washing Machine
|
||||
"""
|
||||
return self.reported_state.get("applianceInfo", {}).get(
|
||||
"applianceType"
|
||||
) or self.state.get("applianceData", {}).get("modelName")
|
||||
|
||||
@property
|
||||
def catalog(self) -> dict[str, ElectroluxDevice]:
|
||||
"""Return the defined catalog for the appliance."""
|
||||
# TODO: Use appliance_type as opposed to model?
|
||||
if self.model in CATALOG_MODEL:
|
||||
_LOGGER.debug("Extending catalog for %s", self.model)
|
||||
# Make a deep copy of the base catalog to preserve it
|
||||
new_catalog = copy.deepcopy(CATALOG_BASE)
|
||||
|
||||
# Get the specific model's extended catalog
|
||||
model_catalog = CATALOG_MODEL[self.model]
|
||||
|
||||
# Update the existing catalog with the extended information for this model
|
||||
for key, device in model_catalog.items():
|
||||
new_catalog[key] = device
|
||||
|
||||
return new_catalog
|
||||
return CATALOG_BASE
|
||||
|
||||
def update_missing_entities(self) -> None:
|
||||
"""Add missing entities when no capabilities returned by the API.
|
||||
|
||||
This is done dynamically but only when the reported state contains the attributes.
|
||||
"""
|
||||
if not self.own_capabilties or not self.reported_state:
|
||||
return
|
||||
|
||||
for key, catalog_item in self.catalog.items():
|
||||
category = self.data.get_category(key)
|
||||
if (
|
||||
category
|
||||
and self.reported_state.get(category, None)
|
||||
and self.reported_state.get(category, None).get(key)
|
||||
) or (not category and self.reported_state.get(key, None)):
|
||||
found: bool = False
|
||||
for entity in self.entities:
|
||||
if entity.entity_attr == key and entity.entity_source == category:
|
||||
found = True
|
||||
keys = key.split("/")
|
||||
capabilities = self.data.capabilities
|
||||
for key in keys[:-1]:
|
||||
capabilities = capabilities.setdefault(key, {})
|
||||
capabilities[keys[-1]] = catalog_item.capability_info
|
||||
break
|
||||
if not found:
|
||||
_LOGGER.debug(
|
||||
"Electrolux discovered new entity from extracted data. Key: %s",
|
||||
key,
|
||||
)
|
||||
if entity := self.get_entity(key):
|
||||
self.entities.extend(entity)
|
||||
|
||||
def get_state(self, attr_name: str) -> dict[str, Any] | None:
|
||||
"""Retrieve the start from self.reported_state using the attribute name.
|
||||
|
||||
May contain slashes for nested keys.
|
||||
"""
|
||||
|
||||
keys = attr_name.split("/")
|
||||
result = self.reported_state
|
||||
|
||||
for key in keys:
|
||||
result = result.get(key)
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return result
|
||||
|
||||
def get_entity(self, capability: str) -> list[ElectroluxEntity] | None:
|
||||
"""Return the entity."""
|
||||
entity_type = self.data.get_entity_type(capability)
|
||||
entity_name = self.data.get_entity_name(capability)
|
||||
entity_attr = self.data.get_entity_attr(capability)
|
||||
category = self.data.get_category(capability)
|
||||
capability_info = self.data.get_capability(capability)
|
||||
device_class = self.data.get_entity_device_class(capability)
|
||||
entity_category = None
|
||||
entity_icon = None
|
||||
unit = self.data.get_entity_unit(capability)
|
||||
display_name = self.data.get_sensor_name(capability)
|
||||
|
||||
# get the item definition from the catalog
|
||||
catalog_item = self.catalog.get(capability, None)
|
||||
if catalog_item:
|
||||
if capability_info is None:
|
||||
capability_info = catalog_item.capability_info
|
||||
elif (
|
||||
"values" not in capability_info
|
||||
and "values" in catalog_item.capability_info
|
||||
):
|
||||
capability_info["values"] = catalog_item.capability_info["values"]
|
||||
|
||||
device_class = catalog_item.device_class
|
||||
unit = catalog_item.unit
|
||||
entity_category = catalog_item.entity_category
|
||||
entity_icon = catalog_item.entity_icon
|
||||
|
||||
# override the api determined type by the catalog entity_type
|
||||
if isinstance(device_class, BinarySensorDeviceClass):
|
||||
entity_type = BINARY_SENSOR
|
||||
if isinstance(device_class, ButtonDeviceClass):
|
||||
entity_type = BUTTON
|
||||
if isinstance(device_class, NumberDeviceClass):
|
||||
entity_type = NUMBER
|
||||
if isinstance(device_class, SensorDeviceClass):
|
||||
entity_type = SENSOR
|
||||
if isinstance(device_class, SwitchDeviceClass):
|
||||
entity_type = SWITCH
|
||||
|
||||
# override the api determined type by the catalog entity_platform
|
||||
if catalog_item and isinstance(catalog_item.entity_platform, Platform):
|
||||
entity_type = catalog_item.entity_platform
|
||||
|
||||
_LOGGER.debug(
|
||||
"Electrolux get_entity. entity_type: %s entity_name: %s entity_attr: %s entity_source: %s capability: %s device_class: %s unit: %s, catalog: %s",
|
||||
entity_type,
|
||||
entity_name,
|
||||
entity_attr,
|
||||
category,
|
||||
capability_info,
|
||||
device_class,
|
||||
unit,
|
||||
catalog_item,
|
||||
)
|
||||
|
||||
def setup(self, data: ElectroluxLibraryEntity):
|
||||
entities = [
|
||||
ApplianceBinary(
|
||||
name=data.get_name(),
|
||||
attr='status',
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
source='APL',
|
||||
),
|
||||
ApplianceSensor(
|
||||
name=f"{data.get_name()} SSID",
|
||||
attr='Ssid',
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
source='NIU',
|
||||
),
|
||||
ApplianceSensor(
|
||||
name=f"{data.get_name()} {data.get_sensor_name('LinkQualityIndicator', 'NIU')}",
|
||||
attr='LinkQualityIndicator',
|
||||
field='numberValue',
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
unit="dBm",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
source='NIU',
|
||||
),
|
||||
]
|
||||
sources = data.sources_list()
|
||||
for src in sources:
|
||||
for sensor_type, sensors_list in sensors.items():
|
||||
for sensorName, params in sensors_list.items():
|
||||
entities.append(
|
||||
ApplianceSensor(
|
||||
name=f"{data.get_name()} {data.get_sensor_name(sensorName, src)}{data.get_suffix(sensorName, src)}",
|
||||
attr=sensorName,
|
||||
field=params[0],
|
||||
device_class=params[1],
|
||||
entity_category=sensor_type,
|
||||
unit=params[2],
|
||||
source=src,
|
||||
)
|
||||
)
|
||||
for sensor_type, sensors_list in sensors_binary.items():
|
||||
for sensorName, params in sensors_list.items():
|
||||
entities.append(
|
||||
ApplianceBinary(
|
||||
name=f"{data.get_name()} {data.get_sensor_name(sensorName, src)}{data.get_suffix(sensorName, src)}",
|
||||
attr=sensorName,
|
||||
field=params[0],
|
||||
device_class=params[1],
|
||||
entity_category=sensor_type,
|
||||
invert=params[2],
|
||||
source=src,
|
||||
)
|
||||
)
|
||||
for key, command in data.commands_list(src).items():
|
||||
entities.append(
|
||||
ApplianceButton(
|
||||
name=f"{data.get_name()} {data.get_command_name(command)}{data.get_suffix('ExecuteCommand', src)}",
|
||||
attr='ExecuteCommand',
|
||||
val_to_send=key,
|
||||
source=src,
|
||||
icon=icon_mapping.get(key, "mdi:gesture-tap-button"),
|
||||
)
|
||||
)
|
||||
def electrolux_entity_factory(
|
||||
name: str,
|
||||
entity_type: Platform | None,
|
||||
entity_name: str,
|
||||
entity_attr: str,
|
||||
entity_source: str,
|
||||
capability: str,
|
||||
unit: str,
|
||||
entity_category: str,
|
||||
device_class: str,
|
||||
icon: str,
|
||||
catalog_entry: ElectroluxDevice | None,
|
||||
commands: Any | None = None,
|
||||
):
|
||||
entity_classes = {
|
||||
BINARY_SENSOR: ElectroluxBinarySensor,
|
||||
BUTTON: ElectroluxButton,
|
||||
NUMBER: ElectroluxNumber,
|
||||
SELECT: ElectroluxSelect,
|
||||
SENSOR: ElectroluxSensor,
|
||||
SWITCH: ElectroluxSwitch,
|
||||
}
|
||||
|
||||
self.entities = [
|
||||
entity_class = entity_classes.get(entity_type)
|
||||
|
||||
if entity_class is None:
|
||||
_LOGGER.debug("Unknown entity type %s for %s", entity_type, name)
|
||||
raise ValueError(f"Unknown entity type: {entity_type}")
|
||||
|
||||
entity_params = {
|
||||
"coordinator": self.coordinator,
|
||||
"config_entry": self.coordinator.config_entry,
|
||||
"pnc_id": self.pnc_id,
|
||||
"name": name,
|
||||
"entity_type": entity_type,
|
||||
"entity_name": entity_name,
|
||||
"entity_attr": entity_attr,
|
||||
"entity_source": entity_source,
|
||||
"capability": capability,
|
||||
"unit": unit,
|
||||
"entity_category": entity_category,
|
||||
"device_class": device_class,
|
||||
"icon": icon,
|
||||
"catalog_entry": catalog_entry,
|
||||
}
|
||||
|
||||
if commands is None:
|
||||
return [entity_class(**entity_params)]
|
||||
|
||||
entities: list[
|
||||
ElectroluxBinarySensor
|
||||
| ElectroluxNumber
|
||||
| ElectroluxSensor
|
||||
| ElectroluxSwitch
|
||||
| ElectroluxButton
|
||||
| ElectroluxSelect
|
||||
] = []
|
||||
# Replace entity name and icons for multi-entities attribute (one value = one entity)
|
||||
for command in commands:
|
||||
entity = {**entity_params, "val_to_send": command}
|
||||
if catalog_item:
|
||||
if catalog_item.entity_value_named:
|
||||
entity["name"] = command
|
||||
if (
|
||||
catalog_item.entity_icons_value_map
|
||||
and catalog_item.entity_icons_value_map.get(command, None)
|
||||
):
|
||||
entity["icon"] = catalog_item.entity_icons_value_map.get(
|
||||
command
|
||||
)
|
||||
# Instanciate the new entity and append it
|
||||
entities.append(entity_class(**entity))
|
||||
return entities
|
||||
|
||||
if entity_type in PLATFORMS:
|
||||
commands = (
|
||||
capability_info.get("values", {}) if entity_type == BUTTON else None
|
||||
)
|
||||
return electrolux_entity_factory(
|
||||
name=display_name,
|
||||
entity_type=entity_type,
|
||||
entity_name=entity_name,
|
||||
entity_attr=entity_attr,
|
||||
entity_source=category,
|
||||
capability=capability_info,
|
||||
unit=unit,
|
||||
entity_category=entity_category,
|
||||
device_class=device_class,
|
||||
icon=entity_icon,
|
||||
catalog_entry=catalog_item,
|
||||
commands=commands,
|
||||
)
|
||||
|
||||
return []
|
||||
|
||||
def setup(self, data: ElectroluxLibraryEntity):
|
||||
"""Configure the entity."""
|
||||
self.data: ElectroluxLibraryEntity = data
|
||||
self.entities: list[ElectroluxEntity] = []
|
||||
entities: list[ElectroluxEntity] = []
|
||||
# Extraction of the appliance capabilities & mapping to the known entities of the component
|
||||
# [ "applianceState", "autoDosing",..., "userSelections/analogTemperature",...]
|
||||
capabilities_names = self.data.sources_list()
|
||||
|
||||
if capabilities_names is None and self.state:
|
||||
# No capabilities returned (unstable API)
|
||||
# We could rebuild them from catalog but this creates entities that are
|
||||
# not required by each device type (fridge, dryer, vacumn etc are all different)
|
||||
_LOGGER.warning("Electrolux API returned no capability definition")
|
||||
|
||||
# Add static attribute
|
||||
# these are attributes that are not in the capability entry
|
||||
# but are returned by the api independantly
|
||||
for static_attribute in STATIC_ATTRIBUTES:
|
||||
_LOGGER.debug("Electrolux static_attribute %s", static_attribute)
|
||||
# attr not found in state, next attr
|
||||
if self.get_state(static_attribute) is None:
|
||||
continue
|
||||
if catalog_item := self.catalog.get(static_attribute, None):
|
||||
if (entity := self.get_entity(static_attribute)) is None:
|
||||
# catalog definition and automatic checks fail to determine type
|
||||
_LOGGER.debug(
|
||||
"Electrolux static_attribute undefined %s", static_attribute
|
||||
)
|
||||
continue
|
||||
# add to the capability dict
|
||||
keys = static_attribute.split("/")
|
||||
capabilities = self.data.capabilities
|
||||
for key in keys[:-1]:
|
||||
capabilities = capabilities.setdefault(key, {})
|
||||
capabilities[keys[-1]] = catalog_item.capability_info
|
||||
_LOGGER.debug("Electrolux adding static_attribute %s", static_attribute)
|
||||
entities.extend(entity)
|
||||
|
||||
# For each capability src
|
||||
if capabilities_names:
|
||||
for capability in capabilities_names:
|
||||
if entity := self.get_entity(capability):
|
||||
entities.extend(entity)
|
||||
else:
|
||||
_LOGGER.debug("Could not create entity for capability %s", capability)
|
||||
|
||||
# Setup each found entity
|
||||
self.entities = entities
|
||||
for entity in entities:
|
||||
entity.setup(data)
|
||||
for entity in entities if data.value_exists(entity.attr, entity.source)
|
||||
]
|
||||
|
||||
def update_reported_data(self, reported_data: dict[str, Any]):
|
||||
"""Update the reported data."""
|
||||
_LOGGER.debug("Electrolux update reported data %s", reported_data)
|
||||
try:
|
||||
self.reported_state.update(deep_merge_dicts(self.reported_state, reported_data))
|
||||
_LOGGER.debug("Electrolux updated reported data %s", self.state)
|
||||
self.update_missing_entities()
|
||||
for entity in self.entities:
|
||||
entity.update(self.state)
|
||||
|
||||
except Exception as ex: # noqa: BLE001
|
||||
_LOGGER.debug(
|
||||
"Electrolux status could not update reported data with %s. %s",
|
||||
reported_data,
|
||||
ex,
|
||||
)
|
||||
|
||||
def update(self, appliance_status: ApplienceStatusResponse):
|
||||
"""Update appliance status."""
|
||||
self.state = appliance_status
|
||||
self.update_missing_entities()
|
||||
for entity in self.entities:
|
||||
entity.update(self.state)
|
||||
|
||||
|
||||
class Appliances:
|
||||
def __init__(self, found_appliances) -> None:
|
||||
self.found_appliances = found_appliances
|
||||
"""Appliance class definition."""
|
||||
|
||||
def get_appliance(self, pnc_id):
|
||||
return self.found_appliances.get(pnc_id, None)
|
||||
def __init__(self, appliances: dict[str, Appliance]) -> None:
|
||||
"""Initialize the class."""
|
||||
self.appliances = appliances
|
||||
|
||||
def get_appliance(self, pnc_id) -> Appliance:
|
||||
"""Return the appliance."""
|
||||
return self.appliances.get(pnc_id, None)
|
||||
|
||||
def get_appliances(self) -> dict[str, Appliance]:
|
||||
"""Return all appliances."""
|
||||
return self.appliances
|
||||
|
||||
def get_appliance_ids(self) -> list[str]:
|
||||
"""Return all appliance ids."""
|
||||
return list(self.appliances)
|
||||
|
||||
@@ -1,30 +1,66 @@
|
||||
"""Binary sensor platform for Electrolux Status."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .entity import ElectroluxStatusEntity
|
||||
from .const import BINARY_SENSOR
|
||||
from .const import DOMAIN
|
||||
from .const import BINARY_SENSOR, DOMAIN
|
||||
from .entity import ElectroluxEntity
|
||||
from .util import string_to_boolean
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_devices):
|
||||
"""Setup binary sensor platform."""
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Configure binary sensor platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
appliances = coordinator.data.get('appliances', None)
|
||||
|
||||
if appliances is not None:
|
||||
for appliance_id, appliance in appliances.found_appliances.items():
|
||||
async_add_devices(
|
||||
[
|
||||
ElectroluxBinarySensor(coordinator, entry, appliance_id, entity.entity_type, entity.attr, entity.source)
|
||||
for entity in appliance.entities if entity.entity_type == BINARY_SENSOR
|
||||
]
|
||||
if appliances := coordinator.data.get("appliances", None):
|
||||
for appliance_id, appliance in appliances.appliances.items():
|
||||
entities = [
|
||||
entity
|
||||
for entity in appliance.entities
|
||||
if entity.entity_type == BINARY_SENSOR
|
||||
]
|
||||
_LOGGER.debug(
|
||||
"Electrolux add %d BINARY_SENSOR entities to registry for appliance %s",
|
||||
len(entities),
|
||||
appliance_id,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ElectroluxBinarySensor(ElectroluxStatusEntity, BinarySensorEntity):
|
||||
class ElectroluxBinarySensor(ElectroluxEntity, BinarySensorEntity):
|
||||
"""Electrolux Status binary_sensor class."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def entity_domain(self):
|
||||
"""Enitity domain for the entry. Used for consistent entity_id."""
|
||||
return BINARY_SENSOR
|
||||
|
||||
@property
|
||||
def invert(self) -> bool:
|
||||
"""Determine if the value returned for the entity needs to be reversed."""
|
||||
if self.catalog_entry:
|
||||
return self.catalog_entry.state_invert
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary_sensor is on."""
|
||||
return self.get_entity.state
|
||||
value = self.extract_value()
|
||||
if isinstance(value, str):
|
||||
value = string_to_boolean(value, True)
|
||||
if value is None:
|
||||
if self.catalog_entry and self.catalog_entry.state_mapping:
|
||||
mapping = self.catalog_entry.state_mapping
|
||||
value = self.get_state_attr(mapping)
|
||||
if value is not None:
|
||||
self._cached_value = value
|
||||
return not self._cached_value if self.invert else self._cached_value
|
||||
|
||||
@@ -1,30 +1,140 @@
|
||||
"""Button platform for Electrolux Status."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyelectroluxocp.oneAppApi import OneAppApi
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .entity import ElectroluxButtonEntity
|
||||
from .const import BUTTON
|
||||
from .const import DOMAIN
|
||||
from .const import BUTTON, DOMAIN, icon_mapping
|
||||
from .entity import ElectroluxEntity
|
||||
from .model import ElectroluxDevice
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_devices):
|
||||
"""Setup button platform."""
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Configure button platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
appliances = coordinator.data.get('appliances', None)
|
||||
|
||||
if appliances is not None:
|
||||
for appliance_id, appliance in appliances.found_appliances.items():
|
||||
async_add_devices(
|
||||
[
|
||||
ElectroluxButton(coordinator, entry, appliance_id, entity.entity_type, entity.attr, entity.source, entity.val_to_send, entity.icon)
|
||||
for entity in appliance.entities if entity.entity_type == BUTTON
|
||||
]
|
||||
if appliances := coordinator.data.get("appliances", None):
|
||||
for appliance_id, appliance in appliances.appliances.items():
|
||||
entities = [
|
||||
entity for entity in appliance.entities if entity.entity_type == BUTTON
|
||||
]
|
||||
_LOGGER.debug(
|
||||
"Electrolux add %d BUTTON entities to registry for appliance %s",
|
||||
len(entities),
|
||||
appliance_id,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ElectroluxButton(ElectroluxButtonEntity, ButtonEntity):
|
||||
class ElectroluxButton(ElectroluxEntity, ButtonEntity):
|
||||
"""Electrolux Status button class."""
|
||||
|
||||
async def async_press(self) -> None:
|
||||
if self.entity_attr == "ExecuteCommand":
|
||||
await self.hass.async_add_executor_job(self.coordinator.api.setHacl, self.get_appliance.pnc_id, "0x0403", self.val_to_send, self.entity_source)
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: Any,
|
||||
name: str,
|
||||
config_entry,
|
||||
pnc_id: str,
|
||||
entity_type: str,
|
||||
entity_name,
|
||||
entity_attr,
|
||||
entity_source,
|
||||
capability: dict[str, Any],
|
||||
unit: str,
|
||||
device_class: str,
|
||||
entity_category: EntityCategory,
|
||||
icon: str,
|
||||
catalog_entry: ElectroluxDevice | None,
|
||||
val_to_send: str,
|
||||
) -> None:
|
||||
"""Initialize the Button Entity."""
|
||||
super().__init__(
|
||||
coordinator=coordinator,
|
||||
capability=capability,
|
||||
name=name,
|
||||
config_entry=config_entry,
|
||||
pnc_id=pnc_id,
|
||||
entity_type=entity_type,
|
||||
entity_name=entity_name,
|
||||
entity_attr=entity_attr,
|
||||
entity_source=entity_source,
|
||||
unit=None,
|
||||
device_class=device_class,
|
||||
entity_category=entity_category,
|
||||
icon=icon,
|
||||
catalog_entry=catalog_entry,
|
||||
)
|
||||
self.val_to_send = val_to_send
|
||||
|
||||
@property
|
||||
def entity_domain(self):
|
||||
"""Enitity domain for the entry. Used for consistent entity_id."""
|
||||
return BUTTON
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID to use for this entity."""
|
||||
return f"{self.config_entry.entry_id}-{self.val_to_send}-{self.entity_attr}-{self.entity_source}-{self.pnc_id}"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
name = self._name
|
||||
if self.catalog_entry and self.catalog_entry.friendly_name:
|
||||
name = (
|
||||
f"{self.get_appliance.name} {self.catalog_entry.friendly_name.lower()}"
|
||||
)
|
||||
# Get the last word from the 'name' variable
|
||||
# and compare to the command we are sending duplicate names
|
||||
# "air filter state reset reset" for instance
|
||||
last_word = name.split()[-1]
|
||||
if last_word.lower() == str(self.val_to_send).lower():
|
||||
return name
|
||||
return f"{name} {self.val_to_send}"
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon of the entity."""
|
||||
return self._icon or icon_mapping.get(
|
||||
self.val_to_send, "mdi:gesture-tap-button"
|
||||
)
|
||||
|
||||
async def send_command(self) -> bool:
|
||||
"""Send a command to the device."""
|
||||
client: OneAppApi = self.api
|
||||
value = self.val_to_send
|
||||
if self.entity_source:
|
||||
if self.entity_source == "userSelections":
|
||||
command = {
|
||||
self.entity_source: {
|
||||
"programUID": self.appliance_status["properties"]['reported']["userSelections"]['programUID'],
|
||||
self.entity_attr: value
|
||||
},
|
||||
}
|
||||
else:
|
||||
command = {self.entity_source: {self.entity_attr: value}}
|
||||
else:
|
||||
command = {self.entity_attr: value}
|
||||
_LOGGER.debug("Electrolux send command %s", command)
|
||||
result = await client.execute_appliance_command(self.pnc_id, command)
|
||||
_LOGGER.debug("Electrolux send command result %s", result)
|
||||
return True
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Execute a button press."""
|
||||
await self.send_command()
|
||||
# await self.hass.async_add_executor_job(self.send_command)
|
||||
# if self.entity_attr == "ExecuteCommand":
|
||||
# await self.hass.async_add_executor_job(self.coordinator.api.setHacl, self.get_appliance.pnc_id, "0x0403", self.val_to_send, self.entity_source)
|
||||
|
||||
@@ -1,169 +1,237 @@
|
||||
"""Adds config flow for Electrolux Status."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.selector import selector
|
||||
import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
from typing import Mapping, Any
|
||||
|
||||
from .pyelectroluxconnect_util import pyelectroluxconnect_util
|
||||
from .const import CONF_PASSWORD, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, CONF_REGION
|
||||
from .const import CONF_LANGUAGE, DEFAULT_LANGUAGE
|
||||
from .const import CONF_USERNAME
|
||||
from .const import DOMAIN
|
||||
from .const import languages
|
||||
from homeassistant.config_entries import (
|
||||
CONN_CLASS_CLOUD_PUSH,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_COUNTRY_CODE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
selector,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_LANGUAGE,
|
||||
CONF_NOTIFICATION_DEFAULT,
|
||||
CONF_NOTIFICATION_DIAG,
|
||||
CONF_NOTIFICATION_WARNING,
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_COUNTRY_CODE,
|
||||
DOMAIN,
|
||||
languages,
|
||||
)
|
||||
from .util import get_electrolux_session
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ElectroluxStatusFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
class ElectroluxStatusFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for Electrolux Status."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
CONNECTION_CLASS = CONN_CLASS_CLOUD_PUSH
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
self._errors = {}
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
self._errors = {}
|
||||
|
||||
# Uncomment the next 2 lines if only a single instance of the integration is allowed:
|
||||
# if self._async_current_entries():
|
||||
# return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
if user_input is not None:
|
||||
# check if the specified account is configured already
|
||||
# to prevent them from being added twice
|
||||
for entry in self._async_current_entries():
|
||||
if user_input[CONF_USERNAME] == entry.data.get("username", None):
|
||||
return self.async_abort(reason="already_configured_account")
|
||||
|
||||
valid = await self._test_credentials(
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
user_input[CONF_REGION],
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], user_input[CONF_COUNTRY_CODE]
|
||||
)
|
||||
if valid:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME], data=user_input
|
||||
)
|
||||
else:
|
||||
self._errors["base"] = "auth"
|
||||
self._errors["base"] = "invalid_auth"
|
||||
|
||||
return await self._show_config_form(user_input)
|
||||
|
||||
return await self._show_config_form(user_input)
|
||||
|
||||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle configuration by re-auth."""
|
||||
return await self.async_step_reauth_validate(entry_data)
|
||||
|
||||
async def async_step_reauth_validate(self, user_input=None):
|
||||
async def async_step_reauth_validate(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Handle reauth and validation."""
|
||||
self._errors = {}
|
||||
if user_input is not None:
|
||||
valid = await self._test_credentials(
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
user_input[CONF_REGION],
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], user_input[CONF_COUNTRY_CODE]
|
||||
)
|
||||
if valid:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME], data=user_input
|
||||
)
|
||||
else:
|
||||
self._errors["base"] = "auth"
|
||||
self._errors["base"] = "invalid_auth"
|
||||
return await self._show_config_form(user_input)
|
||||
return await self._show_config_form(user_input)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
"""Present the configuration options dialog."""
|
||||
return ElectroluxStatusOptionsFlowHandler(config_entry)
|
||||
|
||||
async def _show_config_form(self, user_input): # pylint: disable=unused-argument
|
||||
"""Show the configuration form to edit location data."""
|
||||
data_schema = {
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_REGION, default="EMEA"): selector({
|
||||
"select": {
|
||||
"options": ["APAC", "EMEA", "LATAM", "NA", "Frigidaire"],
|
||||
"mode": "dropdown",
|
||||
}
|
||||
}),
|
||||
vol.Optional(CONF_LANGUAGE, default = DEFAULT_LANGUAGE): selector({
|
||||
"select": {
|
||||
"options": list(languages.keys()),
|
||||
"mode": "dropdown"}
|
||||
}),
|
||||
vol.Required(CONF_USERNAME): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.EMAIL, autocomplete="username")
|
||||
),
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD, autocomplete="current-password"
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_COUNTRY_CODE, default=DEFAULT_COUNTRY_CODE): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.TEXT, autocomplete="country-code"
|
||||
)
|
||||
),
|
||||
}
|
||||
if self.show_advanced_options:
|
||||
data_schema = {
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_REGION, default="EMEA"): selector({
|
||||
"select": {
|
||||
"options": ["APAC", "EMEA", "LATAM", "NA", "Frigidaire"],
|
||||
"mode": "dropdown"}
|
||||
}),
|
||||
vol.Optional(CONF_LANGUAGE, default = DEFAULT_LANGUAGE): selector({
|
||||
"select": {
|
||||
"options": list(languages.keys()),
|
||||
"mode": "dropdown"}
|
||||
}),
|
||||
}
|
||||
data_schema.update(
|
||||
{
|
||||
vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): selector(
|
||||
{
|
||||
"select": {
|
||||
"options": list(languages.keys()),
|
||||
"mode": "dropdown",
|
||||
}
|
||||
}
|
||||
),
|
||||
vol.Optional(CONF_NOTIFICATION_DEFAULT, default=True): cv.boolean,
|
||||
vol.Optional(CONF_NOTIFICATION_WARNING, default=False): cv.boolean,
|
||||
vol.Optional(CONF_NOTIFICATION_DIAG, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(data_schema),
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
async def _test_credentials(self, username, password, region):
|
||||
async def _test_credentials(self, username, password, country_code):
|
||||
"""Return true if credentials is valid."""
|
||||
try:
|
||||
client = pyelectroluxconnect_util.get_session(username, password, region)
|
||||
await self.hass.async_add_executor_job(client.login)
|
||||
return True
|
||||
except Exception as inst: # pylint: disable=broad-except
|
||||
_LOGGER.exception(inst)
|
||||
return False
|
||||
client = get_electrolux_session(
|
||||
username, password, country_code, async_get_clientsession(self.hass)
|
||||
)
|
||||
await client.get_appliances_list()
|
||||
except Exception as inst: # pylint: disable=broad-except # noqa: BLE001
|
||||
_LOGGER.error("Authentication to electrolux failed: %s", inst)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class ElectroluxStatusOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
class ElectroluxStatusOptionsFlowHandler(OptionsFlow):
|
||||
"""Config flow options handler for Electrolux Status."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
def __init__(self, config_entry) -> None:
|
||||
"""Initialize HACS options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.options = dict(config_entry.options)
|
||||
|
||||
async def async_step_init(self, user_input=None): # pylint: disable=unused-argument
|
||||
async def async_step_init(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
if user_input is not None:
|
||||
self.options.update(user_input)
|
||||
return await self._update_options()
|
||||
|
||||
selected_language = self.config_entry.data.get(CONF_LANGUAGE, DEFAULT_LANGUAGE)
|
||||
current_password = self.config_entry.data.get(CONF_PASSWORD, None)
|
||||
current_country_code = self.config_entry.data.get(CONF_COUNTRY_CODE, None)
|
||||
notify_alert = self.config_entry.data.get(CONF_NOTIFICATION_DEFAULT, True)
|
||||
notify_warning = self.config_entry.data.get(CONF_NOTIFICATION_WARNING, False)
|
||||
notify_diagnostic = self.config_entry.data.get(CONF_NOTIFICATION_DIAG, False)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD, default=current_password): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD,
|
||||
autocomplete="current-password",
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_COUNTRY_CODE, default=current_country_code): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.TEXT, autocomplete="country-code"
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_LANGUAGE, default=selected_language): selector(
|
||||
{
|
||||
"select": {
|
||||
"options": list(languages.keys()),
|
||||
"mode": "dropdown",
|
||||
}
|
||||
}
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_SCAN_INTERVAL,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
|
||||
),
|
||||
): cv.positive_int,
|
||||
CONF_NOTIFICATION_DEFAULT, default=notify_alert
|
||||
): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_NOTIFICATION_WARNING, default=notify_warning
|
||||
): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_NOTIFICATION_DIAG, default=notify_diagnostic
|
||||
): cv.boolean,
|
||||
# vol.Optional(
|
||||
# CONF_RENEW_INTERVAL,
|
||||
# default=self.config_entry.options.get(
|
||||
# CONF_RENEW_INTERVAL, DEFAULT_WEBSOCKET_RENEWAL_DELAY
|
||||
# ),
|
||||
# ): cv.positive_int,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def _update_options(self):
|
||||
"""Update config entry options."""
|
||||
_LOGGER.debug("Electrolux updating configuration")
|
||||
data = {
|
||||
**self.config_entry.data,
|
||||
CONF_PASSWORD: self.options[CONF_PASSWORD],
|
||||
CONF_COUNTRY_CODE: self.options[CONF_COUNTRY_CODE],
|
||||
CONF_LANGUAGE: self.options[CONF_LANGUAGE],
|
||||
CONF_NOTIFICATION_DEFAULT: self.options[CONF_NOTIFICATION_DEFAULT],
|
||||
CONF_NOTIFICATION_WARNING: self.options[CONF_NOTIFICATION_WARNING],
|
||||
CONF_NOTIFICATION_DIAG: self.options[CONF_NOTIFICATION_DIAG],
|
||||
}
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, data=data)
|
||||
return self.async_create_entry(
|
||||
title=self.config_entry.data.get(CONF_USERNAME), data=self.options
|
||||
title=self.config_entry.data.get(CONF_USERNAME),
|
||||
data=data,
|
||||
)
|
||||
|
||||
@@ -1,129 +1,58 @@
|
||||
"""The electrolux Status constants."""
|
||||
from homeassistant.const import UnitOfTime
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.const import UnitOfPower
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
|
||||
import re
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
# Base component constants
|
||||
NAME = "Elettrolux status"
|
||||
NAME = "Electrolux status"
|
||||
DOMAIN = "electrolux_status"
|
||||
DOMAIN_DATA = f"{DOMAIN}_data"
|
||||
|
||||
# Icons
|
||||
ICON = "mdi:format-quote-close"
|
||||
|
||||
# Device classes
|
||||
BINARY_SENSOR_DEVICE_CLASS = "connectivity"
|
||||
COMPONENTS_DIRECTORY = "custom_components"
|
||||
LOOKUP_DIRECTORY = "appliance_definitions"
|
||||
LOOKUP_DIRECTORY_PATH = f"{COMPONENTS_DIRECTORY}/{DOMAIN}/{LOOKUP_DIRECTORY}/"
|
||||
|
||||
# Platforms
|
||||
BINARY_SENSOR = "binary_sensor"
|
||||
SENSOR = "sensor"
|
||||
BUTTON = "button"
|
||||
PLATFORMS = [BINARY_SENSOR, SENSOR, BUTTON]
|
||||
BINARY_SENSOR = Platform.BINARY_SENSOR
|
||||
BUTTON = Platform.BUTTON
|
||||
NUMBER = Platform.NUMBER
|
||||
SELECT = Platform.SELECT
|
||||
SENSOR = Platform.SENSOR
|
||||
SWITCH = Platform.SWITCH
|
||||
PLATFORMS = [BINARY_SENSOR, BUTTON, NUMBER, SELECT, SENSOR, SWITCH]
|
||||
|
||||
# Configuration and options
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_USERNAME = "username"
|
||||
CONF_PASSWORD = "password"
|
||||
CONF_REGION = "region"
|
||||
CONF_LANGUAGE = "language"
|
||||
CONF_SCAN_INTERVAL = "scan_interval"
|
||||
CONF_RENEW_INTERVAL = "renew_interval"
|
||||
CONF_NOTIFICATION_DEFAULT = "notifications"
|
||||
CONF_NOTIFICATION_DIAG = "notifications_diagnostic"
|
||||
CONF_NOTIFICATION_WARNING = "notifications_warning"
|
||||
|
||||
# Defaults
|
||||
DEFAULT_NAME = DOMAIN
|
||||
DEFAULT_SCAN_INTERVAL = 30
|
||||
DEFAULT_REGION = "emea"
|
||||
DEFAULT_LANGUAGE = "English"
|
||||
DEFAULT_COUNTRY_CODE = "us"
|
||||
DEFAULT_WEBSOCKET_RENEWAL_DELAY = 43200 # 12 hours
|
||||
|
||||
sensors = {
|
||||
# Sensor Name: [value field, device class, unit]
|
||||
# Device state sensors
|
||||
None: {
|
||||
"TimeToEnd": [None, None, UnitOfTime.MINUTES],
|
||||
"RunningTime": [None, None, UnitOfTime.MINUTES],
|
||||
"CyclePhase": [None, None, None],
|
||||
"CycleSubPhase": ["string", None, None],
|
||||
"ApplianceState": [None, None, None],
|
||||
"DisplayTemperature": ["container", SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS],
|
||||
"DisplayFoodProbeTemperature": ["container", SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS],
|
||||
"SensorTemperature": ["container", SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS],
|
||||
"DefrostTemperature": ["container", SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS],
|
||||
"TargetMicrowavePower": ["numberValue", SensorDeviceClass.ENERGY, UnitOfPower.WATT],
|
||||
"OvenProcessIdentifier": ["valueTransl", None, None],
|
||||
"RemoteControl": [None, None, None],
|
||||
"DefaultExtraRinse": ["numberValue", None, None],
|
||||
"TargetTemperature": ["container", SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS],
|
||||
"StartTime": [None, None, UnitOfTime.MINUTES],
|
||||
"AnalogTemperature": ["numberValue", SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS],
|
||||
"WaterSoftenerMode": [None, None, None],
|
||||
"SteamValue": ["valTransl", None, None],
|
||||
"ELUXTimeManagerLevel": ["valTransl", None, None],
|
||||
"AnalogSpinSpeed": ["valTransl", None, None],
|
||||
"WaterTankWarningMode": [None, None, None],
|
||||
"DryingTime": [None, None, None],
|
||||
"HumidityTarget": ["valTransl", None, None],
|
||||
"AntiCreaseValue": [None, None, None],
|
||||
"DrynessValue": ["valTransl", None, None],
|
||||
"ProgramUID": ["valTransl", None, None]
|
||||
},
|
||||
# Device diagnostic sensors
|
||||
EntityCategory.DIAGNOSTIC : {
|
||||
"SensorHumidity": ["numberValue", SensorDeviceClass.HUMIDITY, PERCENTAGE, None],
|
||||
"AmbientTemperature": ["container", SensorDeviceClass.TEMPERATURE, UnitOfTemperature.CELSIUS, None],
|
||||
"ApplianceTotalWorkingTime": [None, None, UnitOfTime.MINUTES],
|
||||
"TotalCycleCounter": [None, None, None],
|
||||
"RinseAidLevel": [None, None, None],
|
||||
"WaterHardness": ["valueTransl", None, None],
|
||||
"FCTotalWashCyclesCount": [None, None, None],
|
||||
"FCTotalWashingTime": [None, None, None],
|
||||
"ApplianceMode": [None, None, None],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sensors_binary = {
|
||||
# Sensor Name: [value field, device class, invert]
|
||||
# Device state sensors
|
||||
None: {
|
||||
"DoorState": ["numberValue", BinarySensorDeviceClass.DOOR, None],
|
||||
"DoorLock": ["numberValue", BinarySensorDeviceClass.LOCK, True],
|
||||
"UiLockMode": ["numberValue", None, None],
|
||||
"EndOfCycleSound": ["numberValue", None, None],
|
||||
"DefaultSoftPlus": ["numberValue", None, None],
|
||||
"PreWashPhase": ["numberValue", None, None],
|
||||
"RinseHold": ["numberValue", None, None],
|
||||
"NightCycle": ["numberValue", None, None],
|
||||
"Stain": ["numberValue", None, None],
|
||||
"WMEconomy": ["numberValue", None, None],
|
||||
"AnticreaseWSteam": ["numberValue", None, None],
|
||||
"AnticreaseNoSteam": ["numberValue", None, None],
|
||||
"Refresh": ["numberValue", None, None],
|
||||
"ReversePlus": ["numberValue", None, None],
|
||||
"Delicate": ["numberValue", None, None],
|
||||
"TDEnergyLabel": ["numberValue", None, None],
|
||||
"TDEconomy_Eco": ["numberValue", None, None],
|
||||
"TDEconomy_Night": ["numberValue", None, None],
|
||||
},
|
||||
# Device diagnostic sensors
|
||||
EntityCategory.DIAGNOSTIC : {
|
||||
"TankA_reserve": ["numberValue", BinarySensorDeviceClass.PROBLEM, False],
|
||||
"TankB_reserve": ["numberValue", BinarySensorDeviceClass.PROBLEM, False],
|
||||
}
|
||||
}
|
||||
# these are attributes that appear in the state file but not in the capabilities.
|
||||
# defining them here and in the catalog will allow these devices to be added dynamically
|
||||
STATIC_ATTRIBUTES = [
|
||||
"connectivityState",
|
||||
"networkInterface/linkQualityIndicator",
|
||||
"applianceMode",
|
||||
]
|
||||
|
||||
# Icon mappings for default executeCommands
|
||||
icon_mapping = {
|
||||
"0": "mdi:power-off",
|
||||
"1": "mdi:power-on",
|
||||
"2": "mdi:play",
|
||||
"3": "mdi:stop",
|
||||
"4": "mdi:pause",
|
||||
"5": "mdi:play-pause",
|
||||
"OFF": "mdi:power-off",
|
||||
"ON": "mdi:power-on",
|
||||
"START": "mdi:play",
|
||||
"STOPRESET": "mdi:stop",
|
||||
"PAUSE": "mdi:pause",
|
||||
"RESUME": "mdi:play-pause",
|
||||
}
|
||||
|
||||
# List of supported Mobile App languages (from https://emea-production.api.electrolux.net/masterdata-service/api/v1/languages)
|
||||
# List of supported Mobile App languages
|
||||
# refer to https://emea-production.api.electrolux.net/masterdata-service/api/v1/languages
|
||||
languages = {
|
||||
"български": "bul",
|
||||
"český": "ces",
|
||||
@@ -153,3 +82,24 @@ languages = {
|
||||
"Türk": "tur",
|
||||
"Ukrayna": "ukr",
|
||||
}
|
||||
|
||||
# List of attributes to ignore and that won't be added as entities (regex format)
|
||||
ATTRIBUTES_BLACKLIST: list[str] = ["^fCMiscellaneous.+",
|
||||
"fcOptisenseLoadWeight.*",
|
||||
"applianceCareAndMaintenance.*",
|
||||
"applianceMainBoardSwVersion",
|
||||
"coolingValveState",
|
||||
"networkInterface",
|
||||
"temperatureRepresentation",
|
||||
]
|
||||
|
||||
ATTRIBUTES_WHITELIST: list[str] = [".*waterUsage",
|
||||
".*tankAReserve",
|
||||
".*tankBReserve"]
|
||||
|
||||
# Rules to simplify the naming of entities
|
||||
RENAME_RULES: list[str] = [r"^userSelections\/[^_]+_", r"^userSelections\/",
|
||||
r"^fCMiscellaneousState\/[^_]+_", r"^fCMiscellaneousState\/"]
|
||||
|
||||
# List of entity names that need to be updated to 0 manually when they are close to 0
|
||||
TIME_ENTITIES_TO_UPDATE = ["timeToEnd"]
|
||||
|
||||
@@ -1,130 +1,249 @@
|
||||
from homeassistant.components.sensor import ENTITY_ID_FORMAT
|
||||
"""Entity platform for Electrolux Status."""
|
||||
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from pyelectroluxocp import OneAppApi
|
||||
from pyelectroluxocp.apiModels import ApplienceStatusResponse
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from . import ElectroluxStatusDataUpdateCoordinator
|
||||
from .api import Appliance, ApplianceEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .model import ElectroluxDevice
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
class ElectroluxStatusEntity(CoordinatorEntity):
|
||||
def __init__(self, coordinator: ElectroluxStatusDataUpdateCoordinator, config_entry, pnc_id, entity_type, entity_attr, entity_source):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Configure entity platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
if appliances := coordinator.data.get("appliances", None):
|
||||
for appliance_id, appliance in appliances.appliances.items():
|
||||
entities = [
|
||||
entity
|
||||
for entity in appliance.entities
|
||||
if entity.entity_type == "entity"
|
||||
]
|
||||
_LOGGER.debug(
|
||||
"Electrolux add %d entities to registry for appliance %s",
|
||||
len(entities),
|
||||
appliance_id,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ElectroluxEntity(CoordinatorEntity):
|
||||
"""Class for Electorolux devices."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
appliance_status: ApplienceStatusResponse
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: Any,
|
||||
name: str,
|
||||
config_entry,
|
||||
pnc_id: str,
|
||||
entity_type: Platform,
|
||||
entity_name,
|
||||
entity_attr,
|
||||
entity_source,
|
||||
capability: dict[str, Any],
|
||||
unit: str,
|
||||
device_class: str,
|
||||
entity_category: EntityCategory,
|
||||
icon: str,
|
||||
catalog_entry: ElectroluxDevice | None = None,
|
||||
) -> None:
|
||||
"""Initaliaze the entity."""
|
||||
super().__init__(coordinator)
|
||||
self.api = coordinator.api
|
||||
self.root_attribute = ["properties", "reported"]
|
||||
self.data = None
|
||||
self.coordinator = coordinator
|
||||
self._cached_value = None
|
||||
self._name = name
|
||||
self._icon = icon
|
||||
self._device_class = device_class
|
||||
self._entity_category = entity_category
|
||||
self._catalog_entry = catalog_entry
|
||||
self.api: OneAppApi = coordinator.api
|
||||
self.entity_name = entity_name
|
||||
self.entity_attr = entity_attr
|
||||
self.entity_type = entity_type
|
||||
self.entity_source = entity_source
|
||||
self.config_entry = config_entry
|
||||
self.pnc_id = pnc_id
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(f"{self.get_appliance.brand}_{self.get_appliance.name}_{self.entity_source}_{self.entity_attr}")
|
||||
self.unit = unit
|
||||
self.capability = capability
|
||||
self.entity_id = f"{self.entity_domain}.{self.get_appliance.brand}_{self.get_appliance.name}_{self.entity_source}_{self.entity_attr}"
|
||||
if catalog_entry:
|
||||
self.entity_registry_enabled_default = (
|
||||
catalog_entry.entity_registry_enabled_default
|
||||
)
|
||||
_LOGGER.debug("Electrolux new entity %s for appliance %s", name, pnc_id)
|
||||
|
||||
def setup(self, data):
|
||||
"""Initialiaze setup."""
|
||||
self.data = data
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self.get_entity.name
|
||||
def entity_domain(self) -> str:
|
||||
"""Enitity domain for the entry."""
|
||||
return "sensor"
|
||||
|
||||
@property
|
||||
def get_entity(self) -> ApplianceEntity:
|
||||
return self.get_appliance.get_entity(self.entity_type, self.entity_attr, self.entity_source, None)
|
||||
|
||||
@property
|
||||
def get_appliance(self) -> Appliance:
|
||||
return self.coordinator.data['appliances'].get_appliance(self.pnc_id)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID to use for this entity."""
|
||||
return f"{self.config_entry.entry_id}-{self.entity_attr}-{self.entity_source}-{self.pnc_id}"
|
||||
return f"{self.config_entry.entry_id}-{self.entity_attr}-{self.entity_source or 'root'}-{self.pnc_id}"
|
||||
|
||||
# Disabled this as this removes the value from display : there is no readonly property for entities
|
||||
# @property
|
||||
# def available(self) -> bool:
|
||||
# if (self._entity_category == EntityCategory.DIAGNOSTIC
|
||||
# or self.entity_attr in ALWAYS_ENABLED_ATTRIBUTES):
|
||||
# return True
|
||||
# connection_state = self.get_connection_state()
|
||||
# if connection_state and connection_state != "disconnected":
|
||||
# return True
|
||||
# return False
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Confirm if device should be polled."""
|
||||
return False
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
# _LOGGER.debug("Electrolux entity got data %s", self.coordinator.data)
|
||||
if self.coordinator.data is None:
|
||||
return
|
||||
appliances = self.coordinator.data.get("appliances", None)
|
||||
self.appliance_status = appliances.get_appliance(self.pnc_id).state
|
||||
self.async_write_ha_state()
|
||||
|
||||
def get_connection_state(self) -> str | None:
|
||||
"""Return connection state."""
|
||||
if self.appliance_status:
|
||||
return self.appliance_status.get("connectionState", None)
|
||||
return None
|
||||
|
||||
def get_state_attr(self, path: str) -> str | None:
|
||||
"""Return value of other appliance attributes.
|
||||
|
||||
Used for the evaluation of state_mapping one property to another.
|
||||
"""
|
||||
if "/" in path:
|
||||
if self.reported_state.get(path, None):
|
||||
return self.reported_state.get(path)
|
||||
source, attr = path.split("/")
|
||||
return self.reported_state.get(source, {}).get(attr, None)
|
||||
return self.reported_state.get(path, None)
|
||||
|
||||
@property
|
||||
def reported_state(self):
|
||||
"""Return reported state of the appliance."""
|
||||
return self.appliance_status.get("properties", {}).get("reported", {})
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
if self.catalog_entry and self.catalog_entry.friendly_name:
|
||||
return self.catalog_entry.friendly_name.capitalize()
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon of the entity."""
|
||||
return self._icon
|
||||
|
||||
# @property
|
||||
# def get_entity(self) -> ApplianceEntity:
|
||||
# return self.get_appliance.get_entity(self.entity_type, self.entity_attr, self.entity_source, None)
|
||||
|
||||
@property
|
||||
def get_appliance(self):
|
||||
"""Return the appliance device."""
|
||||
return self.coordinator.data["appliances"].get_appliance(self.pnc_id)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return identifiers of the device."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.get_appliance.name)},
|
||||
"identifiers": {(DOMAIN, self.pnc_id)},
|
||||
"name": self.get_appliance.name,
|
||||
"model": self.get_appliance.model,
|
||||
"manufacturer": self.get_appliance.brand,
|
||||
}
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
"integration": DOMAIN,
|
||||
}
|
||||
def entity_category(self) -> EntityCategory | None:
|
||||
"""Return entity category."""
|
||||
return self._entity_category
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the sensor."""
|
||||
return self.get_entity.device_class
|
||||
return self._device_class
|
||||
|
||||
def extract_value(self):
|
||||
"""Return the appliance attributes of the entity."""
|
||||
root_attribute = self.root_attribute
|
||||
attribute = self.entity_attr
|
||||
if self.appliance_status:
|
||||
root = cast(any, self.appliance_status)
|
||||
# Format returned by push is slightly different from format returned by API : fields are at root level
|
||||
# Let's check if we can find the fields at root first
|
||||
if (
|
||||
self.entity_source and root.get(self.entity_source, None) is not None
|
||||
) or root.get(attribute, None):
|
||||
root_attribute = None
|
||||
if root_attribute:
|
||||
for item in root_attribute:
|
||||
if root:
|
||||
root = root.get(item, None)
|
||||
if root:
|
||||
if self.entity_source:
|
||||
category: dict[str, Any] | None = root.get(self.entity_source, None)
|
||||
if category:
|
||||
return category.get(attribute)
|
||||
else:
|
||||
return root.get(attribute, None)
|
||||
return None
|
||||
|
||||
def update(self, appliance_status: ApplienceStatusResponse):
|
||||
"""Update the appliance status."""
|
||||
self.appliance_status = appliance_status
|
||||
# if self.hass:
|
||||
# self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def entity_category(self):
|
||||
"""Return the entity category of the sensor."""
|
||||
return self.get_entity.entity_category
|
||||
|
||||
|
||||
class ElectroluxButtonEntity(Entity):
|
||||
def __init__(self, coordinator, config_entry, pnc_id, entity_type, entity_attr, entity_source, val_to_send, icon):
|
||||
self.coordinator = coordinator
|
||||
self.entity_attr = entity_attr
|
||||
self.entity_type = entity_type
|
||||
self.entity_source = entity_source
|
||||
self.config_entry = config_entry
|
||||
self.pnc_id = pnc_id
|
||||
self.val_to_send = val_to_send
|
||||
self.button_icon = icon
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(f"{self.get_appliance.brand}_{self.get_appliance.name}_{self.entity_source}_{self.entity_attr}_{self.val_to_send}")
|
||||
def json_path(self) -> str | None:
|
||||
"""Return the path to the entry."""
|
||||
if self.entity_source:
|
||||
return f"{self.entity_source}/{self.entity_attr}"
|
||||
return self.entity_attr
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the button."""
|
||||
return self.get_entity.name
|
||||
def catalog_entry(self) -> ElectroluxDevice | None:
|
||||
"""Return matched catalog entry."""
|
||||
return self._catalog_entry
|
||||
|
||||
@property
|
||||
def get_entity(self) -> ApplianceEntity:
|
||||
return self.get_appliance.get_entity(self.entity_type, self.entity_attr, self.entity_source, self.val_to_send)
|
||||
|
||||
@property
|
||||
def get_appliance(self) -> Appliance:
|
||||
return self.coordinator.data['appliances'].get_appliance(self.pnc_id)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID to use for this entity."""
|
||||
return f"{self.config_entry.entry_id}-{self.val_to_send}-{self.entity_attr}-{self.entity_source}-{self.pnc_id}"
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.get_appliance.name)},
|
||||
"name": self.get_appliance.name,
|
||||
"model": self.get_appliance.model,
|
||||
"manufacturer": self.get_appliance.brand,
|
||||
}
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
"integration": DOMAIN,
|
||||
}
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of the button."""
|
||||
return self.get_entity.device_class
|
||||
|
||||
@property
|
||||
def entity_category(self):
|
||||
"""Return the entity category of the button."""
|
||||
return self.get_entity.entity_category
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of the button."""
|
||||
return self.button_icon
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
# available state should depends on connect state
|
||||
return True
|
||||
# @property
|
||||
# def extra_state_attributes(self) -> dict[str, Any]:
|
||||
# """Return the state attributes of the sensor."""
|
||||
# return {
|
||||
# "Path": self.json_path,
|
||||
# "entity_type": str(self.entity_type),
|
||||
# "entity_category": str(self.entity_category),
|
||||
# "device_class": str(self.device_class),
|
||||
# "capability": str(self.capability),
|
||||
# }
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"domain": "electrolux_status",
|
||||
"name": "Electrolux Care Integration",
|
||||
"codeowners": [
|
||||
"@mauro-midolo"
|
||||
],
|
||||
"config_flow": true,
|
||||
"dependencies": [],
|
||||
"documentation": "https://github.com/mauro-midolo/homeassistant_electrolux_status",
|
||||
"homekit": {},
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/mauro-midolo/homeassistant_electrolux_status/issues",
|
||||
"requirements": [
|
||||
"pyelectroluxconnect==0.3.20"
|
||||
],
|
||||
"ssdp": [],
|
||||
"version": "1.2.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
{
|
||||
"domain": "electrolux_status",
|
||||
"name": "Electrolux Status",
|
||||
"codeowners": ["@albaintor","@kingy444"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/albaintor/homeassistant_electrolux_status",
|
||||
"homekit": {},
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"issue_tracker": "https://github.com/albaintor/homeassistant_electrolux_status/issues",
|
||||
"loggers": ["pyelectroluxocp"],
|
||||
"requirements": [
|
||||
"pyelectroluxocp==0.1.3",
|
||||
"aiofiles==24.1.0"
|
||||
],
|
||||
"ssdp": [],
|
||||
"version": "2.2.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import pyelectroluxconnect
|
||||
|
||||
|
||||
class pyelectroluxconnect_util:
|
||||
@staticmethod
|
||||
def get_session(username, password, region="emea", language ="eng"):
|
||||
return pyelectroluxconnect.Session(
|
||||
username, password, deviceId="ElectroluxHomeAssistant", language=language, region=region,
|
||||
)
|
||||
@@ -1,37 +1,128 @@
|
||||
from typing import cast
|
||||
"""Switch platform for Electrolux Status."""
|
||||
|
||||
from .api import ApplianceSensor
|
||||
from .const import DOMAIN
|
||||
from .const import SENSOR
|
||||
from .entity import ElectroluxStatusEntity
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from . import ElectroluxStatusDataUpdateCoordinator
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature, UnitOfTime, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, SENSOR
|
||||
from .entity import ElectroluxEntity
|
||||
from .util import create_notification
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_devices):
|
||||
"""Setup sensor platform."""
|
||||
coordinator: ElectroluxStatusDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
appliances = coordinator.data.get('appliances', None)
|
||||
|
||||
if appliances is not None:
|
||||
for appliance_id, appliance in appliances.found_appliances.items():
|
||||
async_add_devices(
|
||||
[
|
||||
ElectroluxStatusSensor(coordinator, entry, appliance_id, entity.entity_type, entity.attr, entity.source)
|
||||
for entity in appliance.entities if entity.entity_type == SENSOR
|
||||
]
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Configure sensor platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
if appliances := coordinator.data.get("appliances", None):
|
||||
for appliance_id, appliance in appliances.appliances.items():
|
||||
entities = [
|
||||
entity for entity in appliance.entities if entity.entity_type == SENSOR
|
||||
]
|
||||
_LOGGER.debug(
|
||||
"Electrolux add %d SENSOR entities to registry for appliance %s",
|
||||
len(entities),
|
||||
appliance_id,
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ElectroluxStatusSensor(ElectroluxStatusEntity, SensorEntity):
|
||||
class ElectroluxSensor(ElectroluxEntity, SensorEntity):
|
||||
"""Electrolux Status Sensor class."""
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self.get_entity.state
|
||||
def entity_domain(self):
|
||||
"""Enitity domain for the entry. Used for consistent entity_id."""
|
||||
return SENSOR
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
return cast(ApplianceSensor, self.get_entity).unit
|
||||
def suggested_display_precision(self) -> int | None:
|
||||
"""Get the display precision."""
|
||||
if self.unit == UnitOfTemperature.CELSIUS:
|
||||
return 2
|
||||
if self.unit == UnitOfTemperature.FAHRENHEIT:
|
||||
return 2
|
||||
if self.unit == UnitOfVolume.LITERS:
|
||||
return 0
|
||||
if self.unit == UnitOfTime.SECONDS:
|
||||
return 0
|
||||
return None
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | int | float:
|
||||
"""Return the state of the sensor."""
|
||||
value = self.extract_value()
|
||||
if self.capability.get("access") == "constant":
|
||||
value = self.capability.get("default")
|
||||
elif self.entity_attr == "alerts":
|
||||
value = len(value) if value is not None else 0
|
||||
elif value is not None and isinstance(self.unit, UnitOfTime):
|
||||
# Electrolux bug - prevent negative/disabled timers
|
||||
value = max(value, 0)
|
||||
if self.catalog_entry and self.catalog_entry.value_mapping:
|
||||
# Electrolux presents as string but returns an int
|
||||
# the mapping entry allows us to correctly display this to the frontend
|
||||
mapping = self.catalog_entry.value_mapping
|
||||
_LOGGER.debug("Mapping %s: %s to %s", self.json_path, value, mapping)
|
||||
if value in mapping:
|
||||
value = mapping.get(value, value)
|
||||
if isinstance(value, str):
|
||||
if "_" in value:
|
||||
value = value.replace("_", " ")
|
||||
value = value.title()
|
||||
if value is not None:
|
||||
self._cached_value = value
|
||||
else:
|
||||
value = self._cached_value
|
||||
return value
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return unit of measurement."""
|
||||
# store the value of the sensor in the native format
|
||||
return self.unit
|
||||
|
||||
@property
|
||||
def suggested_unit_of_measurement(self) -> str | None:
|
||||
"""Return suggested unit of measurement."""
|
||||
# change the display unit in the HA frontend
|
||||
if self.unit == UnitOfTime.SECONDS:
|
||||
return UnitOfTime.MINUTES
|
||||
with contextlib.suppress(ValueError):
|
||||
if self._is_valid_suggested_unit(self.unit):
|
||||
return self.unit
|
||||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of the sensor."""
|
||||
if self.entity_attr == "alerts":
|
||||
alert_types = self.capability.get("values", {})
|
||||
# default is nullable - set a value for display to user
|
||||
alert_types = {key: "OFF" for key in alert_types}
|
||||
if current_alerts := self.extract_value():
|
||||
for alert in current_alerts:
|
||||
name = alert.get("code", "Unknown")
|
||||
severity = alert.get("severity", "Alert")
|
||||
status = alert.get("acknowledgeStatus", "")
|
||||
alert_types[name] = f"{severity}-{status}"
|
||||
create_notification(
|
||||
self.hass,
|
||||
self.config_entry,
|
||||
alert_name=name,
|
||||
alert_severity=severity,
|
||||
alert_status=status,
|
||||
title=self.name,
|
||||
)
|
||||
return alert_types
|
||||
return {}
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Electrolux Status",
|
||||
"description": "If you need help with the configuration have a look here: https://github.com/mauro-midolo/homeassistant_electrolux_status",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"region": "Region (emea, apac, na, latam or frigidaire)",
|
||||
"language": "Electrolux provided sensor names and values language"
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_configured_account": "Account is already configured",
|
||||
"reconfigure_successful": "Re-configuration was successful",
|
||||
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Invalid authentication"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Language",
|
||||
"notifications": "Raise notifications for ALERT level notices",
|
||||
"notifications_diagnostic": "Raise notifications for DIAGNOSTIC level notices",
|
||||
"notifications_warning": "Raise notifications for WARNING level notices",
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
},
|
||||
"description": "If you need help with the configuration visit: https://github.com/albaintor/homeassistant_electrolux_status",
|
||||
"title": "Electrolux Status"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"auth": "Username/Password is wrong."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single instance is allowed."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"scan_interval": "API update interval (seconds)"
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Language",
|
||||
"notifications": "Raise notifications for ALERT level notices",
|
||||
"notifications_diagnostic": "Raise notifications for DIAGNOSTIC level notices",
|
||||
"notifications_warning": "Raise notifications for WARNING level notices",
|
||||
"password": "Password",
|
||||
"renew_interval": "Renewal interval of websocket (seconds)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,41 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Electrolux Status",
|
||||
"description": "Se hai bisogno di aiuto con la configurazione, dai un'occhiata qui: https://github.com/mauro-midolo/homeassistant_electrolux_status",
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"region": "Regione (emea, apac, na, latam o frigidaire)",
|
||||
"language": "Lingua dei nomi e dei valori dei sensori forniti da Electrolux"
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Il dispositivo è già configurato",
|
||||
"already_configured_account": "L'account è già configurato",
|
||||
"reconfigure_successful": "La riconfigurazione ha avuto successo",
|
||||
"single_instance_allowed": "Già configurato.Solo una singola configurazione possibile."
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Autenticazione non valida"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Lingua",
|
||||
"notifications": "Aumenta le notifiche per gli avvisi di livello di allerta",
|
||||
"notifications_diagnostic": "Aumenta le notifiche per gli avvisi a livello diagnostico",
|
||||
"notifications_warning": "Aumenta le notifiche per gli avvisi di livello di avvertimento",
|
||||
"password": "Password",
|
||||
"username": "Nome utente"
|
||||
},
|
||||
"description": "Se hai bisogno di aiuto con la visita di configurazione: https://github.com/albaintor/homeeasistant_electrolux_status",
|
||||
"title": "Stato Electrolux"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"auth": "Username o password errati."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "È consentita una sola istanza."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"scan_interval": "Intervallo di aggiornamento API in secondi"
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Lingua",
|
||||
"notifications": "Aumenta le notifiche per gli avvisi di livello di allerta",
|
||||
"notifications_diagnostic": "Aumenta le notifiche per gli avvisi a livello diagnostico",
|
||||
"notifications_warning": "Aumenta le notifiche per gli avvisi di livello di avvertimento",
|
||||
"password": "Password",
|
||||
"renew_interval": "Intervallo di rinnovo di WebSocket (secondi)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,41 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Electrolux Status",
|
||||
"description": "Jeśli potrzebujesz pomocy przy konfiguracji, zerknij tutaj: https://github.com/mauro-midolo/homeassistant_electrolux_status",
|
||||
"data": {
|
||||
"username": "Nazwa użytkownika",
|
||||
"password": "Hasło",
|
||||
"region": "Region (emea, apac, na, latam or frigidaire)",
|
||||
"language": "Język nazw czujników i wartości, dostarczony przez Electrolux."
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Urządzenie jest już skonfigurowane",
|
||||
"already_configured_account": "Konto jest już skonfigurowane",
|
||||
"reconfigure_successful": "Ponowna konfiguracja zakończyła się powodzeniem",
|
||||
"single_instance_allowed": "Już skonfigurowano. Możliwa tylko jedna konfiguracja."
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Nieprawidłowe uwierzytelnienie"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Język",
|
||||
"notifications": "Pokazuj alerty",
|
||||
"notifications_diagnostic": "Pokazuj powoadomienia diagnostyczne",
|
||||
"notifications_warning": "Pokazuj powiadomienia ostrzegawcze",
|
||||
"password": "Hasło",
|
||||
"username": "Nazwa użytkownika"
|
||||
},
|
||||
"description": "Jeśli potrzebujesz pomocy w konfiguracji, odwiedź: https://github.com/albaintor/homeassistant_electrolux_status",
|
||||
"title": "Electrolux Status"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"auth": "Nazwa użytkownika lub hasło jest nieprawidłowe."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Dozwolona jest tylko jedna instancja."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"scan_interval": "Interwał aktualizacji API (sekundy)"
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Język",
|
||||
"notifications": "Pokazuj alerty",
|
||||
"notifications_diagnostic": "Pokazuj powoadomienia diagnostyczne",
|
||||
"notifications_warning": "Pokazuj powiadomienia ostrzegawcze",
|
||||
"password": "Hasło",
|
||||
"renew_interval": "Okres odnawiania połączenia WebSocket (sekundy)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Electrolux Status",
|
||||
"description": "Ak potrebujete pomoc s konfiguráciou, pozrite sa sem:https://github.com/mauro-midolo/homeassistant_electrolux_status",
|
||||
"data": {
|
||||
"username": "Užívateľské meno",
|
||||
"password": "Heslo",
|
||||
"region": "Región (emea, apac, na, latam alebo frigidaire)",
|
||||
"language": "Electrolux poskytol názvy senzorov a jazyk hodnôt"
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Zariadenie je už nakonfigurované",
|
||||
"already_configured_account": "Účet je už nakonfigurovaný",
|
||||
"reconfigure_successful": "Opätovná konfigurácia bola úspešná",
|
||||
"single_instance_allowed": "Už nakonfigurované.Iba jedna možná konfigurácia."
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Neplatné overovanie"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Jazyk",
|
||||
"notifications": "Zvýšiť oznámenia o upozorneniach na úrovni výstrahy",
|
||||
"notifications_diagnostic": "Zvýšte upozornenia pre oznámenia o diagnostickej úrovni",
|
||||
"notifications_warning": "Zvýšte oznámenia o upozorneniach na úrovni varovania",
|
||||
"password": "Heslo",
|
||||
"username": "Užívateľské meno"
|
||||
},
|
||||
"description": "Ak potrebujete pomoc s konfiguráciou, navštívte: https://github.com/albaintor/homeassistant_electrolux_status",
|
||||
"title": "Status"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"auth": "Používateľské meno/heslo je nesprávne."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Povolený je len jeden prípad."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"scan_interval": "Interval aktualizácie API (sekundy)"
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Jazyk",
|
||||
"notifications": "Zvýšiť oznámenia o upozorneniach na úrovni výstrahy",
|
||||
"notifications_diagnostic": "Zvýšte upozornenia pre oznámenia o diagnostickej úrovni",
|
||||
"notifications_warning": "Zvýšte oznámenia o upozorneniach na úrovni varovania",
|
||||
"password": "Heslo",
|
||||
"renew_interval": "Interval obnovy WebSocket (sekundy)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import asyncio
|
||||
import logging
|
||||
|
||||
from gardena.exceptions.authentication_exception import AuthenticationException
|
||||
from gardena.smart_system import SmartSystem
|
||||
from gardena.smart_system import SmartSystem, get_ssl_context
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
@@ -25,17 +25,17 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = ["vacuum", "sensor", "switch", "binary_sensor"]
|
||||
PLATFORMS = ["lawn_mower", "sensor", "switch", "binary_sensor", "button"]
|
||||
|
||||
# Create SSL context outside of event loop
|
||||
_SSL_CONTEXT = get_ssl_context()
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the Gardena Smart System integration."""
|
||||
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.debug("Setting up Gardena Smart System component")
|
||||
|
||||
@@ -44,26 +44,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
client_id=entry.data[CONF_CLIENT_ID],
|
||||
client_secret=entry.data[CONF_CLIENT_SECRET],
|
||||
)
|
||||
|
||||
try:
|
||||
await gardena_system.start()
|
||||
except AccessDeniedError as ex:
|
||||
_LOGGER.error('Got Access Denied Error when setting up Gardena Smart System: %s', ex)
|
||||
return False
|
||||
except InvalidClientError as ex:
|
||||
_LOGGER.error('Got Invalid Client Error when setting up Gardena Smart System: %s', ex)
|
||||
return False
|
||||
except MissingTokenError as ex:
|
||||
_LOGGER.error('Got Missing Token Error when setting up Gardena Smart System: %s', ex)
|
||||
return False
|
||||
while True:
|
||||
try:
|
||||
await gardena_system.start()
|
||||
break # If connection is successful, return True
|
||||
except ConnectionError:
|
||||
await asyncio.sleep(60) # Wait for 60 seconds before trying to reconnect
|
||||
except AccessDeniedError as ex:
|
||||
_LOGGER.error('Got Access Denied Error when setting up Gardena Smart System: %s', ex)
|
||||
return False
|
||||
except InvalidClientError as ex:
|
||||
_LOGGER.error('Got Invalid Client Error when setting up Gardena Smart System: %s', ex)
|
||||
return False
|
||||
except MissingTokenError as ex:
|
||||
_LOGGER.error('Got Missing Token Error when setting up Gardena Smart System: %s', ex)
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN][GARDENA_SYSTEM] = gardena_system
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: hass.async_create_task(gardena_system.stop()))
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component))
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
_LOGGER.debug("Gardena Smart System component setup finished")
|
||||
return True
|
||||
@@ -77,7 +78,9 @@ class GardenaSmartSystem:
|
||||
self._hass = hass
|
||||
self.smart_system = SmartSystem(
|
||||
client_id=client_id,
|
||||
client_secret=client_secret)
|
||||
client_secret=client_secret,
|
||||
ssl_context=_SSL_CONTEXT # Use the pre-created SSL context
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
try:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,16 +1,20 @@
|
||||
"""Support for Gardena Smart System websocket connection status."""
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
|
||||
from custom_components.gardena_smart_system import GARDENA_SYSTEM
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Perform the setup for Gardena websocket connection status."""
|
||||
async_add_entities([SmartSystemWebsocketStatus(hass.data[DOMAIN][GARDENA_SYSTEM].smart_system)], True)
|
||||
async_add_entities(
|
||||
[SmartSystemWebsocketStatus(hass.data[DOMAIN][GARDENA_SYSTEM].smart_system)],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class SmartSystemWebsocketStatus(BinarySensorEntity):
|
||||
@@ -54,4 +58,4 @@ class SmartSystemWebsocketStatus(BinarySensorEntity):
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return DEVICE_CLASS_CONNECTIVITY
|
||||
return BinarySensorDeviceClass.CONNECTIVITY
|
||||
|
||||
@@ -85,10 +85,6 @@ class GardenaSmartSystemConfigFlowHandler(config_entries.ConfigFlow, domain=DOMA
|
||||
|
||||
|
||||
class GardenaSmartSystemOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize Gardena Smart Sytem options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
return await self.async_step_user()
|
||||
|
||||
@@ -20,3 +20,5 @@ ATTR_OPERATING_HOURS = "operating_hours"
|
||||
ATTR_LAST_ERROR = "last_error"
|
||||
ATTR_ERROR = "error"
|
||||
ATTR_STATE = "state"
|
||||
ATTR_STINT_START = "stint_start"
|
||||
ATTR_STINT_END = "stint_end"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"domain": "gardena_smart_system",
|
||||
"name": "Gardena Smart System integration",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.1",
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/py-smart-gardena/hass-gardena-smart-system",
|
||||
"issue_tracker": "https://github.com/py-smart-gardena/hass-gardena-smart-system/issues",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@py-smart-gardena"],
|
||||
"requirements": ["py-smart-gardena==1.3.7"]
|
||||
"requirements": ["py-smart-gardena==1.3.12", "oauthlib==3.2.2"]
|
||||
}
|
||||
|
||||
@@ -31,5 +31,17 @@
|
||||
"title": "Gardena Smart System - Options"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"start_override": {
|
||||
"name": "Start override",
|
||||
"description": "Start the mower immediately for specific duration and override the schedule.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"name": "Duration",
|
||||
"description": "The duration in seconds."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,5 +32,17 @@
|
||||
"title": "Gardena Smart System - Options"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"start_override": {
|
||||
"name": "Start override",
|
||||
"description": "Start the mower immediately for specific duration and override the schedule.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"name": "Duration",
|
||||
"description": "The duration in seconds."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
"cleaning": "Mäht",
|
||||
"docked": "Angedockt",
|
||||
"error": "Fehler",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"on": "[%key:common::state::on%]",
|
||||
"paused": "[%key:common::state::paused%]",
|
||||
"idle": "Inaktiv",
|
||||
"off": "Aus",
|
||||
"on": "An",
|
||||
"paused": "Pausiert",
|
||||
"returning": "Zurück zum Dock"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
"""Support for Gardena mower."""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.vacuum import (
|
||||
StateVacuumEntity,
|
||||
SUPPORT_BATTERY,
|
||||
SUPPORT_RETURN_HOME,
|
||||
SUPPORT_STATE,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_START,
|
||||
STATE_PAUSED,
|
||||
STATE_CLEANING,
|
||||
STATE_DOCKED,
|
||||
STATE_RETURNING,
|
||||
STATE_ERROR,
|
||||
ATTR_BATTERY_LEVEL,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ATTR_ACTIVITY,
|
||||
ATTR_BATTERY_STATE,
|
||||
ATTR_NAME,
|
||||
ATTR_OPERATING_HOURS,
|
||||
ATTR_RF_LINK_LEVEL,
|
||||
ATTR_RF_LINK_STATE,
|
||||
ATTR_SERIAL,
|
||||
ATTR_LAST_ERROR,
|
||||
ATTR_ERROR,
|
||||
ATTR_STATE,
|
||||
CONF_MOWER_DURATION,
|
||||
DEFAULT_MOWER_DURATION,
|
||||
DOMAIN,
|
||||
GARDENA_LOCATION,
|
||||
)
|
||||
from .sensor import GardenaSensor
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
SUPPORT_GARDENA = (
|
||||
SUPPORT_BATTERY | SUPPORT_RETURN_HOME | SUPPORT_STOP | SUPPORT_START | SUPPORT_STATE
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Gardena smart mower system."""
|
||||
entities = []
|
||||
for mower in hass.data[DOMAIN][GARDENA_LOCATION].find_device_by_type("MOWER"):
|
||||
entities.append(GardenaSmartMower(hass, mower, config_entry.options))
|
||||
|
||||
_LOGGER.debug("Adding mower as vacuums: %s", entities)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class GardenaSmartMower(StateVacuumEntity):
|
||||
"""Representation of a Gardena Connected Mower."""
|
||||
|
||||
def __init__(self, hass, mower, options):
|
||||
"""Initialize the Gardena Connected Mower."""
|
||||
self.hass = hass
|
||||
self._device = mower
|
||||
self._options = options
|
||||
self._name = "{}".format(self._device.name)
|
||||
self._unique_id = f"{self._device.serial}-mower"
|
||||
self._state = None
|
||||
self._error_message = ""
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to events."""
|
||||
self._device.add_callback(self.update_callback)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""No polling needed for a vacuum."""
|
||||
return False
|
||||
|
||||
def update_callback(self, device):
|
||||
"""Call update for Home Assistant when the device is updated."""
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the states of Gardena devices."""
|
||||
_LOGGER.debug("Running Gardena update")
|
||||
# Managing state
|
||||
state = self._device.state
|
||||
_LOGGER.debug("Mower has state %s", state)
|
||||
if state in ["WARNING", "ERROR", "UNAVAILABLE"]:
|
||||
_LOGGER.debug("Mower has an error")
|
||||
self._state = STATE_ERROR
|
||||
self._error_message = self._device.last_error_code
|
||||
else:
|
||||
_LOGGER.debug("Getting mower state")
|
||||
activity = self._device.activity
|
||||
_LOGGER.debug("Mower has activity %s", activity)
|
||||
if activity == "PAUSED":
|
||||
self._state = STATE_PAUSED
|
||||
elif activity in [
|
||||
"OK_CUTTING",
|
||||
"OK_CUTTING_TIMER_OVERRIDDEN",
|
||||
"OK_LEAVING",
|
||||
]:
|
||||
self._state = STATE_CLEANING
|
||||
elif activity == "OK_SEARCHING":
|
||||
self._state = STATE_RETURNING
|
||||
elif activity in [
|
||||
"OK_CHARGING",
|
||||
"PARKED_TIMER",
|
||||
"PARKED_PARK_SELECTED",
|
||||
"PARKED_AUTOTIMER",
|
||||
]:
|
||||
self._state = STATE_DOCKED
|
||||
elif activity == "NONE":
|
||||
self._state = None
|
||||
_LOGGER.debug("Mower has no activity")
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag lawn mower robot features that are supported."""
|
||||
return SUPPORT_GARDENA
|
||||
|
||||
@property
|
||||
def battery_level(self):
|
||||
"""Return the battery level of the lawn mower."""
|
||||
return self._device.battery_level
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the status of the lawn mower."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if the device is available."""
|
||||
return self._device.state != "UNAVAILABLE"
|
||||
|
||||
def error(self):
|
||||
"""Return the error message."""
|
||||
if self._state == STATE_ERROR:
|
||||
return self._error_message
|
||||
return ""
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes of the lawn mower."""
|
||||
return {
|
||||
ATTR_ACTIVITY: self._device.activity,
|
||||
ATTR_BATTERY_LEVEL: self._device.battery_level,
|
||||
ATTR_BATTERY_STATE: self._device.battery_state,
|
||||
ATTR_RF_LINK_LEVEL: self._device.rf_link_level,
|
||||
ATTR_RF_LINK_STATE: self._device.rf_link_state,
|
||||
ATTR_OPERATING_HOURS: self._device.operating_hours,
|
||||
ATTR_LAST_ERROR: self._device.last_error_code,
|
||||
ATTR_ERROR: "NONE" if self._device.activity != "NONE" else self._device.last_error_code,
|
||||
ATTR_STATE: self._device.activity if self._device.activity != "NONE" else self._device.last_error_code
|
||||
}
|
||||
|
||||
@property
|
||||
def option_mower_duration(self) -> int:
|
||||
return self._options.get(CONF_MOWER_DURATION, DEFAULT_MOWER_DURATION)
|
||||
|
||||
def start(self):
|
||||
"""Start the mower using Gardena API command START_SECONDS_TO_OVERRIDE. Duration is read from integration options."""
|
||||
duration = self.option_mower_duration * 60
|
||||
_LOGGER.debug("Mower command: vacuum.start => START_SECONDS_TO_OVERRIDE, %s", duration)
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self._device.start_seconds_to_override(duration), self.hass.loop
|
||||
).result()
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the mower using Gardena API command PARK_UNTIL_FURTHER_NOTICE."""
|
||||
_LOGGER.debug("Mower command: vacuum.stop => PARK_UNTIL_FURTHER_NOTICE")
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._device.park_until_further_notice(), self.hass.loop
|
||||
).result()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Start the mower using Gardena API command START_DONT_OVERRIDE."""
|
||||
_LOGGER.debug("Mower command: vacuum.turn_on => START_DONT_OVERRIDE")
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._device.start_dont_override(), self.hass.loop
|
||||
).result()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Stop the mower using Gardena API command PARK_UNTIL_FURTHER_NOTICE."""
|
||||
_LOGGER.debug("Mower command: vacuum.turn_off => PARK_UNTIL_FURTHER_NOTICE")
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._device.park_until_further_notice(), self.hass.loop
|
||||
).result()
|
||||
|
||||
def return_to_base(self, **kwargs):
|
||||
"""Stop the mower using Gardena API command PARK_UNTIL_NEXT_TASK."""
|
||||
_LOGGER.debug("Mower command: vacuum.return_to_base => PARK_UNTIL_NEXT_TASK")
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._device.park_until_next_task(), self.hass.loop
|
||||
).result()
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
return {
|
||||
"identifiers": {
|
||||
# Serial numbers are unique identifiers within a specific domain
|
||||
(DOMAIN, self._device.serial)
|
||||
},
|
||||
"name": self._device.name,
|
||||
"manufacturer": "Gardena",
|
||||
"model": self._device.model_type,
|
||||
}
|
||||
@@ -1,80 +1,58 @@
|
||||
"""
|
||||
HACS gives you a powerful UI to handle downloads of all your custom needs.
|
||||
"""HACS gives you a powerful UI to handle downloads of all your custom needs.
|
||||
|
||||
For more details about this integration, please refer to the documentation at
|
||||
https://hacs.xyz/
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
from __future__ import annotations
|
||||
|
||||
from aiogithubapi import AIOGitHubAPIException, GitHub, GitHubAPI
|
||||
from aiogithubapi.const import ACCEPT_HEADERS
|
||||
from awesomeversion import AwesomeVersion
|
||||
from homeassistant.components.frontend import async_remove_panel
|
||||
from homeassistant.components.lovelace.system_health import system_health_info
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import Platform, __version__ as HAVERSION
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.start import async_at_start
|
||||
from homeassistant.loader import async_get_integration
|
||||
import voluptuous as vol
|
||||
|
||||
from .base import HacsBase
|
||||
from .const import DOMAIN, MINIMUM_HA_VERSION, STARTUP
|
||||
from .const import DOMAIN, HACS_SYSTEM_ID, MINIMUM_HA_VERSION, STARTUP
|
||||
from .data_client import HacsDataClient
|
||||
from .enums import ConfigurationType, HacsDisabledReason, HacsStage, LovelaceMode
|
||||
from .enums import HacsDisabledReason, HacsStage, LovelaceMode
|
||||
from .frontend import async_register_frontend
|
||||
from .utils.configuration_schema import hacs_config_combined
|
||||
from .utils.data import HacsData
|
||||
from .utils.logger import LOGGER
|
||||
from .utils.queue_manager import QueueManager
|
||||
from .utils.version import version_left_higher_or_equal_then_right
|
||||
from .websocket import async_register_websocket_commands
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: hacs_config_combined()}, extra=vol.ALLOW_EXTRA)
|
||||
PLATFORMS = [Platform.SWITCH, Platform.UPDATE]
|
||||
|
||||
|
||||
async def async_initialize_integration(
|
||||
async def _async_initialize_integration(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
config_entry: ConfigEntry | None = None,
|
||||
config: dict[str, Any] | None = None,
|
||||
config_entry: ConfigEntry,
|
||||
) -> bool:
|
||||
"""Initialize the integration"""
|
||||
hass.data[DOMAIN] = hacs = HacsBase()
|
||||
hacs.enable_hacs()
|
||||
|
||||
if config is not None:
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
if hacs.configuration.config_type == ConfigurationType.CONFIG_ENTRY:
|
||||
return True
|
||||
hacs.configuration.update_from_dict(
|
||||
{
|
||||
"config_type": ConfigurationType.YAML,
|
||||
**config[DOMAIN],
|
||||
"config": config[DOMAIN],
|
||||
}
|
||||
)
|
||||
if config_entry.source == SOURCE_IMPORT:
|
||||
# Import is not supported
|
||||
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
|
||||
return False
|
||||
|
||||
if config_entry is not None:
|
||||
if config_entry.source == SOURCE_IMPORT:
|
||||
hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id))
|
||||
return False
|
||||
|
||||
hacs.configuration.update_from_dict(
|
||||
{
|
||||
"config_entry": config_entry,
|
||||
"config_type": ConfigurationType.CONFIG_ENTRY,
|
||||
**config_entry.data,
|
||||
**config_entry.options,
|
||||
}
|
||||
)
|
||||
hacs.configuration.update_from_dict(
|
||||
{
|
||||
"config_entry": config_entry,
|
||||
**config_entry.data,
|
||||
**config_entry.options,
|
||||
},
|
||||
)
|
||||
|
||||
integration = await async_get_integration(hass, DOMAIN)
|
||||
|
||||
@@ -104,7 +82,6 @@ async def async_initialize_integration(
|
||||
except BaseException: # lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
# If this happens, the users YAML is not valid, we assume YAML mode
|
||||
pass
|
||||
hacs.log.debug("Configuration type: %s", hacs.configuration.config_type)
|
||||
hacs.core.config_path = hacs.hass.config.path()
|
||||
|
||||
if hacs.core.ha_version is None:
|
||||
@@ -131,19 +108,18 @@ async def async_initialize_integration(
|
||||
"""HACS startup tasks."""
|
||||
hacs.enable_hacs()
|
||||
|
||||
for location in (
|
||||
hass.config.path("custom_components/custom_updater.py"),
|
||||
hass.config.path("custom_components/custom_updater/__init__.py"),
|
||||
):
|
||||
if os.path.exists(location):
|
||||
hacs.log.critical(
|
||||
"This cannot be used with custom_updater. "
|
||||
"To use this you need to remove custom_updater form %s",
|
||||
location,
|
||||
)
|
||||
try:
|
||||
import custom_components.custom_updater
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
hacs.log.critical(
|
||||
"HACS cannot be used with custom_updater. "
|
||||
"To use HACS you need to remove custom_updater from `custom_components`",
|
||||
)
|
||||
|
||||
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
|
||||
return False
|
||||
hacs.disable_hacs(HacsDisabledReason.CONSTRAINS)
|
||||
return False
|
||||
|
||||
if not version_left_higher_or_equal_then_right(
|
||||
hacs.core.ha_version.string,
|
||||
@@ -160,39 +136,23 @@ async def async_initialize_integration(
|
||||
hacs.disable_hacs(HacsDisabledReason.RESTORE)
|
||||
return False
|
||||
|
||||
if not hacs.configuration.experimental:
|
||||
can_update = await hacs.async_can_update()
|
||||
hacs.log.debug("Can update %s repositories", can_update)
|
||||
|
||||
hacs.set_active_categories()
|
||||
|
||||
async_register_websocket_commands(hass)
|
||||
async_register_frontend(hass, hacs)
|
||||
await async_register_frontend(hass, hacs)
|
||||
|
||||
if hacs.configuration.config_type == ConfigurationType.YAML:
|
||||
hass.async_create_task(
|
||||
async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, hacs.configuration.config)
|
||||
)
|
||||
hacs.log.info("Update entities are only supported when using UI configuration")
|
||||
|
||||
else:
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
config_entry,
|
||||
[Platform.SENSOR, Platform.UPDATE]
|
||||
if hacs.configuration.experimental
|
||||
else [Platform.SENSOR],
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
hacs.set_stage(HacsStage.SETUP)
|
||||
if hacs.system.disabled:
|
||||
return False
|
||||
|
||||
# Schedule startup tasks
|
||||
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
|
||||
|
||||
hacs.set_stage(HacsStage.WAITING)
|
||||
hacs.log.info("Setup complete, waiting for Home Assistant before startup tasks starts")
|
||||
|
||||
# Schedule startup tasks
|
||||
async_at_start(hass=hass, at_start_cb=hacs.startup_tasks)
|
||||
|
||||
return not hacs.system.disabled
|
||||
|
||||
async def async_try_startup(_=None):
|
||||
@@ -202,10 +162,7 @@ async def async_initialize_integration(
|
||||
except AIOGitHubAPIException:
|
||||
startup_result = False
|
||||
if not startup_result:
|
||||
if (
|
||||
hacs.configuration.config_type == ConfigurationType.YAML
|
||||
or hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN
|
||||
):
|
||||
if hacs.system.disabled_reason != HacsDisabledReason.INVALID_TOKEN:
|
||||
hacs.log.info("Could not setup HACS, trying again in 15 min")
|
||||
async_call_later(hass, 900, async_try_startup)
|
||||
return
|
||||
@@ -213,37 +170,19 @@ async def async_initialize_integration(
|
||||
|
||||
await async_try_startup()
|
||||
|
||||
# Remove old (v0-v1) sensor if it exists, can be removed in v3
|
||||
er = async_get_entity_registry(hass)
|
||||
if old_sensor := er.async_get_entity_id("sensor", DOMAIN, HACS_SYSTEM_ID):
|
||||
er.async_remove(old_sensor)
|
||||
|
||||
# Mischief managed!
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
|
||||
"""Set up this integration using yaml."""
|
||||
if DOMAIN in config:
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml_configuration",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml_configuration",
|
||||
learn_more_url="https://hacs.xyz/docs/configuration/options",
|
||||
)
|
||||
LOGGER.warning(
|
||||
"YAML configuration of HACS is deprecated and will be "
|
||||
"removed in version 2.0.0, there will be no automatic "
|
||||
"import of this. "
|
||||
"Please remove it from your configuration, "
|
||||
"restart Home Assistant and use the UI to configure it instead."
|
||||
)
|
||||
return await async_initialize_integration(hass=hass, config=config)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
|
||||
setup_result = await async_initialize_integration(hass=hass, config_entry=config_entry)
|
||||
setup_result = await _async_initialize_integration(hass=hass, config_entry=config_entry)
|
||||
hacs: HacsBase = hass.data[DOMAIN]
|
||||
return setup_result and not hacs.system.disabled
|
||||
|
||||
@@ -259,7 +198,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
# Clear out pending queue
|
||||
hacs.queue.clear()
|
||||
|
||||
for task in hacs.recuring_tasks:
|
||||
for task in hacs.recurring_tasks:
|
||||
# Cancel all pending tasks
|
||||
task()
|
||||
|
||||
@@ -269,15 +208,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
try:
|
||||
if hass.data.get("frontend_panels", {}).get("hacs"):
|
||||
hacs.log.info("Removing sidepanel")
|
||||
hass.components.frontend.async_remove_panel("hacs")
|
||||
async_remove_panel(hass, "hacs")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
platforms = ["sensor"]
|
||||
if hacs.configuration.experimental:
|
||||
platforms.append("update")
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, platforms)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
hacs.set_stage(None)
|
||||
hacs.disable_hacs(HacsDisabledReason.REMOVED)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+189
-254
@@ -1,16 +1,17 @@
|
||||
"""Base HACS class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import timedelta
|
||||
import gzip
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiogithubapi import (
|
||||
AIOGitHubAPIException,
|
||||
@@ -24,23 +25,22 @@ from aiogithubapi import (
|
||||
from aiogithubapi.objects.repository import AIOGitHubAPIRepository
|
||||
from aiohttp.client import ClientSession, ClientTimeout
|
||||
from awesomeversion import AwesomeVersion
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.components.persistent_notification import (
|
||||
async_create as async_create_persistent_notification,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.loader import Integration
|
||||
from homeassistant.util import dt
|
||||
|
||||
from custom_components.hacs.repositories.base import (
|
||||
HACS_MANIFEST_KEYS_TO_EXPORT,
|
||||
REPOSITORY_KEYS_TO_EXPORT,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, TV, URL_BASE
|
||||
from .coordinator import HacsUpdateCoordinator
|
||||
from .data_client import HacsDataClient
|
||||
from .enums import (
|
||||
ConfigurationType,
|
||||
HacsCategory,
|
||||
HacsDisabledReason,
|
||||
HacsDispatchEvent,
|
||||
@@ -58,12 +58,14 @@ from .exceptions import (
|
||||
HacsRepositoryExistException,
|
||||
HomeAssistantCoreRepositoryException,
|
||||
)
|
||||
from .repositories import RERPOSITORY_CLASSES
|
||||
from .utils.decode import decode_content
|
||||
from .repositories import REPOSITORY_CLASSES
|
||||
from .repositories.base import HACS_MANIFEST_KEYS_TO_EXPORT, REPOSITORY_KEYS_TO_EXPORT
|
||||
from .utils.file_system import async_exists
|
||||
from .utils.json import json_loads
|
||||
from .utils.logger import LOGGER
|
||||
from .utils.queue_manager import QueueManager
|
||||
from .utils.store import async_load_from_store, async_save_to_store
|
||||
from .utils.workarounds import async_register_static_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .repositories.base import HacsRepository
|
||||
@@ -113,15 +115,11 @@ class HacsConfiguration:
|
||||
appdaemon: bool = False
|
||||
config: dict[str, Any] = field(default_factory=dict)
|
||||
config_entry: ConfigEntry | None = None
|
||||
config_type: ConfigurationType | None = None
|
||||
country: str = "ALL"
|
||||
debug: bool = False
|
||||
dev: bool = False
|
||||
experimental: bool = False
|
||||
frontend_repo_url: str = ""
|
||||
frontend_repo: str = ""
|
||||
netdaemon_path: str = "netdaemon/apps/"
|
||||
netdaemon: bool = False
|
||||
plugin_path: str = "www/community/"
|
||||
python_script_path: str = "python_scripts/"
|
||||
python_script: bool = False
|
||||
@@ -142,6 +140,8 @@ class HacsConfiguration:
|
||||
raise HacsException("Configuration is not valid.")
|
||||
|
||||
for key in data:
|
||||
if key in {"experimental", "netdaemon", "release_limit", "debug"}:
|
||||
continue
|
||||
self.__setattr__(key, data[key])
|
||||
|
||||
|
||||
@@ -217,6 +217,13 @@ class HacsRepositories:
|
||||
"""Return a list of downloaded repositories."""
|
||||
return [repo for repo in self._repositories if repo.data.installed]
|
||||
|
||||
def category_downloaded(self, category: HacsCategory) -> bool:
|
||||
"""Check if a given category has been downloaded."""
|
||||
for repository in self.list_downloaded:
|
||||
if repository.data.category == category:
|
||||
return True
|
||||
return False
|
||||
|
||||
def register(self, repository: HacsRepository, default: bool = False) -> None:
|
||||
"""Register a repository."""
|
||||
repo_id = str(repository.data.id)
|
||||
@@ -348,9 +355,6 @@ class HacsRepositories:
|
||||
class HacsBase:
|
||||
"""Base HACS class."""
|
||||
|
||||
common = HacsCommon()
|
||||
configuration = HacsConfiguration()
|
||||
core = HacsCore()
|
||||
data: HacsData | None = None
|
||||
data_client: HacsDataClient | None = None
|
||||
frontend_version: str | None = None
|
||||
@@ -358,17 +362,24 @@ class HacsBase:
|
||||
githubapi: GitHubAPI | None = None
|
||||
hass: HomeAssistant | None = None
|
||||
integration: Integration | None = None
|
||||
log: logging.Logger = LOGGER
|
||||
queue: QueueManager | None = None
|
||||
recuring_tasks = []
|
||||
repositories: HacsRepositories = HacsRepositories()
|
||||
repository: AIOGitHubAPIRepository | None = None
|
||||
session: ClientSession | None = None
|
||||
stage: HacsStage | None = None
|
||||
status = HacsStatus()
|
||||
system = HacsSystem()
|
||||
validation: ValidationManager | None = None
|
||||
version: str | None = None
|
||||
version: AwesomeVersion | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
self.common = HacsCommon()
|
||||
self.configuration = HacsConfiguration()
|
||||
self.coordinators: dict[HacsCategory, HacsUpdateCoordinator] = {}
|
||||
self.core = HacsCore()
|
||||
self.log = LOGGER
|
||||
self.recurring_tasks: list[Callable[[], None]] = []
|
||||
self.repositories = HacsRepositories()
|
||||
self.status = HacsStatus()
|
||||
self.system = HacsSystem()
|
||||
|
||||
@property
|
||||
def integration_dir(self) -> pathlib.Path:
|
||||
@@ -394,12 +405,7 @@ class HacsBase:
|
||||
if reason != HacsDisabledReason.REMOVED:
|
||||
self.log.error("HACS is disabled - %s", reason)
|
||||
|
||||
if (
|
||||
reason == HacsDisabledReason.INVALID_TOKEN
|
||||
and self.configuration.config_type == ConfigurationType.CONFIG_ENTRY
|
||||
):
|
||||
self.configuration.config_entry.state = ConfigEntryState.SETUP_ERROR
|
||||
self.configuration.config_entry.reason = "Authentication failed"
|
||||
if reason == HacsDisabledReason.INVALID_TOKEN:
|
||||
self.hass.add_job(self.configuration.config_entry.async_start_reauth, self.hass)
|
||||
|
||||
def enable_hacs(self) -> None:
|
||||
@@ -413,12 +419,14 @@ class HacsBase:
|
||||
if category not in self.common.categories:
|
||||
self.log.info("Enable category: %s", category)
|
||||
self.common.categories.add(category)
|
||||
self.coordinators[category] = HacsUpdateCoordinator()
|
||||
|
||||
def disable_hacs_category(self, category: HacsCategory) -> None:
|
||||
"""Disable HACS category."""
|
||||
if category in self.common.categories:
|
||||
self.log.info("Disabling category: %s", category)
|
||||
self.common.categories.pop(category)
|
||||
self.coordinators.pop(category)
|
||||
|
||||
async def async_save_file(self, file_path: str, content: Any) -> bool:
|
||||
"""Save a file."""
|
||||
@@ -451,12 +459,13 @@ class HacsBase:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(_write_file)
|
||||
except (
|
||||
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
# lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
BaseException
|
||||
) as error:
|
||||
self.log.error("Could not write data to %s - %s", file_path, error)
|
||||
return False
|
||||
|
||||
return os.path.exists(file_path)
|
||||
return await async_exists(self.hass, file_path)
|
||||
|
||||
async def async_can_update(self) -> int:
|
||||
"""Helper to calculate the number of repositories we can fetch data for."""
|
||||
@@ -472,24 +481,13 @@ class HacsBase:
|
||||
)
|
||||
self.disable_hacs(HacsDisabledReason.RATE_LIMIT)
|
||||
except (
|
||||
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
# lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
BaseException
|
||||
) as exception:
|
||||
self.log.exception(exception)
|
||||
|
||||
return 0
|
||||
|
||||
async def async_github_get_hacs_default_file(self, filename: str) -> list:
|
||||
"""Get the content of a default file."""
|
||||
response = await self.async_github_api_method(
|
||||
method=self.githubapi.repos.contents.get,
|
||||
repository=HacsGitHubRepo.DEFAULT,
|
||||
path=filename,
|
||||
)
|
||||
if response is None:
|
||||
return []
|
||||
|
||||
return json_loads(decode_content(response.data.content))
|
||||
|
||||
async def async_github_api_method(
|
||||
self,
|
||||
method: Callable[[], Awaitable[TV]],
|
||||
@@ -513,7 +511,8 @@ class HacsBase:
|
||||
except GitHubException as exception:
|
||||
_exception = exception
|
||||
except (
|
||||
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
# lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
BaseException
|
||||
) as exception:
|
||||
self.log.exception(exception)
|
||||
_exception = exception
|
||||
@@ -545,7 +544,7 @@ class HacsBase:
|
||||
):
|
||||
raise AddonRepositoryException()
|
||||
|
||||
if category not in RERPOSITORY_CLASSES:
|
||||
if category not in REPOSITORY_CLASSES:
|
||||
self.log.warning(
|
||||
"%s is not a valid repository category, %s will not be registered.",
|
||||
category,
|
||||
@@ -556,7 +555,7 @@ class HacsBase:
|
||||
if (renamed := self.common.renamed_repositories.get(repository_full_name)) is not None:
|
||||
repository_full_name = renamed
|
||||
|
||||
repository: HacsRepository = RERPOSITORY_CLASSES[category](self, repository_full_name)
|
||||
repository: HacsRepository = REPOSITORY_CLASSES[category](self, repository_full_name)
|
||||
if check:
|
||||
try:
|
||||
await repository.async_registration(ref)
|
||||
@@ -566,7 +565,8 @@ class HacsBase:
|
||||
self.log.error("Validation for %s failed.", repository_full_name)
|
||||
if self.system.action:
|
||||
raise HacsException(
|
||||
f"::error:: Validation for {repository_full_name} failed."
|
||||
f"::error:: Validation for {
|
||||
repository_full_name} failed."
|
||||
)
|
||||
return repository.validate.errors
|
||||
if self.system.action:
|
||||
@@ -582,7 +582,8 @@ class HacsBase:
|
||||
except AIOGitHubAPIException as exception:
|
||||
self.common.skip.add(repository.data.full_name)
|
||||
raise HacsException(
|
||||
f"Validation for {repository_full_name} failed with {exception}."
|
||||
f"Validation for {
|
||||
repository_full_name} failed with {exception}."
|
||||
) from exception
|
||||
|
||||
if self.status.new:
|
||||
@@ -592,7 +593,7 @@ class HacsBase:
|
||||
repository.data.id = repository_id
|
||||
|
||||
else:
|
||||
if self.hass is not None and ((check and repository.data.new) or self.status.new):
|
||||
if self.hass is not None and check and repository.data.new:
|
||||
self.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY,
|
||||
{
|
||||
@@ -613,91 +614,84 @@ class HacsBase:
|
||||
for repo in critical:
|
||||
if not repo["acknowledged"]:
|
||||
self.log.critical("URGENT!: Check the HACS panel!")
|
||||
self.hass.components.persistent_notification.create(
|
||||
title="URGENT!", message="**Check the HACS panel!**"
|
||||
async_create_persistent_notification(
|
||||
self.hass, title="URGENT!", message="**Check the HACS panel!**"
|
||||
)
|
||||
break
|
||||
|
||||
if not self.configuration.experimental:
|
||||
self.recuring_tasks.append(
|
||||
self.hass.helpers.event.async_track_time_interval(
|
||||
self.async_update_downloaded_repositories, timedelta(hours=48)
|
||||
)
|
||||
)
|
||||
self.recuring_tasks.append(
|
||||
self.hass.helpers.event.async_track_time_interval(
|
||||
self.async_update_all_repositories,
|
||||
timedelta(hours=96),
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.recuring_tasks.append(
|
||||
self.hass.helpers.event.async_track_time_interval(
|
||||
self.async_load_hacs_from_github,
|
||||
timedelta(hours=48),
|
||||
)
|
||||
)
|
||||
|
||||
self.recuring_tasks.append(
|
||||
self.hass.helpers.event.async_track_time_interval(
|
||||
self.async_update_downloaded_custom_repositories, timedelta(hours=48)
|
||||
self.recurring_tasks.append(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self.async_load_hacs_from_github,
|
||||
timedelta(hours=48),
|
||||
)
|
||||
)
|
||||
|
||||
self.recuring_tasks.append(
|
||||
self.hass.helpers.event.async_track_time_interval(
|
||||
self.async_get_all_category_repositories, timedelta(hours=6)
|
||||
self.recurring_tasks.append(
|
||||
async_track_time_interval(
|
||||
self.hass, self.async_update_downloaded_custom_repositories, timedelta(hours=48)
|
||||
)
|
||||
)
|
||||
|
||||
self.recuring_tasks.append(
|
||||
self.hass.helpers.event.async_track_time_interval(
|
||||
self.async_check_rate_limit, timedelta(minutes=5)
|
||||
)
|
||||
)
|
||||
self.recuring_tasks.append(
|
||||
self.hass.helpers.event.async_track_time_interval(
|
||||
self.async_prosess_queue, timedelta(minutes=10)
|
||||
self.recurring_tasks.append(
|
||||
async_track_time_interval(
|
||||
self.hass, self.async_get_all_category_repositories, timedelta(hours=6)
|
||||
)
|
||||
)
|
||||
|
||||
self.recuring_tasks.append(
|
||||
self.hass.helpers.event.async_track_time_interval(
|
||||
self.async_handle_critical_repositories, timedelta(hours=6)
|
||||
self.recurring_tasks.append(
|
||||
async_track_time_interval(self.hass, self.async_check_rate_limit, timedelta(minutes=5))
|
||||
)
|
||||
self.recurring_tasks.append(
|
||||
async_track_time_interval(self.hass, self.async_process_queue, timedelta(minutes=10))
|
||||
)
|
||||
|
||||
self.recurring_tasks.append(
|
||||
async_track_time_interval(
|
||||
self.hass, self.async_handle_critical_repositories, timedelta(hours=6)
|
||||
)
|
||||
)
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
unsub = self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_FINAL_WRITE, self.data.async_force_write
|
||||
)
|
||||
if config_entry := self.configuration.config_entry:
|
||||
config_entry.async_on_unload(unsub)
|
||||
|
||||
self.log.debug("There are %s scheduled recurring tasks", len(self.recuring_tasks))
|
||||
self.log.debug("There are %s scheduled recurring tasks", len(self.recurring_tasks))
|
||||
|
||||
self.status.startup = False
|
||||
self.async_dispatch(HacsDispatchEvent.STATUS, {})
|
||||
|
||||
await self.async_handle_removed_repositories()
|
||||
await self.async_get_all_category_repositories()
|
||||
await self.async_update_downloaded_repositories()
|
||||
|
||||
self.set_stage(HacsStage.RUNNING)
|
||||
|
||||
self.async_dispatch(HacsDispatchEvent.RELOAD, {"force": True})
|
||||
|
||||
await self.async_handle_critical_repositories()
|
||||
await self.async_prosess_queue()
|
||||
await self.async_process_queue()
|
||||
|
||||
self.async_dispatch(HacsDispatchEvent.STATUS, {})
|
||||
|
||||
async def async_download_file(self, url: str, *, headers: dict | None = None) -> bytes | None:
|
||||
async def async_download_file(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
headers: dict | None = None,
|
||||
keep_url: bool = False,
|
||||
nolog: bool = False,
|
||||
**_,
|
||||
) -> bytes | None:
|
||||
"""Download files, and return the content."""
|
||||
if url is None:
|
||||
return None
|
||||
|
||||
if "tags/" in url:
|
||||
if not keep_url and "tags/" in url:
|
||||
url = url.replace("tags/", "")
|
||||
|
||||
self.log.debug("Downloading %s", url)
|
||||
self.log.debug("Trying to download %s", url)
|
||||
timeouts = 0
|
||||
|
||||
while timeouts < 5:
|
||||
@@ -713,9 +707,10 @@ class HacsBase:
|
||||
return await request.read()
|
||||
|
||||
raise HacsException(
|
||||
f"Got status code {request.status} when trying to download {url}"
|
||||
f"Got status code {
|
||||
request.status} when trying to download {url}"
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
self.log.warning(
|
||||
"A timeout of 60! seconds was encountered while downloading %s, "
|
||||
"using over 60 seconds to download a single file is not normal. "
|
||||
@@ -731,23 +726,34 @@ class HacsBase:
|
||||
continue
|
||||
|
||||
except (
|
||||
BaseException # lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
# lgtm [py/catch-base-exception] pylint: disable=broad-except
|
||||
BaseException
|
||||
) as exception:
|
||||
self.log.exception("Download failed - %s", exception)
|
||||
if not nolog:
|
||||
self.log.exception("Download failed - %s", exception)
|
||||
|
||||
return None
|
||||
|
||||
async def async_recreate_entities(self) -> None:
|
||||
"""Recreate entities."""
|
||||
if self.configuration == ConfigurationType.YAML or not self.configuration.experimental:
|
||||
return
|
||||
platforms = [Platform.UPDATE]
|
||||
|
||||
platforms = [Platform.SENSOR, Platform.UPDATE]
|
||||
|
||||
await self.hass.config_entries.async_unload_platforms(
|
||||
entry=self.configuration.config_entry,
|
||||
platforms=platforms,
|
||||
)
|
||||
# Workaround for core versions without https://github.com/home-assistant/core/pull/117084
|
||||
if self.core.ha_version < AwesomeVersion("2024.6.0"):
|
||||
unload_platforms_lock = asyncio.Lock()
|
||||
async with unload_platforms_lock:
|
||||
on_unload = self.configuration.config_entry._on_unload
|
||||
self.configuration.config_entry._on_unload = []
|
||||
await self.hass.config_entries.async_unload_platforms(
|
||||
entry=self.configuration.config_entry,
|
||||
platforms=platforms,
|
||||
)
|
||||
self.configuration.config_entry._on_unload = on_unload
|
||||
else:
|
||||
await self.hass.config_entries.async_unload_platforms(
|
||||
entry=self.configuration.config_entry,
|
||||
platforms=platforms,
|
||||
)
|
||||
await self.hass.config_entries.async_forward_entry_setups(
|
||||
self.configuration.config_entry, platforms
|
||||
)
|
||||
@@ -760,49 +766,40 @@ class HacsBase:
|
||||
def set_active_categories(self) -> None:
|
||||
"""Set the active categories."""
|
||||
self.common.categories = set()
|
||||
for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN):
|
||||
for category in (HacsCategory.INTEGRATION, HacsCategory.PLUGIN, HacsCategory.TEMPLATE):
|
||||
self.enable_hacs_category(HacsCategory(category))
|
||||
|
||||
if self.configuration.experimental and self.core.ha_version >= "2023.4.0b0":
|
||||
self.enable_hacs_category(HacsCategory.TEMPLATE)
|
||||
|
||||
if HacsCategory.PYTHON_SCRIPT in self.hass.config.components:
|
||||
if (
|
||||
HacsCategory.PYTHON_SCRIPT in self.hass.config.components
|
||||
or self.repositories.category_downloaded(HacsCategory.PYTHON_SCRIPT)
|
||||
):
|
||||
self.enable_hacs_category(HacsCategory.PYTHON_SCRIPT)
|
||||
|
||||
if self.hass.services.has_service("frontend", "reload_themes"):
|
||||
if self.hass.services.has_service(
|
||||
"frontend", "reload_themes"
|
||||
) or self.repositories.category_downloaded(HacsCategory.THEME):
|
||||
self.enable_hacs_category(HacsCategory.THEME)
|
||||
|
||||
if self.configuration.appdaemon:
|
||||
self.enable_hacs_category(HacsCategory.APPDAEMON)
|
||||
if self.configuration.netdaemon:
|
||||
downloaded_netdaemon = [
|
||||
x
|
||||
for x in self.repositories.list_downloaded
|
||||
if x.data.category == HacsCategory.NETDAEMON
|
||||
]
|
||||
if len(downloaded_netdaemon) != 0:
|
||||
self.log.warning(
|
||||
"NetDaemon in HACS is deprectaded. It will stop working in the future. "
|
||||
"Please remove all your current NetDaemon repositories from HACS "
|
||||
"and download them manually if you want to continue using them."
|
||||
)
|
||||
self.enable_hacs_category(HacsCategory.NETDAEMON)
|
||||
|
||||
async def async_load_hacs_from_github(self, _=None) -> None:
|
||||
"""Load HACS from GitHub."""
|
||||
if self.configuration.experimental and self.status.inital_fetch_done:
|
||||
if self.status.inital_fetch_done:
|
||||
return
|
||||
|
||||
try:
|
||||
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
|
||||
should_recreate_entities = False
|
||||
if repository is None:
|
||||
should_recreate_entities = True
|
||||
await self.async_register_repository(
|
||||
repository_full_name=HacsGitHubRepo.INTEGRATION,
|
||||
category=HacsCategory.INTEGRATION,
|
||||
default=True,
|
||||
)
|
||||
repository = self.repositories.get_by_full_name(HacsGitHubRepo.INTEGRATION)
|
||||
elif self.configuration.experimental and not self.status.startup:
|
||||
elif not self.status.startup:
|
||||
self.log.error("Scheduling update of hacs/integration")
|
||||
self.queue.add(repository.common_update())
|
||||
if repository is None:
|
||||
@@ -813,6 +810,9 @@ class HacsBase:
|
||||
repository.data.new = False
|
||||
repository.data.releases = True
|
||||
|
||||
if should_recreate_entities:
|
||||
await self.async_recreate_entities()
|
||||
|
||||
self.repository = repository.repository_object
|
||||
self.repositories.mark_default(repository)
|
||||
except HacsException as exception:
|
||||
@@ -832,8 +832,6 @@ class HacsBase:
|
||||
await asyncio.gather(
|
||||
*[
|
||||
self.async_get_category_repositories_experimental(category)
|
||||
if self.configuration.experimental
|
||||
else self.async_get_category_repositories(HacsCategory(category))
|
||||
for category in self.common.categories or []
|
||||
]
|
||||
)
|
||||
@@ -842,7 +840,7 @@ class HacsBase:
|
||||
"""Update all category repositories."""
|
||||
self.log.debug("Fetching updated content for %s", category)
|
||||
try:
|
||||
category_data = await self.data_client.get_data(category)
|
||||
category_data = await self.data_client.get_data(category, validate=True)
|
||||
except HacsNotModifiedException:
|
||||
self.log.debug("No updates for %s", category)
|
||||
return
|
||||
@@ -853,14 +851,14 @@ class HacsBase:
|
||||
await self.data.register_unknown_repositories(category_data, category)
|
||||
|
||||
for repo_id, repo_data in category_data.items():
|
||||
repo = repo_data["full_name"]
|
||||
if self.common.renamed_repositories.get(repo):
|
||||
repo = self.common.renamed_repositories[repo]
|
||||
if self.repositories.is_removed(repo):
|
||||
repo_name = repo_data["full_name"]
|
||||
if self.common.renamed_repositories.get(repo_name):
|
||||
repo_name = self.common.renamed_repositories[repo_name]
|
||||
if self.repositories.is_removed(repo_name):
|
||||
continue
|
||||
if repo in self.common.archived_repositories:
|
||||
if repo_name in self.common.archived_repositories:
|
||||
continue
|
||||
if repository := self.repositories.get_by_full_name(repo):
|
||||
if repository := self.repositories.get_by_full_name(repo_name):
|
||||
self.repositories.set_repository_id(repository, repo_id)
|
||||
self.repositories.mark_default(repository)
|
||||
if repository.data.last_fetched is None or (
|
||||
@@ -871,15 +869,6 @@ class HacsBase:
|
||||
repository.repository_manifest.update_data(
|
||||
{**dict(HACS_MANIFEST_KEYS_TO_EXPORT), **manifest}
|
||||
)
|
||||
self.async_dispatch(
|
||||
HacsDispatchEvent.REPOSITORY,
|
||||
{
|
||||
"id": 1337,
|
||||
"action": "update",
|
||||
"repository": repository.data.full_name,
|
||||
"repository_id": repository.data.id,
|
||||
},
|
||||
)
|
||||
|
||||
if category == "integration":
|
||||
self.status.inital_fetch_done = True
|
||||
@@ -896,50 +885,8 @@ class HacsBase:
|
||||
)
|
||||
self.repositories.unregister(repository)
|
||||
|
||||
async def async_get_category_repositories(self, category: HacsCategory) -> None:
|
||||
"""Get repositories from category."""
|
||||
if self.system.disabled:
|
||||
return
|
||||
try:
|
||||
repositories = await self.async_github_get_hacs_default_file(category)
|
||||
except HacsException:
|
||||
return
|
||||
|
||||
for repo in repositories:
|
||||
if self.common.renamed_repositories.get(repo):
|
||||
repo = self.common.renamed_repositories[repo]
|
||||
if self.repositories.is_removed(repo):
|
||||
continue
|
||||
if repo in self.common.archived_repositories:
|
||||
continue
|
||||
repository = self.repositories.get_by_full_name(repo)
|
||||
if repository is not None:
|
||||
self.repositories.mark_default(repository)
|
||||
if self.status.new and self.configuration.dev:
|
||||
# Force update for new installations
|
||||
self.queue.add(repository.common_update())
|
||||
continue
|
||||
|
||||
self.queue.add(
|
||||
self.async_register_repository(
|
||||
repository_full_name=repo,
|
||||
category=category,
|
||||
default=True,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_update_all_repositories(self, _=None) -> None:
|
||||
"""Update all repositories."""
|
||||
if self.system.disabled:
|
||||
return
|
||||
self.log.debug("Starting recurring background task for all repositories")
|
||||
|
||||
for repository in self.repositories.list_all:
|
||||
if repository.data.category in self.common.categories:
|
||||
self.queue.add(repository.common_update())
|
||||
|
||||
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {"action": "reload"})
|
||||
self.log.debug("Recurring background task for all repositories done")
|
||||
self.async_dispatch(HacsDispatchEvent.REPOSITORY, {})
|
||||
self.coordinators[category].async_update_listeners()
|
||||
|
||||
async def async_check_rate_limit(self, _=None) -> None:
|
||||
"""Check rate limit."""
|
||||
@@ -951,9 +898,9 @@ class HacsBase:
|
||||
self.log.debug("Ratelimit indicate we can update %s", can_update)
|
||||
if can_update > 0:
|
||||
self.enable_hacs()
|
||||
await self.async_prosess_queue()
|
||||
await self.async_process_queue()
|
||||
|
||||
async def async_prosess_queue(self, _=None) -> None:
|
||||
async def async_process_queue(self, _=None) -> None:
|
||||
"""Process the queue."""
|
||||
if self.system.disabled:
|
||||
self.log.debug("HACS is disabled")
|
||||
@@ -993,12 +940,7 @@ class HacsBase:
|
||||
self.log.info("Loading removed repositories")
|
||||
|
||||
try:
|
||||
if self.configuration.experimental:
|
||||
removed_repositories = await self.data_client.get_data("removed")
|
||||
else:
|
||||
removed_repositories = await self.async_github_get_hacs_default_file(
|
||||
HacsCategory.REMOVED
|
||||
)
|
||||
removed_repositories = await self.data_client.get_data("removed", validate=True)
|
||||
except HacsException:
|
||||
return
|
||||
|
||||
@@ -1013,21 +955,20 @@ class HacsBase:
|
||||
continue
|
||||
if repository.data.installed:
|
||||
if removed.removal_type != "critical":
|
||||
if self.configuration.experimental:
|
||||
async_create_issue(
|
||||
hass=self.hass,
|
||||
domain=DOMAIN,
|
||||
issue_id=f"removed_{repository.data.id}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="removed",
|
||||
translation_placeholders={
|
||||
"name": repository.data.full_name,
|
||||
"reason": removed.reason,
|
||||
"repositry_id": repository.data.id,
|
||||
},
|
||||
)
|
||||
async_create_issue(
|
||||
hass=self.hass,
|
||||
domain=DOMAIN,
|
||||
issue_id=f"removed_{repository.data.id}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="removed",
|
||||
translation_placeholders={
|
||||
"name": repository.data.full_name,
|
||||
"reason": removed.reason,
|
||||
"repositry_id": repository.data.id,
|
||||
},
|
||||
)
|
||||
self.log.warning(
|
||||
"You have '%s' installed with HACS "
|
||||
"this repository has been removed from HACS, please consider removing it. "
|
||||
@@ -1042,30 +983,43 @@ class HacsBase:
|
||||
if need_to_save:
|
||||
await self.data.async_write()
|
||||
|
||||
async def async_update_downloaded_repositories(self, _=None) -> None:
|
||||
"""Execute the task."""
|
||||
if self.system.disabled or self.configuration.experimental:
|
||||
return
|
||||
self.log.info("Starting recurring background task for downloaded repositories")
|
||||
|
||||
for repository in self.repositories.list_downloaded:
|
||||
if repository.data.category in self.common.categories:
|
||||
self.queue.add(repository.update_repository(ignore_issues=True))
|
||||
|
||||
self.log.debug("Recurring background task for downloaded repositories done")
|
||||
|
||||
async def async_update_downloaded_custom_repositories(self, _=None) -> None:
|
||||
"""Execute the task."""
|
||||
if self.system.disabled or not self.configuration.experimental:
|
||||
if self.system.disabled:
|
||||
return
|
||||
self.log.info("Starting recurring background task for downloaded custom repositories")
|
||||
|
||||
repositories_to_update = 0
|
||||
repositories_updated = asyncio.Event()
|
||||
|
||||
async def update_repository(repository: HacsRepository) -> None:
|
||||
"""Update a repository"""
|
||||
nonlocal repositories_to_update
|
||||
await repository.update_repository(ignore_issues=True)
|
||||
repositories_to_update -= 1
|
||||
if not repositories_to_update:
|
||||
repositories_updated.set()
|
||||
|
||||
for repository in self.repositories.list_downloaded:
|
||||
if (
|
||||
repository.data.category in self.common.categories
|
||||
and not self.repositories.is_default(repository.data.id)
|
||||
):
|
||||
self.queue.add(repository.update_repository(ignore_issues=True))
|
||||
repositories_to_update += 1
|
||||
self.queue.add(update_repository(repository))
|
||||
|
||||
async def update_coordinators() -> None:
|
||||
"""Update all coordinators."""
|
||||
await repositories_updated.wait()
|
||||
for coordinator in self.coordinators.values():
|
||||
coordinator.async_update_listeners()
|
||||
|
||||
if config_entry := self.configuration.config_entry:
|
||||
config_entry.async_create_background_task(
|
||||
self.hass, update_coordinators(), "update_coordinators"
|
||||
)
|
||||
else:
|
||||
self.hass.async_create_background_task(update_coordinators(), "update_coordinators")
|
||||
|
||||
self.log.debug("Recurring background task for downloaded custom repositories done")
|
||||
|
||||
@@ -1077,10 +1031,7 @@ class HacsBase:
|
||||
was_installed = False
|
||||
|
||||
try:
|
||||
if self.configuration.experimental:
|
||||
critical = await self.data_client.get_data("critical")
|
||||
else:
|
||||
critical = await self.async_github_get_hacs_default_file("critical")
|
||||
critical = await self.data_client.get_data("critical", validate=True)
|
||||
except (GitHubNotModifiedException, HacsNotModifiedException):
|
||||
return
|
||||
except HacsException:
|
||||
@@ -1134,11 +1085,10 @@ class HacsBase:
|
||||
self.log.critical("Restarting Home Assistant")
|
||||
self.hass.async_create_task(self.hass.async_stop(100))
|
||||
|
||||
@callback
|
||||
def async_setup_frontend_endpoint_plugin(self) -> None:
|
||||
async def async_setup_frontend_endpoint_plugin(self) -> None:
|
||||
"""Setup the http endpoints for plugins if its not already handled."""
|
||||
if self.status.active_frontend_endpoint_plugin or not os.path.exists(
|
||||
self.hass.config.path("www/community")
|
||||
if self.status.active_frontend_endpoint_plugin or not await async_exists(
|
||||
self.hass, self.hass.config.path("www/community")
|
||||
):
|
||||
return
|
||||
|
||||
@@ -1150,26 +1100,11 @@ class HacsBase:
|
||||
use_cache,
|
||||
)
|
||||
|
||||
self.hass.http.register_static_path(
|
||||
await async_register_static_path(
|
||||
self.hass,
|
||||
URL_BASE,
|
||||
self.hass.config.path("www/community"),
|
||||
cache_headers=use_cache,
|
||||
)
|
||||
|
||||
self.status.active_frontend_endpoint_plugin = True
|
||||
|
||||
@callback
|
||||
def async_setup_frontend_endpoint_themes(self) -> None:
|
||||
"""Setup the http endpoints for themes if its not already handled."""
|
||||
if (
|
||||
self.configuration.experimental
|
||||
or self.status.active_frontend_endpoint_theme
|
||||
or not os.path.exists(self.hass.config.path("themes"))
|
||||
):
|
||||
return
|
||||
|
||||
self.log.info("Setting up themes endpoint")
|
||||
# Register themes
|
||||
self.hass.http.register_static_path(f"{URL_BASE}/themes", self.hass.config.path("themes"))
|
||||
|
||||
self.status.active_frontend_endpoint_theme = True
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
"""Adds config flow for HACS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiogithubapi import GitHubDeviceAPI, GitHubException
|
||||
from aiogithubapi import (
|
||||
GitHubDeviceAPI,
|
||||
GitHubException,
|
||||
GitHubLoginDeviceModel,
|
||||
GitHubLoginOauthModel,
|
||||
)
|
||||
from aiogithubapi.common.const import OAUTH_USER_LOGIN
|
||||
from awesomeversion import AwesomeVersion
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigFlow, OptionsFlow
|
||||
from homeassistant.const import __version__ as HAVERSION
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import UnknownFlow
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.loader import async_get_integration
|
||||
import voluptuous as vol
|
||||
|
||||
from .base import HacsBase
|
||||
from .const import CLIENT_ID, DOMAIN, LOCALE, MINIMUM_HA_VERSION
|
||||
from .enums import ConfigurationType
|
||||
from .utils.configuration_schema import (
|
||||
APPDAEMON,
|
||||
COUNTRY,
|
||||
DEBUG,
|
||||
EXPERIMENTAL,
|
||||
NETDAEMON,
|
||||
RELEASE_LIMIT,
|
||||
SIDEPANEL_ICON,
|
||||
SIDEPANEL_TITLE,
|
||||
)
|
||||
@@ -33,23 +36,22 @@ if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
class HacsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for HACS."""
|
||||
|
||||
hass: HomeAssistant
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
hass: HomeAssistant
|
||||
activation_task: asyncio.Task | None = None
|
||||
device: GitHubDeviceAPI | None = None
|
||||
|
||||
_registration: GitHubLoginDeviceModel | None = None
|
||||
_activation: GitHubLoginOauthModel | None = None
|
||||
_reauth: bool = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
self._errors = {}
|
||||
self.device = None
|
||||
self.activation = None
|
||||
self.log = LOGGER
|
||||
self._progress_task = None
|
||||
self._login_device = None
|
||||
self._reauth = False
|
||||
self._user_input = {}
|
||||
|
||||
async def async_step_user(self, user_input):
|
||||
@@ -69,48 +71,55 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self.async_step_device(user_input)
|
||||
|
||||
## Initial form
|
||||
# Initial form
|
||||
return await self._show_config_form(user_input)
|
||||
|
||||
async def async_step_device(self, _user_input):
|
||||
"""Handle device steps"""
|
||||
"""Handle device steps."""
|
||||
|
||||
async def _wait_for_activation(_=None):
|
||||
if self._login_device is None or self._login_device.expires_in is None:
|
||||
async_call_later(self.hass, 1, _wait_for_activation)
|
||||
return
|
||||
async def _wait_for_activation() -> None:
|
||||
try:
|
||||
response = await self.device.activation(device_code=self._registration.device_code)
|
||||
self._activation = response.data
|
||||
finally:
|
||||
|
||||
response = await self.device.activation(device_code=self._login_device.device_code)
|
||||
self.activation = response.data
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
|
||||
)
|
||||
async def _progress():
|
||||
with suppress(UnknownFlow):
|
||||
await self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
|
||||
|
||||
if not self.activation:
|
||||
if not self.device:
|
||||
integration = await async_get_integration(self.hass, DOMAIN)
|
||||
if not self.device:
|
||||
self.device = GitHubDeviceAPI(
|
||||
client_id=CLIENT_ID,
|
||||
session=aiohttp_client.async_get_clientsession(self.hass),
|
||||
**{"client_name": f"HACS/{integration.version}"},
|
||||
)
|
||||
async_call_later(self.hass, 1, _wait_for_activation)
|
||||
self.device = GitHubDeviceAPI(
|
||||
client_id=CLIENT_ID,
|
||||
session=aiohttp_client.async_get_clientsession(self.hass),
|
||||
**{"client_name": f"HACS/{integration.version}"},
|
||||
)
|
||||
try:
|
||||
response = await self.device.register()
|
||||
self._login_device = response.data
|
||||
return self.async_show_progress(
|
||||
step_id="device",
|
||||
progress_action="wait_for_device",
|
||||
description_placeholders={
|
||||
"url": OAUTH_USER_LOGIN,
|
||||
"code": self._login_device.user_code,
|
||||
},
|
||||
)
|
||||
self._registration = response.data
|
||||
except GitHubException as exception:
|
||||
self.log.error(exception)
|
||||
return self.async_abort(reason="github")
|
||||
LOGGER.exception(exception)
|
||||
return self.async_abort(reason="could_not_register")
|
||||
|
||||
return self.async_show_progress_done(next_step_id="device_done")
|
||||
if self.activation_task is None:
|
||||
self.activation_task = self.hass.async_create_task(_wait_for_activation())
|
||||
|
||||
if self.activation_task.done():
|
||||
if (exception := self.activation_task.exception()) is not None:
|
||||
LOGGER.exception(exception)
|
||||
return self.async_show_progress_done(next_step_id="could_not_register")
|
||||
return self.async_show_progress_done(next_step_id="device_done")
|
||||
|
||||
show_progress_kwargs = {
|
||||
"step_id": "device",
|
||||
"progress_action": "wait_for_device",
|
||||
"description_placeholders": {
|
||||
"url": OAUTH_USER_LOGIN,
|
||||
"code": self._registration.user_code,
|
||||
},
|
||||
"progress_task": self.activation_task,
|
||||
}
|
||||
return self.async_show_progress(**show_progress_kwargs)
|
||||
|
||||
async def _show_config_form(self, user_input):
|
||||
"""Show the configuration form to edit location data."""
|
||||
@@ -133,9 +142,6 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"acc_untested", default=user_input.get("acc_untested", False)
|
||||
): bool,
|
||||
vol.Required("acc_disable", default=user_input.get("acc_disable", False)): bool,
|
||||
vol.Optional(
|
||||
"experimental", default=user_input.get("experimental", False)
|
||||
): bool,
|
||||
}
|
||||
),
|
||||
errors=self._errors,
|
||||
@@ -146,7 +152,7 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if self._reauth:
|
||||
existing_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
|
||||
self.hass.config_entries.async_update_entry(
|
||||
existing_entry, data={**existing_entry.data, "token": self.activation.access_token}
|
||||
existing_entry, data={**existing_entry.data, "token": self._activation.access_token}
|
||||
)
|
||||
await self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
@@ -154,13 +160,17 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_create_entry(
|
||||
title="",
|
||||
data={
|
||||
"token": self.activation.access_token,
|
||||
"token": self._activation.access_token,
|
||||
},
|
||||
options={
|
||||
"experimental": self._user_input.get("experimental", False),
|
||||
"experimental": True,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_could_not_register(self, _user_input=None):
|
||||
"""Handle issues that need transition await from progress step."""
|
||||
return self.async_abort(reason="could_not_register")
|
||||
|
||||
async def async_step_reauth(self, _user_input=None):
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
@@ -181,12 +191,13 @@ class HacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return HacsOptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class HacsOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
class HacsOptionsFlowHandler(OptionsFlow):
|
||||
"""HACS config flow options handler."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize HACS options flow."""
|
||||
self.config_entry = config_entry
|
||||
if AwesomeVersion(HAVERSION) < "2024.11.99":
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, _user_input=None):
|
||||
"""Manage the options."""
|
||||
@@ -196,10 +207,7 @@ class HacsOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle a flow initialized by the user."""
|
||||
hacs: HacsBase = self.hass.data.get(DOMAIN)
|
||||
if user_input is not None:
|
||||
limit = int(user_input.get(RELEASE_LIMIT, 5))
|
||||
if limit <= 0 or limit > 100:
|
||||
return self.async_abort(reason="release_limit_value")
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
return self.async_create_entry(title="", data={**user_input, "experimental": True})
|
||||
|
||||
if hacs is None or hacs.configuration is None:
|
||||
return self.async_abort(reason="not_setup")
|
||||
@@ -207,18 +215,11 @@ class HacsOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
if hacs.queue.has_pending_tasks:
|
||||
return self.async_abort(reason="pending_tasks")
|
||||
|
||||
if hacs.configuration.config_type == ConfigurationType.YAML:
|
||||
schema = {vol.Optional("not_in_use", default=""): str}
|
||||
else:
|
||||
schema = {
|
||||
vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str,
|
||||
vol.Optional(SIDEPANEL_ICON, default=hacs.configuration.sidepanel_icon): str,
|
||||
vol.Optional(RELEASE_LIMIT, default=hacs.configuration.release_limit): int,
|
||||
vol.Optional(COUNTRY, default=hacs.configuration.country): vol.In(LOCALE),
|
||||
vol.Optional(APPDAEMON, default=hacs.configuration.appdaemon): bool,
|
||||
vol.Optional(NETDAEMON, default=hacs.configuration.netdaemon): bool,
|
||||
vol.Optional(DEBUG, default=hacs.configuration.debug): bool,
|
||||
vol.Optional(EXPERIMENTAL, default=hacs.configuration.experimental): bool,
|
||||
}
|
||||
schema = {
|
||||
vol.Optional(SIDEPANEL_TITLE, default=hacs.configuration.sidepanel_title): str,
|
||||
vol.Optional(SIDEPANEL_ICON, default=hacs.configuration.sidepanel_icon): str,
|
||||
vol.Optional(COUNTRY, default=hacs.configuration.country): vol.In(LOCALE),
|
||||
vol.Optional(APPDAEMON, default=hacs.configuration.appdaemon): bool,
|
||||
}
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=vol.Schema(schema))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Constants for HACS"""
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
from aiogithubapi.common.const import ACCEPT_HEADERS
|
||||
@@ -6,7 +7,7 @@ from aiogithubapi.common.const import ACCEPT_HEADERS
|
||||
NAME_SHORT = "HACS"
|
||||
DOMAIN = "hacs"
|
||||
CLIENT_ID = "395a8e669c5de9f7c6e8"
|
||||
MINIMUM_HA_VERSION = "2023.6.0"
|
||||
MINIMUM_HA_VERSION = "2024.4.1"
|
||||
|
||||
URL_BASE = "/hacsfiles"
|
||||
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
"""HACS Data client."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
import voluptuous as vol
|
||||
|
||||
from .exceptions import HacsException, HacsNotModifiedException
|
||||
from .utils.logger import LOGGER
|
||||
from .utils.validate import (
|
||||
VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA,
|
||||
VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA,
|
||||
VALIDATE_FETCHED_V2_REPO_DATA,
|
||||
)
|
||||
|
||||
CRITICAL_REMOVED_VALIDATORS = {
|
||||
"critical": VALIDATE_FETCHED_V2_CRITICAL_REPO_SCHEMA,
|
||||
"removed": VALIDATE_FETCHED_V2_REMOVED_REPO_SCHEMA,
|
||||
}
|
||||
|
||||
|
||||
class HacsDataClient:
|
||||
@@ -39,7 +52,7 @@ class HacsDataClient:
|
||||
response.raise_for_status()
|
||||
except HacsNotModifiedException:
|
||||
raise
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
raise HacsException("Timeout of 60s reached") from None
|
||||
except Exception as exception:
|
||||
raise HacsException(f"Error fetching data from HACS: {exception}") from exception
|
||||
@@ -48,9 +61,37 @@ class HacsDataClient:
|
||||
|
||||
return await response.json()
|
||||
|
||||
async def get_data(self, section: str | None) -> dict[str, dict[str, Any]]:
|
||||
async def get_data(self, section: str | None, *, validate: bool) -> dict[str, dict[str, Any]]:
|
||||
"""Get data."""
|
||||
return await self._do_request(filename="data.json", section=section)
|
||||
data = await self._do_request(filename="data.json", section=section)
|
||||
if not validate:
|
||||
return data
|
||||
|
||||
if section in VALIDATE_FETCHED_V2_REPO_DATA:
|
||||
validated = {}
|
||||
for key, repo_data in data.items():
|
||||
try:
|
||||
validated[key] = VALIDATE_FETCHED_V2_REPO_DATA[section](repo_data)
|
||||
except vol.Invalid as exception:
|
||||
LOGGER.info(
|
||||
"Got invalid data for %s (%s)", repo_data.get("full_name", key), exception
|
||||
)
|
||||
continue
|
||||
|
||||
return validated
|
||||
|
||||
if not (validator := CRITICAL_REMOVED_VALIDATORS.get(section)):
|
||||
raise ValueError(f"Do not know how to validate {section}")
|
||||
|
||||
validated = []
|
||||
for repo_data in data:
|
||||
try:
|
||||
validated.append(validator(repo_data))
|
||||
except vol.Invalid as exception:
|
||||
LOGGER.info("Got invalid data for %s (%s)", section, exception)
|
||||
continue
|
||||
|
||||
return validated
|
||||
|
||||
async def get_repositories(self, section: str) -> list[str]:
|
||||
"""Get repositories."""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Diagnostics support for HACS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
@@ -10,7 +11,6 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from .base import HacsBase
|
||||
from .const import DOMAIN
|
||||
from .utils.configuration_schema import TOKEN
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
@@ -48,8 +48,6 @@ async def async_get_config_entry_diagnostics(
|
||||
"country",
|
||||
"debug",
|
||||
"dev",
|
||||
"experimental",
|
||||
"netdaemon",
|
||||
"python_script",
|
||||
"release_limit",
|
||||
"theme",
|
||||
@@ -79,4 +77,4 @@ async def async_get_config_entry_diagnostics(
|
||||
except GitHubException as exception:
|
||||
data["rate_limit"] = str(exception)
|
||||
|
||||
return async_redact_data(data, (TOKEN,))
|
||||
return async_redact_data(data, ("token",))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""HACS Base entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
@@ -7,8 +8,10 @@ from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import BaseCoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, HACS_SYSTEM_ID, NAME_SHORT
|
||||
from .coordinator import HacsUpdateCoordinator
|
||||
from .enums import HacsDispatchEvent, HacsGitHubRepo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -39,6 +42,10 @@ class HacsBaseEntity(Entity):
|
||||
"""Initialize."""
|
||||
self.hacs = hacs
|
||||
|
||||
|
||||
class HacsDispatcherEntity(HacsBaseEntity):
|
||||
"""Base HACS entity listening to dispatcher signals."""
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register for status events."""
|
||||
self.async_on_remove(
|
||||
@@ -64,7 +71,7 @@ class HacsBaseEntity(Entity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class HacsSystemEntity(HacsBaseEntity):
|
||||
class HacsSystemEntity(HacsDispatcherEntity):
|
||||
"""Base system entity."""
|
||||
|
||||
_attr_icon = "hacs:hacs"
|
||||
@@ -76,7 +83,7 @@ class HacsSystemEntity(HacsBaseEntity):
|
||||
return system_info(self.hacs)
|
||||
|
||||
|
||||
class HacsRepositoryEntity(HacsBaseEntity):
|
||||
class HacsRepositoryEntity(BaseCoordinatorEntity[HacsUpdateCoordinator], HacsBaseEntity):
|
||||
"""Base repository entity."""
|
||||
|
||||
def __init__(
|
||||
@@ -85,9 +92,11 @@ class HacsRepositoryEntity(HacsBaseEntity):
|
||||
repository: HacsRepository,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(hacs=hacs)
|
||||
BaseCoordinatorEntity.__init__(self, hacs.coordinators[repository.data.category])
|
||||
HacsBaseEntity.__init__(self, hacs=hacs)
|
||||
self.repository = repository
|
||||
self._attr_unique_id = str(repository.data.id)
|
||||
self._repo_last_fetched = repository.data.last_fetched
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@@ -100,20 +109,35 @@ class HacsRepositoryEntity(HacsBaseEntity):
|
||||
if self.repository.data.full_name == HacsGitHubRepo.INTEGRATION:
|
||||
return system_info(self.hacs)
|
||||
|
||||
def _manufacturer():
|
||||
if authors := self.repository.data.authors:
|
||||
return ", ".join(author.replace("@", "") for author in authors)
|
||||
return self.repository.data.full_name.split("/")[0]
|
||||
|
||||
return {
|
||||
"identifiers": {(DOMAIN, str(self.repository.data.id))},
|
||||
"name": self.repository.display_name,
|
||||
"model": self.repository.data.category,
|
||||
"manufacturer": ", ".join(
|
||||
author.replace("@", "") for author in self.repository.data.authors
|
||||
),
|
||||
"configuration_url": "homeassistant://hacs",
|
||||
"manufacturer": _manufacturer(),
|
||||
"configuration_url": f"homeassistant://hacs/repository/{self.repository.data.id}",
|
||||
"entry_type": DeviceEntryType.SERVICE,
|
||||
}
|
||||
|
||||
@callback
|
||||
def _update_and_write_state(self, data: dict) -> None:
|
||||
"""Update the entity and write state."""
|
||||
if data.get("repository_id") == self.repository.data.id:
|
||||
self._update()
|
||||
self.async_write_ha_state()
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if (
|
||||
self._repo_last_fetched is not None
|
||||
and self.repository.data.last_fetched is not None
|
||||
and self._repo_last_fetched >= self.repository.data.last_fetched
|
||||
):
|
||||
return
|
||||
|
||||
self._repo_last_fetched = self.repository.data.last_fetched
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity.
|
||||
|
||||
Only used by the generic entity update service.
|
||||
"""
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
"""Helper constants."""
|
||||
|
||||
# pylint: disable=missing-class-docstring
|
||||
import sys
|
||||
|
||||
if sys.version_info.minor >= 11:
|
||||
# Needs Python 3.11
|
||||
from enum import StrEnum # # pylint: disable=no-name-in-module
|
||||
else:
|
||||
try:
|
||||
# https://github.com/home-assistant/core/blob/dev/homeassistant/backports/enum.py
|
||||
# Considered internal to Home Assistant, can be removed whenever.
|
||||
from homeassistant.backports.enum import StrEnum
|
||||
except ImportError:
|
||||
from enum import Enum
|
||||
|
||||
class StrEnum(str, Enum):
|
||||
pass
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class HacsGitHubRepo(StrEnum):
|
||||
@@ -29,7 +16,6 @@ class HacsCategory(StrEnum):
|
||||
INTEGRATION = "integration"
|
||||
LOVELACE = "lovelace"
|
||||
PLUGIN = "plugin" # Kept for legacy purposes
|
||||
NETDAEMON = "netdaemon"
|
||||
PYTHON_SCRIPT = "python_script"
|
||||
TEMPLATE = "template"
|
||||
THEME = "theme"
|
||||
@@ -59,11 +45,6 @@ class RepositoryFile(StrEnum):
|
||||
MAINIFEST_JSON = "manifest.json"
|
||||
|
||||
|
||||
class ConfigurationType(StrEnum):
|
||||
YAML = "yaml"
|
||||
CONFIG_ENTRY = "config_entry"
|
||||
|
||||
|
||||
class LovelaceMode(StrEnum):
|
||||
"""Lovelace Modes."""
|
||||
|
||||
|
||||
@@ -1,61 +1,53 @@
|
||||
""""Starting setup task: Frontend"."""
|
||||
"""Starting setup task: Frontend."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.components.frontend import (
|
||||
add_extra_js_url,
|
||||
async_register_built_in_panel,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, URL_BASE
|
||||
from .hacs_frontend import VERSION as FE_VERSION, locate_dir
|
||||
from .hacs_frontend_experimental import (
|
||||
VERSION as EXPERIMENTAL_FE_VERSION,
|
||||
locate_dir as experimental_locate_dir,
|
||||
)
|
||||
from .utils.workarounds import async_register_static_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .base import HacsBase
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
|
||||
async def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
|
||||
"""Register the frontend."""
|
||||
|
||||
# Setup themes endpoint if needed
|
||||
hacs.async_setup_frontend_endpoint_themes()
|
||||
|
||||
# Register frontend
|
||||
if hacs.configuration.dev and (frontend_path := os.getenv("HACS_FRONTEND_DIR")):
|
||||
hacs.log.warning(
|
||||
"<HacsFrontend> Frontend development mode enabled. Do not run in production!"
|
||||
)
|
||||
hass.http.register_static_path(
|
||||
f"{URL_BASE}/frontend", f"{frontend_path}/hacs_frontend", cache_headers=False
|
||||
)
|
||||
elif hacs.configuration.experimental:
|
||||
hacs.log.info("<HacsFrontend> Using experimental frontend")
|
||||
hass.http.register_static_path(
|
||||
f"{URL_BASE}/frontend", experimental_locate_dir(), cache_headers=False
|
||||
await async_register_static_path(
|
||||
hass, f"{URL_BASE}/frontend", f"{frontend_path}/hacs_frontend", cache_headers=False
|
||||
)
|
||||
hacs.frontend_version = "dev"
|
||||
else:
|
||||
#
|
||||
hass.http.register_static_path(f"{URL_BASE}/frontend", locate_dir(), cache_headers=False)
|
||||
await async_register_static_path(
|
||||
hass, f"{URL_BASE}/frontend", locate_dir(), cache_headers=False
|
||||
)
|
||||
hacs.frontend_version = FE_VERSION
|
||||
|
||||
# Custom iconset
|
||||
hass.http.register_static_path(
|
||||
f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js")
|
||||
)
|
||||
if "frontend_extra_module_url" not in hass.data:
|
||||
hass.data["frontend_extra_module_url"] = set()
|
||||
hass.data["frontend_extra_module_url"].add(f"{URL_BASE}/iconset.js")
|
||||
|
||||
hacs.frontend_version = (
|
||||
FE_VERSION if not hacs.configuration.experimental else EXPERIMENTAL_FE_VERSION
|
||||
await async_register_static_path(
|
||||
hass, f"{URL_BASE}/iconset.js", str(hacs.integration_dir / "iconset.js")
|
||||
)
|
||||
add_extra_js_url(hass, f"{URL_BASE}/iconset.js")
|
||||
|
||||
# Add to sidepanel if needed
|
||||
if DOMAIN not in hass.data.get("frontend_panels", {}):
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
async_register_built_in_panel(
|
||||
hass,
|
||||
component_name="custom",
|
||||
sidebar_title=hacs.configuration.sidepanel_title,
|
||||
sidebar_icon=hacs.configuration.sidepanel_icon,
|
||||
@@ -72,4 +64,4 @@ def async_register_frontend(hass: HomeAssistant, hacs: HacsBase) -> None:
|
||||
)
|
||||
|
||||
# Setup plugin endpoint if needed
|
||||
hacs.async_setup_frontend_endpoint_plugin()
|
||||
await hacs.async_setup_frontend_endpoint_plugin()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,23 +0,0 @@
|
||||
import{a as t,r as i,n as a}from"./main-ad130be7.js";import{L as n,s}from"./c.82eccc94.js";let r=t([a("ha-list-item")],(function(t,a){return{F:class extends a{constructor(...i){super(...i),t(this)}},d:[{kind:"get",static:!0,key:"styles",value:function(){return[s,i`
|
||||
:host {
|
||||
padding-left: var(--mdc-list-side-padding, 20px);
|
||||
padding-right: var(--mdc-list-side-padding, 20px);
|
||||
}
|
||||
:host([graphic="avatar"]:not([twoLine])),
|
||||
:host([graphic="icon"]:not([twoLine])) {
|
||||
height: 48px;
|
||||
}
|
||||
span.material-icons:first-of-type {
|
||||
margin-inline-start: 0px !important;
|
||||
margin-inline-end: var(
|
||||
--mdc-list-item-graphic-margin,
|
||||
16px
|
||||
) !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
span.material-icons:last-of-type {
|
||||
margin-inline-start: auto !important;
|
||||
margin-inline-end: 0px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`]}}]}}),n);const e=t=>`https://brands.home-assistant.io/${t.useFallback?"_/":""}${t.domain}/${t.darkOptimized?"dark_":""}${t.type}.png`,o=t=>t.split("/")[4],p=t=>t.startsWith("https://brands.home-assistant.io/");export{r as H,e as b,o as e,p as i};
|
||||
Binary file not shown.
@@ -1,24 +0,0 @@
|
||||
import{a as e,h as t,Y as i,e as n,i as o,$ as r,L as l,N as a,r as d,n as s}from"./main-ad130be7.js";import"./c.9b92f489.js";e([s("ha-button-menu")],(function(e,t){class s extends t{constructor(...t){super(...t),e(this)}}return{F:s,d:[{kind:"field",key:i,value:void 0},{kind:"field",decorators:[n()],key:"corner",value:()=>"TOP_START"},{kind:"field",decorators:[n()],key:"menuCorner",value:()=>"START"},{kind:"field",decorators:[n({type:Number})],key:"x",value:()=>null},{kind:"field",decorators:[n({type:Number})],key:"y",value:()=>null},{kind:"field",decorators:[n({type:Boolean})],key:"multi",value:()=>!1},{kind:"field",decorators:[n({type:Boolean})],key:"activatable",value:()=>!1},{kind:"field",decorators:[n({type:Boolean})],key:"disabled",value:()=>!1},{kind:"field",decorators:[n({type:Boolean})],key:"fixed",value:()=>!1},{kind:"field",decorators:[o("mwc-menu",!0)],key:"_menu",value:void 0},{kind:"get",key:"items",value:function(){var e;return null===(e=this._menu)||void 0===e?void 0:e.items}},{kind:"get",key:"selected",value:function(){var e;return null===(e=this._menu)||void 0===e?void 0:e.selected}},{kind:"method",key:"focus",value:function(){var e,t;null!==(e=this._menu)&&void 0!==e&&e.open?this._menu.focusItemAtIndex(0):null===(t=this._triggerButton)||void 0===t||t.focus()}},{kind:"method",key:"render",value:function(){return r`
|
||||
<div @click=${this._handleClick}>
|
||||
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot>
|
||||
</div>
|
||||
<mwc-menu
|
||||
.corner=${this.corner}
|
||||
.menuCorner=${this.menuCorner}
|
||||
.fixed=${this.fixed}
|
||||
.multi=${this.multi}
|
||||
.activatable=${this.activatable}
|
||||
.y=${this.y}
|
||||
.x=${this.x}
|
||||
>
|
||||
<slot></slot>
|
||||
</mwc-menu>
|
||||
`}},{kind:"method",key:"firstUpdated",value:function(e){l(a(s.prototype),"firstUpdated",this).call(this,e),"rtl"===document.dir&&this.updateComplete.then((()=>{this.querySelectorAll("mwc-list-item").forEach((e=>{const t=document.createElement("style");t.innerHTML="span.material-icons:first-of-type { margin-left: var(--mdc-list-item-graphic-margin, 32px) !important; margin-right: 0px !important;}",e.shadowRoot.appendChild(t)}))}))}},{kind:"method",key:"_handleClick",value:function(){this.disabled||(this._menu.anchor=this,this._menu.show())}},{kind:"get",key:"_triggerButton",value:function(){return this.querySelector('ha-icon-button[slot="trigger"], mwc-button[slot="trigger"]')}},{kind:"method",key:"_setTriggerAria",value:function(){this._triggerButton&&(this._triggerButton.ariaHasPopup="menu")}},{kind:"get",static:!0,key:"styles",value:function(){return d`
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
::slotted([disabled]) {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
`}}]}}),t);
|
||||
Binary file not shown.
@@ -1,390 +0,0 @@
|
||||
import{a as e,h as t,e as i,g as a,t as s,$ as o,j as r,R as n,w as l,r as h,n as c,m as d,L as p,N as u,o as v,b as f,aI as b,ai as m,c as k,E as g,aJ as y,aC as w,aK as x,aL as $,d as _,s as R}from"./main-ad130be7.js";import{f as z}from"./c.3243a8b0.js";import{c as j}from"./c.4a97632a.js";import"./c.f1291e50.js";import"./c.2d5ed670.js";import"./c.97b7c4b0.js";import{r as F}from"./c.4204ca09.js";import{i as P}from"./c.21c042d4.js";import{s as I}from"./c.2645c235.js";import"./c.a5f69ed4.js";import"./c.3f859915.js";import"./c.9b92f489.js";import"./c.82eccc94.js";import"./c.8e28b461.js";import"./c.4feb0cb8.js";import"./c.0ca5587f.js";import"./c.5d3ce9d6.js";import"./c.f6611997.js";import"./c.743a15a1.js";import"./c.4266acdb.js";e([c("ha-tab")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[i({type:Boolean,reflect:!0})],key:"active",value:()=>!1},{kind:"field",decorators:[i({type:Boolean,reflect:!0})],key:"narrow",value:()=>!1},{kind:"field",decorators:[i()],key:"name",value:void 0},{kind:"field",decorators:[a("mwc-ripple")],key:"_ripple",value:void 0},{kind:"field",decorators:[s()],key:"_shouldRenderRipple",value:()=>!1},{kind:"method",key:"render",value:function(){return o`
|
||||
<div
|
||||
tabindex="0"
|
||||
role="tab"
|
||||
aria-selected=${this.active}
|
||||
aria-label=${r(this.name)}
|
||||
@focus=${this.handleRippleFocus}
|
||||
@blur=${this.handleRippleBlur}
|
||||
@mousedown=${this.handleRippleActivate}
|
||||
@mouseup=${this.handleRippleDeactivate}
|
||||
@mouseenter=${this.handleRippleMouseEnter}
|
||||
@mouseleave=${this.handleRippleMouseLeave}
|
||||
@touchstart=${this.handleRippleActivate}
|
||||
@touchend=${this.handleRippleDeactivate}
|
||||
@touchcancel=${this.handleRippleDeactivate}
|
||||
@keydown=${this._handleKeyDown}
|
||||
>
|
||||
${this.narrow?o`<slot name="icon"></slot>`:""}
|
||||
<span class="name">${this.name}</span>
|
||||
${this._shouldRenderRipple?o`<mwc-ripple></mwc-ripple>`:""}
|
||||
</div>
|
||||
`}},{kind:"field",key:"_rippleHandlers",value(){return new n((()=>(this._shouldRenderRipple=!0,this._ripple)))}},{kind:"method",key:"_handleKeyDown",value:function(e){13===e.keyCode&&e.target.click()}},{kind:"method",decorators:[l({passive:!0})],key:"handleRippleActivate",value:function(e){this._rippleHandlers.startPress(e)}},{kind:"method",key:"handleRippleDeactivate",value:function(){this._rippleHandlers.endPress()}},{kind:"method",key:"handleRippleMouseEnter",value:function(){this._rippleHandlers.startHover()}},{kind:"method",key:"handleRippleMouseLeave",value:function(){this._rippleHandlers.endHover()}},{kind:"method",key:"handleRippleFocus",value:function(){this._rippleHandlers.startFocus()}},{kind:"method",key:"handleRippleBlur",value:function(){this._rippleHandlers.endFocus()}},{kind:"get",static:!0,key:"styles",value:function(){return h`
|
||||
div {
|
||||
padding: 0 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: var(--header-height);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:host([active]) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
:host(:not([narrow])[active]) div {
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host([narrow]) div {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`}}]}}),t),e([c("hass-tabs-subpage")],(function(e,t){class a extends t{constructor(...t){super(...t),e(this)}}return{F:a,d:[{kind:"field",decorators:[i({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[i({type:Boolean})],key:"supervisor",value:()=>!1},{kind:"field",decorators:[i({attribute:!1})],key:"localizeFunc",value:void 0},{kind:"field",decorators:[i({type:String,attribute:"back-path"})],key:"backPath",value:void 0},{kind:"field",decorators:[i()],key:"backCallback",value:void 0},{kind:"field",decorators:[i({type:Boolean,attribute:"main-page"})],key:"mainPage",value:()=>!1},{kind:"field",decorators:[i({attribute:!1})],key:"route",value:void 0},{kind:"field",decorators:[i({attribute:!1})],key:"tabs",value:void 0},{kind:"field",decorators:[i({type:Boolean,reflect:!0})],key:"narrow",value:()=>!1},{kind:"field",decorators:[i({type:Boolean,reflect:!0,attribute:"is-wide"})],key:"isWide",value:()=>!1},{kind:"field",decorators:[i({type:Boolean,reflect:!0})],key:"rtl",value:()=>!1},{kind:"field",decorators:[s()],key:"_activeTab",value:void 0},{kind:"field",decorators:[F(".content")],key:"_savedScrollPos",value:void 0},{kind:"field",key:"_getTabs",value(){return d(((e,t,i,a,s,r,n)=>{const l=e.filter((e=>(!e.component||e.core||P(this.hass,e.component))&&(!e.advancedOnly||i)));if(l.length<2){if(1===l.length){const e=l[0];return[e.translationKey?n(e.translationKey):e.name]}return[""]}return l.map((e=>o`
|
||||
<a href=${e.path}>
|
||||
<ha-tab
|
||||
.hass=${this.hass}
|
||||
.active=${e.path===(null==t?void 0:t.path)}
|
||||
.narrow=${this.narrow}
|
||||
.name=${e.translationKey?n(e.translationKey):e.name}
|
||||
>
|
||||
${e.iconPath?o`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${e.iconPath}
|
||||
></ha-svg-icon>`:""}
|
||||
</ha-tab>
|
||||
</a>
|
||||
`))}))}},{kind:"method",key:"willUpdate",value:function(e){if(e.has("route")&&(this._activeTab=this.tabs.find((e=>`${this.route.prefix}${this.route.path}`.includes(e.path)))),e.has("hass")){const t=e.get("hass");t&&t.language===this.hass.language||(this.rtl=j(this.hass))}p(u(a.prototype),"willUpdate",this).call(this,e)}},{kind:"method",key:"render",value:function(){var e,t;const i=this._getTabs(this.tabs,this._activeTab,null===(e=this.hass.userData)||void 0===e?void 0:e.showAdvanced,this.hass.config.components,this.hass.language,this.narrow,this.localizeFunc||this.hass.localize),a=i.length>1;return o`
|
||||
<div class="toolbar">
|
||||
${this.mainPage||!this.backPath&&null!==(t=history.state)&&void 0!==t&&t.root?o`
|
||||
<ha-menu-button
|
||||
.hassio=${this.supervisor}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`:this.backPath?o`
|
||||
<a href=${this.backPath}>
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
></ha-icon-button-arrow-prev>
|
||||
</a>
|
||||
`:o`
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
${this.narrow||!a?o`<div class="main-title">
|
||||
<slot name="header">${a?"":i[0]}</slot>
|
||||
</div>`:""}
|
||||
${a?o`
|
||||
<div id="tabbar" class=${v({"bottom-bar":this.narrow})}>
|
||||
${i}
|
||||
</div>
|
||||
`:""}
|
||||
<div id="toolbar-icon">
|
||||
<slot name="toolbar-icon"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content ${v({tabs:a})}"
|
||||
@scroll=${this._saveScrollPos}
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div id="fab" class=${v({tabs:a})}>
|
||||
<slot name="fab"></slot>
|
||||
</div>
|
||||
`}},{kind:"method",decorators:[l({passive:!0})],key:"_saveScrollPos",value:function(e){this._savedScrollPos=e.target.scrollTop}},{kind:"method",key:"_backTapped",value:function(){this.backCallback?this.backCallback():history.back()}},{kind:"get",static:!0,key:"styles",value:function(){return h`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
ha-menu-button {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
height: var(--header-height);
|
||||
background-color: var(--sidebar-background-color);
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toolbar a {
|
||||
color: var(--sidebar-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
.bottom-bar a {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#tabbar {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#tabbar > a {
|
||||
overflow: hidden;
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
#tabbar.bottom-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--sidebar-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
justify-content: space-around;
|
||||
z-index: 2;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
#tabbar:not(.bottom-bar) {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:host(:not([narrow])) #toolbar-icon {
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
ha-menu-button,
|
||||
ha-icon-button-arrow-prev,
|
||||
::slotted([slot="toolbar-icon"]) {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
pointer-events: auto;
|
||||
color: var(--sidebar-icon-color);
|
||||
}
|
||||
|
||||
.main-title {
|
||||
flex: 1;
|
||||
max-height: var(--header-height);
|
||||
line-height: 20px;
|
||||
color: var(--sidebar-text-color);
|
||||
margin: var(--main-title-margin, 0 0 0 24px);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
width: calc(
|
||||
100% - env(safe-area-inset-left) - env(safe-area-inset-right)
|
||||
);
|
||||
margin-left: env(safe-area-inset-left);
|
||||
margin-right: env(safe-area-inset-right);
|
||||
height: calc(100% - 1px - var(--header-height));
|
||||
height: calc(
|
||||
100% - 1px - var(--header-height) - env(safe-area-inset-bottom)
|
||||
);
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
:host([narrow]) .content.tabs {
|
||||
height: calc(100% - 2 * var(--header-height));
|
||||
height: calc(
|
||||
100% - 2 * var(--header-height) - env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
|
||||
#fab {
|
||||
position: fixed;
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
}
|
||||
:host([narrow]) #fab.tabs {
|
||||
bottom: calc(84px + env(safe-area-inset-bottom));
|
||||
}
|
||||
#fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
:host([rtl]) #fab {
|
||||
right: auto;
|
||||
left: calc(16px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl][is-wide]) #fab {
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
right: auto;
|
||||
}
|
||||
`}}]}}),t);let E=e([c("hacs-store-panel")],(function(e,t){return{F:class extends t{constructor(...t){super(...t),e(this)}},d:[{kind:"field",decorators:[i({attribute:!1})],key:"filters",value:()=>({})},{kind:"field",decorators:[i({attribute:!1})],key:"hacs",value:void 0},{kind:"field",decorators:[i()],key:"_searchInput",value:()=>""},{kind:"field",decorators:[i({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[i({attribute:!1})],key:"narrow",value:void 0},{kind:"field",decorators:[i({attribute:!1})],key:"isWide",value:void 0},{kind:"field",decorators:[i({attribute:!1})],key:"route",value:void 0},{kind:"field",decorators:[i({attribute:!1})],key:"sections",value:void 0},{kind:"field",decorators:[i()],key:"section",value:void 0},{kind:"field",key:"_repositoriesInActiveSection",value(){return d(((e,t)=>[(null==e?void 0:e.filter((e=>{var i,a,s;return(null===(i=this.hacs.sections)||void 0===i||null===(a=i.find((e=>e.id===t)))||void 0===a||null===(s=a.categories)||void 0===s?void 0:s.includes(e.category))&&e.installed})))||[],(null==e?void 0:e.filter((e=>{var i,a,s;return(null===(i=this.hacs.sections)||void 0===i||null===(a=i.find((e=>e.id===t)))||void 0===a||null===(s=a.categories)||void 0===s?void 0:s.includes(e.category))&&e.new&&!e.installed})))||[]]))}},{kind:"get",key:"allRepositories",value:function(){const[e,t]=this._repositoriesInActiveSection(this.hacs.repositories,this.section);return t.concat(e)}},{kind:"field",key:"_filterRepositories",value:()=>d(z)},{kind:"get",key:"visibleRepositories",value:function(){const e=this.allRepositories.filter((e=>{var t,i;return null===(t=this.filters[this.section])||void 0===t||null===(i=t.find((t=>t.id===e.category)))||void 0===i?void 0:i.checked}));return this._filterRepositories(e,this._searchInput)}},{kind:"method",key:"firstUpdated",value:async function(){this.addEventListener("filter-change",(e=>this._updateFilters(e)))}},{kind:"method",key:"_updateFilters",value:function(e){var t;const i=null===(t=this.filters[this.section])||void 0===t?void 0:t.find((t=>t.id===e.detail.id));this.filters[this.section].find((e=>e.id===i.id)).checked=!i.checked,this.requestUpdate()}},{kind:"method",key:"render",value:function(){var e;if(!this.hacs)return o``;const t=this._repositoriesInActiveSection(this.hacs.repositories,this.section)[1];if(!this.filters[this.section]&&this.hacs.info.categories){var i;const e=null===(i=f(this.hacs.language,this.route))||void 0===i?void 0:i.categories;this.filters[this.section]=[],null==e||e.filter((e=>{var t;return null===(t=this.hacs.info)||void 0===t?void 0:t.categories.includes(e)})).forEach((e=>{this.filters[this.section].push({id:e,value:e,checked:!0})}))}return o`<hass-tabs-subpage
|
||||
back-path="/hacs/entry"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${this.hacs.sections}
|
||||
hasFab
|
||||
>
|
||||
<ha-icon-overflow-menu
|
||||
slot="toolbar-icon"
|
||||
narrow
|
||||
.hass=${this.hass}
|
||||
.items=${[{path:b,label:this.hacs.localize("menu.documentation"),action:()=>m.open("https://hacs.xyz/","_blank","noreferrer=true")},{path:k,label:"GitHub",action:()=>m.open("https://github.com/hacs","_blank","noreferrer=true")},{path:g,label:this.hacs.localize("menu.open_issue"),action:()=>m.open("https://hacs.xyz/docs/issues","_blank","noreferrer=true")},{path:y,label:this.hacs.localize("menu.custom_repositories"),disabled:this.hacs.info.disabled_reason,action:()=>this.dispatchEvent(new CustomEvent("hacs-dialog",{detail:{type:"custom-repositories",repositories:this.hacs.repositories},bubbles:!0,composed:!0}))},{path:w,label:this.hacs.localize("menu.about"),action:()=>I(this,this.hacs)}]}
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
${this.narrow?o`
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
class="header"
|
||||
slot="header"
|
||||
.label=${this.hacs.localize("search.downloaded")}
|
||||
.filter=${this._searchInput||""}
|
||||
@value-changed=${this._inputValueChanged}
|
||||
></search-input>
|
||||
`:o`<div class="search">
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.label=${0===t.length?this.hacs.localize("search.downloaded"):this.hacs.localize("search.downloaded_new")}
|
||||
.filter=${this._searchInput||""}
|
||||
@value-changed=${this._inputValueChanged}
|
||||
></search-input>
|
||||
</div>`}
|
||||
<div class="content ${this.narrow?"narrow-content":""}">
|
||||
${(null===(e=this.filters[this.section])||void 0===e?void 0:e.length)>1?o`<div class="filters">
|
||||
<hacs-filter
|
||||
.hacs=${this.hacs}
|
||||
.filters="${this.filters[this.section]}"
|
||||
></hacs-filter>
|
||||
</div>`:""}
|
||||
${null!=t&&t.length?o`<ha-alert .rtl=${j(this.hass)}>
|
||||
${this.hacs.localize("store.new_repositories_note")}
|
||||
<mwc-button
|
||||
class="max-content"
|
||||
slot="action"
|
||||
.label=${this.hacs.localize("menu.dismiss")}
|
||||
@click=${this._clearAllNewRepositories}
|
||||
>
|
||||
</mwc-button>
|
||||
</ha-alert> `:""}
|
||||
<div class="container ${this.narrow?"narrow":""}">
|
||||
${void 0===this.hacs.repositories?"":0===this.allRepositories.length?this._renderEmpty():0===this.visibleRepositories.length?this._renderNoResultsFound():this._renderRepositories()}
|
||||
</div>
|
||||
</div>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
.label=${this.hacs.localize("store.explore")}
|
||||
.extended=${!this.narrow}
|
||||
@click=${this._addRepository}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${x}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage>`}},{kind:"method",key:"_renderRepositories",value:function(){return this.visibleRepositories.map((e=>o`<hacs-repository-card
|
||||
.hass=${this.hass}
|
||||
.hacs=${this.hacs}
|
||||
.repository=${e}
|
||||
.narrow=${this.narrow}
|
||||
?narrow=${this.narrow}
|
||||
></hacs-repository-card>`))}},{kind:"method",key:"_clearAllNewRepositories",value:async function(){var e;await $(this.hass,{categories:(null===(e=f(this.hacs.language,this.route))||void 0===e?void 0:e.categories)||[]})}},{kind:"method",key:"_renderNoResultsFound",value:function(){return o`<ha-alert
|
||||
.rtl=${j(this.hass)}
|
||||
alert-type="warning"
|
||||
.title="${this.hacs.localize("store.no_repositories")} 😕"
|
||||
>
|
||||
${this.hacs.localize("store.no_repositories_found_desc1",{searchInput:this._searchInput})}
|
||||
<br />
|
||||
${this.hacs.localize("store.no_repositories_found_desc2")}
|
||||
</ha-alert>`}},{kind:"method",key:"_renderEmpty",value:function(){return o`<ha-alert
|
||||
.title="${this.hacs.localize("store.no_repositories")} 😕"
|
||||
.rtl=${j(this.hass)}
|
||||
>
|
||||
${this.hacs.localize("store.no_repositories_desc1")}
|
||||
<br />
|
||||
${this.hacs.localize("store.no_repositories_desc2")}
|
||||
</ha-alert>`}},{kind:"method",key:"_inputValueChanged",value:function(e){this._searchInput=e.detail.value,window.localStorage.setItem("hacs-search",this._searchInput)}},{kind:"method",key:"_addRepository",value:function(){this.dispatchEvent(new CustomEvent("hacs-dialog",{detail:{type:"add-repository",repositories:this.hacs.repositories,section:this.section},bubbles:!0,composed:!0}))}},{kind:"get",static:!0,key:"styles",value:function(){return[_,R,h`
|
||||
.filter {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
.content {
|
||||
height: calc(100vh - 128px);
|
||||
overflow: auto;
|
||||
}
|
||||
.narrow-content {
|
||||
height: calc(100vh - 128px);
|
||||
}
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(480px, 1fr));
|
||||
justify-items: center;
|
||||
grid-gap: 8px 8px;
|
||||
padding: 8px 16px 16px;
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
color: var(--hcv-text-color-on-background);
|
||||
}
|
||||
hacs-repository-card {
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
hacs-repository-card[narrow] {
|
||||
width: 100%;
|
||||
}
|
||||
hacs-repository-card[narrow]:last-of-type {
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
ha-alert {
|
||||
color: var(--hcv-text-color-primary);
|
||||
display: block;
|
||||
margin-top: -4px;
|
||||
}
|
||||
.narrow {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 0px;
|
||||
margin: 0;
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
search-input.header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
position: fixed !important;
|
||||
}
|
||||
.max-content {
|
||||
width: max-content;
|
||||
}
|
||||
`]}}]}}),t);export{E as HacsStorePanel};
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,16 +0,0 @@
|
||||
import{a as e,e as t,i,L as a,N as d,$ as r,r as n,n as o}from"./main-ad130be7.js";import{H as s}from"./c.0a1cf8d0.js";e([o("ha-clickable-list-item")],(function(e,o){class s extends o{constructor(...t){super(...t),e(this)}}return{F:s,d:[{kind:"field",decorators:[t()],key:"href",value:void 0},{kind:"field",decorators:[t({type:Boolean})],key:"disableHref",value:()=>!1},{kind:"field",decorators:[t({type:Boolean,reflect:!0})],key:"openNewTab",value:()=>!1},{kind:"field",decorators:[i("a")],key:"_anchor",value:void 0},{kind:"method",key:"render",value:function(){const e=a(d(s.prototype),"render",this).call(this),t=this.href||"";return r`${this.disableHref?r`<a aria-role="option">${e}</a>`:r`<a
|
||||
aria-role="option"
|
||||
target=${this.openNewTab?"_blank":""}
|
||||
href=${t}
|
||||
>${e}</a
|
||||
>`}`}},{kind:"method",key:"firstUpdated",value:function(){a(d(s.prototype),"firstUpdated",this).call(this),this.addEventListener("keydown",(e=>{"Enter"!==e.key&&" "!==e.key||this._anchor.click()}))}},{kind:"get",static:!0,key:"styles",value:function(){return[a(d(s),"styles",this),n`
|
||||
a {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: var(--mdc-list-side-padding, 20px);
|
||||
padding-right: var(--mdc-list-side-padding, 20px);
|
||||
overflow: hidden;
|
||||
}
|
||||
`]}}]}}),s);
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user