From 1173160cfd7dda3a9d3d7c7641361210b47b75a6 Mon Sep 17 00:00:00 2001 From: John-WE1DER Date: Sat, 17 May 2025 05:50:27 -0500 Subject: [PATCH] Initial commit to new repo --- __init__.py | 81 ++++ __pycache__/__init__.cpython-313.pyc | Bin 0 -> 4165 bytes __pycache__/binary_sensor.cpython-313.pyc | Bin 0 -> 2829 bytes __pycache__/button.cpython-313.pyc | Bin 0 -> 2651 bytes __pycache__/climate.cpython-313.pyc | Bin 0 -> 2733 bytes __pycache__/config_flow.cpython-313.pyc | Bin 0 -> 6754 bytes __pycache__/const.cpython-313.pyc | Bin 0 -> 562 bytes __pycache__/exceptions.cpython-313.pyc | Bin 0 -> 1090 bytes __pycache__/humidifier.cpython-313.pyc | Bin 0 -> 2664 bytes __pycache__/light.cpython-313.pyc | Bin 0 -> 2792 bytes __pycache__/number.cpython-313.pyc | Bin 0 -> 2649 bytes __pycache__/select.cpython-313.pyc | Bin 0 -> 2797 bytes __pycache__/sensor.cpython-313.pyc | Bin 0 -> 4635 bytes __pycache__/switch.cpython-313.pyc | Bin 0 -> 2795 bytes .../update_coordinator.cpython-313.pyc | Bin 0 -> 22697 bytes __pycache__/water_heater.cpython-313.pyc | Bin 0 -> 2782 bytes binary_sensor.py | 47 +++ button.py | 45 +++ climate.py | 47 +++ config_flow.py | 129 ++++++ const.py | 15 + devices/__init__.py | 66 ++++ devices/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 3754 bytes devices/__pycache__/advantium.cpython-313.pyc | Bin 0 -> 3900 bytes devices/__pycache__/base.cpython-313.pyc | Bin 0 -> 8661 bytes devices/__pycache__/biac.cpython-313.pyc | Bin 0 -> 2297 bytes .../__pycache__/coffee_maker.cpython-313.pyc | Bin 0 -> 3654 bytes devices/__pycache__/cooktop.cpython-313.pyc | Bin 0 -> 3694 bytes .../__pycache__/dehumidifier.cpython-313.pyc | Bin 0 -> 2225 bytes .../__pycache__/dishwasher.cpython-313.pyc | Bin 0 -> 3874 bytes devices/__pycache__/dryer.cpython-313.pyc | Bin 0 -> 5248 bytes .../dual_dishwasher.cpython-313.pyc | Bin 0 -> 5006 bytes .../espresso_maker.cpython-313.pyc | Bin 0 -> 1673 bytes devices/__pycache__/fridge.cpython-313.pyc | Bin 0 -> 8938 bytes devices/__pycache__/hood.cpython-313.pyc | Bin 0 -> 2676 bytes devices/__pycache__/microwave.cpython-313.pyc | Bin 0 -> 3017 bytes devices/__pycache__/oim.cpython-313.pyc | Bin 0 -> 2021 bytes devices/__pycache__/oven.cpython-313.pyc | Bin 0 -> 11050 bytes devices/__pycache__/pac.cpython-313.pyc | Bin 0 -> 1934 bytes devices/__pycache__/sac.cpython-313.pyc | Bin 0 -> 2569 bytes devices/__pycache__/ucim.cpython-313.pyc | Bin 0 -> 2368 bytes devices/__pycache__/wac.cpython-313.pyc | Bin 0 -> 2281 bytes devices/__pycache__/washer.cpython-313.pyc | Bin 0 -> 5225 bytes .../__pycache__/washer_dryer.cpython-313.pyc | Bin 0 -> 2183 bytes .../__pycache__/water_filter.cpython-313.pyc | Bin 0 -> 2342 bytes .../__pycache__/water_heater.cpython-313.pyc | Bin 0 -> 3113 bytes .../water_softener.cpython-313.pyc | Bin 0 -> 2432 bytes devices/advantium.py | 45 +++ devices/base.py | 154 ++++++++ devices/biac.py | 35 ++ devices/coffee_maker.py | 66 ++++ devices/cooktop.py | 62 +++ devices/dehumidifier.py | 44 +++ devices/dishwasher.py | 55 +++ devices/dryer.py | 66 ++++ devices/dual_dishwasher.py | 71 ++++ devices/espresso_maker.py | 34 ++ devices/fridge.py | 138 +++++++ devices/hood.py | 52 +++ devices/microwave.py | 56 +++ devices/oim.py | 41 ++ devices/oven.py | 146 +++++++ devices/pac.py | 31 ++ devices/sac.py | 37 ++ devices/ucim.py | 45 +++ devices/wac.py | 33 ++ devices/washer.py | 63 +++ devices/washer_dryer.py | 36 ++ devices/water_filter.py | 38 ++ devices/water_heater.py | 54 +++ devices/water_softener.py | 38 ++ entities/__init__.py | 13 + entities/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 461 bytes entities/ac/__init__.py | 4 + .../ac/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 366 bytes .../fan_mode_options.cpython-313.pyc | Bin 0 -> 3901 bytes .../ge_biac_climate.cpython-313.pyc | Bin 0 -> 3432 bytes .../ge_pac_climate.cpython-313.pyc | Bin 0 -> 5230 bytes .../ge_sac_climate.cpython-313.pyc | Bin 0 -> 5602 bytes .../ge_wac_climate.cpython-313.pyc | Bin 0 -> 3423 bytes entities/ac/fan_mode_options.py | 45 +++ entities/ac/ge_biac_climate.py | 41 ++ entities/ac/ge_pac_climate.py | 71 ++++ entities/ac/ge_sac_climate.py | 75 ++++ entities/ac/ge_wac_climate.py | 41 ++ entities/advantium/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 212 bytes .../__pycache__/const.cpython-313.pyc | Bin 0 -> 477 bytes .../__pycache__/ge_advantium.cpython-313.pyc | Bin 0 -> 15099 bytes entities/advantium/const.py | 5 + entities/advantium/ge_advantium.py | 283 ++++++++++++++ entities/ccm/__init__.py | 5 + .../ccm/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 523 bytes .../ge_ccm_brew_cups.cpython-313.pyc | Bin 0 -> 1715 bytes .../ge_ccm_brew_settings.cpython-313.pyc | Bin 0 -> 1214 bytes .../ge_ccm_brew_strength.cpython-313.pyc | Bin 0 -> 4068 bytes .../ge_ccm_brew_temperature.cpython-313.pyc | Bin 0 -> 2095 bytes .../ge_ccm_cached_value.cpython-313.pyc | Bin 0 -> 1077 bytes ..._not_present_binary_sensor.cpython-313.pyc | Bin 0 -> 926 bytes entities/ccm/ge_ccm_brew_cups.py | 19 + entities/ccm/ge_ccm_brew_settings.py | 13 + entities/ccm/ge_ccm_brew_strength.py | 47 +++ entities/ccm/ge_ccm_brew_temperature.py | 26 ++ entities/ccm/ge_ccm_cached_value.py | 20 + .../ge_ccm_pot_not_present_binary_sensor.py | 8 + entities/common/__init__.py | 17 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 1124 bytes .../bool_converter.cpython-313.pyc | Bin 0 -> 1770 bytes .../__pycache__/ge_climate.cpython-313.pyc | Bin 0 -> 13159 bytes .../__pycache__/ge_entity.cpython-313.pyc | Bin 0 -> 4905 bytes .../ge_erd_binary_sensor.cpython-313.pyc | Bin 0 -> 2811 bytes .../__pycache__/ge_erd_button.cpython-313.pyc | Bin 0 -> 1377 bytes .../__pycache__/ge_erd_entity.cpython-313.pyc | Bin 0 -> 8565 bytes .../__pycache__/ge_erd_light.cpython-313.pyc | Bin 0 -> 4111 bytes .../__pycache__/ge_erd_number.cpython-313.pyc | Bin 0 -> 6502 bytes ...erd_property_binary_sensor.cpython-313.pyc | Bin 0 -> 2809 bytes .../ge_erd_property_sensor.cpython-313.pyc | Bin 0 -> 3131 bytes .../__pycache__/ge_erd_select.cpython-313.pyc | Bin 0 -> 2769 bytes .../__pycache__/ge_erd_sensor.cpython-313.pyc | Bin 0 -> 8262 bytes .../__pycache__/ge_erd_switch.cpython-313.pyc | Bin 0 -> 2927 bytes .../ge_erd_timer_sensor.cpython-313.pyc | Bin 0 -> 1930 bytes .../__pycache__/ge_humidifier.cpython-313.pyc | Bin 0 -> 6014 bytes .../ge_water_heater.cpython-313.pyc | Bin 0 -> 2521 bytes .../options_converter.cpython-313.pyc | Bin 0 -> 1097 bytes entities/common/bool_converter.py | 19 + entities/common/ge_climate.py | 201 ++++++++++ entities/common/ge_entity.py | 85 ++++ entities/common/ge_erd_binary_sensor.py | 38 ++ entities/common/ge_erd_button.py | 17 + entities/common/ge_erd_entity.py | 153 ++++++++ entities/common/ge_erd_light.py | 64 +++ entities/common/ge_erd_number.py | 127 ++++++ .../common/ge_erd_property_binary_sensor.py | 32 ++ entities/common/ge_erd_property_sensor.py | 50 +++ entities/common/ge_erd_select.py | 35 ++ entities/common/ge_erd_sensor.py | 164 ++++++++ entities/common/ge_erd_switch.py | 33 ++ entities/common/ge_erd_timer_sensor.py | 30 ++ entities/common/ge_humidifier.py | 105 +++++ entities/common/ge_water_heater.py | 42 ++ entities/common/options_converter.py | 10 + entities/dehumidifier/__init__.py | 2 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 301 bytes .../__pycache__/const.cpython-313.pyc | Bin 0 -> 263 bytes .../__pycache__/dehumidifier.cpython-313.pyc | Bin 0 -> 4095 bytes .../dehumidifier_fan_options.cpython-313.pyc | Bin 0 -> 2308 bytes ...umidifier_fan_speed_sensor.cpython-313.pyc | Bin 0 -> 1949 bytes entities/dehumidifier/const.py | 3 + entities/dehumidifier/dehumidifier.py | 74 ++++ .../dehumidifier/dehumidifier_fan_options.py | 29 ++ .../dehumidifier_fan_speed_sensor.py | 40 ++ entities/dishwasher/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 256 bytes ...sher_control_locked_switch.cpython-313.pyc | Bin 0 -> 994 bytes .../ge_dishwasher_control_locked_switch.py | 12 + entities/fridge/__init__.py | 5 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 469 bytes .../fridge/__pycache__/const.cpython-313.pyc | Bin 0 -> 765 bytes ...rtable_drawer_mode_options.cpython-313.pyc | Bin 0 -> 3596 bytes .../ge_abstract_fridge.cpython-313.pyc | Bin 0 -> 12180 bytes .../__pycache__/ge_dispenser.cpython-313.pyc | Bin 0 -> 7404 bytes .../__pycache__/ge_freezer.cpython-313.pyc | Bin 0 -> 1703 bytes .../__pycache__/ge_fridge.cpython-313.pyc | Bin 0 -> 3381 bytes ..._fridge_ice_control_switch.cpython-313.pyc | Bin 0 -> 3589 bytes entities/fridge/const.py | 16 + .../fridge/convertable_drawer_mode_options.py | 56 +++ entities/fridge/ge_abstract_fridge.py | 199 ++++++++++ entities/fridge/ge_dispenser.py | 126 ++++++ entities/fridge/ge_freezer.py | 35 ++ entities/fridge/ge_fridge.py | 62 +++ .../fridge/ge_fridge_ice_control_switch.py | 47 +++ entities/hood/__init__.py | 2 + .../hood/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 288 bytes .../ge_hood_fan_speed.cpython-313.pyc | Bin 0 -> 4087 bytes .../ge_hood_light_level.cpython-313.pyc | Bin 0 -> 3769 bytes entities/hood/ge_hood_fan_speed.py | 46 +++ entities/hood/ge_hood_light_level.py | 42 ++ entities/opal_ice_maker/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 246 bytes .../oim_light_level_options.cpython-313.pyc | Bin 0 -> 2077 bytes .../opal_ice_maker/oim_light_level_options.py | 26 ++ entities/oven/__init__.py | 4 + .../oven/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 414 bytes .../oven/__pycache__/const.cpython-313.pyc | Bin 0 -> 2028 bytes .../oven/__pycache__/ge_oven.cpython-313.pyc | Bin 0 -> 12543 bytes ...ge_oven_light_level_select.cpython-313.pyc | Bin 0 -> 5221 bytes ..._oven_warming_state_select.cpython-313.pyc | Bin 0 -> 4400 bytes entities/oven/const.py | 47 +++ entities/oven/ge_oven.py | 229 +++++++++++ entities/oven/ge_oven_light_level_select.py | 66 ++++ entities/oven/ge_oven_warming_state_select.py | 56 +++ entities/water_filter/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 232 bytes .../filter_position.cpython-313.pyc | Bin 0 -> 5076 bytes entities/water_filter/filter_position.py | 63 +++ entities/water_heater/__init__.py | 2 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 279 bytes .../ge_water_heater.cpython-313.pyc | Bin 0 -> 5373 bytes .../__pycache__/heater_modes.cpython-313.pyc | Bin 0 -> 1967 bytes entities/water_heater/ge_water_heater.py | 86 ++++ entities/water_heater/heater_modes.py | 26 ++ entities/water_softener/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 236 bytes .../shutoff_position.cpython-313.pyc | Bin 0 -> 5192 bytes entities/water_softener/shutoff_position.py | 65 ++++ exceptions.py | 10 + humidifier.py | 44 +++ light.py | 48 +++ manifest.json | 11 + number.py | 45 +++ select.py | 48 +++ sensor.py | 88 +++++ services.yaml | 47 +++ strings.json | 21 + switch.py | 44 +++ translations/en.json | 29 ++ update_coordinator.py | 366 ++++++++++++++++++ water_heater.py | 46 +++ 218 files changed, 6126 insertions(+) create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-313.pyc create mode 100644 __pycache__/binary_sensor.cpython-313.pyc create mode 100644 __pycache__/button.cpython-313.pyc create mode 100644 __pycache__/climate.cpython-313.pyc create mode 100644 __pycache__/config_flow.cpython-313.pyc create mode 100644 __pycache__/const.cpython-313.pyc create mode 100644 __pycache__/exceptions.cpython-313.pyc create mode 100644 __pycache__/humidifier.cpython-313.pyc create mode 100644 __pycache__/light.cpython-313.pyc create mode 100644 __pycache__/number.cpython-313.pyc create mode 100644 __pycache__/select.cpython-313.pyc create mode 100644 __pycache__/sensor.cpython-313.pyc create mode 100644 __pycache__/switch.cpython-313.pyc create mode 100644 __pycache__/update_coordinator.cpython-313.pyc create mode 100644 __pycache__/water_heater.cpython-313.pyc create mode 100644 binary_sensor.py create mode 100644 button.py create mode 100644 climate.py create mode 100644 config_flow.py create mode 100644 const.py create mode 100644 devices/__init__.py create mode 100644 devices/__pycache__/__init__.cpython-313.pyc create mode 100644 devices/__pycache__/advantium.cpython-313.pyc create mode 100644 devices/__pycache__/base.cpython-313.pyc create mode 100644 devices/__pycache__/biac.cpython-313.pyc create mode 100644 devices/__pycache__/coffee_maker.cpython-313.pyc create mode 100644 devices/__pycache__/cooktop.cpython-313.pyc create mode 100644 devices/__pycache__/dehumidifier.cpython-313.pyc create mode 100644 devices/__pycache__/dishwasher.cpython-313.pyc create mode 100644 devices/__pycache__/dryer.cpython-313.pyc create mode 100644 devices/__pycache__/dual_dishwasher.cpython-313.pyc create mode 100644 devices/__pycache__/espresso_maker.cpython-313.pyc create mode 100644 devices/__pycache__/fridge.cpython-313.pyc create mode 100644 devices/__pycache__/hood.cpython-313.pyc create mode 100644 devices/__pycache__/microwave.cpython-313.pyc create mode 100644 devices/__pycache__/oim.cpython-313.pyc create mode 100644 devices/__pycache__/oven.cpython-313.pyc create mode 100644 devices/__pycache__/pac.cpython-313.pyc create mode 100644 devices/__pycache__/sac.cpython-313.pyc create mode 100644 devices/__pycache__/ucim.cpython-313.pyc create mode 100644 devices/__pycache__/wac.cpython-313.pyc create mode 100644 devices/__pycache__/washer.cpython-313.pyc create mode 100644 devices/__pycache__/washer_dryer.cpython-313.pyc create mode 100644 devices/__pycache__/water_filter.cpython-313.pyc create mode 100644 devices/__pycache__/water_heater.cpython-313.pyc create mode 100644 devices/__pycache__/water_softener.cpython-313.pyc create mode 100644 devices/advantium.py create mode 100644 devices/base.py create mode 100644 devices/biac.py create mode 100644 devices/coffee_maker.py create mode 100644 devices/cooktop.py create mode 100644 devices/dehumidifier.py create mode 100644 devices/dishwasher.py create mode 100644 devices/dryer.py create mode 100644 devices/dual_dishwasher.py create mode 100644 devices/espresso_maker.py create mode 100644 devices/fridge.py create mode 100644 devices/hood.py create mode 100644 devices/microwave.py create mode 100644 devices/oim.py create mode 100644 devices/oven.py create mode 100644 devices/pac.py create mode 100644 devices/sac.py create mode 100644 devices/ucim.py create mode 100644 devices/wac.py create mode 100644 devices/washer.py create mode 100644 devices/washer_dryer.py create mode 100644 devices/water_filter.py create mode 100644 devices/water_heater.py create mode 100644 devices/water_softener.py create mode 100644 entities/__init__.py create mode 100644 entities/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/ac/__init__.py create mode 100644 entities/ac/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/ac/__pycache__/fan_mode_options.cpython-313.pyc create mode 100644 entities/ac/__pycache__/ge_biac_climate.cpython-313.pyc create mode 100644 entities/ac/__pycache__/ge_pac_climate.cpython-313.pyc create mode 100644 entities/ac/__pycache__/ge_sac_climate.cpython-313.pyc create mode 100644 entities/ac/__pycache__/ge_wac_climate.cpython-313.pyc create mode 100644 entities/ac/fan_mode_options.py create mode 100644 entities/ac/ge_biac_climate.py create mode 100644 entities/ac/ge_pac_climate.py create mode 100644 entities/ac/ge_sac_climate.py create mode 100644 entities/ac/ge_wac_climate.py create mode 100644 entities/advantium/__init__.py create mode 100644 entities/advantium/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/advantium/__pycache__/const.cpython-313.pyc create mode 100644 entities/advantium/__pycache__/ge_advantium.cpython-313.pyc create mode 100644 entities/advantium/const.py create mode 100644 entities/advantium/ge_advantium.py create mode 100644 entities/ccm/__init__.py create mode 100644 entities/ccm/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/ccm/__pycache__/ge_ccm_brew_cups.cpython-313.pyc create mode 100644 entities/ccm/__pycache__/ge_ccm_brew_settings.cpython-313.pyc create mode 100644 entities/ccm/__pycache__/ge_ccm_brew_strength.cpython-313.pyc create mode 100644 entities/ccm/__pycache__/ge_ccm_brew_temperature.cpython-313.pyc create mode 100644 entities/ccm/__pycache__/ge_ccm_cached_value.cpython-313.pyc create mode 100644 entities/ccm/__pycache__/ge_ccm_pot_not_present_binary_sensor.cpython-313.pyc create mode 100644 entities/ccm/ge_ccm_brew_cups.py create mode 100644 entities/ccm/ge_ccm_brew_settings.py create mode 100644 entities/ccm/ge_ccm_brew_strength.py create mode 100644 entities/ccm/ge_ccm_brew_temperature.py create mode 100644 entities/ccm/ge_ccm_cached_value.py create mode 100644 entities/ccm/ge_ccm_pot_not_present_binary_sensor.py create mode 100644 entities/common/__init__.py create mode 100644 entities/common/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/common/__pycache__/bool_converter.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_climate.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_entity.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_erd_binary_sensor.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_erd_button.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_erd_entity.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_erd_light.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_erd_number.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_erd_property_binary_sensor.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_erd_property_sensor.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_erd_select.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_erd_sensor.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_erd_switch.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_erd_timer_sensor.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_humidifier.cpython-313.pyc create mode 100644 entities/common/__pycache__/ge_water_heater.cpython-313.pyc create mode 100644 entities/common/__pycache__/options_converter.cpython-313.pyc create mode 100644 entities/common/bool_converter.py create mode 100644 entities/common/ge_climate.py create mode 100644 entities/common/ge_entity.py create mode 100644 entities/common/ge_erd_binary_sensor.py create mode 100644 entities/common/ge_erd_button.py create mode 100644 entities/common/ge_erd_entity.py create mode 100644 entities/common/ge_erd_light.py create mode 100644 entities/common/ge_erd_number.py create mode 100644 entities/common/ge_erd_property_binary_sensor.py create mode 100644 entities/common/ge_erd_property_sensor.py create mode 100644 entities/common/ge_erd_select.py create mode 100644 entities/common/ge_erd_sensor.py create mode 100644 entities/common/ge_erd_switch.py create mode 100644 entities/common/ge_erd_timer_sensor.py create mode 100644 entities/common/ge_humidifier.py create mode 100644 entities/common/ge_water_heater.py create mode 100644 entities/common/options_converter.py create mode 100644 entities/dehumidifier/__init__.py create mode 100644 entities/dehumidifier/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/dehumidifier/__pycache__/const.cpython-313.pyc create mode 100644 entities/dehumidifier/__pycache__/dehumidifier.cpython-313.pyc create mode 100644 entities/dehumidifier/__pycache__/dehumidifier_fan_options.cpython-313.pyc create mode 100644 entities/dehumidifier/__pycache__/dehumidifier_fan_speed_sensor.cpython-313.pyc create mode 100644 entities/dehumidifier/const.py create mode 100644 entities/dehumidifier/dehumidifier.py create mode 100644 entities/dehumidifier/dehumidifier_fan_options.py create mode 100644 entities/dehumidifier/dehumidifier_fan_speed_sensor.py create mode 100644 entities/dishwasher/__init__.py create mode 100644 entities/dishwasher/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/dishwasher/__pycache__/ge_dishwasher_control_locked_switch.cpython-313.pyc create mode 100644 entities/dishwasher/ge_dishwasher_control_locked_switch.py create mode 100644 entities/fridge/__init__.py create mode 100644 entities/fridge/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/fridge/__pycache__/const.cpython-313.pyc create mode 100644 entities/fridge/__pycache__/convertable_drawer_mode_options.cpython-313.pyc create mode 100644 entities/fridge/__pycache__/ge_abstract_fridge.cpython-313.pyc create mode 100644 entities/fridge/__pycache__/ge_dispenser.cpython-313.pyc create mode 100644 entities/fridge/__pycache__/ge_freezer.cpython-313.pyc create mode 100644 entities/fridge/__pycache__/ge_fridge.cpython-313.pyc create mode 100644 entities/fridge/__pycache__/ge_fridge_ice_control_switch.cpython-313.pyc create mode 100644 entities/fridge/const.py create mode 100644 entities/fridge/convertable_drawer_mode_options.py create mode 100644 entities/fridge/ge_abstract_fridge.py create mode 100644 entities/fridge/ge_dispenser.py create mode 100644 entities/fridge/ge_freezer.py create mode 100644 entities/fridge/ge_fridge.py create mode 100644 entities/fridge/ge_fridge_ice_control_switch.py create mode 100644 entities/hood/__init__.py create mode 100644 entities/hood/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/hood/__pycache__/ge_hood_fan_speed.cpython-313.pyc create mode 100644 entities/hood/__pycache__/ge_hood_light_level.cpython-313.pyc create mode 100644 entities/hood/ge_hood_fan_speed.py create mode 100644 entities/hood/ge_hood_light_level.py create mode 100644 entities/opal_ice_maker/__init__.py create mode 100644 entities/opal_ice_maker/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/opal_ice_maker/__pycache__/oim_light_level_options.cpython-313.pyc create mode 100644 entities/opal_ice_maker/oim_light_level_options.py create mode 100644 entities/oven/__init__.py create mode 100644 entities/oven/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/oven/__pycache__/const.cpython-313.pyc create mode 100644 entities/oven/__pycache__/ge_oven.cpython-313.pyc create mode 100644 entities/oven/__pycache__/ge_oven_light_level_select.cpython-313.pyc create mode 100644 entities/oven/__pycache__/ge_oven_warming_state_select.cpython-313.pyc create mode 100644 entities/oven/const.py create mode 100644 entities/oven/ge_oven.py create mode 100644 entities/oven/ge_oven_light_level_select.py create mode 100644 entities/oven/ge_oven_warming_state_select.py create mode 100644 entities/water_filter/__init__.py create mode 100644 entities/water_filter/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/water_filter/__pycache__/filter_position.cpython-313.pyc create mode 100644 entities/water_filter/filter_position.py create mode 100644 entities/water_heater/__init__.py create mode 100644 entities/water_heater/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/water_heater/__pycache__/ge_water_heater.cpython-313.pyc create mode 100644 entities/water_heater/__pycache__/heater_modes.cpython-313.pyc create mode 100644 entities/water_heater/ge_water_heater.py create mode 100644 entities/water_heater/heater_modes.py create mode 100644 entities/water_softener/__init__.py create mode 100644 entities/water_softener/__pycache__/__init__.cpython-313.pyc create mode 100644 entities/water_softener/__pycache__/shutoff_position.cpython-313.pyc create mode 100644 entities/water_softener/shutoff_position.py create mode 100644 exceptions.py create mode 100644 humidifier.py create mode 100644 light.py create mode 100644 manifest.json create mode 100644 number.py create mode 100644 select.py create mode 100644 sensor.py create mode 100644 services.yaml create mode 100644 strings.json create mode 100644 switch.py create mode 100644 translations/en.json create mode 100644 update_coordinator.py create mode 100644 water_heater.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..f5337e8 --- /dev/null +++ b/__init__.py @@ -0,0 +1,81 @@ +"""The ge_home integration.""" + +import logging +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_REGION +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from .const import DOMAIN +from .exceptions import HaAuthError, HaCannotConnect +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass: HomeAssistant, config: dict): + return True + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version == 1: + + new = {**config_entry.data} + new[CONF_REGION] = "US" + + config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry, data=new) + + _LOGGER.info("Migration to version %s successful", config_entry.version) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up ge_home from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + #try to get existing coordinator + existing: GeHomeUpdateCoordinator = dict.get(hass.data[DOMAIN],entry.entry_id) + + coordinator = GeHomeUpdateCoordinator(hass, entry) + hass.data[DOMAIN][entry.entry_id] = coordinator + + # try to unload the existing coordinator + try: + if existing: + await coordinator.async_reset() + except: + _LOGGER.warning("Could not reset existing coordinator.") + + try: + if not await coordinator.async_setup(): + return False + except HaCannotConnect: + raise ConfigEntryNotReady("Could not connect to SmartHQ") + except HaAuthError: + raise ConfigEntryAuthFailed("Could not authenticate to SmartHQ") + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + ok = await coordinator.async_reset() + if ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return ok + + +async def async_update_options(hass, config_entry): + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5974ed545c515092c9f4a978abd84ae8725e816d GIT binary patch literal 4165 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 literal 0 HcmV?d00001 diff --git a/__pycache__/button.cpython-313.pyc b/__pycache__/button.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b8dc19428eaa64f8f5f3e49e8cd2c17920c1f09 GIT binary patch literal 2651 zcmai0-EY%Y6u-7($N6fSrlp^B4gR1*&{P1+vzfcCb3U}{!Ss7JLgc;KxlAjaFywd14%q+KQVoO{mo zJ;&$ccU|>(oCwC{p?!OC~dxXclvF4(Vh!Z;v-X3*D+}Hy=A9qBXB3|q@cxSXZ;=?|JcSTzwe(X1R zceFLqhT8-*N5u}*yxBqapt<$|$`s&^xM$8DZ<=G{-Z`e9i#K zI=3VX*F;V|uVi2kE(K}bLmnK{G({_j zYC-4Iuya97zY9A%M6INzQ<9?P#X@>f#;J5pRpoR6bY7X*D5bERfhTe40r3;$+_BR$ zV<(7B_gFrkRYWx{kL49uyQbtxESVm{vVBS>mJ@kNEXWhN9F`PSEab3mTNE`-cY`VL zG6iOENpCSd5haNTjCV^jW?+oj4)X`_uYLw@oTJaNFCzg`nyhb$?c80H#wjbf0!pBm zt-iU=B&`xbEXWd6jIXQe4AH1e(kL?`jJfNoIa$-N7 zpc8CA9cLiT`~}-UO{v-vOsv(~*`Tl1STO*8+Q1$Y+ttuoT&&aL8q_-3@=rh+VxzmU z!{U;=E$(e5)@$(%ny?MjqP+90Bg}a!$SyZqCAwggs1{o89FrjB837c}l&x+P^NN-x z6=e+vFn;(~^B^6e|39@!5=CP)LLaBSD9#*a8tx_F(6iK8T4TaUqy1<)2Fau#(e=bz zrx5d?9|1j`B;FV!K{paVK$|iv6%wpxx)7RWW1iaf9V9`=>SfRnTb@~Gh~$i6NGW}W znHfdM1!X-mYf_a_De$HK@QX`)M;z+!ZN$O0GY+#x$ynB*1Rxp$(ioLisq-kvo?7OP z=ZdN%=#;eV6-2AEg|RaywB>f8sG2w+V_6bv85V>fuiH~6Pftxv#&lMa7m693$;ztE zTRp5h6-`kI@T6r-`bPJgjjm`lU?B(UjvCe|Qjo)>O>_ndMCVK~?f?lUeWddi8fhed z0*sXvy)SG+NjP293b`e~u%&!Xg=e*JMoujP28B(JL-|r2^5lz3TGj~krE?bzCk~sQ zX>y^MH&H1xoXr7RY9pZGl8^RZ`@`$AjmzgoKPTV1;T=K8Lo zTm0}t4t4ZAV!2l5U+vwWGIzRme|_M~1D|qty`8K3uk}^Dp{gBu{M$Tg?Wy>~Wqu;_1hS$T#*LO|b z;-@$HuGOQr`Mpo8KG>}Jz|C)i;?BAe=klOi3ia~H*IQP%V>0_>WSQN=9e zeF-9-vaTUapj~&@m7-LFA|S1Uy<{@-?n*cA4=fNdfu~v#%nQOOehJpP@3|D>gCHkw zu6+g5(+6k;J>n4S{%Gd%%zDegTei@hzVP>hHwJ$^TkadaIahfjR)$|+d?k9<>$`I4 zlS`||HoSqA>3ghan|8AMt`)vJS$<{k2?D;#Ag;-fZucVIU$OVXZ<80+x+{U9a$u+u z7%vCLHv-3Q^OM^gaW-`Q}Vd(2RddFrY0Iv-9j6Lu=t z1&x)Ga_Lk`w`X&i4B)8F6-s%am|(EpL+TptQS(+0?YK)c6)K6Vvv>ZJ6AXcrUTSf@!D)qiF1s*&#+{m~Cq`eM`~!2!PJ4e+6Iy znYsfi>Lk1#lo+2%sp68HN}2UY@^%ANVMbQ!O>Uth_fh8-3K+j5e>i;=XZNqp?$zlH=e`yDwylSnr`8T`BSLS! QXVUfg#m9yK55dX*0zTGj)&Kwi literal 0 HcmV?d00001 diff --git a/__pycache__/climate.cpython-313.pyc b/__pycache__/climate.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60b7056717b565ec37abd1b69184a2255950a4ef GIT binary patch literal 2733 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 literal 0 HcmV?d00001 diff --git a/__pycache__/const.cpython-313.pyc b/__pycache__/const.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c86e595e9737465a390ccb2cfec72245a1879bd GIT binary patch literal 562 zcmY*W&5GMF5SBNaI6s7RlWe*yv<;M{Yzv!0k1Z|2(SpITgXJvENf<}#7;G!ZPGNgW zAEFP@w@GiQPJy2GR>%u_K16jE!D(yY8%WmN5q+Usx>lgNcG$)8xjjc!3cl;kLdh6b#8Ah(!Qrx<;LC_r)5WZF(;u-dw!;iOyvYH2wm^7pi;! literal 0 HcmV?d00001 diff --git a/__pycache__/exceptions.cpython-313.pyc b/__pycache__/exceptions.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d68ab456b747e16a553d96f8ea2f103e418e2601 GIT binary patch literal 1090 zcmbVL&x;c=6izz*wbfFo?6UL}MHkCL=g<`dMOcea6nb#33n6CGcA%ZqOh(<_ym(%O zg8#z)IXx8zZ{ndhq3hlEX8Pl>;KJtc^78T}`QGilgkUS)w(;!~{%z0Pd9NNuKoy<3d6j=ad@djiuMV0_rz5!Vp z>@_RX2VFf@>@@p2~bIi@hkHw#IvD>@)A#Kw^db6W4|3`oX}Jab)$FOFf=wxWz{9K=C2Sg3JwKK>e%tyhEHi`rA#GC-$0Fa+9l-| zS+~734cNebk2+xfIF!;KLBCx>7$;Hv4emt)qW8?%Q9s2!f P(P+;b?Y|J}(Dadi9C`gv literal 0 HcmV?d00001 diff --git a/__pycache__/humidifier.cpython-313.pyc b/__pycache__/humidifier.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79eca0fd3dba552cafc6e15f0792c41155ca5a81 GIT binary patch literal 2664 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 literal 0 HcmV?d00001 diff --git a/__pycache__/number.cpython-313.pyc b/__pycache__/number.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77fe16ef1e16fa2048a3a4b216b8066623868d42 GIT binary patch literal 2649 zcmai0U2M}<6u!1&$N6iTrlr4hE(5lxkf6~Hs&*ZvEiDNxENRfBbfj)X?tOsG^rB%sU{v^nzTLa0qt$?duUcrs7JLwc;GFlAjaFywd14%q+KQVoO{mo zJ;&$gyWaMAoCwC{;eAWLa0vZP7S@2ZF}8mK@;QnjjAB%bj!>8a%0%cegBgLyY!O?S z#jL@z5iV@U_ArlmW6ebzVJCJPyglLyyRip&KIVutg}vBo@Xknc*oS=v?~1g9{n&5t z?nrC64Yvtso{Ao*d9#D;LG$f{lqtX+G0(g`)-=z?yz|Te7i;c97i_pQO$U6-?NgJ& zbS^6kv&HP9jD<~VnznuHJ($?D{0t+O95K|7L6@50UwQ7fsbq@-whv5;DlaWa)tRXJ4vomVC{N=YoI;YnP2K>P$bH*tDq z>?E=29?R!5im0aKvAhCn*OWYoCDTJ#wol2#ay&1I1$jJ|!;+$kg&fvxOQNRfZZHL2 zCcz9Y=`E%wq9hT4@op)`42&_`Vg3OA+n<3O=je0n%SeEfChA+FJ9pQlG0F zt8cC|39Cd93$i#Bnau*=O>iK<44YQg2sF$q$h7C`Y#+3GeiuV^V! zQPyw(#1JA(<2;x}JFJ z6k;CqqoAh~#2aJ8=|77eaGv)KlBOgT(1*y$l*+%QNc?k(@CCDW%Ua zGsh5eL0QktnN(#|3Vf+Q{NfVd5r_JF8*#AhjKiE!GM05H0f>fxG)ARW>O2atv&-Cx zTv3$-osyQlf@pQNFm~pow%jHZRTBqfEK5Qy!vS8mCr_Q8nwpI2tRycM(>jxpRh_pw zSa&L#q7v9i$(VGF?l;?9(Q2?lj?*1Aq*0^*he?y@43vk?nPS`l5=^>C=PfMKNcIF6 zD=T_`$OMv5s;CumSwOICKBvO7S|}|imjHo6roX{_sSbGZMI|L`1o%?9i-r@2P0uvB zP|TZ{6dcLq04uf8U=1aodx&_ZeFM`!E9f5Yz2f=Uv&wy;tqoi)UH7eKufMszYxovF z@{mIvJ&#zf)%jO@_ovLAuH9cB`0~J~++Ai%o}6>o6cjy(Pw{!rN;`hlw) zI#xb(tl}Sg;sDj3UHc!hKsVdFt1NQ(9wIpGQ4?}B*KHoX`u_E|)_X(ip^5cflehR2 zn|#;m(cApqr`tZ*togvrZv)3)<9=@MCG(II=Gj0qZUY4-P@>xas6|Y0&PX9HB^61h zWvn~Q0HoytCV|&&8FC}I2SdE8{jGBs#Y|Dw^ezJHnvzz<4CFlv z5l>o|5GKH`yX#6(DnSvD#=%}P8F_c58n*`)h?sy=tq2wb;TV1i*1GSx6yk#*CvUEO z1=G_9Xazmu5bOSE=JL#X%fVZ=;GO=^_d_>^emq<5AGtYSc_UheUw>>Ra@Xs-a_N&x zs}mdEzLn{Ftfxvl*?rdv-<>MIGV}xizs(@7$&gli5$~_qd*Qdq3v1n#zTtA;aHa2f zx$pQ!-^6WxvdSSxYlRP%`QTmm-nF?(|9H87e0}nr4fnan4CPp$o*J+7;W#sHrvhEj zSV<|DN+xxCCYMeFit1dUln06l0_#1buHhCn@ATk~s}w#8FuCaTmS?wM8577fQz-mb zyCi4wGS-5PF99<#Z7+=*;>z049K` zJD{RY!RtYZ@yVnrX60nktVfc!8)zG5WagG1$>yYDMjpigmWW&&S{$Y-Ls8Tw+Wi}9 z+Cts;(ZKKM^({2Hg^t`uom;5S_#OGf>8m)qe|2`RPH#B(t=Ox!9%_MFJ6J`8-h9ub O>kCVd4FMj8lm7)PsB0Pk literal 0 HcmV?d00001 diff --git a/__pycache__/select.cpython-313.pyc b/__pycache__/select.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a29bedf48c1cd1297273b02b2f52e694ca11938d GIT binary patch literal 2797 zcmai0>u=Lm6u-7($9XkL(~>@*bIWU4QG!N0K;_X=N=rh4C4@~%M=-HT%~B`2PSH{^ zsc4g??F-waX_eT6Nl1ff()M8=rfJf&{RPbm3iYV=f)9Kv3W)J-=i0H;0n)7G-rqgv z`kr&|`Q39|_xaoio{K}9rvpxe{w4$K;ToX+1AvcE3SpF@QgnjC6!c7jjx(4Mh|H02 z#97Q5m`!kTCw9hp%o}4a;flMl+rZ9*C+@{Qzr=-@xufARfd)1A7v!@emFf z*qdmJx8rsJO;O2hmRrrf4^4#!DRYcFQobo?s%45z`KOoxE*0oP=Nz~*Pe+1t;qftH zqBtW9DY+o$%EFjhR?3R3-6QWFjZiv2Di#XjnSu=1Gg?&7DtVZLD-l}vkrziaP0`At zTGsg-%seCJUWb_-qE=CJSxM1KVmUW0<7}>|s$c+gewo;)WU-uwH*w`2@e}0S!DEvn zhlx$^NU2m%L^UUmloS|y#^o_AnI6J0JT4Q<>5?Rt<dn)(Q<@N*5d*Y zf`9!-knW@ZKdu>agvQf6_Z00%DP{-L^eqjGo}^CF8WTer9YPatKqM)MvK@766=ELr z66om+@y2-4bTjIMv?;UVAi;RD3t~Y(iw|JK=0IaH=#n-UnzA8)##+M&onVvX6rOm% z!ZY|_?b7l`B>tQs{v2l}&m!c3`!P9Z_9~-NurGVvXWx}IXKJ6Jai)&7XL`cmo@E`l z1ScyXjZtZpI*TIg(K+r=aaNTCos#DKf@q}(VdVH>ZLVFIRn3GVV_6a!REcw&Pwu`*}TpaWL4*_RG_;RO;Jhy%E_2qKTu%_l9eRuF586e&z5kpBxA^0IT6yC z9x}6vqBXJ@F{-;7iA|9r942>CXMm78XNqwLNH7VM&ReNZBYY9ynV;ADV`kop@+02n7ONHsbc3>`Mi>oHIip@#q)*}J50|sxjb7kQ($yYp$Ms2 z+ZSyl<)>Z(wju2&X#SZ;cXE* z{wv)5K69&U!{^&R-S$3r+uymc|VEAMI2mUKNzTEr8-rC0DD*OV& zbr(^(QQP`jC{_){zU6A$hpXF%YoU=x9#H++wdFnw{c^Z_l|`=LeFT;s1duCWJGATa zo7Y}m?1?SL4lb@6yTKn?=DQYl-Q+htu7_Z<7KS~16&dZ|eh&AMc5e@~zdYxL_Gl!4 z+d+*49Iz&t|ufYXe$tsdg%NT-g6t28n#w0enqd;nuhd_^_LirF2${fVH#U@9V zBpsbQFBWEHP46OwMpN>tSO99wKu~}&W?96f)aYJYDM}UK3AxSKPa1<7o?Nq@;f+KL zX&E(ma#|3EaUYEJ;8Rh?+d)o9r+o&^<9lcxJ>U`ReP{CG*8Z)NQ<#SYg35aKRvr zQ!+1+!3^_e8`-0E*7zua%WHp?U>|we1&lfZ+XIU6*{mwg$l0vPM-sQ|s17YMW6STE zDN3^ic^}>nGE&X7SD;yCD2iG}8-7DAE2#S}8u%T(w1UP~(9XN4a|QJpzny=$gEe>e mukP-Ji6!^udFLwEL!G8>xx=fB0k*!_029X_8WOw@*8Uet4~PE% literal 0 HcmV?d00001 diff --git a/__pycache__/sensor.cpython-313.pyc b/__pycache__/sensor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7504dfd65928f16c75b4f20ca1fa3293236076ec GIT binary patch literal 4635 zcmcf^TWlN0aqr3FOO!;tELjpI$98EYk`hZv6v?t>nUO_Va>^6!rnG%NlV^%4d1UuU zBT`d9X$llgKb(&Q5SkP*QUh^{K3t$^@)4j;fBGwi4rpI&G;Pxl|H+kt)cxx09rviZ zD$@RRLGI4Y%+Bu2&d$tk`u!dR?d9>kOF#7@^cV6$n^*%j{{g@oNJbdRjLgn4n1PX- zW1}4ALN<=a0^s;LG3vrD3n$D;Q8#v5usG+5da*a^!#?ZVHRq2u;06np<^s`1+-SjW zfSYhLz#iE<7mT*xmS`((jke*oXghAV@V>c@=q|j=g8g%y(JtI&!3}e}qusb0-~jHC z8*%R$CU%6XQXl)#nZ8lR{>J@s(;2Vae8w#Y&u}B6+|r3&;PD>0wG-h-O?$3c0Qv;aLQH~K7CV#LN=r7No}f-f$#Pijp!0Dq|~A|ozLS` zMpuh@Y)YvTR*RXuZVH#wY)LcuCDkxYA7}%HP(TyBY&P4^`I zEyTH42j#4fU#o#?A7~`N&bU}-ol2otH-+jWl}t4Wp_6-Ic5j`7LJ5#GkNqg|DE*=`6ZIK`lvW>MMx8!f$5ROCPf&OV zi6fL&AN!Ha%YrP9v7@Yw9cB_wQaaTbd~Ihfr&xml3n~p_%nos59LSceBM2=DF=B;> z8m!>NkUEKKN%y0Y{ahgXd5)XEh>#bB(EN%$>YUDiUe4s5qv|%BIySUy%5S&nB2Quu zT^-IJEE_@wr?Wb95ru_?74g}8Nl%4LCbbd>sWhF0rk5IGtAxF# zOF40JW(KmCkkT%c(k7SHbW@^9!}Mf~j82kiQp3cEK*A}=oGHz8J4=`WrGWDV4MPe| zs*qdFpq+m*hEiYDbl0+MCKVR(E+UKrhcrbSjY(X@?baGJ=^DgeLeN%M&4D31!wn@% zMlqj*J>&{`9p)NCX-!#zTsdTGHCR}7lD&2*lhh27^pp8ZM04|?y)#2AmI`*V9GuAJ zp-wO+2Wu6=zfB@>*7zNCe_utPOM%P&ANtqCw~Q+zZ!TYJTFYHKyxul`Q<}IhqK@5L zg4p8ub9>io+^x>;w;%n4dM;MA9HnD?j7efI?zZ?|`C2*}-ZAHhmn4anW- zZ0PAXzkBWL>pes3L&w(JA~&UHZ%dtPPk$mk@?f(GmKJL&L;Tu%FD} zF6FbP6w|(4$`FU~Pc1ISlwO-av3Nxg_x?R;W~gz)$Bwf z5DpMo9&55y@|k|Z0235z>tPpeC2XWNvAy1#++O9;QWTRUg-Nh6h=D?(^Kc!G;4r{u zlda7*U&b?lC+A|EhLe174}rt--XF}rJip$2;3hwCYhdVCWABaq=2UrL;)65QL$Naa z2ISSbJAtOlFTMKG+ObNYZ}s?=h#GsVfw7MRW0k=8>haG7{|4(3_FgUi{6u;G*p~?4 zO%90-7IvctNx`bC2Y$Du(3P%g-*~xiyxMoP+;_CnckB}>0xs`vsY-)oY4DEkkt>VU zf$8$V^m^oM#dmItW8CMN4G9VUDu1BNAGpJ}elB@0`(E|Eezqd*xhlVV`km8ve1Xf+ zSEH{RKRWrA{Py!de*W6y*Y}jWpWNiweVz@D7sM?O5*oGwE+Med;>fx=FnNQ$F@KYf z!4~QZ?4k>Nvm%AA?s<3rJNtK7pxpf!h~HBe-~0uL4?eJ>()VXNW_B}4awTXZ!Ri(G zZ>~aDE2!ZV?3Sn{Zb%f&PNlQVkjzTMyUdP3;(#KXocTj7&Qh*fcrz}6+)KRg1-H3W)Qm8XNzIOrkZy={N1+RIcs~qqQS3`#-5--J8V1=h zIGg2b7OU0CfHe+6N1W@9yZNnyYcExMhs(Xg*E`F-6P2z*W%uOj(OaIzs;9H;>0EoF z;_0dKJr%y^fk8sV_eLg}-!c>Y`{Lw(u%8ZGw*9~Yjs*c(YyX=q|0`>;3z+2kFlXOm z>x$M56=p3P;V7^e+R-=kuhbX?q!(G^r9ya0{lNooMFlaQwsY;+=>Tb0$-T$t zT;FrUI~SNwi2f_AQd^YlL)LVuA5yTP;BR=$Jb6O=?4C8;DGr!WO06Q^SgW(1;h z#a%HLv!=|(xtJTfV?5@~XD;rEd9l}&-Em*60sBGblb(2EEPw;1?2R|Yf;ec(zIbyi zghQs>5O0aC!D|FGP9?V0qFFU;K;vzFl%>G!N&mPz**MN71LI6Dmu%`lr(Jk$mJSDJ z+eSx(LxsE|B;Qm^(zGz5mDG}|==aIHN5hoC4~w~+IGIyG_6--bDK!goa5+pHe)8gw zuB&=U)Jg^~!OTffdIM&*i+Wj;GP0@{MOaqB8L6OYic|tyKp{TL8LVXCOGG+hjvjS2VqV^~h}7kPIo$3XZ|VuA&<* zv8d`2!IRxQAcWw*@;zwx(f^;=G&w`F)ek;N2T+pP!PJ9G!J;RrleEr6kxqxup|>EF z6r|ZnyS)l=4|W;sbecqCwiI1YdmnA->_o`$Jk|lppq~c_@Zo@CP_TUnXxH^d~iX=X01_UGz#|Rto!`Svg%BoGc?cC zwfam?nBcR$1DN1s1*9_?tx;1b%pRTP4i;uKSuiMhHXw*LDF{Qy59_n7!i;7yih>nc z&})zg`wVyH$g$DUk%Ym@%H&MeU~-CP@HQD3UR75$!eEku$yEdsmVvHh#qc;jj6kM{ z3q=J3Z%HB$nh~;iMb&FOM!XuH8nvl%n8V~&8Vn%P;4C$62Ms2fGI*Q(bOIOw+T5Jc z6SbHtD$VGnLLOqs7YiD^t4Fg+W*R6jYHcf0EISOVoKYo3Crm39&X_^$up-ly(oE4J z!N{Im0mxb37pc+mGcUD46#5U){XK{7@_~>1ANc3Fi~6PB3*{@p`TUiEh1UL?{GJCK zYG3z|X&U1GHYv;FI?x_YMD{kZuEpw>%m2U^W9{5rEIe$H0 zJt$S+w?ndwJZf224Mi)V=vQ2I`(S1JU^O)K*aPqV(Xr(L3*%y2=Q4{t!3PLDJ#0jt zCMTd>7v8z@>Oxm^Av&_qdhjMcy2y9T@4Cfrda@FP$vWA{=i%XY?t0q>((mnp{-+nb z&>syq;Wc2!gx(An6b}*mh)A%2%NbQRXaz&k&C-=sN|@xvaOFsqBGhd7R1FGkqRHV)3E8%iE7Vq zrDu3yl zb=~^Ph^#8^@R@>1={1_4o`MxjXxvJm@L%h+l7oA!N9x}uR$A6PcN!*j8*e3@d3#NU zU?B*=gb5zEY+WV`AGaG8P(1)~=!z=#iCK zeoww2&*YSScmQ;ynCU6#mKlno7SZ}&QR5QoyoY*!LoY9(ktMYA9_n5~YnMpB^LKBs p>h1i++c|&ehIjLvd)d`VO;DF!T1I5N_O>-HOiVvA6?h-4{4ZPBjEevO literal 0 HcmV?d00001 diff --git a/__pycache__/update_coordinator.cpython-313.pyc b/__pycache__/update_coordinator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a098b7fd333ff6396fa106dee44ed3abfefd120d GIT binary patch literal 22697 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 e!MnmonUl<=tt*JiUp}zq(#g38mI1?kg8lWR literal 0 HcmV?d00001 diff --git a/binary_sensor.py b/binary_sensor.py new file mode 100644 index 0000000..0a35ef5 --- /dev/null +++ b/binary_sensor.py @@ -0,0 +1,47 @@ +"""GE Home Sensor Entities""" +import logging +from typing import Callable + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er + +from .const import DOMAIN +from .devices import ApplianceApi +from .entities import GeErdBinarySensor +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): + """GE Home binary sensors.""" + + _LOGGER.debug('Adding GE Binary Sensor Entities') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdBinarySensor) and not isinstance(entity, SwitchEntity) + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered binary sensors') + async_add_entities(entities) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/button.py b/button.py new file mode 100644 index 0000000..748ee6b --- /dev/null +++ b/button.py @@ -0,0 +1,45 @@ +"""GE Home Button Entities""" +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er + +from .const import DOMAIN +from .devices import ApplianceApi +from .entities import GeErdButton +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): + """GE Home buttons.""" + + _LOGGER.debug('Adding GE Button Entities') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdButton) + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered buttons ') + async_add_entities(entities) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/climate.py b/climate.py new file mode 100644 index 0000000..4512b61 --- /dev/null +++ b/climate.py @@ -0,0 +1,47 @@ +"""GE Home Climate Entities""" +import logging +from typing import Callable + +from homeassistant.components.climate import ClimateEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er + +from .entities import GeClimate +from .const import DOMAIN +from .devices import ApplianceApi +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): + """GE Climate Devices.""" + + _LOGGER.debug('Adding GE Climate Entities') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeClimate) + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered climate entities') + async_add_entities(entities) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/config_flow.py b/config_flow.py new file mode 100644 index 0000000..a8070a1 --- /dev/null +++ b/config_flow.py @@ -0,0 +1,129 @@ +"""Config flow for GE Home integration.""" + +import logging +from typing import Dict, Optional + +import aiohttp +import asyncio +import async_timeout + +from gehomesdk import ( + GeAuthFailedError, + GeNotAuthenticatedError, + GeGeneralServerError, + async_get_oauth2_token, + LOGIN_REGIONS +) +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN # pylint:disable=unused-import +from .exceptions import HaAuthError, HaCannotConnect, HaAlreadyConfigured + +_LOGGER = logging.getLogger(__name__) + +GEHOME_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_REGION): vol.In(LOGIN_REGIONS.keys()) + } +) + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + + session = async_get_clientsession(hass) + + # noinspection PyBroadException + try: + async with async_timeout.timeout(10): + _ = await async_get_oauth2_token(session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_REGION]) + except (asyncio.TimeoutError, aiohttp.ClientError): + raise HaCannotConnect('Connection failure') + except (GeAuthFailedError, GeNotAuthenticatedError): + raise HaAuthError('Authentication failure') + except GeGeneralServerError: + raise HaCannotConnect('Cannot connect (server error)') + except Exception as exc: + _LOGGER.exception("Unknown connection failure", exc_info=exc) + raise HaCannotConnect('Unknown connection failure') + + # Return info that you want to store in the config entry. + return {"title": f"{data[CONF_USERNAME]:s}"} + +class GeHomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for GE Home.""" + + VERSION = 2 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + async def _async_validate_input(self, user_input): + """Validate form input.""" + errors = {} + info = None + + if user_input is not None: + # noinspection PyBroadException + try: + info = await validate_input(self.hass, user_input) + except HaCannotConnect: + errors["base"] = "cannot_connect" + except HaAuthError: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + return info, errors + + def _ensure_not_configured(self, username: str): + """Ensure that we haven't configured this account""" + existing_accounts = { + entry.data[CONF_USERNAME] for entry in self._async_current_entries() + } + _LOGGER.debug(f"Existing accounts: {existing_accounts}") + if username in existing_accounts: + raise HaAlreadyConfigured + + async def async_step_user(self, user_input: Optional[Dict] = None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + self._ensure_not_configured(user_input[CONF_USERNAME]) + info, errors = await self._async_validate_input(user_input) + if info: + return self.async_create_entry(title=info["title"], data=user_input) + except HaAlreadyConfigured: + return self.async_abort(reason="already_configured_account") + + + return self.async_show_form( + step_id="user", data_schema=GEHOME_SCHEMA, errors=errors + ) + + async def async_step_reauth(self, user_input: Optional[dict] = None): + """Handle re-auth if login is invalid.""" + errors = {} + + if user_input is not None: + _, errors = await self._async_validate_input(user_input) + + if not errors: + for entry in self._async_current_entries(): + if entry.unique_id == self.unique_id: + self.hass.config_entries.async_update_entry( + entry, data=user_input + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + + if errors["base"] != "invalid_auth": + return self.async_abort(reason=errors["base"]) + + return self.async_show_form( + step_id="reauth", data_schema=GEHOME_SCHEMA, errors=errors, + ) diff --git a/const.py b/const.py new file mode 100644 index 0000000..76cef76 --- /dev/null +++ b/const.py @@ -0,0 +1,15 @@ +"""Constants for the gehome integration.""" + +DOMAIN = "ge_home" + +EVENT_ALL_APPLIANCES_READY = 'all_appliances_ready' + +UPDATE_INTERVAL = 30 +ASYNC_TIMEOUT = 30 +MIN_RETRY_DELAY = 15 +MAX_RETRY_DELAY = 1800 +RETRY_OFFLINE_COUNT = 5 + +SERVICE_SET_TIMER = "set_timer" +SERVICE_CLEAR_TIMER = "clear_timer" +SERVICE_SET_INT_VALUE = "set_int_value" diff --git a/devices/__init__.py b/devices/__init__.py new file mode 100644 index 0000000..297da34 --- /dev/null +++ b/devices/__init__.py @@ -0,0 +1,66 @@ +import logging +from typing import Type + +from gehomesdk.erd import ErdApplianceType + +from .base import ApplianceApi +from .oven import OvenApi +from .cooktop import CooktopApi +from .fridge import FridgeApi +from .dishwasher import DishwasherApi +from .washer import WasherApi +from .dryer import DryerApi +from .washer_dryer import WasherDryerApi +from .water_filter import WaterFilterApi +from .advantium import AdvantiumApi +from .wac import WacApi +from .sac import SacApi +from .pac import PacApi +from .biac import BiacApi +from .hood import HoodApi +from .microwave import MicrowaveApi +from .water_softener import WaterSoftenerApi +from .water_heater import WaterHeaterApi +from .oim import OimApi +from .ucim import UcimApi +from .coffee_maker import CcmApi +from .dual_dishwasher import DualDishwasherApi +from .espresso_maker import EspressoMakerApi +from .dehumidifier import DehumidifierApi + +_LOGGER = logging.getLogger(__name__) + + +def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: + """Get the appropriate appliance type""" + _LOGGER.debug(f"Found device type: {appliance_type}") + known_types = { + ErdApplianceType.OVEN: OvenApi, + ErdApplianceType.COOKTOP: CooktopApi, + ErdApplianceType.ELECTRIC_COOKTOP: CooktopApi, + ErdApplianceType.FRIDGE: FridgeApi, + ErdApplianceType.BEVERAGE_CENTER: FridgeApi, + ErdApplianceType.DISH_WASHER: DishwasherApi, + ErdApplianceType.DUAL_DISH_WASHER: DualDishwasherApi, + ErdApplianceType.WASHER: WasherApi, + ErdApplianceType.DRYER: DryerApi, + ErdApplianceType.COMBINATION_WASHER_DRYER: WasherDryerApi, + ErdApplianceType.POE_WATER_FILTER: WaterFilterApi, + ErdApplianceType.WATER_SOFTENER: WaterSoftenerApi, + ErdApplianceType.WATER_HEATER: WaterHeaterApi, + ErdApplianceType.ADVANTIUM: AdvantiumApi, + ErdApplianceType.AIR_CONDITIONER: WacApi, + ErdApplianceType.SPLIT_AIR_CONDITIONER: SacApi, + ErdApplianceType.PORTABLE_AIR_CONDITIONER: PacApi, + ErdApplianceType.BUILT_IN_AIR_CONDITIONER: BiacApi, + ErdApplianceType.HOOD: HoodApi, + ErdApplianceType.MICROWAVE: MicrowaveApi, + ErdApplianceType.OPAL_ICE_MAKER: OimApi, + ErdApplianceType.UNDER_COUNTER_ICE_MAKER: UcimApi, + ErdApplianceType.CAFE_COFFEE_MAKER: CcmApi, + ErdApplianceType.ESPRESSO_MAKER: EspressoMakerApi, + ErdApplianceType.DEHUMIDIFIER: DehumidifierApi + } + + # Get the appliance type + return known_types.get(appliance_type, ApplianceApi) diff --git a/devices/__pycache__/__init__.cpython-313.pyc b/devices/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99dec06e683161c0ff8b2ce7112e55c498e86601 GIT binary patch literal 3754 zcmZ{m&u`nv6~{+EEK#y7%fIE{jAF-@liJR2CmT2`OOz#25)J*Z+tMhjNVLR_B?==c zMsjGgr=q8x7TCiceP{t6^9Qv5fwdbT0JjDTboa0~7HRLD8FEZ1izOtz@B7Tt<8a7% zd>=Dp_9u;Q!IXFjjNSNmr-~!QO!Xm!}mxvx0mU$MkL{A7e_y~*; zJt^GeSKtcKQ^G2L3*I7nT3F-P;X2VXLX_Wt8$^eN+x#7Phv-@1SNta2BzjKR;8 z(euJx{vNzX^n!4oe*hn_;27k;pi67p2snQ95Unph;N!yLvAeKz?0n!ZEYE>2+;FGn zh_XN7!Ccg#yNj<|3bG*%R%5N!sakGU6wH3Y8PGuBn#Nix^1b3KrD;lkT+?25w3g}l zl2EPI6w?_=sO|b$xm{PFHG0(NFbVjY4g}4CLDErRQawe#=AQ$x>Z@|Iqjnpn?>Q=0 zELFHx2UL0Y)N7g2G;~>b-hr%If!bO5l{LfAQ5oegXeS+|Nza4kf>=tyzgB)xwa)1+ zRjBmDD;69}bjzooUTTPIx1iE)YuWNknrAej)VmF}s-CC>>A^BxY`KG0TlYYv(}m57 zb>Hyq0b}UvLf?-qv~Ruw5@zJcWBSQaaFjq0+;`GD&w_0r(ftPUo=?J&D>0PGj=o5; zd+CU2r2zE@B7>CL_7@^Ub_7+@I8_5VCn>eVe36l-gE$K=nZ6BT8*6 z?NB`s`IJ)IO3$bsh}@;rw$kTR4@B-!YFjBm^+05jQrk)?s{5W}o-*4?8LIo@a+KLt zI-t5QE>D?lr6SdRafg)IR{Ay7eR02~%(l`o)qQclqs+F_@2T#KdqJ6Pr7G3i4CiP% zN{)Pu65E!5RHxoUr>Ryz)cGc|uXNZ>U13qJf?5lzs4ik{z9d*&x86)7wQjS@R+U$3 zMIq=Z8-1vI<&3z$&*k-@s`8>+gSZgsZt;-IK`)lxxF`xm@c`l)gkC-wE?&&1~jz`5I`T@;|{reY@d_kT&{?c4kyxulzbE`q_{l9^-2#VO0kUGkLNM=STV!c z8A{}TgW_>5)5CEwyO++zifJ)tC6dkI5t8me zrP*n3)fBm|HIyy$8EbFJvf5NTvb@=Ptxu!e^7SK$luFXV=O}9gecS&)FVwf-2lw!w zvR`H|C)fTOyj**HIllAW{lsw2O?dvvgg-EgJ!bKnGsA`aAF$8%81{U_7()JkJ-|2n zfr<8*=*3gRiwQ%>a*tU)pEZ1#_z79>G3ysmV;GYFAuBy*<-B2xU=qB}^ZxcTV-%xM zf1)u=#tB*KF-zx+F@eb>A$NPs-FLgj6eiQxi7s}H8BD^2to4|+_t7WDEJkw#t@fDJ z^FJ8#m@E(y=`oS>jIoHx5+P*Pw~S>>SVA^?%;r0faRZYGA>{OrjGLIO*yM5F;t^?P zClb%*Vw%Y99<%*!jmV82vvHBU4nWf-n#}W$|GAIyzZHz9ATaZn=YM|w_3p*9?}m3S z-8&!AodnS*y7yG8)zoH9_n_K?EW~|~K2lTg^U zX2WV$9nT4ASj`&auUd;Ct~OF+le=0-k^Ss`fm&8*?%g_SS*^X>pe?Jlcg@qZt)A5?KPIe`rqwJz zE=_Hi8{6GQO)Dh3I8f0}P83CM;HDIk4Ln*!-K*Sp;iP5;6{~%3d~-C8 f|NPgf@BFK8y@rQ%Jh{C6nE{afyZe9Gkh%RI0(u?a literal 0 HcmV?d00001 diff --git a/devices/__pycache__/advantium.cpython-313.pyc b/devices/__pycache__/advantium.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4740527e3e5df7bdedb44b6489a89d3aefcaf61 GIT binary patch literal 3900 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 literal 0 HcmV?d00001 diff --git a/devices/__pycache__/coffee_maker.cpython-313.pyc b/devices/__pycache__/coffee_maker.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bef6fa71df825ad2fb39430060ad18919d79843a GIT binary patch literal 3654 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 Y>Bl#BLKn6IFa4uJ{A>Lc0_N=XFKJ6E>i_@% literal 0 HcmV?d00001 diff --git a/devices/__pycache__/dishwasher.cpython-313.pyc b/devices/__pycache__/dishwasher.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b243031cc6a5524e421e4eb164ac1c1765e416b GIT binary patch literal 3874 zcmb7H%WoUU8DEmi_fl_J@{3|z$pG6*YT7tOV%0JwEo4zNC2**^sg+Dt%MrEqa+jH1 zN+I)rdntS@0t9e@9$X-o{1^EXxYa?)#6=3UIrwHk4w_s0&5}!smh6CYn8$Cv-+Ygq z`DXSwk%$RU{&an2@5_WB{F@KF5^Q&lKZDNa0uqpKU7)=6uluro>QC{0U_Fo(sp$5_ z^yd1fMxh@@k@Z+MPUBgLO4$TWWC!R#O4t?(Nl#t3y&`N6UGufZ zba+y@BB1C60mZhX*8**Hr0h$_pYlOI?PFq2vutQXvSDj>J?&>LYvgA5^8&3ig!( zxEHx6(ijTT_?GZHAC;zsbciKZG;8lbvGxe%UceU=8mY|YT1wRw%S!ElY$k zuiIYZfrauChP(M%5?+mb$p2R0CGuxPtD|AiTcO9MArI}gD1b!1Io}H29re&2&X|XG zTfTw8F0l_r6vg^v#F6AO&W`T6t~Fk-G9)&hj!baU|i}-dcB`uoN;_RjI*Pg z@>cG)D1ioE5v#KpgJ`Hv#xNS`lQHV<#g~2av#c6J<9+HlhbH=DOrrCBGA_Jw@1Ety zMRci89hXtEPsS8Vy@GM2yF0uddJSFalku~@8PhGs{CUIwhml!OZ3MOgkZWi1jp0g3 zd)u-Vo6J@ebx$)0Tq6lhH4SX;6G}C>Zp5ltvM5h_PyYv1I>4lwS?SJ4E1H4#6}?7S zw4!|2?k1E{2~*9m2v)RGBkn5N(<&80nWXAOG4QUY+k`g8_*6Hg&nh}xL@a74hGuIG zQfdiUnhpjXD3r{)Y$jG}HfaoU^1i9pD&PSCOwvvDJ_ZMDYU+&$pXLY)TguLkV(&Eu zTuW?)&X$yYt;Aw#9TvnDezS(Flvt*6zY*m^2M`v@Vmqd7gTOsvHzqmU8qeCMp{cWG z&DJ4mEXr>zY?Dg0F~NmY)6^Bip5+Lk3fxT~(+LNQm1rHCRa-NSMwrX*Dn@&ahG~$7 z#IX|C4Qi?^ULv|u$9ratS}prk?$hk9V%gvr_+mqt+z_i~&EQr?RkKpn3E(UUJ`p=T z!h%)}Y+*xXVq-ru^P!Qzzy(H&zfX$hR|Vc`o$(Om0i2{PJUCC!|1NEx#fc zP|w=MwcB!+OXHk%(x#P-jRNK-Z6P*#Vzo7d!G}zqB~O5$VxuT?@j?;H`OH#IW)mmo zw-7jwN_5Hi2PV^JM~Gy+YHQ!x*u3D$R;BiQ6T zbLT@xs`rBHO%obKgU7H>gfE43f<-VklnTL^#V}8cHAsG#Nf>`nQ}p)CAcpKsEjRZW zCQOGhhIEB7S$^32XRy@;KSE2gx&%!t2&%Yj@IU0Z68Grpj z{+T!voP0d}c>nLe{I3ArA9zJ?c*OK53+raNtQln%vg=i7UE|m#e&*n31OC7uhtKU1 z9rC6%=bqxaYdTAm2^X?T_vZ*LxyiHTy2$eZ<-ZN|ZC<&#l_xAW*R|4O6b@+)6vG93 z6J{WIxsBCTxzO@ykqf&+AKf~P%{&M{52byRf64s-e~2yT{|^D1lvw}( literal 0 HcmV?d00001 diff --git a/devices/__pycache__/dryer.cpython-313.pyc b/devices/__pycache__/dryer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9bda220591c6f6af52b69563e1ccb9055bf01ada GIT binary patch literal 5248 zcmbtY&2JmW6`!RjlHxA)VMnnnnG7RJZCKZqVViJlIe{s1B~heEl}p-^C3{SfONur> zl-ZS0xjnd-Lg?Lq3*?XjIkdMNdMxrca6$oew=QC!$iY3Su!{ufrEiv8Qk2$BP0wLI z-n{qb_h$ADf5TTUmlHwx&Fu8nKL-)|2OAirztws5JLo(?5|X$mBCNGUxg`s+cv-(M z+P7pSR=sbH_Al9pZOKmTFz%OZQOD8%8GycBazvd=JmHrFB6!gTN{$=LblOR@F&N;Q zV=^>`CXqBSjwI*CK%lQB9WHQw{v{j4{hVfv6%`d)n^;kc>aO3S*+r5IRdcd744vTi zcBz=DWaZTEw)~Pg23?^o4Q>}-TA0wH@e4BOOL9f25^Y2u%oQscvfC78*@8xCK|!xp zL2?&1W&;sULc}6*q))OC>qdW4FqAtju}gg$j*S7ftCuZlbeWT^uvM7-HjMp3-^D)Hvnq_?_xU(TwEW+Sq?Ln_(U z=IN^rLCvtfIsrj`$~{5rx|mg|{xrG+KmsjEgP{@%}`7_j9#aeILUIg?+yC; z$JEhfJqh;Yjru10)Y50@_0olB*pPNwzymvZ)%pfRCpwx&FTf5R${Q@+YtA07+ZN`i z-ao9{?Ei?x*l~+?9Oi47qzg+h7WvFT1-_@G`?$~c`Sz`zI0w_|K2}$(X)F7};*W;Q zx#Bf-r@UE`&*>i&<#XjscB@#CAzg->Ivq7_q$$bg zJC?)cV(wa|Bop;qwn(xixw+h7iGN7zS9Z2#q74^h6=zB%EVE25$_inh5@BDZHW~}2 z{+V$i;XVC+3?|+WUqy6 zQd|y3;*t1*cE*s0#aM6+ADI=}vx!8qgA2?PsV1znqX{= zTIePfZnZ^MsOL&;;Wic4p9}7%xi1$QQ#a_;jhZAMz>UYnEdN5twQOnYU#Fhyb>Wlt zc#eAJ>O!b({4t&SxGr36kArk7SWD%>*{`@|-B}!*wN{GJsaOqv29xuaRef^Ja&Xc) z;oKWK7(?Tp#_0?6^o9EAi+h7FPI~Fdi(lQPCqsLVXG5p;;?I z4|OYhdj8(beD;FPj1v6+*iIQ+-j%0jK6}AtLM7hfOwF;G`RoOo32Au?Z07fyna^IZ zoiR?O)4nq{6Q7JOOMO7n-sbN0bTApccgAMs(`?^3poZ4?BbQDMNoMz4ey3E@Y|54_ ztBT)3MgWl}>;RQ8IHG@86Sgu6mProiMNV$Awux>U1pF;7N)nEVH^rD{lYgnom7GrU zG>@rfT)fp%K0>Hc;_`}^45rda5hqqskwjecb$d9Z!MwDTjsQxfm*-+_(ssVvgSeUk zx(KYTrPXwSB{3$3^c6qo*1r{$7R6*N5|8TSG%Yo`=Mh|b?BFCWC6YKCk%9mqto^jx z6*v-8I4uPiL>x-Q=OYVg2$n_ZHdBnm8L>mqYgAY}6!!Kw5sQ&p@Bd|&Xt99R6ZSwZMu>Oo6>KKKnJJjIAln);>TBm{ax^JU4d1qg^ z+hrtDx7gt4DL;SA=nf6ssr&BMCh@*-ughqWx)&RKgz}MNM)zpoUfq|eO>XWB*)F4= zVv?v^YVaw_r;e@kfCe7aeWluDd0(h>trVv2aDx{qFSd>LeV4yV{pCtyc9YI-)_vL9 zWNu%OyR1xtYFp*$Y`*R*)F!v~g<_XgjJjhDewp&i?X`@P%F#fs?vrbi`F)|#Wi&_K za}7R3`Oq<=+ca>yc7GSBbj0!tMy02c78v*>%WEL;DNC21CF)*k@KMS~kNMf8fliz+ zhTJuWo3J1MKOz61=D@g;Da#mZPK?Xd+)j!01&lx2$&^|%Lm20aL{UrdPOVg7+KzFq zn#Gu~Cy~|<86hUM!QjeDEE0@|M4Vb%5ecL1gmH2G<gjfOoN%ixWJ8O8o8JoRxv)QoQ3L#-=e=-r=N{ZH%8CW(X)@^2i6n) zV_!^t@$e5n{~kg2`;XlHo&yAx9;|NdAJOckYN1f96f~Q g!a34#PJH8>c)D13PCvH4wgx!Q-mm|SpwXTF7xiKR?EnA( literal 0 HcmV?d00001 diff --git a/devices/__pycache__/dual_dishwasher.cpython-313.pyc b/devices/__pycache__/dual_dishwasher.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9849efd31c2a711528a2daf985081c962881a832 GIT binary patch literal 5006 zcmb7I&2JmW6`x&Fq)3sHX-O6%+p_FRYJ`+ho!SN5I*#>0NFrrQ)>3vH(cZ4OBWh)G zmziD4C7ZVHrSK`Y04~sj3*?ep|AZWKTqXx6Qx_>vji4nKj$r$|DQkVb^nVp>RvL>y;rU)q=Q z6F+bJ(>l)@knT-|NC?_NsW%-?^^v|*ghWzN5*xq<6~ z(u#}8cY&(!B8)D97%B`9NC10*L7h=nOP25D+1j#>&_ImgH zBrNrizD@L=Kq3=pIzVHK6{WbSnY&LEa~Bild=yx<$+`8F@qAG+&G8+BjH{qEt~S-i zjqN?0w@ex!*s2gc-&kJmLL5rw_+ft#p~L2(GF$kN|Rlp&Epz}Xq)P~%3ho>aJogC7bgOoZqYX8F_(Vnr(L7X zixUG*w`lX?3<9THw0UucfzvJ8yg288(=FP(IC0=~i#9LLC~&$(n-}K-aJogC7v~~y zx<#88=Mr$bMVlArGH~2z^L=nO0Y_!i$4cG5>4yjR?F@|-jVGAM`D(t1Wz$kDT#J=d z?N_{+C@J|}O~;UPL@-fhV;>WuLE;hB@`m2(4mn0Ft%PNQONypzdSO~1gD{jkf@&Cq z4^S~_R8du#Xu64IMN?_NLkFg&tC-aKxC6Udse}oQfMbd-?`TB}pdkjeUjBqqgj|J& zOhwl$t%g-c!PJW2=@W(E1ZPu!sbYcG&NwEvEby;3z-;Xs#Yzb$I36h)`NuNM%pyjy z*2`cfp+QsG-d3#LT0eKpQb$-=n zTAYcUe!?tN~*F^wlqVp z1)2ViqBm^R3?0`ZOs0b0Fie^Dsko?A!8F(w1#pH%W zN7|&D5{xG$vso}mFSG;e>m8n4I))zg$AjMH5FI^6Z+Ud_7}o9=B*T6a7k6lMTY)ff z7tf8AR^M6nX1}-D3du6hm}S{ynd~_Gp3mUOASe&vfoFOF+%H{RsW|6upXH{7bMN|M}PPcb`Sy z1W+{o-2d0Y%dwmFv72`K(E&n+FfaZGq3DA6Dl+1L@7KrPw=-%zf?q{0K36|mdO0y) zpP0ArG7&Q}{=)xR;pNnPeQN%THG7+YnFVp3o4E^SF15iKTlom!dGP}dKj?td_9GqO zKMI>1z9+VsPu8cBU+mkt$Dr`BaEB{oT6_-w!B$Ox7sU*R*E-;;Ej#AdIK18ge{63% z&bgyEedorT>=b9GPh#gd{ICQ5ovoC?wK?%a4sV?Vdwbq=EzQ}hC$YUJ_Io?;Y>LF$ zS@GbVbEDyB{co-{Pr!VAjO{3RSP_=F)(VW#I04D}Sklh!)gzCaCt$fgv25RGqP8ts zu1_t0Icx9!7R;=O_qdt+Emt`_V;7zPyevNAu-pN^W2+XxNpXwAx%Lx~u1}@EyytF` zJD27rarlwFR|S4b%yRx_%RGl4*w`_DY>PRZvv(cm+|iF3Z?caa?xXB&K6bui>@B;n z53VhVzu~ZQ6721H)3v*ty>SxTdtwi5&DoR(oc*DAkQiYn;NV>}bh$ow_3xSb;QX`R zZw5x54IILSfj&pmQQ9lZx&nEGOvADaFKY$fPnkw!`6IZF8pwbQ`2aEb^QjvE2W1&v zJXt2}rlWoAW|f_Hh4#;_udlAmWfqch_Q86Ru>S%G+)T*1%!c85gsex)%Zgoi)xOvzLq&p=+rA5&-Qe)d`r_J0Mr$qN3~vNzLt8$)nWufmv+#?6Be z)@^ldX(_qkOlOYi-DU-wBJI8DEdMRS{_kOPH-8Dmflm;Gf1v(?04uF-=neb lP^ykn{|d)mhR=T$KL33A>+s~$;G4h=A^tq_1A>a%{U6Sugf0L8 literal 0 HcmV?d00001 diff --git a/devices/__pycache__/espresso_maker.cpython-313.pyc b/devices/__pycache__/espresso_maker.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34a1512a9e6a394fcc9aa3e62ddbabb0aee879ee GIT binary patch literal 1673 zcmah}&2Jk;6rWwMzt>LMB#M`Hxr|8y=&gA zBN=fB7Zh>gQYlCriqs4L4JL<_X*eMcxkXAtdt_$qjR93+4)48r^WMzwz5TttjzmI; zuWwf7_n&(Z`a^8oXJel1W4<--uptDweF6_uvkXNGzXRileie-9%uG>5d{6w*S~;3CH+7K}Ee zWcX5;n@Nc&MT6R0`*Nmj*xjVe{5hy+tvX?&+{m=sO`~S&q}*+jOK~xeRd2nSws9#7 zr_AoG5gz}MVb-9l5tCZL!hKiAwk=Z^54vjr1znf8=Lv626@jE7kTnTBnhZ+STT8nI$ zdp)izJ*ib<5l{8fm>l zZL5WKtJStloW!N%xDUX)@p!ZV7^=+FckI70kMna(S2fZx<$c7(L|T%`v)& zP5O0%K~|Y(vL2+XiE|V-w2nj12m(6V4Evlwg`?P%&{I!i-3cxIzpr zeV2=2iS z+eQ8qpMg1k!aG*nT3gGN`c=$|;J6T?G8S0vH~tYk;9F5LUE$)&BT3Rr6#X4d{)y7B sWsfA^gpl-(V_k(%`0jb=-mjs1r=OmM=8yfad?Qlq`>$^hSI*^s19G;Mpa1{> literal 0 HcmV?d00001 diff --git a/devices/__pycache__/fridge.cpython-313.pyc b/devices/__pycache__/fridge.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3a946056473824c9dba1f7cce6bee5abdbbc1fc GIT binary patch literal 8938 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 literal 0 HcmV?d00001 diff --git a/devices/__pycache__/hood.cpython-313.pyc b/devices/__pycache__/hood.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f509c038eee7b66de276685c3948def8f741224 GIT binary patch literal 2676 zcmbUjOHUg|cy_&h;0G82!GVw%6iS^3PEt@w3MdXS598X%Yttk+87=nMY`W~avkM84 zDy5f-KM*OEdPu5DRiz$s>>)p)2V5hWPFtx`)k?Whku*7WX6?0sri#>&HS^8){^onE z_k2D#pzE^_23O8F0r;I=tYxnQdtXuDArOJc%>&F%ex8f*m=7}8HgAgwSTJE>-X3#c zN6d+xF&B2lT5wCujop-H7ajASm=}8~>=a$|zE~@6jkV#nm>>IN?YKSGfjfdA3sUb` z=9}jL$aao$wL9(#fB_)3^Z?PF^^V%=*rP=*7{NTdV#kU@t{6w`uCK0COV1yvSjj$YUXEPkAlo z=82u6E$hm*u)L4(88E_$LJzp(x+7q>XvdxmnBcJY6bLzplf}a#JVtr%pNuTb1#^m| z>%oGCgUNNI1{p(8TfRf%)QJNlV-2ghdJavKF1j;E_r9aAG|4>#OD39aX=eu9qF#*F zF)Eh#SZcHMX7>B*x3JA~ z>4*heaf_VDi?&PlFsIrx{JV9sbQypQpX#)@n`eU~L^bb-yomDFlz_WJDW^i zgVT`&6c?h=YcR4HnTtoR&c)}_H%OmJjL*%?reQp~7>yq!cbnw&+=tQB0Rm>FNm@8K#$kV++_=5cG2zu^h?F{J;n2e^fy`D6mKt|0EF3n3Q~ zbfgrBZ&}h&Gje}HQZ1XciQJ*EQG5tzuxWV^=Ubr@sHw=+c+3_E%~=JZ&Zb7NwjPiR zVVb0ZTny*dbVFOE>02#nDizbiMFdy0RTR#nbvcLhFjGBT+Q7Xmia>o94j)b%t90E% zXWr`v;Fmz(7tV6vd?j#k)3@sYmnN!XiSk(Dhr87zE+=tiOy6<^cE+z%$5Z9;)V6rD zD&8xL_bTHXTdtm+p)=K?iSp3Ivto7fR(bMPW#~3Vzipd}3rfdb1ME zZfT$H0+8Y*{x1N0%lxk0cFeu$``Zn?$G3v#tHBBS+wmT&dV^(eaJzr>Y5v(v&1$7z z-1esb^z>~Fj8q3A^tX@i{qF^4eX%^bSn2<0+k1V-)Ax1Yo8BkA70=myd>5XcdgiQ- z#mZx`O8@+}H~!m^VEM@TP1jCa_h#E3eG~942>FN$LRDHt5E3_pt6F|dVXzm%Pu3)* zPHBU%AYq>3~_jvO6cqZbp?l+Fq_8a#zv;jro`*d!B0YHr)#j< zb-LVj`axn>IBE|(KJ|G0$=O$cf-l*IFIa-1ZsJt5Vo_F$#9?fd=xI7fje7QZP&(e+|Qu( zSJ3?j2*2VT9RCuq%@4Sf&Fiiocsi=?-XGn)k7g_G!3WM4jvg+ssk{XAFwOh}Li=xS literal 0 HcmV?d00001 diff --git a/devices/__pycache__/microwave.cpython-313.pyc b/devices/__pycache__/microwave.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b589dd8e2a5f0ba47336fec912c2e5153237ba91 GIT binary patch literal 3017 zcmbVO-)|E~9-p<>zt@TLi@1Rl69}p$ihC{4&?XIzSrR+Oj=VOMCcV*OPn?DIu9;m2 zOnVada^mq`D$zG8p_5MPLmzqI;r@Vo$exkRO{r9=x|4W|6ev92%-U_^>bI$No$J2Qon%q&kn}T@7UfEYPt}@~?(7LwG1NjE6H39LbE} zk<2I_&5Yr(7}x-X_nZ*=at3TnoaQ=nd>{&DfE1VlQgA~!?e3zJRW2TS#wNKqNBFE_ zm{fX2%~Z^0+(UdKuB7w|A`v=B*6XSwYh_ey))6^KkriF9T#z-Xj!-4JEi0;gRZ*!Y zneHmHN_ET3qHUxOsQpx(*Yca2&scbLN1?ZNvaURHG0;m6EF-o-e+>z#sBDt)4(@E+ zMaPyAU3CrXb%f2Pgfv6PY|2oUGF5d49*+u9ZRmp0|5F z>}X1_?>OY3eR-PY1a0dh9SOjI$Ebt$rNlW|zCPigzw6_Y+!BA5e~Vpcl}f(1^6*Q6 zxAX`~A=_i}H_s7u(7x>5)^B^T%-Yu+kAz?2OD=~@W!j9}K;V<3X#4%(M*M)3Lg`&6xYN*-F7LXLP1 zQ#Kn02~~6*!|sG<6Utr_6|}9C5wze~oog>q(~tn1fdjne1s;aWxcWRsHF`?OfE zqb79v#= znYNYL^zup(X2nZlc0i3;@sY`k=~N+qIeAHh1@U6OC_*upT*``gm}!UmTB(>Uia0`1 z%<7Yj_f@HUeiate7sbNs%dMu1sTDETRS1d!<<3V%MZ_!D>qx6$#tmd>@C=yI2#M^E zJ?0IyS#wtIe6B`S_MIBUhH8w6`(FOu%?4oirJ>=Y8^`Q+hnz{! z2i;VviE_g*^%^YewYsiRFC$S!aErcJqLU{^qUP}Y$$AsBIw8|NW~bDDa{pt#OHGVL zO7XAmfTz*vZ+%wugI4szyWw3gIJ?lE%UN@|f8K28v6aWIIb+8keKLQpJzucq3y-BQ z+R`mcy49L*?)aykdqMc!dzA;v?PDqHSgIwQe<~cl&p)WPk1trq7arbf2`f*9+5466 zm)j>&)``?VzTCN1w}cymW?$`maorNsLGVvI8&yl#dIPLkg7#E6bg%f}*!)j zIMj29NLL}(2gz8-xFb~EgZ3gBadg!{9r!1++L8Z1*tVt zd@Pjy8=BskIn|y?(%)-#a}UQK-E1#jwiYk9Wj$XhOBlq!w_yN*f3RvzM@XmJ_sv%8A8lHjwH}aXEKyr zTg#@Cxs(WtSJp(#KE9Y|o&C5I&}Iy!3}w^A%GHL647yynn@43t>lVg2{1Npq=qrPB z_!M&eZ^ERf*SZ)~U3EBZpW%(tj2751*`=C%2GFS;RPL=Xlc&odl|g85x!9HgVX8#w_76 z`$vESv?DyLEF5C<4_U_w(JoP&3_7*XNTWjlbVV~uqoMpbM?rd@+5GadSm+$|V`gVZ z&JIuf^POk;46o2ByA?yE-L9MCxF5mLPhj#tAo0@WmqiP7ui(LZE3M${9p7{BG#CBr{0l&Zz0LmsVNS-x literal 0 HcmV?d00001 diff --git a/devices/__pycache__/oim.cpython-313.pyc b/devices/__pycache__/oim.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2dfec84bcfba054bd5a8f45af57b9d4f8deb4c71 GIT binary patch literal 2021 zcmZ`)TW=dh6rNqL@9Rs_npnLAQ#l|5jRQ(lmC}own$(xXk=JfOb~`xs*x7WwYi2ho z=~H+?`2!V2La0c6;orohQp!-C5D$Gzl~(B^XVzYaRxq;WocYe2Gv~TK421#+)_0et z)?P>m{lyQ?au1oqpMiOR6r>0RL^%};LSCd|g7dC|D=$&W=B0u=@1dT&mwNL)>I2NJ zcnbb}fChl~D!xK6FH<=mqM>}4hVu~`$wz54KSswAsE*1>=jmg54%JW034=2|8As=k z;y;CyKs_+$8ltg|kP1HMhozLjq@rqCKs{OAQmw6&i+QuOHE*;CivpAB^)$7iH%Voy zN0<+gl3r@JpYta`OCHHHJ@vWBK@U4yQoBX9vt|{^CecbgOEq+J-q1G*wFqUw1p;Oi zqMHU~WA@>?syFD?5OU-iXxQ9?3^sWv&Kd@Z4$FYtfilz)6%<58 zMW8N4q*C2omYvJTlzJ6c-BY!PShe7QqBk}EBXmVj#EY)~xgU0{ z9Mj?bF$a0Wqj3j4rnQ*EJMlQ3SetMpZ`k08eQVWDJAh+DaVe7GzUWT7b@9vb^B~q; zRTnSN67%+`v8s_S^k=L^Hq|DSsOU9--$A{eI|Qr*<1AAiN6w zg`1;}jjF(7{u>%#V*{k!9zm%vbDJAI=rc)-xlq}=aBELq8KLR^Wctaq>fW`L-Suw|5Ly)L;tPaAx5a~?Cmz_552EPg zd;JsX9pBT)#7^W8TtmO2REYU7)*D@dF$-YaHCh`Q=Vgq)*=T4(Nd)7zN=-{sAtMHe zy%@KQCdQP{CkvOBGew-6&*H^QAzNnQ%<^(EmstX!^3`&d^3kX84}u0ci(zb94NGm} zhGkK8b;BYi*bEKJ(j?GMI3>{cLBr&qk!$n=uFU7n-uTd%T>AQZ^t&|uG&Z##o9)MD zx0eni*&TnBe6;z?$43a*SA3%>M=|vd^J+$?qv{>zv9@|ZZRgk-USRmJ!QVqex2D&K z2F-0w+by$YyUzR_!Y`SvyLNZmky9^NW)g2Z>gSJ4@y%{jdklEj+udyUt=+8Lj#UI7 zU^+kI+Fo2NEi6DQ+u?o4HEe%*$jtZopdwf3B{=e2m|55kT!J9{fx>^HiN8_$NR$L9 vHf*nCB)oehdIj+a!FDi?g5&#v)4v5yKfJjYnA-Nf^hAaDPW=@EVn6c_)ei2V literal 0 HcmV?d00001 diff --git a/devices/__pycache__/oven.cpython-313.pyc b/devices/__pycache__/oven.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..366c24d4b2d951cf194f4371fdc0d7e1404bd1c4 GIT binary patch literal 11050 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 W+kH#@qiTaBUF7Zfp9!HNi2i>a8Ls32 literal 0 HcmV?d00001 diff --git a/devices/__pycache__/pac.cpython-313.pyc b/devices/__pycache__/pac.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17b2bc571de90c39731363ec0da1242efc45ecb9 GIT binary patch literal 1934 zcmah~&2Jk;6rZ)%pV@Viwjrhk%PkGag2ojjl+rX}>u!Uc)V929J`|VHW<7Sctar`q zrd4u4;ev8Ts#GKn<;Z`*pMWt^%TQD#)I)DUX+@8`vAs@_kJ_)&D-~WGkYM* zNd)7o3v-(vi3t74jbKDZ%E33F+(H`CgjGcO99k7hAsWhZJ-iw&iBufu;%cN6rO{H1 z#=wqf(bae2__)a7?0acgZ6j z81_nIROwoeLL3yCv_h(eS+K2+;S)BoLcm)ij_1zshck_7;YPfX;Kg4E)zN~WiO13P z_;rycwFsT4ql*HSPNQ6uMIlw5y9`@*PAb)6*0c@J%eGvat-91VHf)kr3R!pKIx&5Z zMJe&OsAG;QJ2;d3@=OoPfG!KS(E0%8LKo}k8lI^CGV?j2C$PM=k!xBQ=cotkeW8bUyro6Q-WsIn>7|91Yf<`-)=$dN{Ao26|8z zEv$)JAE)wJ>xn% z@-tSel?$`f1`rE_$Bd4sq7_ym8+MEYWaCQ1^5-Ej5Sr)i{%wI4R&SVRjicp z)yid+^7gSwz^j#y)f(1xh^SFcuUv$Q9f)@qot|dpy@H zj9qp%54C7n?Yz0=`ECa))9Jbn_;~p?!JBS}9_af$5AA|eEV2m%G{S`Z;Rix?`AIaG8g*N(%4?=WtRHV z#j(B6%Pjm@>`CP@a;}#-*O%TK+l#%-;xAX$`&X|0)ctZF!TY%p`U@f14DHLYbaGGH zKZ1^(>>bVT#qUkd?oA#*V(15ylUW>N$LJ7@SrX%p+uX9bE@Aw|mSK-pCNXYV)bni% z>fyq24CAJ2VoakDhNY{Onyy@0Q?XL4;XZF-+LXz)34F5 z;+ywo=I+ic_hy!N%lo1nN#8kr=f?MM{f$6JFI7 z62Ox+I2i8e8vsHj*D5P3YHj#U{8NTW^6zBvrC~#s={YcY0larX?1u$G_zk6gN3#!6 s{sDUDA<8^JdLQY3B&Y8tPyC!bar?u5a&9;FI65t)_Zm+SOoO=p0L&fHf&c&j literal 0 HcmV?d00001 diff --git a/devices/__pycache__/sac.cpython-313.pyc b/devices/__pycache__/sac.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..254d768b1402a446648c3adda8824491d8f9a097 GIT binary patch literal 2569 zcmbVO&2JM&6rWvx+v|J~(wL7JgP`EHiBU)jGy&BF6YL~*H8p(7@MWu=Zw^RX@W8bW^F|zRxF5tStBu`jZRp=i6bnzXawUl94RTBFd>aE968fCOGez_2j(N>+s%LU(Qec zxd07-?34Yo!CZ)jfDg#Q*>EmGBRPpmxhRe1dT37qEumuEU9L-yp{2eFp(E4&7&?aJ z&_N`JmqHVs9qqoFkc>RzLOv-l@0?~@K>Zor(yX4x@9WCS=&Ll}Jw~jOE)z zwM^!x#A;Gq-ASvnAa~?pZhr^czbM>8i;kKPC2twsgcv4vG`g_pmIEpy_iz>iu0t6u z23@}Bl$0xV=mbwNd~fjBBN1=$=;`3SI?949p7*?Ag}zs3cKF>D23*@~yV&n)yL55L z<=wo<9@#7V&ihh+9S*B_z_oYjD|tEC)w-OU$x}NDWelORSR8ikUAnk;lzU+A5v3m* z5TbNfOTZbvCDuLK()Iq|vF?GnN320P^m?q<_l-arbWhm>WgpJ32UaWEM^#0KCXQ;V zp<@Huk7^aNE^$l4xJ&4Wp|3|@k*d|ChxIH&w_@<U7#jF$3 z=m0wGh*7Ps4{?h^sd0CtLR8Jv41GjtTE_a3HyB2Yrd8K;lI&wXv)Le&_0@=lm3kc$ zesO8Uq`Y@%gb%#m*kF1Zm(sPP^*gK$3&tJ-B@~`WAKz}8j$8x4r z%I0TSKgb2gD_h9p`NHK4<*mkgLBCMAmMLPn1c)-_R{@h&6cZC#ffT{b*(1Xwc`lP# za2%OmX*7slVFO&BE|&^ez6NVJ#-xvVO;WG2=(1vx-Q6(BRMf5%ba%-@7&~_r#wOop z0=^A)It52l)oLlVXB6o&ZpX)e%Gy$XS98{mpKVF!cI62>KJoC%V(ZGypBtZTA$TK~#lH}Ws^V5O5DRZg zTlzqA&t{pjVN6!BTYu+B4e|XDm-D(0z7GeT&N4)0Y zSl2xq>xO5`kM-#M6cR^cI^V1nzy0MU< z{1u{JJ~&4@2z6m8mZoCGvZ%J)w1^4N(VePM33Loj3iMs@F!_u2F+Ithd)PD@m={4rJ)*3*a z;MieafAG4(kA|XKqbsBi{cDao6}{$!&Z0HKCz+K`MxlwE{_P~e%bSAo_mQ69L$}9y zGj@6}1m!yi3&TzJJ}4oLbA_3iOtG`4_xL;~NWM=Noa&s$MLG#0Pk{L$3|k&S5Pm~F wzoVh2DD?!Ld5YpsP^pDVe}wzn;X}WK58c1o3ZK{rJogU?vCX9y2#ACG4_47e(*OVf literal 0 HcmV?d00001 diff --git a/devices/__pycache__/ucim.cpython-313.pyc b/devices/__pycache__/ucim.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f5b727f0f89c5f09c433768ed60a21b6ac75045 GIT binary patch literal 2368 zcmah~&2Jk;6rWwMKeJApq$XC=8dJGQ1{$|0Q3%iv$JwTK96NaJ2C&<~vB%D)>s>Ru zNl8!Pg7ObkDiT7i#D)JRiz}rJ<%Br&7L-=$k(ssErd2COtM~Dn_h#n3d2jYO7z_Y9 zzW?m}rr-hKFQz!d-4~93row$71DRU^m~noE%kr3yG1;}^$_iMpWnsmg^FQ0w1d<_$5eF9NbA}T!Y0CG zWEs(AWTY87CgZlUsAx63+lQQa71d40_^x8sH%Zvpeo@!e!Gd)biZl8_g5x4ZZqsY1 z0?f$(^D>8BGLMC-yC^!wLyo<&tLm%z*&WXEWiZRh!fCMO+Y)dq?tN8=?mwRhaf%C+L4_7xZxnrwG~{#a66mOm^f-9c;|;OP zoXjt{-eS*A`P%5)k2~-yCz^23A+LlT*@?#~#mba3^U4}5v9FEFv;#P6$Szrs-3#uw zWaf-xKjeeVyg8TAp`|1Jk4Mb08}fn2o3npzXz573Y$p90|K-F53Tm#hi`7}4csp3% zP*Iz^`Mp4e_EpLX=S2w%{(d%^!`%_eQ+(GA)rus&$KkV0k1 zzWxGayoWTK11K>;+{Si?wrQw^OjuJ@h}grRAOo}7!7Ohr*fJ6GG^sO#@I|QvwWijbJ9&Ihgq`l7@P95skOqoRKy{s zkJ@^%uvAW$GKD-DFQ=(NIwz&_&OXyN)ZcqeDyF4;DYY!YvMeo;pgp;m$;0dATrS}z zf`QZ~32xL3Gzg4{y3!tMJ||$}hy9F0gM}TVV~<=)(kwQWRSH8Z z2yW_alk=&*)#Y|@xvtlHX{<|^rdS2PAK`42-2cAaG{^+$L7rT*<_k+(pAQ*Wd z{M0&(UbLbYyW*v%;@Jm{UzQK!m#z5agYs=lgahcR6~B70{*5Kpo{CdFuQ@9^_jtb4 zoxj-?*9U0QiY6alD|fH0A8dVh1i&g^<^KU7c#9vH%v#aet~fW)=dI{`SNvq4r>tnI zD=rT9Ua;Z|2VZ<`iItZ_pHg}zVFG{)c)8pjSl<@5<%jFP^+~Ogd_lA zTW@TuOco*hcDttbXT~6GD%dbpg{D=f(_RQ0dL2T{UOrkAYpEQ}q$RkTT9JxmG_|&t z%cSxYD1EgiVfLtEfe}0JVF(SgW-4`9Gfk{)Y@5iSYXXSP!EGW(AV&X z%$dDpHet_^@jfLle*nJ=Gf%?j55u!ocy=#;B!t|Nhw+Cyk3Kp9RD8h*W?gJMCf*@l zRd2NvtwlWMZijOFaO4cD68g)af8LsA&TJx;Hk>hI*WIptbmDIzW@I$(*d1p_j=gkc zBCy)Uezr-15xaTp)?}S%cbMHvy#}J|m^pd|`eG!Rw-@FL%geOA?C@rohkafaGVy)h zyOAsSDpgr742hB>7sqjbfYCp})ZZX^!V4U&7fN1;fP43Z_j3FRpyX%*j7%H`rhf}e XKe*8ioZs_4_nhJ)`|1lonZ3=w+O#T@ literal 0 HcmV?d00001 diff --git a/devices/__pycache__/wac.cpython-313.pyc b/devices/__pycache__/wac.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98d6775eafb8f4f59b43c93936a0d277eae466d4 GIT binary patch literal 2281 zcmah~&2Jk;6rZ)%pIPUliJSDxm?}jw(71w_Qks6a&NkRd?8xgVAiEto>#?(~y=!JS zjgkWj7nFaXC`cTN#D)KYBjQwJP|8r$N~nk4g3^i}d9(J~P1TAyyqWiVAM@VZ_ht`5 zp&)|w)x|Sw1rI`hbH-V~+w0&P*zO_)DZ)IWe0R+Y85ea$`Pn`1&WKdB&*Ho%QV&iR$NppdGeB@+~28B zahLq10RJY_u4ObWDB^K+%YRFxLB&G{meD1FN|Pw&WnPG$rxOCzt+$d(>8M&Sn`X3X z(CCV$SB#BlG8HveZxPipnU@l4o$6}0)b@>oKzW=8j{&_b+(ktj=Avc@{pligWyszj0C%hL~IGMtQWPVOA;DWrkBeb?ofYAH}$ct_CaN^XrzShLK_QVG}p=VwcI<`C3mLd6Vo3A?I6>^KI$l9-nAM62Dz9wy)o~-~4hP!JlT;^$$WJ z)%8$1wOjdNZf|m?H92#?aHA#N>{(rDMXt1^tB3fXD=kUc9}P|fcclHJXzcCQXne>2 zU}$`2=m4@oKcZNO`7zeZ4T3QXV%#t)>vevXF#ck_T<=PTFs^FUwCWlZ(EzazYdQe7+=8{*Y3U6T8k~j7_U-X(}#T7S&eQ zEn0CwWuA9|HcLW!;)uBXt-v zbINYBO*?cJsu8YaR&GxbTCvBhlfKA1mGUQ^#yIJZA|DKUaO^o@0cflRxEi_(<{L8DwrPu*>?+q@FyDl3ynWP@ki*xCn)j=)qY2{ g$H9@k;E7*@Cw4z=2hVK#o_R-wiJj7O1e@*mKYpJfh5!Hn literal 0 HcmV?d00001 diff --git a/devices/__pycache__/washer.cpython-313.pyc b/devices/__pycache__/washer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8831c3eaa5c28d6ad97a806a07acadfd00ab1b08 GIT binary patch literal 5225 zcmbstO>f)CbtuvLB4yd}I&p1F5n?5+i#RUwxoI5Rl*~GkWl0s~II)$mibTswBvBdC z+1fyX^it$hbPo%pz#g)|9=f+2dgx&<{Rf)LfKoQa25t8c^x`%tc8{H*M9NYaI8Np; zGw;oNZ{B+|^Y|WLdA%Nh+pn*j-2Ar}fPWIeXnpO;tKZ_uXFvb~wFDp;ZA(Bu2y(sBJki2vZT-Nj4UD;sJ(dNK5&*Ob6toLANN4-ftU=rvp;NGDU0FAA_j%h2I8O=m zD7fpoOGD586`>DfX~+l;$gY5^6!eC`q*HUR=9End5<~C-f}L?L6%FRgIYkL>$T0Yc zz8aM8+?DdGqB)?XZoz84?O|OD2KPkwSO37+S?V*euA@np>01G}uwob5m@2hy+zPM) z)*S|6+GWf-;|1bZ+}^^W-@6ut(F>GdyKH|8y@lu6^ge^XOQQh;+Zhkg=tzgsm>t0J zkwMoPO%7|Vj&>-G*#R6Q23=<~IHp}yd|%RIofTtdo|pZ`ktvK^v6H?^uh96;*b_U0 zl|t#KikefUsY))tS*l83QJS}um#d=uK!UJTkS6=JK4lBvb#0(1sba2N7A5k+N|FM} z*+FsFF|ZO{OPm9VIzAh~qfM+TlxEQraU zwU)G_1~wC2=EXF>%taH?#G*EC;1~EfcT?2jM$bR;y%-9jp|ej#G_>%@^~`tdk?$2Y0(3;@q?z&l@sfA3EV+qd7^r6ml)VML7(YO)saHiI}S;!Pzg8R_A zS!9GCteb^Q!6Pt-)-BNSmofWb-K=E3&wUFT8g<4)=r&<+b;S@1nF1?#h5kd{w~!&d zGaf=W>lk7s)5>oY&Ubw6d`GItDoJssYO-8|B-`8Z>3L91k_z}^tV(c)(Gi=aN(BQW zIv_)JDqoiK_oi}6UaA&K)#6U@Z7WmqmRiQ?6fQ#sTIp0LqtXUjn2ilH$=Vqpg6>JG zT9&31RmxRbo9@RLB6s=;?I$>AgHyHgmeQhZ!4TW_Kr$#bPg%|t6tOIAs9K*sCq>T| zPC?AGtEz3eGn$YvI7WY^WcRHF7 zcrlsEM3V`P-OMSX1PeGV!nN8+59tEV;>36|vZOhrU#i&ct&DNBhb+S-mh_wxCwFFk z*fyiJ#>e?chF^GN1EQwDUFXwUulcFuDxVfIJh$9QMcX1%X`am9V9Bg3hw(mKnt#J% z!ZMf6hzn66#be_kDPy$B9xJ+O#C=9h+Td}_0Z-sgAs0LtgeM7t1P;yLohJ6=r{s+& zpJJwm;9o{2IEMmr_2eDI=FPM&f&!5SyU+#Kg#uzF6UCtbSO58K#EN_FK0UeX2kbzT z8Ar_c=c`TsRph_gWM&aF`-~lHvL_IGVmEZ*Y2oX|=1dmNWE-Jd^@-cNtoV%ab-3n` zf3C^QBWB*@`f}+h{l{YS;th21Mk92yKC!mTu6HqU$j>#IFk-@QFs-4BYmLx)eImQd z-s)mHNWVP*YWR-^9&X#_?f(JbBxlD!C~SWXfHz|A5;lwcvrT3WF>|H{rm?rs%q`4# zpNbG+tG4?#A`B8Bq}Yfs)z&5K3i4lRGBb#oc~jULnpvw?eua6{wz4j$q6@0(g5+*N z2Wiy^-L6lFyX-HzwhtqJxXDBi6WO=@r9aVM7n`$rG@GwW_wn|e?V7%QRNp?PZ@<*N zeFFItO(uz$q$$Wa%Obj1Y=k!J6Qy1D?h9X_?h3#k;;Z=;z$Kiif0- zfHdhyN!5BV!?-}hRc|RT=2R7y?rf=&f(3fpcWSKf_>;^ZPJNO64&dp3 zNv9jG3z|HlIm>dfh`%P9L*1_7QP)@-Cv_qIdxQUK<*MqLO{t7)aOI3%YHsWHXzrp! zh?K%Ty$El)fhN@}B$W#!pYv9DCslSUiF;2{{cMca{mtrNha0+ literal 0 HcmV?d00001 diff --git a/devices/__pycache__/washer_dryer.cpython-313.pyc b/devices/__pycache__/washer_dryer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..022ae60b52c7f20356d086a0675f4a684683e70d GIT binary patch literal 2183 zcmah~%}*Og6d&8`@3oB~fPuuZsA+2{#fSujC^X7qZ;3y!k=GVQu{1LFVm39q?(CAv zJyhwX;@C?h4n3q&FTM3o=wDE2>x1buQ6g12e&Ad1J-t1W@ z6aZv>dw*en$_>C@RI!$4D4ZP=;W-e1;3xn@xwGKNJCQR%WmmzKXAon^Ou?P^AWzY!3~iF3Zkqk2uBTNU6LE9XPTPyWNW#G!daFG z&q-CtApqnQ9LOa&5mR?pSPMGh$Sb(&zPg`WJ-Fi(m|5_IK>@*yf^ARAi^FDic3L3< z`+A<+$|ahehN>p^6qNYNC?MUUsV9`(Ct_1mu?LCTA(ETJ3p5T45=xUi3zO=egBqxu z_ZkoYR)$=5JhloV$j8GA2cTj##Sll?3cW3|{wKm`de74x*}rS!i~5c@!wfp8zC*}iijx-H{w=A+9x~7H=7xGEXn?6F5;iFtRrsw zna{^RA?BLPpbVw5yXH=kK`5odINBC9xZ6-gDAECvL>1AVL9{vXWRa_tvXvdU$z>kp zN<0**T$RTm%beNC6nPxARH3#G4HKeWkFQyFzLbUK&rBDdvaHqICJ!t8CYLMaN*nmP zWzX_OZU}v2hH5!9r+fp9PsA6Xihb|P3BTePt zK#^@6j$Ga>d~qY}vo1}gBa$!{zL#z)@}AUAHxE@!Ie=u>t|Akpnr@45UpWxdE%B+; z6xH-_e`D9hZV!*qWE-8%D21Eb#;9k9kJRF2K$M)T{zk|%a1y&dh%M@|#eQt*DD>U~ zLa}4!hxQ2FZtd@?oFlZ2y`b zU+c4<+WNX4U+=RS8^5Y2SNrTeTj%s7*JsyHqyD+TQRH+A%-tN!F6*<){nz)1*It)WRx*w8d2?H+2PN;dt70zRbV14`&a zWDxoys0l*81Aj2LP9_%ylMnRC2hU2U%(y%DBKhL!kGK8-L^OkGTxtS)J4(AP$!+Y> zdR@YeB=B|GDCE5-zqf|0E$)k*u87n{qv3i+irC*4DM)R7VT`L;tHM#_P0+Vmfl{v9rbAHM8qh z=>ft8<%p0f6^TQUdO_lk;6ETs4xvL3NT`QiP^7eKkId|P?IvoO!+Z1Iyf^P--*5IP z91a4yzFwT#eCz?>A0{}9%GNsijmy`RN88z zr|e>CEA+HtTY2hYN}hyLh}z3Y)iq2;ETMq4Hp0eMS2<)UKT*^Qc2sX$s-)615H4uC zVknv_qb8~vRhFQ$x~YSsBGufY4J`xA$pE`#4!dO+=F6U<;28Hg_Q~$DzZ_s+W6re# z<~W%@1#bIq^EfDbaA*a5#9?6)#JwbPvqDp_6JbRSQq(ZB}$WR@ZRsE;EVM ztz=``#%-FVPP`Zy9jsQnJ-32KC}k-h{Y$l%x%*(lhaO&aThrS*PiGgNHSZwuM7wPIfK0*Tar{ECS2&#Og&Dat@Apu%?_?vRme5&jP>x zdXLc)=2VouvXAC8?wlNp5OuVDF8iIyeM8-+`nJwa4*XAyGmcfC%fViZ*VpkHq}g>O z0;+3u_yWC3b+5STD24L(hFUe{L*1a$a-mhJZYnCGmtZ^e&Zo#||HQgO%t^&k+(URBHR~k2 zQPEMaXcD?x?+bpX7bF0?t%W-4NQ|y+vXG!3v#vA})sAjxEm+lBZB3;&O)92g13B{G-PHv}jI?zbx(v%e~td%!!5FPghN0t!I=pr<1$a zR`-~y8{mpLd1ZHX!xXBABf;_DPU!G0F#3);GP~pdePDcN;E0w4egg3@@k6LqS_ncC zgs`R6I!(40ApD|JX?9NrAnhC0jV48qs?lQ~gf*=SA!g4c87wSI@FpFLVs}JIL@X3? zX)(Vf!O~hm!t4#kJR?@u#~@^=RI0FI7+Bfp7)YnLhPz+5iYNwPoWt)@51qZ|SMYnx zdB7p;{nMu@dHD@^%+LHiHnl%?(Hy(Doj>FUJkf`f5AXaq^8(QAb4D Gy8S;LSw1HK literal 0 HcmV?d00001 diff --git a/devices/__pycache__/water_heater.cpython-313.pyc b/devices/__pycache__/water_heater.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36826114bb9a1d584a31f0b38bad8f0ffbaf9db4 GIT binary patch literal 3113 zcmcH*S#KLfcy_%$Vy|=2ByLk|Q(C~#CQWFNpj?i-Bz6+p-L+dFDWi=&wzoLDW_Fz@ zML>B$d909-io^p*h(ki+r9Xi`U|h*%00{|!)VH9tw2#cJy|F`DDufuVX1;soJLa3+ z2?qUuj`vR=t9<7L;8%t?!`%UPzM#NIKmiK305Dtm1un&7KFVO%f-5Cp!G?tecgll3 zDKGY>eAs8x+zWjvKlW4Dqj(nrDG`el_9=Y}!Bju)PYvLK)F2*A4dJ2GFdmMA6(B$6 z9HF}&0V@Y)xHgVQLf{Bc{D*)NSP^Gj9qMSAi-|WGl8$jiSkz35t{zFZG;1^FB3=m> z6Gjn{K?=lcwW_A-1(e&YA>yNyjGif#s6ucKomVY{=MkH4vOOtP+*OFzw3|F5x=COT zQAGvmrh&j=}$3heOC=(q)TWJ-C1 zCSV0%P63!#IP6k*EUdU?(Gl*h*sHi!d@Fq{d#Ct3IK?T#A#l-mQNVu1jRSe`0*A$; zAm$lm)E0@hV-Yo0FH%oT;=#zOW4+MHhrKzgBX;DS z59$0O_YqjO$!yAb^56nJ&rFA+b9o>;)wL@Hz_Q0dWS;|dS10>Ab?O1yc1?FR8MLeI zS`Ip#eD99^4z;_^ur$+gAC18_w|9Pk#&EA}8-9Ssfq$(r`VSf`!S=}S#$(c>3wi#D z4qY_>$n(pGIw+72J5YChZUX?7CO0q0kuKGN?h~tf<+BrACo|FeS$xE?y(eEs9BOx6 zKIXtqr74`kE3RkVJ$tzJ`0Dx}e7#ro{Wo9l(GTz8tDv~|`uggD6Eq;YJMU)KJ$-3t zt*AYRjl!GAI#E^YdZBXZ;6Bm`qi$6-9mRMus1*zyvX8bWtkKRn)TWe5yQE8f%tFoB zK)7w+22Yrlsux!`V+V-atk)1GgJonvwOWOUwPOvLWXQsskWmpX!gaM;N0{{lGID+% z&P(x}B*WQEM#(`XM-ejGCC3xFmO}Jx8KlzO*PMa&j^OZNa7Zt1y>bl6oc1#S`&dGLzmbfmz*S z`mh4Rr{lz<)@n#E;sHh&>zZ%-naHGba%K^;zCuD>ZpYmYJLV;ViK-@2%gNM*$M~m9yGI@5Arn{t-Cku7cGS*75jypsBL1_SV&~=b29Pwj6Y0;%nE%o?p_8zg&zLc z+X$U(hNicIzeFBwMP?e2nQP_NGfLwbr5VX>`EQIzTH{lV@u_Q%x2Cg=>1=cS?3U-2 z2LwY`g-^<@@N6SI+Y}Q&iDPa2bR&GaDL%i8pJ{~8G(~9_f29$Ar76zs#}8gT+7u(( zLGPG!W38!lV=CQ@WUh*SmrFA17x%IZ24{sjBO3<(13%lK`cmo z5bEk0f{^$jE0KDY!6Jlj)zxZ;GXP;p!=`=tb%V0K5EhLBgqS_cL_BX_$@a}8{qbye zF&R%MB$#_GD`EEXV}VU}DU8t4vQ$eeK-IFawpzE4Ni7CDg;zkd5x^LSk5dhk{YAWt zS*ybAfwU)P#bj;o?g!up;rNY_W37?t#>n*L^tKRkhdw>}>H25K?*IzkW)q%t6k}t= zTQ$mMO)nFVwOOO9eK>NM6)^p)p?`&{ZcS8Bm9`~w!oC!n_R&dS88IQV_@>>7Y`@!= z_oaPTf>yVECE5mPKbzQdCn&cUxBCDQ*p0>PdB9JxiM1}w{zfqS`LMe}`|{I+Sc}vN z+I6QWhh}0iGdCy6?f9Hz!nVUKX2kb=JE1S)S&Fh?nn^lsyEu;f5%m8I#(o2ncX)xL zg+iy>BH&)KwfA1f^I^XmkyX?K?In0HwtlS24vA6mQzG=F< literal 0 HcmV?d00001 diff --git a/devices/__pycache__/water_softener.cpython-313.pyc b/devices/__pycache__/water_softener.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a3fa0b1e575aabe85b9a34dc42cede345e87452 GIT binary patch literal 2432 zcmZ`)-A@}w5Z|-U-)Hk35NW6r5mZ+JR~0v$4IVfnRYHp=`c1R%QS-q^--JtM|MopD8St)Kc zl;?C~;BqG@-%y|XD9GBec?I~Qq8U1|`<%rR(Hjaew_0G21Ad}v67is6*Jz_@>Xpip zZm6cJYobz9WK*UQdZQIPIxYZm7jAS75k^GBCo;q@`UtxgC~zLRJ(G~=Ukk5A=yOc^ zN@$W1*BU#Z& z@&P3!jTS4ZuWv%6h8-k|*(93Wx_BpmI7k}y@qd6`V;-SZ2d1LPme6Mq(^L!5m{l)f zPzkLVo2YytHIj8O%(NG5q&hgNhKiq*IclZ~)FQ-o2q0pvi+QUdk zG~eysanLk>$q%WN$ZbdJW%Z+tW?j-;mFVlb36<5Y%DRT>@4iBa3MJO#B9%%Tid1U) z156;%>~LL&;%pKHYHpyc+*f4AGa#Q83PnP@>YMx< z{2g2p`FX)U2YfLy93;Ap+V6%o&tJv{%$xu#i#*RHO+KaGv zVRi(!9uXV#+LADK?5k-=jfz@L%T2@7>sZ$74P65pBVASShF(|FZUPu-=f$ztgC-hV zLkiy(@RJT5<-KN#h$QGTz8_l$=Y z_thcy?i2QD^QHMrY#3Sb+p7oFto_;Fp%N3w&-?z{NXD0-=qauv9pAKY%f< zAyvkbX%cn4X(|Tn8E?I`tU$rw6hl&AVbDK`>*RfE+~o<{QjIYXufIWmuoM3bTs|0> zv<4=3az|`eVCc!{llwnjd4)iINd>v$8K(N|P))B^Rjq0V&8-H^PH^@+G-u#%3mS-| znG+jI4a(P;aI$dA3EhrV6-qM7_ni85PtMXDCiIVoyhjDCMyG_`LU&3649zS!g(fxu zpW*JZ`T2RF;GT!}LN`EqMms#?KEX9|4VLuq#teufKf^G8qQqZl@EJ<~jndE1$SYrf n@f{-&M+p?`J&2zFHG2N>&Hd=*ozRP5KQr|07cUV^&hGyMKk;G= literal 0 HcmV?d00001 diff --git a/devices/advantium.py b/devices/advantium.py new file mode 100644 index 0000000..c3baf36 --- /dev/null +++ b/devices/advantium.py @@ -0,0 +1,45 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType, ErdDataType + +from .base import ApplianceApi +from ..entities import GeAdvantium, GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdPropertyBinarySensor, UPPER_OVEN + +_LOGGER = logging.getLogger(__name__) + +class AdvantiumApi(ApplianceApi): + """API class for Advantium objects""" + APPLIANCE_TYPE = ErdApplianceType.ADVANTIUM + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + advantium_entities = [ + GeErdSensor(self, ErdCode.PERSONALITY), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED)), + GeErdBinarySensor(self, ErdCode.MICROWAVE_REMOTE_ENABLE), + GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE)), + GeErdSensor(self, ErdCode.ADVANTIUM_KITCHEN_TIME_REMAINING), + GeErdSensor(self, ErdCode.ADVANTIUM_COOK_TIME_REMAINING), + GeAdvantium(self), + + #Cook Status + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "cook_mode"), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "termination_reason", icon_override="mdi:information-outline"), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "preheat_status", icon_override="mdi:fire"), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "temperature", icon_override="mdi:thermometer", data_type_override=ErdDataType.INT), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "power_level", icon_override="mdi:gauge", data_type_override=ErdDataType.INT), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "warm_status", icon_override="mdi:radiator"), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "door_status", device_class_override="door"), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "sensing_active", icon_on_override="mdi:flash-auto", icon_off_override="mdi:flash-off"), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "cooling_fan_status", icon_on_override="mdi:fan", icon_off_override="mdi:fan-off"), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "oven_light_status", icon_on_override="mdi:lightbulb-on", icon_off_override="mdi:lightbulb-off"), + ] + entities = base_entities + advantium_entities + return entities + + def _single_name(self, erd_code: ErdCode): + return erd_code.name.replace(UPPER_OVEN+"_","").replace("_", " ").title() + diff --git a/devices/base.py b/devices/base.py new file mode 100644 index 0000000..179e3e9 --- /dev/null +++ b/devices/base.py @@ -0,0 +1,154 @@ +import asyncio +import logging +from typing import Dict, List, Optional + +from gehomesdk import GeAppliance +from gehomesdk.erd import ErdCode, ErdCodeType, ErdApplianceType + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from ..const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +class ApplianceApi: + """ + API class to represent a single physical device. + + Since a physical device can have many entities, we"ll pool common elements here + """ + APPLIANCE_TYPE = None # type: Optional[ErdApplianceType] + + def __init__(self, coordinator: DataUpdateCoordinator, appliance: GeAppliance): + if not appliance.initialized: + raise RuntimeError("Appliance not ready") + self._appliance = appliance + self._loop = appliance.client.loop + self._hass = coordinator.hass + self.coordinator = coordinator + self.initial_update = False + self._entities = {} # type: Optional[Dict[str, Entity]] + + @property + def hass(self) -> HomeAssistant: + return self._hass + + @property + def loop(self) -> Optional[asyncio.AbstractEventLoop]: + if self._loop is None: + self._loop = self._appliance.client.loop + return self._loop + + @property + def appliance(self) -> GeAppliance: + return self._appliance + + @appliance.setter + def appliance(self, value: GeAppliance): + self._appliance = value + + @property + def available(self) -> bool: + #Note - online will be there since we're using the GE coordinator + #Didn't want to deal with the circular references to get the type hints + #working. + return self.appliance.available and self.coordinator.online + + @property + def serial_number(self) -> str: + return self.appliance.get_erd_value(ErdCode.SERIAL_NUMBER) + + @property + def mac_addr(self) -> str: + return self.appliance.mac_addr + + @property + def serial_or_mac(self) -> str: + def is_zero(val: str) -> bool: + try: + intVal = int(val) + return intVal == 0 + except: + return False + + if (self.serial_number and not + self.serial_number.isspace() and not + is_zero(self.serial_number)): + return self.serial_number + return self.mac_addr + + @property + def model_number(self) -> str: + return self.appliance.get_erd_value(ErdCode.MODEL_NUMBER) + + @property + def sw_version(self) -> str: + appVer = self.try_get_erd_value(ErdCode.APPLIANCE_SW_VERSION) + wifiVer = self.try_get_erd_value(ErdCode.WIFI_MODULE_SW_VERSION) + + return 'Appliance=' + str(appVer or 'Unknown') + '/Wifi=' + str(wifiVer or 'Unknown') + + @property + def name(self) -> str: + appliance_type = self.appliance.appliance_type + if appliance_type is None or appliance_type == ErdApplianceType.UNKNOWN: + appliance_type = "Appliance" + else: + appliance_type = appliance_type.name.replace("_", " ").title() + return f"GE {appliance_type} {self.serial_or_mac}" + + @property + def device_info(self) -> Dict: + """Device info dictionary.""" + + return { + "identifiers": {(DOMAIN, self.serial_or_mac)}, + "name": self.name, + "manufacturer": "GE", + "model": self.model_number, + "sw_version": self.sw_version + } + + @property + def entities(self) -> List[Entity]: + return list(self._entities.values()) + + def get_all_entities(self) -> List[Entity]: + """Create Entities for this device.""" + return self.get_base_entities() + + def get_base_entities(self) -> List[Entity]: + """Create base entities (i.e. common between all appliances).""" + from ..entities import GeErdSensor, GeErdSwitch + entities = [ + GeErdSensor(self, ErdCode.CLOCK_TIME), + GeErdSwitch(self, ErdCode.SABBATH_MODE), + ] + return entities + + def build_entities_list(self) -> None: + """Build the entities list, adding anything new.""" + from ..entities import GeErdEntity, GeErdButton + entities = [ + e for e in self.get_all_entities() + if not isinstance(e, GeErdEntity) or isinstance(e, GeErdButton) or e.erd_code in self.appliance.known_properties + ] + + for entity in entities: + if entity.unique_id not in self._entities: + self._entities[entity.unique_id] = entity + + def try_get_erd_value(self, code: ErdCodeType): + try: + return self.appliance.get_erd_value(code) + except: + return None + + def has_erd_code(self, code: ErdCodeType): + try: + self.appliance.get_erd_value(code) + return True + except: + return False diff --git a/devices/biac.py b/devices/biac.py new file mode 100644 index 0000000..916b6cb --- /dev/null +++ b/devices/biac.py @@ -0,0 +1,35 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeSacClimate, GeErdSensor, GeErdSwitch, ErdOnOffBoolConverter, GeErdBinarySensor + + +_LOGGER = logging.getLogger(__name__) + + +class BiacApi(ApplianceApi): + """API class for Built-In AC objects""" + APPLIANCE_TYPE = ErdApplianceType.BUILT_IN_AIR_CONDITIONER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + sac_entities = [ + GeSacClimate(self), + GeErdSensor(self, ErdCode.AC_TARGET_TEMPERATURE), + GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE), + GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), + GeErdSensor(self, ErdCode.AC_OPERATION_MODE), + GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + GeErdBinarySensor(self, ErdCode.AC_FILTER_STATUS, device_class_override="problem"), + GeErdSensor(self, ErdCode.WAC_DEMAND_RESPONSE_STATE), + GeErdSensor(self, ErdCode.WAC_DEMAND_RESPONSE_POWER, uom_override="kW"), + ] + + entities = base_entities + sac_entities + return entities + diff --git a/devices/coffee_maker.py b/devices/coffee_maker.py new file mode 100644 index 0000000..d3f39c9 --- /dev/null +++ b/devices/coffee_maker.py @@ -0,0 +1,66 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + GeAppliance, + ErdCode, + ErdApplianceType, + ErdCcmBrewSettings +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .base import ApplianceApi +from ..entities import ( + GeCcmPotNotPresentBinarySensor, + GeErdSensor, + GeErdBinarySensor, + GeErdButton, + GeCcmBrewStrengthSelect, + GeCcmBrewTemperatureNumber, + GeCcmBrewCupsNumber, + GeCcmBrewSettingsButton +) + +_LOGGER = logging.getLogger(__name__) + + +class CcmApi(ApplianceApi): + """API class for Cafe Coffee Maker objects""" + APPLIANCE_TYPE = ErdApplianceType.CAFE_COFFEE_MAKER + + def __init__(self, coordinator: DataUpdateCoordinator, appliance: GeAppliance): + super().__init__(coordinator, appliance) + + self._brew_strengh_entity = GeCcmBrewStrengthSelect(self) + self._brew_temperature_entity = GeCcmBrewTemperatureNumber(self) + self._brew_cups_entity = GeCcmBrewCupsNumber(self) + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + ccm_entities = [ + GeErdBinarySensor(self, ErdCode.CCM_IS_BREWING), + GeErdBinarySensor(self, ErdCode.CCM_IS_DESCALING), + GeCcmBrewSettingsButton(self), + GeErdButton(self, ErdCode.CCM_CANCEL_DESCALING), + GeErdButton(self, ErdCode.CCM_START_DESCALING), + GeErdButton(self, ErdCode.CCM_CANCEL_BREWING), + self._brew_strengh_entity, + self._brew_temperature_entity, + self._brew_cups_entity, + GeErdSensor(self, ErdCode.CCM_CURRENT_WATER_TEMPERATURE), + GeErdBinarySensor(self, ErdCode.CCM_OUT_OF_WATER, device_class_override="problem"), + GeCcmPotNotPresentBinarySensor(self, ErdCode.CCM_POT_PRESENT, device_class_override="problem") + ] + + entities = base_entities + ccm_entities + return entities + + async def start_brewing(self) -> None: + """Aggregate brew settings and start brewing.""" + + new_mode = ErdCcmBrewSettings(self._brew_cups_entity.native_value, + self._brew_strengh_entity.brew_strength, + self._brew_temperature_entity.native_value) + await self.appliance.async_set_erd_value(ErdCode.CCM_BREW_SETTINGS, new_mode) \ No newline at end of file diff --git a/devices/cooktop.py b/devices/cooktop.py new file mode 100644 index 0000000..32fb597 --- /dev/null +++ b/devices/cooktop.py @@ -0,0 +1,62 @@ +import logging +from typing import List +from gehomesdk.erd.erd_data_type import ErdDataType + +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdCooktopConfig, + CooktopStatus +) + +from .base import ApplianceApi +from ..entities import ( + GeErdBinarySensor, + GeErdPropertySensor, + GeErdPropertyBinarySensor +) + +_LOGGER = logging.getLogger(__name__) + +class CooktopApi(ApplianceApi): + """API class for cooktop objects""" + APPLIANCE_TYPE = ErdApplianceType.COOKTOP + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + cooktop_config = ErdCooktopConfig.NONE + if self.has_erd_code(ErdCode.COOKTOP_CONFIG): + cooktop_config: ErdCooktopConfig = self.appliance.get_erd_value(ErdCode.COOKTOP_CONFIG) + + _LOGGER.debug(f"Cooktop Config: {cooktop_config}") + cooktop_entities = [] + + if cooktop_config == ErdCooktopConfig.PRESENT: + # attempt to get the cooktop status using legacy status + cooktop_status_erd = ErdCode.COOKTOP_STATUS + cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) + + # if we didn't get it, try using the new version + if cooktop_status is None: + cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT + cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) + + # if we got a status through either mechanism, we can add the entities + if cooktop_status is not None: + cooktop_entities.append(GeErdBinarySensor(self, cooktop_status_erd)) + + for (k, v) in cooktop_status.burners.items(): + if v.exists: + prop = self._camel_to_snake(k) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on")) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized")) + if not v.on_off_only: + cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) + + return base_entities + cooktop_entities + + def _camel_to_snake(self, s): + return ''.join(['_'+c.lower() if c.isupper() else c for c in s]).lstrip('_') diff --git a/devices/dehumidifier.py b/devices/dehumidifier.py new file mode 100644 index 0000000..fa0d8cc --- /dev/null +++ b/devices/dehumidifier.py @@ -0,0 +1,44 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdOnOff +) + +from .base import ApplianceApi +from ..entities import ( + GeErdSensor, + GeErdSelect, + GeErdPropertySensor, + GeErdSwitch, + ErdOnOffBoolConverter, + GeDehumidifierFanSpeedSensor, + GeDehumidifier +) + +_LOGGER = logging.getLogger(__name__) + + +class DehumidifierApi(ApplianceApi): + """API class for Dehumidifier objects""" + APPLIANCE_TYPE = ErdApplianceType.DEHUMIDIFIER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + dhum_entities = [ + GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + GeDehumidifierFanSpeedSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), + GeErdSensor(self, ErdCode.DHUM_CURRENT_HUMIDITY), + GeErdSensor(self, ErdCode.DHUM_TARGET_HUMIDITY), + GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "empty_bucket", device_class_override="problem"), + GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "clean_filter", device_class_override="problem"), + GeDehumidifier(self) + ] + + entities = base_entities + dhum_entities + return entities + diff --git a/devices/dishwasher.py b/devices/dishwasher.py new file mode 100644 index 0000000..8c4e91c --- /dev/null +++ b/devices/dishwasher.py @@ -0,0 +1,55 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdNumber + +_LOGGER = logging.getLogger(__name__) + + +class DishwasherApi(ApplianceApi): + """API class for dishwasher objects""" + APPLIANCE_TYPE = ErdApplianceType.DISH_WASHER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + dishwasher_entities = [ + #GeDishwasherControlLockedSwitch(self, ErdCode.USER_INTERFACE_LOCKED), + GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_NAME), + GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_STATE, icon_override="mdi:state-machine"), + GeErdSensor(self, ErdCode.DISHWASHER_OPERATING_MODE), +# GeErdSensor(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE, uom_override="pods"), + GeErdNumber(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE, uom_override="pods", min_value=0, max_value=255), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", icon_override="mdi:dishwasher-alert"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", icon_override="mdi:silverware-clean"), + GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING), + GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS), + GeErdBinarySensor(self, ErdCode.DISHWASHER_IS_CLEAN), + GeErdBinarySensor(self, ErdCode.DISHWASHER_REMOTE_START_ENABLE), + + #User Setttings + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "mute", icon_override="mdi:volume-mute"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control", icon_override="mdi:lock"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath", icon_override="mdi:star-david"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode", icon_override="mdi:state-machine"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak", icon_override="mdi:water"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet", icon_override="mdi:bottle-tonic-outline"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", icon_override="mdi:coolant-temperature"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "rinse_aid", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "dry_option", icon_override="mdi:fan"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_zone", icon_override="mdi:dock-top"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours", icon_override="mdi:clock-fast"), + + #Cycle Counts + GeErdPropertySensor(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "started", icon_override="mdi:counter"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "completed", icon_override="mdi:counter"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "reset", icon_override="mdi:counter") + ] + entities = base_entities + dishwasher_entities + return entities + diff --git a/devices/dryer.py b/devices/dryer.py new file mode 100644 index 0000000..cc0110d --- /dev/null +++ b/devices/dryer.py @@ -0,0 +1,66 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeErdSensor, GeErdBinarySensor + +_LOGGER = logging.getLogger(__name__) + +class DryerApi(ApplianceApi): + """API class for dryer objects""" + APPLIANCE_TYPE = ErdApplianceType.DRYER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + common_entities = [ + GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_STATE, icon_override="mdi:tumble-dryer"), + GeErdSensor(self, ErdCode.LAUNDRY_CYCLE, icon_override="mdi:state-machine"), + GeErdSensor(self, ErdCode.LAUNDRY_SUB_CYCLE, icon_override="mdi:state-machine"), + GeErdBinarySensor(self, ErdCode.LAUNDRY_END_OF_CYCLE, icon_on_override="mdi:tumble-dryer", icon_off_override="mdi:tumble-dryer"), + GeErdSensor(self, ErdCode.LAUNDRY_TIME_REMAINING), + GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, icon_on_override="mdi:tumble-dryer", icon_off_override="mdi:tumble-dryer"), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_BLOCKED_VENT_FAULT, icon_on_override="mid:alert-circle", icon_off_override="mdi:alert-circle"), + ] + + dryer_entities = self.get_dryer_entities() + + entities = base_entities + common_entities + dryer_entities + return entities + + def get_dryer_entities(self): + #Not all options appear to exist on every dryer... we'll look for the presence of + #a code to figure out which sensors are applicable beyond the common ones. + dryer_entities = [ + ] + + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_DRYNESS_LEVEL): + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_DRYNESS_LEVEL)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_DRYNESSNEW_LEVEL): + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_DRYNESSNEW_LEVEL)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_TEMPERATURE_OPTION): + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_TEMPERATURE_OPTION)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_TEMPERATURENEW_OPTION): + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_TEMPERATURENEW_OPTION)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_TUMBLE_STATUS): + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_TUMBLE_STATUS)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_EXTENDED_TUMBLE_OPTION_SELECTION): + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_EXTENDED_TUMBLE_OPTION_SELECTION)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_WASHERLINK_STATUS): + dryer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_WASHERLINK_STATUS)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_LEVEL_SENSOR_DISABLED): + dryer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_LEVEL_SENSOR_DISABLED)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_SHEET_USAGE_CONFIGURATION): + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_SHEET_USAGE_CONFIGURATION)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_SHEET_INVENTORY): + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_SHEET_INVENTORY, icon_override="mdi:tray-full", uom_override="sheets")]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_ECODRY_OPTION_SELECTION): + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_ECODRY_OPTION_SELECTION)]) + + return dryer_entities + diff --git a/devices/dual_dishwasher.py b/devices/dual_dishwasher.py new file mode 100644 index 0000000..158da3c --- /dev/null +++ b/devices/dual_dishwasher.py @@ -0,0 +1,71 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor + +_LOGGER = logging.getLogger(__name__) + + +class DualDishwasherApi(ApplianceApi): + """API class for dual dishwasher objects""" + APPLIANCE_TYPE = ErdApplianceType.DISH_WASHER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + lower_entities = [ + GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_STATE, erd_override="lower_cycle_state", icon_override="mdi:state-machine"), + GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING, erd_override="lower_time_remaining"), + GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS, erd_override="lower_door_status"), + + #Reminders + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", erd_override="lower_reminder", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", erd_override="lower_reminder", icon_override="mdi:dishwasher-alert"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", erd_override="lower_reminder", icon_override="mdi:silverware-clean"), + + #User Setttings + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "mute", erd_override="lower_setting", icon_override="mdi:volume-mute"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control", erd_override="lower_setting", icon_override="mdi:lock"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath", erd_override="lower_setting", icon_override="mdi:star-david"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode", erd_override="lower_setting", icon_override="mdi:state-machine"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak", erd_override="lower_setting", icon_override="mdi:water"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet", erd_override="lower_setting", icon_override="mdi:bottle-tonic-outline"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", erd_override="lower_setting", icon_override="mdi:coolant-temperature"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "rinse_aid", erd_override="lower_setting", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "dry_option", erd_override="lower_setting", icon_override="mdi:fan"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_zone", erd_override="lower_setting", icon_override="mdi:dock-top"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours", erd_override="lower_setting", icon_override="mdi:clock-fast") + ] + + upper_entities = [ + #GeDishwasherControlLockedSwitch(self, ErdCode.USER_INTERFACE_LOCKED), + GeErdSensor(self, ErdCode.DISHWASHER_UPPER_CYCLE_STATE, erd_override="upper_cycle_state", icon_override="mdi:state-machine"), + GeErdSensor(self, ErdCode.DISHWASHER_UPPER_TIME_REMAINING, erd_override="upper_time_remaining"), + GeErdBinarySensor(self, ErdCode.DISHWASHER_UPPER_DOOR_STATUS, erd_override="upper_door_status"), + + #Reminders + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "add_rinse_aid", erd_override="upper_reminder", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "clean_filter", erd_override="upper_reminder", icon_override="mdi:dishwasher-alert"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "sanitized", erd_override="upper_reminder", icon_override="mdi:silverware-clean"), + + #User Setttings + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "mute", erd_override="upper_setting", icon_override="mdi:volume-mute"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "lock_control", erd_override="upper_setting", icon_override="mdi:lock"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "sabbath", erd_override="upper_setting", icon_override="mdi:star-david"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "cycle_mode", erd_override="upper_setting", icon_override="mdi:state-machine"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "presoak", erd_override="upper_setting", icon_override="mdi:water"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "bottle_jet", erd_override="upper_setting", icon_override="mdi:bottle-tonic-outline"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "wash_temp", erd_override="upper_setting", icon_override="mdi:coolant-temperature"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "rinse_aid", erd_override="upper_setting", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "dry_option", erd_override="upper_setting", icon_override="mdi:fan"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "wash_zone", erd_override="upper_setting", icon_override="mdi:dock-top"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "delay_hours", erd_override="upper_setting", icon_override="mdi:clock-fast") + ] + + entities = base_entities + lower_entities + upper_entities + return entities + diff --git a/devices/espresso_maker.py b/devices/espresso_maker.py new file mode 100644 index 0000000..efb184e --- /dev/null +++ b/devices/espresso_maker.py @@ -0,0 +1,34 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType +) + +from .base import ApplianceApi +from ..entities import ( + GeErdBinarySensor, + GeErdButton +) + +_LOGGER = logging.getLogger(__name__) + + +class EspressoMakerApi(ApplianceApi): + """API class for Espresso Maker objects""" + APPLIANCE_TYPE = ErdApplianceType.ESPRESSO_MAKER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + em_entities = [ + GeErdBinarySensor(self, ErdCode.CCM_IS_DESCALING), + GeErdButton(self, ErdCode.CCM_CANCEL_DESCALING), + GeErdButton(self, ErdCode.CCM_START_DESCALING), + GeErdBinarySensor(self, ErdCode.CCM_OUT_OF_WATER, device_class_override="problem"), + ] + + entities = base_entities + em_entities + return entities diff --git a/devices/fridge.py b/devices/fridge.py new file mode 100644 index 0000000..5f5dd8c --- /dev/null +++ b/devices/fridge.py @@ -0,0 +1,138 @@ +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.sensor import SensorDeviceClass +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdOnOff, + ErdHotWaterStatus, + FridgeIceBucketStatus, + IceMakerControlStatus, + ErdFilterStatus, + HotWaterStatus, + FridgeModelInfo, + ErdConvertableDrawerMode, + ErdDataType +) + +from .base import ApplianceApi +from ..entities import ( + ErdOnOffBoolConverter, + GeErdSensor, + GeErdBinarySensor, + GeErdSwitch, + GeErdSelect, + GeErdLight, + GeFridge, + GeFreezer, + GeDispenser, + GeErdPropertySensor, + GeErdPropertyBinarySensor, + ConvertableDrawerModeOptionsConverter, + GeFridgeIceControlSwitch +) + +_LOGGER = logging.getLogger(__name__) + +class FridgeApi(ApplianceApi): + """API class for fridge objects""" + APPLIANCE_TYPE = ErdApplianceType.FRIDGE + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + fridge_entities = [] + freezer_entities = [] + dispenser_entities = [] + + # Get the statuses used to determine presence + + ice_maker_control: IceMakerControlStatus = self.try_get_erd_value(ErdCode.ICE_MAKER_CONTROL) + ice_bucket_status: FridgeIceBucketStatus = self.try_get_erd_value(ErdCode.ICE_MAKER_BUCKET_STATUS) + water_filter: ErdFilterStatus = self.try_get_erd_value(ErdCode.WATER_FILTER_STATUS) + air_filter: ErdFilterStatus = self.try_get_erd_value(ErdCode.AIR_FILTER_STATUS) + hot_water_status: HotWaterStatus = self.try_get_erd_value(ErdCode.HOT_WATER_STATUS) + fridge_model_info: FridgeModelInfo = self.try_get_erd_value(ErdCode.FRIDGE_MODEL_INFO) + convertable_drawer: ErdConvertableDrawerMode = self.try_get_erd_value(ErdCode.CONVERTABLE_DRAWER_MODE) + + interior_light: int = self.try_get_erd_value(ErdCode.INTERIOR_LIGHT) + proximity_light: ErdOnOff = self.try_get_erd_value(ErdCode.PROXIMITY_LIGHT) + display_mode: ErdOnOff = self.try_get_erd_value(ErdCode.DISPLAY_MODE) + lockout_mode: ErdOnOff = self.try_get_erd_value(ErdCode.LOCKOUT_MODE) + turbo_cool: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_COOL_STATUS) + turbo_freeze: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_FREEZE_STATUS) + ice_boost: ErdOnOff = self.try_get_erd_value(ErdCode.FRIDGE_ICE_BOOST) + + units = self.hass.config.units + + # Common entities + common_entities = [ + GeErdSensor(self, ErdCode.FRIDGE_MODEL_INFO), + GeErdSwitch(self, ErdCode.SABBATH_MODE), + GeErdSensor(self, ErdCode.DOOR_STATUS), + GeErdPropertyBinarySensor(self, ErdCode.DOOR_STATUS, "any_open") + ] + if(ice_bucket_status and (ice_bucket_status.is_present_fridge or ice_bucket_status.is_present_freezer)): + common_entities.append(GeErdSensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS)) + + # Fridge entities + if fridge_model_info is None or fridge_model_info.has_fridge: + fridge_entities.extend([ + GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "fridge"), + GeFridge(self), + ]) + if turbo_cool is not None: + fridge_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST)) + if(ice_maker_control and ice_maker_control.status_fridge != ErdOnOff.NA): + fridge_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_fridge")) + fridge_entities.append(GeFridgeIceControlSwitch(self, "fridge")) + if(water_filter and water_filter != ErdFilterStatus.NA): + fridge_entities.append(GeErdSensor(self, ErdCode.WATER_FILTER_STATUS)) + if(air_filter and air_filter != ErdFilterStatus.NA): + fridge_entities.append(GeErdSensor(self, ErdCode.AIR_FILTER_STATUS)) + if(ice_bucket_status and ice_bucket_status.is_present_fridge): + fridge_entities.append(GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_fridge")) + if(interior_light and interior_light != 255): + fridge_entities.append(GeErdLight(self, ErdCode.INTERIOR_LIGHT)) + if(proximity_light and proximity_light != ErdOnOff.NA): + fridge_entities.append(GeErdSwitch(self, ErdCode.PROXIMITY_LIGHT, ErdOnOffBoolConverter(), icon_on_override="mdi:lightbulb-on", icon_off_override="mdi:lightbulb")) + if(convertable_drawer and convertable_drawer != ErdConvertableDrawerMode.NA): + fridge_entities.append(GeErdSelect(self, ErdCode.CONVERTABLE_DRAWER_MODE, ConvertableDrawerModeOptionsConverter(units))) + if(display_mode and display_mode != ErdOnOff.NA): + fridge_entities.append(GeErdSwitch(self, ErdCode.DISPLAY_MODE, ErdOnOffBoolConverter(), icon_on_override="mdi:lightbulb-on", icon_off_override="mdi:lightbulb")) + if(lockout_mode and lockout_mode != ErdOnOff.NA): + fridge_entities.append(GeErdSwitch(self, ErdCode.LOCKOUT_MODE, ErdOnOffBoolConverter(), icon_on_override="mdi:lock", icon_off_override="mdi:lock-open")) + + # Freezer entities + if fridge_model_info is None or fridge_model_info.has_freezer: + freezer_entities.extend([ + GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "freezer"), + GeFreezer(self), + ]) + if turbo_freeze is not None: + freezer_entities.append(GeErdSwitch(self, ErdCode.TURBO_FREEZE_STATUS)) + if ice_boost is not None: + freezer_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST)) + if(ice_maker_control and ice_maker_control.status_freezer != ErdOnOff.NA): + freezer_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_freezer")) + freezer_entities.append(GeFridgeIceControlSwitch(self, "freezer")) + if(ice_bucket_status and ice_bucket_status.is_present_freezer): + freezer_entities.append(GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_freezer")) + + # Dispenser entities + if(hot_water_status and hot_water_status.status != ErdHotWaterStatus.NA): + dispenser_entities.extend([ + GeErdBinarySensor(self, ErdCode.HOT_WATER_IN_USE), + GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "status", icon_override="mdi:information-outline"), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_until_ready", icon_override="mdi:timer-outline"), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "current_temp", device_class_override=SensorDeviceClass.TEMPERATURE, data_type_override=ErdDataType.INT), + GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=BinarySensorDeviceClass.PROBLEM), + GeDispenser(self) + ]) + + entities = base_entities + common_entities + fridge_entities + freezer_entities + dispenser_entities + return entities diff --git a/devices/hood.py b/devices/hood.py new file mode 100644 index 0000000..439c775 --- /dev/null +++ b/devices/hood.py @@ -0,0 +1,52 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdHoodFanSpeedAvailability, + ErdHoodLightLevelAvailability, + ErdOnOff +) + +from .base import ApplianceApi +from ..entities import ( + GeHoodLightLevelSelect, + GeHoodFanSpeedSelect, + GeErdTimerSensor, + GeErdSwitch, + ErdOnOffBoolConverter +) + +_LOGGER = logging.getLogger(__name__) + + +class HoodApi(ApplianceApi): + """API class for Oven Hood objects""" + APPLIANCE_TYPE = ErdApplianceType.HOOD + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + #get the availabilities + fan_availability: ErdHoodFanSpeedAvailability = self.try_get_erd_value(ErdCode.HOOD_FAN_SPEED_AVAILABILITY) + light_availability: ErdHoodLightLevelAvailability = self.try_get_erd_value(ErdCode.HOOD_LIGHT_LEVEL_AVAILABILITY) + timer_availability: ErdOnOff = self.try_get_erd_value(ErdCode.HOOD_TIMER_AVAILABILITY) + + hood_entities = [ + #looks like this is always available? + GeErdSwitch(self, ErdCode.HOOD_DELAY_OFF, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + ] + + if fan_availability and fan_availability.is_available: + hood_entities.append(GeHoodFanSpeedSelect(self, ErdCode.HOOD_FAN_SPEED)) + #for now, represent as a select + if light_availability and light_availability.is_available: + hood_entities.append(GeHoodLightLevelSelect(self, ErdCode.HOOD_LIGHT_LEVEL)) + if timer_availability == ErdOnOff.ON: + hood_entities.append(GeErdTimerSensor(self, ErdCode.HOOD_TIMER)) + + entities = base_entities + hood_entities + return entities + diff --git a/devices/microwave.py b/devices/microwave.py new file mode 100644 index 0000000..ec943fa --- /dev/null +++ b/devices/microwave.py @@ -0,0 +1,56 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdHoodFanSpeedAvailability, + ErdHoodLightLevelAvailability, + ErdOnOff +) + +from .base import ApplianceApi +from ..entities import ( + GeHoodLightLevelSelect, + GeHoodFanSpeedSelect, + GeErdPropertySensor, + GeErdPropertyBinarySensor, + GeErdBinarySensor, + GeErdTimerSensor +) + +_LOGGER = logging.getLogger(__name__) + + +class MicrowaveApi(ApplianceApi): + """API class for Microwave objects""" + APPLIANCE_TYPE = ErdApplianceType.MICROWAVE + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + #get the availabilities + fan_availability: ErdHoodFanSpeedAvailability = self.try_get_erd_value(ErdCode.HOOD_FAN_SPEED_AVAILABILITY) + light_availability: ErdHoodLightLevelAvailability = self.try_get_erd_value(ErdCode.HOOD_LIGHT_LEVEL_AVAILABILITY) + + mwave_entities = [ + GeErdBinarySensor(self, ErdCode.MICROWAVE_REMOTE_ENABLE), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "status"), + GeErdPropertyBinarySensor(self, ErdCode.MICROWAVE_STATE, "door_status", device_class_override="door"), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "cook_mode", icon_override="mdi:food-turkey"), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "power_level", icon_override="mdi:gauge"), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "temperature", icon_override="mdi:thermometer"), + GeErdTimerSensor(self, ErdCode.MICROWAVE_COOK_TIMER), + GeErdTimerSensor(self, ErdCode.MICROWAVE_KITCHEN_TIMER) + ] + + if fan_availability and fan_availability.is_available: + mwave_entities.append(GeHoodFanSpeedSelect(self, ErdCode.HOOD_FAN_SPEED)) + #for now, represent as a select + if light_availability and light_availability.is_available: + mwave_entities.append(GeHoodLightLevelSelect(self, ErdCode.HOOD_LIGHT_LEVEL)) + + entities = base_entities + mwave_entities + return entities + diff --git a/devices/oim.py b/devices/oim.py new file mode 100644 index 0000000..2eebd39 --- /dev/null +++ b/devices/oim.py @@ -0,0 +1,41 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdOnOff +) + +from .base import ApplianceApi +from ..entities import ( + OimLightLevelOptionsConverter, + GeErdSensor, + GeErdBinarySensor, + GeErdSelect, + GeErdSwitch, + ErdOnOffBoolConverter +) + +_LOGGER = logging.getLogger(__name__) + + +class OimApi(ApplianceApi): + """API class for Opal Ice Maker objects""" + APPLIANCE_TYPE = ErdApplianceType.OPAL_ICE_MAKER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + oim_entities = [ + GeErdSensor(self, ErdCode.OIM_STATUS), + GeErdBinarySensor(self, ErdCode.OIM_FILTER_STATUS, device_class_override="problem"), + GeErdBinarySensor(self, ErdCode.OIM_NEEDS_DESCALING, device_class_override="problem"), + GeErdSelect(self, ErdCode.OIM_LIGHT_LEVEL, OimLightLevelOptionsConverter()), + GeErdSwitch(self, ErdCode.OIM_POWER, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + ] + + entities = base_entities + oim_entities + return entities + diff --git a/devices/oven.py b/devices/oven.py new file mode 100644 index 0000000..914b38e --- /dev/null +++ b/devices/oven.py @@ -0,0 +1,146 @@ +import logging +from typing import List +from gehomesdk.erd.erd_data_type import ErdDataType + +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + OvenConfiguration, + ErdCooktopConfig, + CooktopStatus, + ErdOvenLightLevel, + ErdOvenLightLevelAvailability, + ErdOvenWarmingState +) + +from .base import ApplianceApi +from ..entities import ( + GeErdSensor, + GeErdTimerSensor, + GeErdBinarySensor, + GeErdPropertySensor, + GeErdPropertyBinarySensor, + GeOven, + GeOvenLightLevelSelect, + GeOvenWarmingStateSelect, + UPPER_OVEN, + LOWER_OVEN +) + +_LOGGER = logging.getLogger(__name__) + +class OvenApi(ApplianceApi): + """API class for oven objects""" + APPLIANCE_TYPE = ErdApplianceType.OVEN + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + oven_config: OvenConfiguration = self.appliance.get_erd_value(ErdCode.OVEN_CONFIGURATION) + + cooktop_config = ErdCooktopConfig.NONE + if self.has_erd_code(ErdCode.COOKTOP_CONFIG): + cooktop_config: ErdCooktopConfig = self.appliance.get_erd_value(ErdCode.COOKTOP_CONFIG) + + has_upper_raw_temperature = self.has_erd_code(ErdCode.UPPER_OVEN_RAW_TEMPERATURE) + has_lower_raw_temperature = self.has_erd_code(ErdCode.LOWER_OVEN_RAW_TEMPERATURE) + + has_upper_probe_temperature = self.has_erd_code(ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP) + has_lower_probe_temperature = self.has_erd_code(ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP) + + upper_light : ErdOvenLightLevel = self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT) + upper_light_availability: ErdOvenLightLevelAvailability = self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT_AVAILABILITY) + lower_light : ErdOvenLightLevel = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT) + lower_light_availability: ErdOvenLightLevelAvailability = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT_AVAILABILITY) + + upper_warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE) + lower_warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE) + warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.WARMING_DRAWER_STATE) + + _LOGGER.debug(f"Oven Config: {oven_config}") + _LOGGER.debug(f"Cooktop Config: {cooktop_config}") + oven_entities = [] + cooktop_entities = [] + + if oven_config.has_lower_oven: + oven_entities.extend([ + GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE), + GeErdSensor(self, ErdCode.LOWER_OVEN_CURRENT_STATE), + GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING), + GeErdTimerSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER), + GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET), + GeErdSensor(self, ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE), + GeErdBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED), + + GeOven(self, LOWER_OVEN, True, self._temperature_code(has_lower_raw_temperature)) + ]) + if has_lower_raw_temperature: + oven_entities.append(GeErdSensor(self, ErdCode.LOWER_OVEN_RAW_TEMPERATURE)) + if lower_light_availability is None or lower_light_availability.is_available or lower_light is not None: + oven_entities.append(GeOvenLightLevelSelect(self, ErdCode.LOWER_OVEN_LIGHT)) + if lower_warm_drawer is not None: + oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE)) + if has_lower_probe_temperature: + oven_entities.append(GeErdSensor(self, ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP)) + + oven_entities.extend([ + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_CURRENT_STATE, self._single_name(ErdCode.UPPER_OVEN_CURRENT_STATE, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, ~oven_config.has_lower_oven)), + GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, self._single_name(ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, ~oven_config.has_lower_oven)), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED, ~oven_config.has_lower_oven)), + + GeOven(self, UPPER_OVEN, False, self._temperature_code(has_upper_raw_temperature)) + ]) + if has_upper_raw_temperature: + oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_RAW_TEMPERATURE, ~oven_config.has_lower_oven))) + if upper_light_availability is None or upper_light_availability.is_available or upper_light is not None: + oven_entities.append(GeOvenLightLevelSelect(self, ErdCode.UPPER_OVEN_LIGHT, self._single_name(ErdCode.UPPER_OVEN_LIGHT, ~oven_config.has_lower_oven))) + if upper_warm_drawer is not None: + oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, self._single_name(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, ~oven_config.has_lower_oven))) + if has_upper_probe_temperature: + oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, self._single_name(ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, ~oven_config.has_lower_oven))) + + if oven_config.has_warming_drawer and warm_drawer is not None: + oven_entities.append(GeErdSensor(self, ErdCode.WARMING_DRAWER_STATE)) + + if cooktop_config == ErdCooktopConfig.PRESENT: + # attempt to get the cooktop status using legacy status + cooktop_status_erd = ErdCode.COOKTOP_STATUS + cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) + + # if we didn't get it, try using the new version + if cooktop_status is None: + cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT + cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) + + # if we got a status through either mechanism, we can add the entities + if cooktop_status is not None: + cooktop_entities.append(GeErdBinarySensor(self, cooktop_status_erd)) + + for (k, v) in cooktop_status.burners.items(): + if v.exists: + prop = self._camel_to_snake(k) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on")) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized")) + if not v.on_off_only: + cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) + + return base_entities + oven_entities + cooktop_entities + + def _single_name(self, erd_code: ErdCode, make_single: bool): + name = erd_code.name + + if make_single: + name = name.replace(UPPER_OVEN+"_","") + + return name.replace("_", " ").title() + + def _camel_to_snake(self, s): + return ''.join(['_'+c.lower() if c.isupper() else c for c in s]).lstrip('_') + + def _temperature_code(self, has_raw: bool): + return "RAW_TEMPERATURE" if has_raw else "DISPLAY_TEMPERATURE" diff --git a/devices/pac.py b/devices/pac.py new file mode 100644 index 0000000..fa2da9d --- /dev/null +++ b/devices/pac.py @@ -0,0 +1,31 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GePacClimate, GeErdSensor, GeErdSwitch, ErdOnOffBoolConverter + +_LOGGER = logging.getLogger(__name__) + + +class PacApi(ApplianceApi): + """API class for Portable AC objects""" + APPLIANCE_TYPE = ErdApplianceType.PORTABLE_AIR_CONDITIONER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + pac_entities = [ + GePacClimate(self), + GeErdSensor(self, ErdCode.AC_TARGET_TEMPERATURE), + GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE), + GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), + GeErdSensor(self, ErdCode.AC_OPERATION_MODE), + GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + ] + + entities = base_entities + pac_entities + return entities + diff --git a/devices/sac.py b/devices/sac.py new file mode 100644 index 0000000..a1dfad5 --- /dev/null +++ b/devices/sac.py @@ -0,0 +1,37 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeSacClimate, GeErdSensor, GeErdSwitch, ErdOnOffBoolConverter + +_LOGGER = logging.getLogger(__name__) + + +class SacApi(ApplianceApi): + """API class for Split AC objects""" + APPLIANCE_TYPE = ErdApplianceType.SPLIT_AIR_CONDITIONER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + sac_entities = [ + GeSacClimate(self), + GeErdSensor(self, ErdCode.AC_TARGET_TEMPERATURE), + GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE), + GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), + GeErdSensor(self, ErdCode.AC_OPERATION_MODE), + GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + ] + + if self.has_erd_code(ErdCode.SAC_SLEEP_MODE): + sac_entities.append(GeErdSwitch(self, ErdCode.SAC_SLEEP_MODE, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:sleep", icon_off_override="mdi:sleep-off")) + if self.has_erd_code(ErdCode.SAC_AUTO_SWING_MODE): + sac_entities.append(GeErdSwitch(self, ErdCode.SAC_AUTO_SWING_MODE, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:arrow-decision-auto", icon_off_override="mdi:arrow-decision-auto-outline")) + + + entities = base_entities + sac_entities + return entities + diff --git a/devices/ucim.py b/devices/ucim.py new file mode 100644 index 0000000..f4b60e3 --- /dev/null +++ b/devices/ucim.py @@ -0,0 +1,45 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdOnOff +) + +from .base import ApplianceApi +from ..entities import ( + OimLightLevelOptionsConverter, + GeErdSensor, + GeErdBinarySensor, + GeErdSelect, + GeErdSwitch, + ErdOnOffBoolConverter +) + +_LOGGER = logging.getLogger(__name__) + + +class UcimApi(ApplianceApi): + """API class for Opal Ice Maker objects""" + APPLIANCE_TYPE = ErdApplianceType.OPAL_ICE_MAKER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + oim_entities = [ + GeErdSensor(self, ErdCode.OIM_STATUS), + GeErdBinarySensor(self, ErdCode.OIM_FILTER_STATUS, device_class_override="problem"), + GeErdBinarySensor(self, ErdCode.OIM_NEEDS_DESCALING, device_class_override="problem"), + GeErdSelect(self, ErdCode.OIM_LIGHT_LEVEL, OimLightLevelOptionsConverter()), + GeErdSwitch(self, ErdCode.OIM_POWER, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + GeErdSensor(self, ErdCode.OIM_PRODUCTION), + GeErdSensor(self, ErdCode.UCIM_CLEAN_STATUS), + GeErdSensor(self, ErdCode.UCIM_FILTER_PERCENTAGE_USED), + GeErdBinarySensor(self, ErdCode.UCIM_BIN_FULL, device_class_override="problem"), + ] + + entities = base_entities + oim_entities + return entities + diff --git a/devices/wac.py b/devices/wac.py new file mode 100644 index 0000000..6208a82 --- /dev/null +++ b/devices/wac.py @@ -0,0 +1,33 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeWacClimate, GeErdSensor, GeErdBinarySensor, GeErdSwitch, ErdOnOffBoolConverter + +_LOGGER = logging.getLogger(__name__) + + +class WacApi(ApplianceApi): + """API class for Window AC objects""" + APPLIANCE_TYPE = ErdApplianceType.AIR_CONDITIONER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + wac_entities = [ + GeWacClimate(self), + GeErdSensor(self, ErdCode.AC_TARGET_TEMPERATURE), + GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE), + GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), + GeErdSensor(self, ErdCode.AC_OPERATION_MODE), + GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + GeErdBinarySensor(self, ErdCode.AC_FILTER_STATUS, device_class_override="problem"), + GeErdSensor(self, ErdCode.WAC_DEMAND_RESPONSE_STATE), + GeErdSensor(self, ErdCode.WAC_DEMAND_RESPONSE_POWER, uom_override="kW"), + ] + entities = base_entities + wac_entities + return entities + diff --git a/devices/washer.py b/devices/washer.py new file mode 100644 index 0000000..9cc0372 --- /dev/null +++ b/devices/washer.py @@ -0,0 +1,63 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor + +_LOGGER = logging.getLogger(__name__) + + +class WasherApi(ApplianceApi): + """API class for washer objects""" + APPLIANCE_TYPE = ErdApplianceType.WASHER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + common_entities = [ + GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_STATE), + GeErdSensor(self, ErdCode.LAUNDRY_CYCLE, icon_override="mdi:state-machine"), + GeErdSensor(self, ErdCode.LAUNDRY_SUB_CYCLE, icon_override="mdi:state-machine"), + GeErdBinarySensor(self, ErdCode.LAUNDRY_END_OF_CYCLE), + GeErdSensor(self, ErdCode.LAUNDRY_TIME_REMAINING), + GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS), + ] + + washer_entities = self.get_washer_entities() + + entities = base_entities + common_entities + washer_entities + return entities + + def get_washer_entities(self) -> List[Entity]: + washer_entities = [ + GeErdSensor(self, ErdCode.LAUNDRY_WASHER_SOIL_LEVEL, icon_override="mdi:emoticon-poop"), + GeErdSensor(self, ErdCode.LAUNDRY_WASHER_WASHTEMP_LEVEL), + GeErdSensor(self, ErdCode.LAUNDRY_WASHER_SPINTIME_LEVEL, icon_override="mdi:speedometer"), + GeErdSensor(self, ErdCode.LAUNDRY_WASHER_RINSE_OPTION, icon_override="mdi:shimmer"), + ] + + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_DOOR_LOCK): + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_DOOR_LOCK)]) + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_TANK_STATUS): + washer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_WASHER_TANK_STATUS)]) + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_TANK_SELECTED): + washer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_WASHER_TANK_SELECTED)]) + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_TIMESAVER): + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_TIMESAVER, icon_on_override="mdi:sort-clock-ascending", icon_off_override="mdi:sort-clock-ascending-outline")]) + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_POWERSTEAM): + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_POWERSTEAM, icon_on_override="mdi:kettle-steam", icon_off_override="mdi:kettle-steam-outline")]) + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_PREWASH): + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_PREWASH, icon_on_override="mdi:water-plus", icon_off_override="mdi:water-remove-outline")]) + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_TUMBLECARE): + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_TUMBLECARE)]) + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_SMART_DISPENSE): + washer_entities.extend([GeErdPropertySensor(self, ErdCode.LAUNDRY_WASHER_SMART_DISPENSE, "loads_left", uom_override="loads")]) + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_SMART_DISPENSE_TANK_STATUS): + washer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_WASHER_SMART_DISPENSE_TANK_STATUS)]) + + return washer_entities diff --git a/devices/washer_dryer.py b/devices/washer_dryer.py new file mode 100644 index 0000000..f701ff2 --- /dev/null +++ b/devices/washer_dryer.py @@ -0,0 +1,36 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ErdCode, ErdApplianceType + +from .washer import WasherApi +from .dryer import DryerApi +from ..entities import GeErdSensor, GeErdBinarySensor + +_LOGGER = logging.getLogger(__name__) + +class WasherDryerApi(WasherApi, DryerApi): + """API class for washer/dryer objects""" + APPLIANCE_TYPE = ErdApplianceType.COMBINATION_WASHER_DRYER + + def get_all_entities(self) -> List[Entity]: + base_entities = self.get_base_entities() + + common_entities = [ + GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_STATE), + GeErdSensor(self, ErdCode.LAUNDRY_CYCLE), + GeErdSensor(self, ErdCode.LAUNDRY_SUB_CYCLE), + GeErdBinarySensor(self, ErdCode.LAUNDRY_END_OF_CYCLE), + GeErdSensor(self, ErdCode.LAUNDRY_TIME_REMAINING), + GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS), + ] + + washer_entities = self.get_washer_entities() + dryer_entities = self.get_dryer_entities() + + entities = base_entities + common_entities + washer_entities + dryer_entities + return entities + diff --git a/devices/water_filter.py b/devices/water_filter.py new file mode 100644 index 0000000..7cebd6a --- /dev/null +++ b/devices/water_filter.py @@ -0,0 +1,38 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import ( + GeErdSensor, + GeErdPropertySensor, + GeErdBinarySensor, + GeErdFilterPositionSelect, +) + +_LOGGER = logging.getLogger(__name__) + + +class WaterFilterApi(ApplianceApi): + """API class for water filter objects""" + + APPLIANCE_TYPE = ErdApplianceType.POE_WATER_FILTER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + wf_entities = [ + GeErdSensor(self, ErdCode.WH_FILTER_MODE), + GeErdSensor(self, ErdCode.WH_FILTER_VALVE_STATE, icon_override="mdi:state-machine"), + GeErdFilterPositionSelect(self, ErdCode.WH_FILTER_POSITION), + GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE, icon_on_override="mdi:human", icon_off_override="mdi:robot"), + GeErdBinarySensor(self, ErdCode.WH_FILTER_LEAK_VALIDITY, device_class_override="moisture"), + GeErdPropertySensor(self, ErdCode.WH_FILTER_FLOW_RATE, "flow_rate"), + GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE, device_class_override="water"), + GeErdPropertySensor(self, ErdCode.WH_FILTER_LIFE_REMAINING, "life_remaining"), + GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"), + ] + entities = base_entities + wf_entities + return entities diff --git a/devices/water_heater.py b/devices/water_heater.py new file mode 100644 index 0000000..e571a75 --- /dev/null +++ b/devices/water_heater.py @@ -0,0 +1,54 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdOnOff +) + +from custom_components.ge_home.entities.water_heater.ge_water_heater import GeWaterHeater + +from .base import ApplianceApi +from ..entities import ( + GeErdSensor, + GeErdBinarySensor, + GeErdSelect, + GeErdSwitch, + ErdOnOffBoolConverter +) + +_LOGGER = logging.getLogger(__name__) + + +class WaterHeaterApi(ApplianceApi): + """API class for Water Heater objects""" + APPLIANCE_TYPE = ErdApplianceType.WATER_HEATER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + boost_mode: ErdOnOff = self.try_get_erd_value(ErdCode.WH_HEATER_BOOST_STATE) + active: ErdOnOff = self.try_get_erd_value(ErdCode.WH_HEATER_ACTIVE_STATE) + + wh_entities = [ + GeErdSensor(self, ErdCode.WH_HEATER_TARGET_TEMPERATURE), + GeErdSensor(self, ErdCode.WH_HEATER_TEMPERATURE), + GeErdSensor(self, ErdCode.WH_HEATER_MODE_HOURS_REMAINING), + GeErdSensor(self, ErdCode.WH_HEATER_ELECTRIC_MODE_MAX_TIME), + GeErdSensor(self, ErdCode.WH_HEATER_VACATION_MODE_MAX_TIME), + GeWaterHeater(self) + ] + + if(boost_mode and boost_mode != ErdOnOff.NA): + wh_entities.append(GeErdSensor(self, ErdCode.WH_HEATER_BOOST_STATE)) + wh_entities.append(GeErdSwitch(self, ErdCode.WH_HEATER_BOOST_CONTROL, ErdOnOffBoolConverter(), icon_on_override="mdi:rocket-launch", icon_off_override="mdi:rocket-launch-outline")) + + if(active and active != ErdOnOff.NA): + wh_entities.append(GeErdSensor(self, ErdCode.WH_HEATER_ACTIVE_STATE)) + wh_entities.append(GeErdSwitch(self, ErdCode.WH_HEATER_ACTIVE_CONTROL, ErdOnOffBoolConverter(), icon_on_override="mdi:power", icon_off_override="mdi:power-standby")) + + entities = base_entities + wh_entities + return entities + diff --git a/devices/water_softener.py b/devices/water_softener.py new file mode 100644 index 0000000..a0afa83 --- /dev/null +++ b/devices/water_softener.py @@ -0,0 +1,38 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import ( + GeErdSensor, + GeErdPropertySensor, + GeErdBinarySensor, + GeErdShutoffPositionSelect, +) + +_LOGGER = logging.getLogger(__name__) + + +class WaterSoftenerApi(ApplianceApi): + """API class for water softener objects""" + + APPLIANCE_TYPE = ErdApplianceType.WATER_SOFTENER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + ws_entities = [ + GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE, icon_on_override="mdi:human", icon_off_override="mdi:robot"), + GeErdPropertySensor(self, ErdCode.WH_FILTER_FLOW_RATE, "flow_rate"), + GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"), + GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE, device_class_override="water"), + GeErdSensor(self, ErdCode.WH_SOFTENER_ERROR_CODE, icon_override="mdi:alert-circle"), + GeErdBinarySensor(self, ErdCode.WH_SOFTENER_LOW_SALT, icon_on_override="mdi:alert", icon_off_override="mdi:grain"), + GeErdSensor(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_STATE, icon_override="mdi:state-machine"), + GeErdSensor(self, ErdCode.WH_SOFTENER_SALT_LIFE_REMAINING, icon_override="mdi:calendar-clock"), + GeErdShutoffPositionSelect(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_CONTROL), + ] + entities = base_entities + ws_entities + return entities diff --git a/entities/__init__.py b/entities/__init__.py new file mode 100644 index 0000000..c063f8a --- /dev/null +++ b/entities/__init__.py @@ -0,0 +1,13 @@ +from .common import * +from .dishwasher import * +from .fridge import * +from .oven import * +from .water_filter import * +from .advantium import * +from .ac import * +from .hood import * +from .water_softener import * +from .water_heater import * +from .opal_ice_maker import * +from .ccm import * +from .dehumidifier import * \ No newline at end of file diff --git a/entities/__pycache__/__init__.cpython-313.pyc b/entities/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..300de7dd680ab99f505337f520bce3239d08c6d8 GIT binary patch literal 461 zcmX}o!Ab)$5CG80Zmn%?3swcC2wpr?#J%aE;tvR3`T-#%*-fxX$|l?5(Tji3kMNhm zdh+B==nt4}YvwR{12bWg&0x@j5bx7#F+~91P4OSe9-EJVHOyd!p5Y<6+6hG^QE5~w z>L98ebr{u&>PGdVj-vXFie!@hge_r$Ji2+A41Aj!rHuAHCaqWzD|qSKc_~@JeakGl z_Wc!cyu`Vjhr@RXTN3SLrF=rDZwX_VAMRSL$sO0BX+I)(GwFwBLFQOejum+gbxNu7 zea1zlB$K)1KpYrz^H9D6?Ml0)#!eO_C(Of&56&wqrcoqujB)1vHQcH*N-Qvx6 x9+hF$oIjPP;hN@s_BN#76d_c@Sq&H4d{V=>hVvSZYY4vFmLZS6U|f#Ez#joxeA@s3 literal 0 HcmV?d00001 diff --git a/entities/ac/__init__.py b/entities/ac/__init__.py new file mode 100644 index 0000000..aefb995 --- /dev/null +++ b/entities/ac/__init__.py @@ -0,0 +1,4 @@ +from .ge_wac_climate import GeWacClimate +from .ge_sac_climate import GeSacClimate +from .ge_pac_climate import GePacClimate +from .ge_biac_climate import GeBiacClimate diff --git a/entities/ac/__pycache__/__init__.cpython-313.pyc b/entities/ac/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd73fb19b7123afbf59280cc444a93184850cc13 GIT binary patch literal 366 zcmey&%ge<81dZ0J8Own5V-N=hn4pZ$CP2nihG2$ZMsEf$#v(=q5Sz)H$&0y&8O&z( zX7OSzVg<8VyxF|ii`W$y(pfdxUNQo;YBJv9aZe3TOm@!6%uOswh46#X_yNd#UiVa| zOt^rbCdVzl^wjwB#N_y7h#5u9K!d@8#YloIFu?*OK~|6;KTvxT&_uX?HlXNAhR;Bf z;g+R-a(-S~X1acIX>mz@ZhUfnZb5!tYF4BO~Ko1`P@CSJ8pbeKGkSGs%TjYd>K5@>>>}I`j zY@}Y<=iWKz+^=)a@7%MSkw}n0`_WMM{Hl+Tf8s~2dELS0PhhY@6r#`zgkkTwK&L#+ zGbJ!#N@QXJdBO$nl#lsfEGpg${waw`36dtMJ+J2I6$3m7TQk^6UBFcDE_oG zAh_JH&6R)~NQdj(NS?~Uhp2K%7Pv58TH@lxf?;xLX3;F@rA$$#Ja&%Fjb~rZloZW0 z3#I%+59k9w?&ug3dg*nInHtMFdpWdz*&oCA3LKnKg)omonUEG!wvigu3F)VbCoT4S z6yX55Dqf|`m-Z`ST1p3SDd2mNKj2PcLB*F2fIQSMDt?r(FmOV^iMTUF1+EMHs1jhY zd{7SYHv1aW`kdB$^oIdhmq+KcT&7$!Ro%XQ7B5;GMkT{rN| zf1qvSFNoaxdZt*`wnSmwRd+ybg}m6{tXY~uCpNX3u~IcWHpmsKcm`Y-qqvK7fHAMs z9hX8-G&Mz-rBQOyQ%g4qGE25Yv*Brn^+tqtlB2SCiPHq9>JJV~=;h*EqNJM%Lo*W) z;|YxMgsCTL?(&uw7d6KHOj|5wvKsfR7iZ3&KbPYEH!`dQE@KEl*J-Q+##`LBC&vI= zj$>5E$NbpyJ@~9P#)2^gWSLYwVxZ$e>*3o2za9G3(62`-or6~A;N8}{%0}n7)%wy} z_@yURj|X(Zt#8lVoLPHu@b135{d#%k_eG%jMS#2WzT3chXV5oDw|Ns9q&x46iMFXV%-vavK4@w?&pqK! zVU#qrSz5zHs%F61?n9m07CB_;#ef}yynHzDZA^AXgni{~+Z4+QZO3bcG0Z?D9= zta#UldZp)#)pO8b&p8Tk*I zJsYv)nv^s!R^FUAF>#Xo@uX+sl)z%ZxFpB%@hxQ*G${N58cV)eyS#&POk!f%s|aZ*?(n8i)`?rnI!W=ez$_V|EZ7B4I8WG- zH&ZGuK>@mL<<<+!Y$<2ZmiWIp24s{r7BsKYS@Ip&_e$-sBvpUyK-q0I94DZ%bFc<) zu~7!cG4!J2RD_)DmJR_`%@#8Tc*nufvf~}wJIc4*7OiOC_BSwSyz4ISN8JK2&20@?ZyT%h3)_@u+f)d(Ot)Kk z=6z~MJ7FZRb|S?fNNmkJ?6y6Fbo+I3}FM&|Ku(57(jZw4%b2hTni3d{{spI z>O=!hmZRKX)bn{LjkwQTTEr3w1Hw5I-h&778ved8=Dug!ocrLn$AVsB_*$dTya@9l zy6rpSVVuNbpAWD9G~;k-*hzh(YzhQ;8Dk7cRiKpql|=tedMwiOFVg)t(*2KMOC@-4 mJ$Ufe7&-6`uVgcPUb0EZdbWTU!0up;U=(Be_i)Go}PtHl>IF*K+EL?RK+T(M-u* zc6RAj4P2`|1TcDNaxkI*0g_9A2r$qT2+$Ng6zwTT(N>^ztH1@C9&{rh2R3@@d%Gk> zv8*r60Q+XtCCxRLU&z~zE2c|M-ZHg9jB&-w*OD_YTPp^PkId4Qq@FGrR6{v(_wWc1g_EUH zA+MRbku2r+SY)!UG-u2d@Tfr*aK%f>MK!&+a2
  • {!&he_Va8+KgNk@qV2K-K;D zPL$T%pyi>)c88zlor#)O1WvV216e0uMtk0lycK!(=kMKm=hnR&I}*0zzJ<1*-mmbS#yZ}Tu2ATn9*tSe-msgDkNg;`=U-3RmfsE+SW@*G>=5B56n zLz{oG*?uxiE^z&1m5H&7zv!K@%7qo#v>e$s92ql#Tm;0+jwQ!J9=7OlIFJT`xZ&Bw z+05K^RY_i(&CqWkzX$nT&T!nIx{#imo6WevpJ~+0n>kk~mmraBvh)OM_5ramaI#EK z;;JL76j11cMYSD@OX5!j)_*UE;-;?yStnIil*Vd6+wT9?PJg1(pV;+*=grSo`p?6B zCpu7x4r~ui?F=Ohv=C!KCM@~GchFEEAH{AW(^qurp-}9f0 z{CVt;V}Ba|q<`zwmF;k9w|iu#d$iI$dOtk+ZyU40ual$6aq^e(?@piP4#60M;KA7W zdSToSV!+c>%$tU6)n7Ru z@kOmv0tm{h7L{X?2Y4!j!&I4Q`;$kuT2ZYVu_;5oi&O$bvd~5v=>GcuLnJY z1fOpj1L7W2AtgW!qN|z%so~YKW7w)n$4~*^6fcEP%$6R(D`+nOd4qf|M5-(!B$&*W+Ae7^$D8YU4R(&$DH z(yolv6?gdB{!}fl}4ZyOZWzr7Z76I=@ zS2Yu_pHa&-Hf;pNg}s~wu*#smkGy2%1Wgu0KVVJ*@ z&WGeog`D{pnfih}`vn<#NX|VZ*Z&cEW+&8pKh(Q%`9Wx8J@|<4VFtE_o_j=~e>BN4 Ox0yGud_}PLob?~r)W978 literal 0 HcmV?d00001 diff --git a/entities/ac/__pycache__/ge_pac_climate.cpython-313.pyc b/entities/ac/__pycache__/ge_pac_climate.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16bc5dd5d8070210fa5439678d9c4a9065156922 GIT binary patch literal 5230 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 I&Un|q0M1DDLI3~& literal 0 HcmV?d00001 diff --git a/entities/ac/__pycache__/ge_sac_climate.cpython-313.pyc b/entities/ac/__pycache__/ge_sac_climate.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e3856ab24f1ea262428b9db6c4f87fa9185ffad GIT binary patch literal 5602 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 N^X94V2==bC{tFP^y)ys+ literal 0 HcmV?d00001 diff --git a/entities/ac/fan_mode_options.py b/entities/ac/fan_mode_options.py new file mode 100644 index 0000000..c8a50e6 --- /dev/null +++ b/entities/ac/fan_mode_options.py @@ -0,0 +1,45 @@ +import logging +from typing import Any, List, Optional + +from gehomesdk import ErdAcFanSetting +from ..common import OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class AcFanModeOptionsConverter(OptionsConverter): + def __init__(self, default_option: ErdAcFanSetting = ErdAcFanSetting.AUTO): + self._default = default_option + + @property + def options(self) -> List[str]: + return [i.stringify() for i in [ErdAcFanSetting.AUTO, ErdAcFanSetting.LOW, ErdAcFanSetting.MED, ErdAcFanSetting.HIGH]] + + def from_option_string(self, value: str) -> Any: + try: + return ErdAcFanSetting[value.upper().replace(" ","_")] + except: + _LOGGER.warning(f"Could not set fan mode to {value}") + return self._default + + def to_option_string(self, value: Any) -> Optional[str]: + try: + return { + ErdAcFanSetting.AUTO: ErdAcFanSetting.AUTO, + ErdAcFanSetting.LOW: ErdAcFanSetting.LOW, + ErdAcFanSetting.LOW_AUTO: ErdAcFanSetting.AUTO, + ErdAcFanSetting.MED: ErdAcFanSetting.MED, + ErdAcFanSetting.MED_AUTO: ErdAcFanSetting.AUTO, + ErdAcFanSetting.HIGH: ErdAcFanSetting.HIGH, + ErdAcFanSetting.HIGH_AUTO: ErdAcFanSetting.HIGH + }.get(value).stringify() + except: + pass + return self._default.stringify() + +class AcFanOnlyFanModeOptionsConverter(AcFanModeOptionsConverter): + def __init__(self): + super().__init__(ErdAcFanSetting.LOW) + + @property + def options(self) -> List[str]: + return [i.stringify() for i in [ErdAcFanSetting.LOW, ErdAcFanSetting.MED, ErdAcFanSetting.HIGH]] diff --git a/entities/ac/ge_biac_climate.py b/entities/ac/ge_biac_climate.py new file mode 100644 index 0000000..db25033 --- /dev/null +++ b/entities/ac/ge_biac_climate.py @@ -0,0 +1,41 @@ +import logging +from typing import Any, List, Optional + +from homeassistant.components.climate import HVACMode +from gehomesdk import ErdAcOperationMode +from ...devices import ApplianceApi +from ..common import GeClimate, OptionsConverter +from .fan_mode_options import AcFanModeOptionsConverter, AcFanOnlyFanModeOptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class BiacHvacModeOptionsConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [HVACMode.AUTO, HVACMode.COOL, HVACMode.FAN_ONLY] + def from_option_string(self, value: str) -> Any: + try: + return { + HVACMode.AUTO: ErdAcOperationMode.ENERGY_SAVER, + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY + }.get(value) + except: + _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") + return ErdAcOperationMode.COOL + def to_option_string(self, value: Any) -> Optional[str]: + try: + return { + ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, + ErdAcOperationMode.AUTO: HVACMode.AUTO, + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY + }.get(value) + except: + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") + return HVACMode.COOL + +class GeBiacClimate(GeClimate): + """Class for Built-In AC units""" + def __init__(self, api: ApplianceApi): + super().__init__(api, BiacHvacModeOptionsConverter(), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) diff --git a/entities/ac/ge_pac_climate.py b/entities/ac/ge_pac_climate.py new file mode 100644 index 0000000..42a7a98 --- /dev/null +++ b/entities/ac/ge_pac_climate.py @@ -0,0 +1,71 @@ +import logging +from typing import Any, List, Optional + +from homeassistant.components.climate import HVACMode +from gehomesdk import ErdCode, ErdAcOperationMode, ErdSacAvailableModes, ErdSacTargetTemperatureRange +from ...devices import ApplianceApi +from ..common import GeClimate, OptionsConverter +from .fan_mode_options import AcFanOnlyFanModeOptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class PacHvacModeOptionsConverter(OptionsConverter): + def __init__(self, available_modes: ErdSacAvailableModes): + self._available_modes = available_modes + + @property + def options(self) -> List[str]: + modes = [HVACMode.COOL, HVACMode.FAN_ONLY] + if self._available_modes and self._available_modes.has_heat: + modes.append(HVACMode.HEAT) + if self._available_modes and self._available_modes.has_dry: + modes.append(HVACMode.DRY) + return modes + def from_option_string(self, value: str) -> Any: + try: + return { + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.HEAT: ErdAcOperationMode.HEAT, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY, + HVACMode.DRY: ErdAcOperationMode.DRY + }.get(value) + except: + _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") + return ErdAcOperationMode.COOL + def to_option_string(self, value: Any) -> Optional[str]: + try: + return { + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.HEAT: HVACMode.HEAT, + ErdAcOperationMode.DRY: HVACMode.DRY, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY + }.get(value) + except: + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") + return HVACMode.COOL + +class GePacClimate(GeClimate): + """Class for Portable AC units""" + def __init__(self, api: ApplianceApi): + #initialize the climate control + super().__init__(api, None, AcFanOnlyFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) + + #get a couple ERDs that shouldn't change if available + self._modes: ErdSacAvailableModes = self.api.try_get_erd_value(ErdCode.SAC_AVAILABLE_MODES) + self._temp_range: ErdSacTargetTemperatureRange = self.api.try_get_erd_value(ErdCode.SAC_TARGET_TEMPERATURE_RANGE) + #construct the converter based on the available modes + self._hvac_mode_converter = PacHvacModeOptionsConverter(self._modes) + + @property + def min_temp(self) -> float: + temp = 64 + if self._temp_range: + temp = self._temp_range.min + return self._convert_temp(temp) + + @property + def max_temp(self) -> float: + temp = 86 + if self._temp_range: + temp = self._temp_range.max + return self._convert_temp(temp) \ No newline at end of file diff --git a/entities/ac/ge_sac_climate.py b/entities/ac/ge_sac_climate.py new file mode 100644 index 0000000..5b239c7 --- /dev/null +++ b/entities/ac/ge_sac_climate.py @@ -0,0 +1,75 @@ +import logging +from typing import Any, List, Optional + +from homeassistant.components.climate import HVACMode +from gehomesdk import ErdCode, ErdAcOperationMode, ErdSacAvailableModes, ErdSacTargetTemperatureRange +from ...devices import ApplianceApi +from ..common import GeClimate, OptionsConverter +from .fan_mode_options import AcFanOnlyFanModeOptionsConverter, AcFanModeOptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class SacHvacModeOptionsConverter(OptionsConverter): + def __init__(self, available_modes: ErdSacAvailableModes): + self._available_modes = available_modes + + @property + def options(self) -> List[str]: + modes = [HVACMode.COOL, HVACMode.FAN_ONLY] + if self._available_modes and self._available_modes.has_heat: + modes.append(HVACMode.HEAT) + modes.append(HVACMode.AUTO) + if self._available_modes and self._available_modes.has_dry: + modes.append(HVACMode.DRY) + return modes + def from_option_string(self, value: str) -> Any: + try: + return { + HVACMode.AUTO: ErdAcOperationMode.AUTO, + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.HEAT: ErdAcOperationMode.HEAT, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY, + HVACMode.DRY: ErdAcOperationMode.DRY + }.get(value) + except: + _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") + return ErdAcOperationMode.COOL + def to_option_string(self, value: Any) -> Optional[str]: + try: + return { + ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, + ErdAcOperationMode.AUTO: HVACMode.AUTO, + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.HEAT: HVACMode.HEAT, + ErdAcOperationMode.DRY: HVACMode.DRY, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY + }.get(value) + except: + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") + return HVACMode.COOL + +class GeSacClimate(GeClimate): + """Class for Split AC units""" + def __init__(self, api: ApplianceApi): + #initialize the climate control + super().__init__(api, None, AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) + + #get a couple ERDs that shouldn't change if available + self._modes: ErdSacAvailableModes = self.api.try_get_erd_value(ErdCode.SAC_AVAILABLE_MODES) + self._temp_range: ErdSacTargetTemperatureRange = self.api.try_get_erd_value(ErdCode.SAC_TARGET_TEMPERATURE_RANGE) + #construct the converter based on the available modes + self._hvac_mode_converter = SacHvacModeOptionsConverter(self._modes) + + @property + def min_temp(self) -> float: + temp = 60 + if self._temp_range: + temp = self._temp_range.min + return self._convert_temp(temp) + + @property + def max_temp(self) -> float: + temp = 86 + if self._temp_range: + temp = self._temp_range.max + return self._convert_temp(temp) \ No newline at end of file diff --git a/entities/ac/ge_wac_climate.py b/entities/ac/ge_wac_climate.py new file mode 100644 index 0000000..2754b90 --- /dev/null +++ b/entities/ac/ge_wac_climate.py @@ -0,0 +1,41 @@ +import logging +from typing import Any, List, Optional + +from homeassistant.components.climate import HVACMode +from gehomesdk import ErdAcOperationMode +from ...devices import ApplianceApi +from ..common import GeClimate, OptionsConverter +from .fan_mode_options import AcFanModeOptionsConverter, AcFanOnlyFanModeOptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class WacHvacModeOptionsConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [HVACMode.AUTO, HVACMode.COOL, HVACMode.FAN_ONLY] + def from_option_string(self, value: str) -> Any: + try: + return { + HVACMode.AUTO: ErdAcOperationMode.ENERGY_SAVER, + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY + }.get(value) + except: + _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") + return ErdAcOperationMode.COOL + def to_option_string(self, value: Any) -> Optional[str]: + try: + return { + ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, + ErdAcOperationMode.AUTO: HVACMode.AUTO, + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY + }.get(value) + except: + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") + return HVACMode.COOL + +class GeWacClimate(GeClimate): + """Class for Window AC units""" + def __init__(self, api: ApplianceApi): + super().__init__(api, WacHvacModeOptionsConverter(), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) diff --git a/entities/advantium/__init__.py b/entities/advantium/__init__.py new file mode 100644 index 0000000..a4cfe30 --- /dev/null +++ b/entities/advantium/__init__.py @@ -0,0 +1 @@ +from .ge_advantium import GeAdvantium \ No newline at end of file diff --git a/entities/advantium/__pycache__/__init__.cpython-313.pyc b/entities/advantium/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48a45c40a6d680b8f7f80d81ec62bf159e6266a5 GIT binary patch literal 212 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 literal 0 HcmV?d00001 diff --git a/entities/advantium/const.py b/entities/advantium/const.py new file mode 100644 index 0000000..81e6490 --- /dev/null +++ b/entities/advantium/const.py @@ -0,0 +1,5 @@ +from homeassistant.components.water_heater import WaterHeaterEntityFeature + +SUPPORT_NONE = WaterHeaterEntityFeature(0) +GE_ADVANTIUM_WITH_TEMPERATURE = (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) +GE_ADVANTIUM = WaterHeaterEntityFeature.OPERATION_MODE diff --git a/entities/advantium/ge_advantium.py b/entities/advantium/ge_advantium.py new file mode 100644 index 0000000..2088d36 --- /dev/null +++ b/entities/advantium/ge_advantium.py @@ -0,0 +1,283 @@ +"""GE Home Sensor Entities - Advantium""" +import logging +from typing import Any, Dict, List, Mapping, Optional, Set +from random import randrange + +from gehomesdk import ( + ErdCode, + ErdPersonality, + ErdAdvantiumCookStatus, + ErdAdvantiumCookSetting, + AdvantiumOperationMode, + AdvantiumCookSetting, + ErdAdvantiumRemoteCookModeConfig, + ADVANTIUM_OPERATION_MODE_COOK_SETTING_MAPPING +) +from gehomesdk.erd.values.advantium.advantium_enums import CookAction, CookMode + +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ATTR_TEMPERATURE +from ...const import DOMAIN +from ...devices import ApplianceApi +from ..common import GeAbstractWaterHeater +from .const import * + +_LOGGER = logging.getLogger(__name__) + +class GeAdvantium(GeAbstractWaterHeater): + """GE Appliance Advantium""" + + icon = "mdi:microwave" + + def __init__(self, api: ApplianceApi): + super().__init__(api) + + @property + def supported_features(self): + if self.remote_enabled: + return GE_ADVANTIUM_WITH_TEMPERATURE if self.can_set_temperature else GE_ADVANTIUM + else: + return SUPPORT_NONE + + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.serial_number}" + + @property + def name(self) -> Optional[str]: + return f"{self.serial_number} Advantium" + + @property + def personality(self) -> Optional[ErdPersonality]: + try: + return self.appliance.get_erd_value(ErdCode.PERSONALITY) + except: + return None + + @property + def remote_enabled(self) -> bool: + """Returns whether the oven is remote enabled""" + value = self.appliance.get_erd_value(ErdCode.UPPER_OVEN_REMOTE_ENABLED) + return value == True + + @property + def current_temperature(self) -> Optional[int]: + return self.appliance.get_erd_value(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE) + + @property + def current_operation(self) -> Optional[str]: + try: + return self.current_operation_mode.stringify() + except: + return None + + @property + def operation_list(self) -> List[str]: + invalid = [] + if not self._remote_config.broil_enable: + invalid.append(CookMode.BROIL) + if not self._remote_config.convection_bake_enable: + invalid.append(CookMode.CONVECTION_BAKE) + if not self._remote_config.proof_enable: + invalid.append(CookMode.PROOF) + if not self._remote_config.warm_enable: + invalid.append(CookMode.WARM) + + return [ + k.stringify() + for k, v in ADVANTIUM_OPERATION_MODE_COOK_SETTING_MAPPING.items() + if v.cook_mode not in invalid] + + @property + def current_cook_setting(self) -> ErdAdvantiumCookSetting: + """Get the current cook setting.""" + return self.appliance.get_erd_value(ErdCode.ADVANTIUM_COOK_SETTING) + + @property + def current_cook_status(self) -> ErdAdvantiumCookStatus: + """Get the current status.""" + return self.appliance.get_erd_value(ErdCode.ADVANTIUM_COOK_STATUS) + + @property + def current_operation_mode(self) -> AdvantiumOperationMode: + """Gets the current operation mode""" + return self._current_operation_mode + + @property + def current_operation_setting(self) -> Optional[AdvantiumCookSetting]: + if self.current_operation_mode is None: + return None + try: + return ADVANTIUM_OPERATION_MODE_COOK_SETTING_MAPPING[self.current_operation_mode] + except: + _LOGGER.debug(f"Unable to determine operation setting, mode = {self.current_operation_mode}") + return None + + @property + def can_set_temperature(self) -> bool: + """Indicates whether we can set the temperature based on the current mode""" + try: + return self.current_operation_setting.allow_temperature_set + except: + return False + + @property + def target_temperature(self) -> Optional[int]: + """Return the temperature we try to reach.""" + try: + cook_mode = self.current_cook_setting + if cook_mode.target_temperature and cook_mode.target_temperature > 0: + return cook_mode.target_temperature + except: + pass + return None + + @property + def min_temp(self) -> int: + """Return the minimum temperature.""" + min_temp, _ = self.appliance.get_erd_value(ErdCode.OVEN_MODE_MIN_MAX_TEMP) + return min_temp + + @property + def max_temp(self) -> int: + """Return the maximum temperature.""" + _, max_temp = self.appliance.get_erd_value(ErdCode.OVEN_MODE_MIN_MAX_TEMP) + return max_temp + + @property + def extra_state_attributes(self) -> Optional[Mapping[str, Any]]: + data = {} + + cook_time_remaining = self.appliance.get_erd_value(ErdCode.ADVANTIUM_COOK_TIME_REMAINING) + kitchen_timer = self.appliance.get_erd_value(ErdCode.ADVANTIUM_KITCHEN_TIME_REMAINING) + data["unit_type"] = self._stringify(self.personality) + if cook_time_remaining: + data["cook_time_remaining"] = self._stringify(cook_time_remaining) + if kitchen_timer: + data["kitchen_timer"] = self._stringify(kitchen_timer) + return data + + @property + def _remote_config(self) -> ErdAdvantiumRemoteCookModeConfig: + return self.appliance.get_erd_value(ErdCode.ADVANTIUM_REMOTE_COOK_MODE_CONFIG) + + async def async_set_operation_mode(self, operation_mode: str): + """Set the operation mode.""" + + #try to get the mode/setting for the selection + try: + mode = AdvantiumOperationMode(operation_mode) + setting = ADVANTIUM_OPERATION_MODE_COOK_SETTING_MAPPING[mode] + except: + _LOGGER.debug(f"Attempted to set mode to {operation_mode}, unknown.") + return + + #determine the target temp for this mode + target_temp = self._convert_target_temperature(setting.target_temperature_120v_f, setting.target_temperature_240v_f) + + #if we allow temperature to be set in this mode, and already have a temperature, use it + if setting.allow_temperature_set and self.target_temperature: + target_temp = self.target_temperature + + #by default we will start an operation, but handle other actions too + action = CookAction.START + if mode == AdvantiumOperationMode.OFF: + action = CookAction.STOP + elif self.current_cook_setting.cook_action == CookAction.PAUSE: + action = CookAction.RESUME + elif self.current_cook_setting.cook_action in [CookAction.START, CookAction.RESUME]: + action = CookAction.UPDATED + + #construct the new mode based on the existing mode + new_cook_mode = self.current_cook_setting + new_cook_mode.d = randrange(255) + new_cook_mode.target_temperature = target_temp + if(setting.target_power_level != 0): + new_cook_mode.power_level = setting.target_power_level + new_cook_mode.cook_mode = setting.cook_mode + new_cook_mode.cook_action = action + + await self.appliance.async_set_erd_value(ErdCode.ADVANTIUM_COOK_SETTING, new_cook_mode) + + async def async_set_temperature(self, **kwargs): + """Set the cook temperature""" + + target_temp = kwargs.get(ATTR_TEMPERATURE) + if target_temp is None: + return + + #get the current mode/operation + mode = self.current_operation_mode + setting = self.current_operation_setting + + #if we can't figure out the mode/setting, exit + if mode is None or setting is None: + return + + #if we're off or can't set temperature, just exit + if mode == AdvantiumOperationMode.OFF or not setting.allow_temperature_set: + return + + #should only need to update + action = CookAction.UPDATED + + #construct the new mode based on the existing mode + new_cook_mode = self.current_cook_setting + new_cook_mode.d = randrange(255) + new_cook_mode.target_temperature = target_temp + new_cook_mode.cook_action = action + + await self.appliance.async_set_erd_value(ErdCode.ADVANTIUM_COOK_SETTING, new_cook_mode) + + async def _ensure_operation_mode(self): + cook_setting = self.current_cook_setting + cook_mode = cook_setting.cook_mode + + #if we have a current mode + if(self._current_operation_mode is not None): + #and the cook mode is the same as what the appliance says, we'll just leave things alone + #and assume that things are in sync + if ADVANTIUM_OPERATION_MODE_COOK_SETTING_MAPPING[self._current_operation_mode].cook_mode == cook_mode: + return + else: + self._current_operation_mode = None + + #synchronize the operation mode with the device state + if cook_mode == CookMode.MICROWAVE: + #microwave matches on cook mode and power level + if cook_setting.power_level == 3: + self._current_operation_mode = AdvantiumOperationMode.MICROWAVE_PL3 + elif cook_setting.power_level == 5: + self._current_operation_mode = AdvantiumOperationMode.MICROWAVE_PL5 + elif cook_setting.power_level == 7: + self._current_operation_mode = AdvantiumOperationMode.MICROWAVE_PL7 + else: + self._current_operation_mode = AdvantiumOperationMode.MICROWAVE_PL10 + elif cook_mode == CookMode.WARM: + for key, value in ADVANTIUM_OPERATION_MODE_COOK_SETTING_MAPPING.items(): + #warm matches on the mode, warm status, and target temp + if (cook_mode == value.cook_mode and + cook_setting.warm_status == value.warm_status and + cook_setting.target_temperature == self._convert_target_temperature( + value.target_temperature_120v_f, value.target_temperature_240v_f)): + self._current_operation_mode = key + return + + #just pick the first match based on cook mode if we made it here + if self._current_operation_mode is None: + for key, value in ADVANTIUM_OPERATION_MODE_COOK_SETTING_MAPPING.items(): + if cook_mode == value.cook_mode: + self._current_operation_mode = key + return + + async def _convert_target_temperature(self, temp_120v: int, temp_240v: int): + unit_type = self.personality + target_temp_f = temp_240v if unit_type in [ErdPersonality.PERSONALITY_240V_MONOGRAM, ErdPersonality.PERSONALITY_240V_CAFE, ErdPersonality.PERSONALITY_240V_STANDALONE_CAFE] else temp_120v + if self.temperature_unit == SensorDeviceClass.FAHRENHEIT: + return float(target_temp_f) + else: + return (target_temp_f - 32.0) * (5/9) + + async def async_device_update(self, warning: bool) -> None: + await super().async_device_update(warning=warning) + await self._ensure_operation_mode() diff --git a/entities/ccm/__init__.py b/entities/ccm/__init__.py new file mode 100644 index 0000000..614d130 --- /dev/null +++ b/entities/ccm/__init__.py @@ -0,0 +1,5 @@ +from .ge_ccm_pot_not_present_binary_sensor import GeCcmPotNotPresentBinarySensor +from .ge_ccm_brew_strength import GeCcmBrewStrengthSelect +from .ge_ccm_brew_temperature import GeCcmBrewTemperatureNumber +from .ge_ccm_brew_cups import GeCcmBrewCupsNumber +from .ge_ccm_brew_settings import GeCcmBrewSettingsButton \ No newline at end of file diff --git a/entities/ccm/__pycache__/__init__.cpython-313.pyc b/entities/ccm/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87618fe0d6c491f24cffd716264fd7239d60de90 GIT binary patch literal 523 zcmZXQ!AiqG6h$XZ+q8vB!9u}>bfJrG3~q#~sI&_qfl`S3p&g%TpqUA8CP(k)JE1?|ON_PPK!!UxFPD?qcDo%$>*eZnbzw2~Q5=3(tqR*a3a>0+f#oyLLTgy$ znqTv5w99P!b+3VqGS~g4=b%&OhTrnq*dDU5IdVR!pN*{S_#UPro+~p`W{#jinn@%V zxCuaNg)}iJCngBbfkBX=Sp~2LVF(@V&;!H?;KHN`vou~p@!{c)PSZs1UhnmRF_8@Q zBsE6K*{GA9gusP}d7=!L^dvtXVqe~Lwx-!Fhfd>1?{Fz~+puK%6 z$PAnR`zkYZ2)(f_?JT==g_2KE=!#Su6?38DL`k|Z?Na4R#o*E*G7)H(e!}HEl9A#3 pEZJnO>sUReYq-H4RTX!y`?P)8mSyE^p0jbzZgY12&c=8^;2Q(Lp6rjZ)t{cV>2pkOvRUnRCC+J?Hm3=k{{B zAwc=&y{U~~HGn_Wpf9P!=-onN7i_S}GAt26@?dMWzN{_jLPy)M&1GZB6eij!JH4D* zN{e(J#$Xa`Iu3TGVph^~M(j<{+K@HeRRfEJ=^LV2Y*l$zkH^^s->-X4)8z}k*TsO; zrs10$x0TwBH7>d&o_p~oFS?DQ<8JWkCr-W1U7R0?@}{Ks5vtEI_6i7MgV1aubX%(! zvl#z4d|?RFHpW4uD(N!J65GT%Iu{)U5Y$eIOc_2PV(2nh>2Nfo$Ch!2P^kE;I0R&zFZX5HdJgn0G!r8vc zFsCp=K{}uebu%6T)W9-JLLKia(nEc%b>A{YPT7=;Dsz#EFw^jwY*RssrgJ|rjRvk+ zDUpsgGv+nDfH9$R40FZe$82%s#$8q{-?oLyJse_^2k}`;3k}KZ>(ND0<)6=oiv_pU zT=!}Pw=IKK1FvrQttM{-uEIo!X zGe9vMld9I+b(Jhsgj9OA}-0DKsO}5`e`kYU*W2ZZ+MAil!5In>=B2<}A?8 z-m9uZUO?52Ktz-#59s8>*~hf-2hASRseL;2JDvVVDrmmFGDF;?`fv!KvW@!S9W<_! zU4_8ub+rd&bi7{^cgmbB8r7s#=dGh^<-s)v2{<#pzdL%x3GN_JW&~oJX#_3 zMGMmjw&EbqVdO}#=Q1Y1%(1G9w@V0x3_FO#lD@ literal 0 HcmV?d00001 diff --git a/entities/ccm/__pycache__/ge_ccm_brew_settings.cpython-313.pyc b/entities/ccm/__pycache__/ge_ccm_brew_settings.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f72e52dfe930bdfb57a748d8117452d2e13df3e GIT binary patch literal 1214 zcmZ`&&2JM&6o0c{UXwTxLa5BM_(93rin6DQvy45GdD&DskU5>MJU@6G#|_wn0)rE&t0p8a&b z^_l>@*GfV5Q0UL2um=<1LzGM#K zh0}EXa!)Hh$x17tv79z|Pg_wRUF>w?C`dxS*ok_^;BsV@quWwfsWb_7U_rx_(7%c| z_pqpK5QKs-C=n(#wylfC7F;CMoP-DN154P{p%%)PaHuUxTX3BScN+Xsc4n0?hwUZ7 zA5*SWlr-hwxt{JiGlNbP>S7AgxBBsX7V~5e?iLo<#3z42`xd!t6_*=MA$>h5xng+G z1mCD*-!2yo7UCd$E3;*{!$syY7A28lOz4-glgrC@*wV(z&y22YZr-Y|Qs2xh$>SX{ zqU~kI!Z?tUF{%AMZ#TP93)5sLYSzN8RB0RYw>xRV6D4a+&RS`k*HDR6#APiE+xij@ zb|0s}k=l8xdp}C?=-J?8A2auOd|-RNgu6-Y4QDW|q26PfQ@sMgsXH30WSN zO?__^|8pwq1m}?TU>A;E7@zpP^|Ez1HhnNQ{cddL(4Beb&ipG!(c}3K7sx9zWBuV= zAb%kqGDmRL6-nJMYY-$sn=_W3V62@sy0Ml=8T+*x#6!zCV>^+MDvofsDdI(=Ghdph zqLiIRNT@(XAq$ie5l(<3O0vojFd>g)#<1s8=$!YwkoPQf3pvyI2Alm2jtycjk^S4B zL7$%;%bxuJ$i(TwyNVX}i}3 ktBpalxOsg~L}dFnZ{OO}-7 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 literal 0 HcmV?d00001 diff --git a/entities/ccm/__pycache__/ge_ccm_cached_value.cpython-313.pyc b/entities/ccm/__pycache__/ge_ccm_cached_value.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..806b94a7e5752c71ded60291632c5bec8a9cda36 GIT binary patch literal 1077 zcmZ`&&ubGw6n?Y$(QS82qEW1AX+v5gdugEHMa3Mdc&p%2@gNM_?PR;Ko2@gOinn@^ z2o>=Vyn5|_)1qS7Q^AwB(jZ=aZ#RvtwFCL)y?O6_JKvj^;q>$r;CgfK)>cmg_(9I; z%8!iU3>tlyfjZbw1)JT$Oj;5=&C0o%lOY>9!v-3Ccma~xAT^uGoUK(s=ItC}!OkNV z?Sh=}iq=F@ek@kqaMf|Q#O5<6=!&1jG;2G#jqey=pB zH+uPR`ofU(xTE%x{a5!6I(=0AG(A2K>+J3;0H*}-ynI<#*4YwOpXDBrY?yFK1S_yg z7BO*OlR9p40>RScByNd$ww;QYYc)ypluUZ#&Q~wAVH_h~)k4^X<06>l??(O5sDBQ= z7%PLu%2#u7FkkOI+s{;6XbdVhKD9o!4$S&dY4$*$mD9-a|E`rW z(m&S~FVkbOb5bV9qjFY#et8r7I7{OES5AJ*~hPQu3{(lUGBuO^f$V(P@L~W_9bez zq`yT9G|1^(y@eA^E8ITOjl$foDl05y*_613!qb9Ts-nhaFQ(tmF8Dys;aSQ=GLCbM OvEN#SEu8>DTJQ&SDd(>M literal 0 HcmV?d00001 diff --git a/entities/ccm/__pycache__/ge_ccm_pot_not_present_binary_sensor.cpython-313.pyc b/entities/ccm/__pycache__/ge_ccm_pot_not_present_binary_sensor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d43d63642ff44632c650e1f536ac43f537cbff82 GIT binary patch literal 926 zcmaJ<&1=*^6rahLYjInum6i%iDs5>{x)cI#3z3(^gBSo`0gMgl2Tiaj62>sB?3E8z! ztb;H?KJxKhbO&?nq9t?@`NlHx%|6~Tt2l0qEYChNK=ZJg-=??u;AWHteC*RyFdkN6 z71lYZxD5IP^%2KD;)aj8*|&V7Z(lKda~VCdw(2bI_!d7kvOPz&w&|@f*;OT$ug$w5a~>*wYXK+JmDL) z{SsP*y*%}H%f8(EJg06nbfNi3j@?MODU)udziOmS&r%Jt$5(LtE+k7JB*|C`&x!Vkl6{uYcKLQh zMLP@=Z3IT30|K3px(l*bUXZ|&;e?ynSlN*vELE%jKf-1iWkDyx19*mx9kj6Yy7Q{@ zX5n4yZR_hw=krSE!{Af$F*#hhcX+P*Wv+YB=oWfg?@`Zd!0;pu5=w}gAtYggJl66V zLLTQqTzlpS8Ae>lID*L-xEmQ~8Rc@UEWJ8D2j$dxLWB%t6p}zn9_{6l3PSi<@bP&K zvl{e;OVGcFXaT%H-|XhGVc0$VQWi+_!bHw&QJSh$oz?S9<-mRumU5k3mHNB9YFw{o hxWO+#h`y0n1UNP^#^2HUPs_qRe6V))7t%sm{5NQ% None: + """Handle the button press.""" + + # Forward the call up to the Coffee Maker device to handle + await self.api.start_brewing() \ No newline at end of file diff --git a/entities/ccm/ge_ccm_brew_strength.py b/entities/ccm/ge_ccm_brew_strength.py new file mode 100644 index 0000000..a1d2395 --- /dev/null +++ b/entities/ccm/ge_ccm_brew_strength.py @@ -0,0 +1,47 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCode, ErdCcmBrewStrength +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter +from .ge_ccm_cached_value import GeCcmCachedValue + +_LOGGER = logging.getLogger(__name__) + +class GeCcmBrewStrengthOptionsConverter(OptionsConverter): + def __init__(self): + self._default = ErdCcmBrewStrength.MEDIUM + + @property + def options(self) -> List[str]: + return [i.stringify() for i in [ErdCcmBrewStrength.LIGHT, ErdCcmBrewStrength.MEDIUM, ErdCcmBrewStrength.BOLD, ErdCcmBrewStrength.GOLD]] + + def from_option_string(self, value: str) -> Any: + try: + return ErdCcmBrewStrength[value.upper()] + except: + _LOGGER.warning(f"Could not set brew strength to {value.upper()}") + return self._default + + def to_option_string(self, value: ErdCcmBrewStrength) -> Optional[str]: + try: + return value.stringify() + except: + return self._default.stringify() + +class GeCcmBrewStrengthSelect(GeErdSelect, GeCcmCachedValue): + def __init__(self, api: ApplianceApi): + GeErdSelect.__init__(self, api = api, erd_code = ErdCode.CCM_BREW_STRENGTH, converter = GeCcmBrewStrengthOptionsConverter()) + GeCcmCachedValue.__init__(self) + + @property + def brew_strength(self) -> ErdCcmBrewStrength: + return self._converter.from_option_string(self.current_option) + + async def async_select_option(self, value): + GeCcmCachedValue.set_value(self, value) + self.schedule_update_ha_state() + + @property + def current_option(self): + return self.get_value(device_value = super().current_option) \ No newline at end of file diff --git a/entities/ccm/ge_ccm_brew_temperature.py b/entities/ccm/ge_ccm_brew_temperature.py new file mode 100644 index 0000000..81b13a4 --- /dev/null +++ b/entities/ccm/ge_ccm_brew_temperature.py @@ -0,0 +1,26 @@ +from gehomesdk import ErdCode +from ...devices import ApplianceApi +from ..common import GeErdNumber +from .ge_ccm_cached_value import GeCcmCachedValue + +DEFAULT_MIN_TEMP = 100 +DEFAULT_MAX_TEMP = 225 + +class GeCcmBrewTemperatureNumber(GeErdNumber, GeCcmCachedValue): + def __init__(self, api: ApplianceApi): + try: + min_temp, max_temp, _ = api.appliance.get_erd_value(ErdCode.CCM_BREW_TEMPERATURE_RANGE) + except: + min_temp = DEFAULT_MIN_TEMP + max_temp = DEFAULT_MAX_TEMP + + GeErdNumber.__init__(self, api = api, erd_code = ErdCode.CCM_BREW_TEMPERATURE, min_value=min_temp, max_value=max_temp, mode="slider") + GeCcmCachedValue.__init__(self) + + async def async_set_native_value(self, value): + GeCcmCachedValue.set_value(self, value) + self.schedule_update_ha_state() + + @property + def native_value(self): + return int(self.get_value(device_value = super().native_value)) diff --git a/entities/ccm/ge_ccm_cached_value.py b/entities/ccm/ge_ccm_cached_value.py new file mode 100644 index 0000000..95c2b94 --- /dev/null +++ b/entities/ccm/ge_ccm_cached_value.py @@ -0,0 +1,20 @@ +class GeCcmCachedValue(): + def __init__(self): + self._set_value = None + self._last_device_value = None + + def get_value(self, device_value): + + # If the last device value is different from the current one, return the device value which overrides the set value + if self._last_device_value != device_value: + self._last_device_value = device_value + self._set_value = None + return device_value + + if self._set_value is not None: + return self._set_value + + return device_value + + def set_value(self, set_value): + self._set_value = set_value \ No newline at end of file diff --git a/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py b/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py new file mode 100644 index 0000000..124914a --- /dev/null +++ b/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py @@ -0,0 +1,8 @@ +from ..common import GeErdBinarySensor + +class GeCcmPotNotPresentBinarySensor(GeErdBinarySensor): + @property + def is_on(self) -> bool: + """Return True if entity is not pot present.""" + return not self._boolify(self.appliance.get_erd_value(self.erd_code)) + diff --git a/entities/common/__init__.py b/entities/common/__init__.py new file mode 100644 index 0000000..546ef73 --- /dev/null +++ b/entities/common/__init__.py @@ -0,0 +1,17 @@ +from .options_converter import OptionsConverter +from .bool_converter import BoolConverter, ErdOnOffBoolConverter +from .ge_entity import GeEntity +from .ge_erd_entity import GeErdEntity +from .ge_erd_binary_sensor import GeErdBinarySensor +from .ge_erd_property_binary_sensor import GeErdPropertyBinarySensor +from .ge_erd_sensor import GeErdSensor +from .ge_erd_light import GeErdLight +from .ge_erd_timer_sensor import GeErdTimerSensor +from .ge_erd_property_sensor import GeErdPropertySensor +from .ge_erd_switch import GeErdSwitch +from .ge_erd_button import GeErdButton +from .ge_erd_number import GeErdNumber +from .ge_water_heater import GeAbstractWaterHeater +from .ge_erd_select import GeErdSelect +from .ge_climate import GeClimate +from .ge_humidifier import GeHumidifier \ No newline at end of file diff --git a/entities/common/__pycache__/__init__.cpython-313.pyc b/entities/common/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f5ac2ad88a21ce21b8dc8b52aa8fa8e392a1454 GIT binary patch literal 1124 zcmZ9L%Wm306o$t@2sd+2nxwgq^b)ZtYP)EqUeqRyh-Rt~iAr5s0R|We+vwOdV%dl2 zL-c*RvgCHrRd<#00PUF*V?jn}_|Jd-Ib(l<8`I1&Twh;38vjc$_FET=g*C04ANci+ zby?RCj2mDyv`&ZwPePKXAVqajtnoCYNvFg*&p?Lsn#l4T^j2u0Ev zQQ~DNlg^3?uR@h{PSkiE>ZJ2xgKxqn>4Lb&w_uC3DYp3z?2s;s`+OI6NteVP--mtD zWpTh8&>&qAhx`bR8f;W;*8UjyYE2_3cc#8_9q-h2zDV#TG!ub&;<}UA2sX}O&~ZA$ z;fjrCGi`b1_{yK7&ZCAwTvn(&QO*_2yVCJoz|1z8m*7tErum)b?SMF&qwGgD8vFVw zsrsaB32~yfGA*{FIoHbXkK^Y$nfbo!#O1}z?%|sn!A4uY?|D94_5IIR`10pc?{Tgx zC$g{a&$i|1MA_KHqR^J-Gg}SRP)WFGJ_suAEkmonfCx#X8x(uUamh@f32VBO9Z5?P zI!No<)HMti-n#bck=G-LmPZsJqg#7XYZ{@nRx{bf#wRF;z-F^8)5a+vJja);U? zG>+Lr9lh`D26q;`X(9cg8nyZ}&v$LaZckkYA$u*HGInj*BH~JVn6z!zX<3$Xly6y2 zrt={4+I9!CiF^x9oTmRA-b>v6CJn;~S?d>j7qWwpJr3FPkbNNWFl0|d_JTC}_<2VA L;ZJr9$9VWZ0BTt0 literal 0 HcmV?d00001 diff --git a/entities/common/__pycache__/bool_converter.cpython-313.pyc b/entities/common/__pycache__/bool_converter.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72b242ccd48ae767803522ab764cc6995acc94c7 GIT binary patch literal 1770 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 literal 0 HcmV?d00001 diff --git a/entities/common/__pycache__/ge_entity.cpython-313.pyc b/entities/common/__pycache__/ge_entity.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a68df91c48da08e698646e556b1ca17db3d0139b GIT binary patch literal 4905 zcmcIo-ESMm5#Ku=siR1V`u-qg`}8d~h?PLL?8dbN+p*n91>5=}G_46-PvnWZ=6J{K z9qljy0=ET<3TROktpEndgZyT{sc-oY@=C=IRu4^EpzVX+*it}{hr*rNBTxLW+yIFe z&~SHVcK5e)voo`*_4ITTXg|L+yZKX*kbmHy(S&-Z_5#M8?PzkwbAJcb(_G6=`=)Ook6}kmTI3qvy4( zW;kjx;ELZbIJ#x3hAX_GXB;VsN^ zL*QvbIhjyi=4e3XsUQbvP!`gmbG#gklb;E+D=o^QbQs26I6hyWPP-W&^~4r_gz=;4 zo^&|fdyWr+KK_ATxJ54pdfjp2uh+-s^@CodMQ?!d2Z0}L;SVuB;_7MP5BvPy7XAq1 zN7Latnplf;v`yOAA{}d!_P0pKnG}%^wD9Bdggk^CntrKcb=~H0O1`a_UW&G1V zzn|d~n)bOhpW2ICn5*IdA3*hL#5eTJ{E|7JbCge$cXUvP1`f zy5XX!zgyH4J?jqmsC-@wRB8}40Me zssN4-`z;~V4xjXtnfVa(bI^6hI2KOmI8JN3uC8FJdCl$dZvYM#Q4v!?=O%upz-!Jz zNOPD49=1k;Z-fn~X_TDiC^}?>P5{Vl0)RubfQ_-wixeMjefT(brXrsC#_;qrJoVVb zGGzy@XtUHE_6^j`zJNF}M=+dEVi+AB$L1^Id?!*r1$~9c51FSAR_(wPWa-xJ^}(?5 zB}8!?{Xf|Hpd!W@fK*HF!Eg6V_^Tm;X(GY*W7!=>Ud!<*}ds$DYKUDUFQV=1WYP8$Yy%4n9aIR$6Yrb|_`3U=Vt`nbS8)ibk^vJ8?3=YAd>ms-bu8 zsWfNXI8T!5rB=7%Pjd%-D<3n_eD-&N+$VpD3~y^6|M*E{em^q**SVMexU^mT!)xVC znQp&ci7xF806B>CZCySX96f;g2;#|;*sGt#UfqKWd+n2DAQkbNcV&GG!^(&U|7sV3 zoLDcRMq4|!cq0Vzo+tjT z6mvs!SbYm8rIBEvkmS8!i1`5eBk+?-PUHIka}~S~x)mjFWs3&(ql)rwQ8nt4n4)ax z)OHLV9v&8myA&mBWuOzWH?5+PRSK42&@gU=HMA=hs8!IYQ=<6BrxQrCJ=atnI$UY@JC3{_MUNu z6>xOb(IJ33aCz2?j%F*0XHB#Ta}kPBf)U)={uHkBugIT;g@b|7`>R!f4^EW7cSPW) zDw0rdmFI#}<)s>dpIV^7sD|pj@$EE<#(mMaFPbU8y_OAVr_;+%E;ENO64lbPVMTw&jA50vH%)Y_kb#6CMCHT9y@X6AjUOr;|YA+YO zP|j3g=7rtOht31@2)oq~7aS^I+`azMWgHK&@$SWk*B@N|0>{kn*_TZvBd(ZL9S#2} zZF5OCoYU}8ZD-$Mj_7uIH_GA#V=eKPkiGWj`~{z6D`+|YjrQU>mSlq%dJ literal 0 HcmV?d00001 diff --git a/entities/common/__pycache__/ge_erd_binary_sensor.cpython-313.pyc b/entities/common/__pycache__/ge_erd_binary_sensor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84f8ebaf9f4c834cc0d4085b9521fe9662c5272f GIT binary patch literal 2811 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 literal 0 HcmV?d00001 diff --git a/entities/common/__pycache__/ge_erd_property_binary_sensor.cpython-313.pyc b/entities/common/__pycache__/ge_erd_property_binary_sensor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65b859ce37386fb5ba4d86cf03da0f83751a9b35 GIT binary patch literal 2809 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 Wo|~`MaRKf-#~ys`H-w?=9sdUpjAHTt literal 0 HcmV?d00001 diff --git a/entities/common/__pycache__/ge_erd_switch.cpython-313.pyc b/entities/common/__pycache__/ge_erd_switch.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8022fa1119a2882a391550abf1495eef4ffdf03 GIT binary patch literal 2927 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 literal 0 HcmV?d00001 diff --git a/entities/common/__pycache__/ge_water_heater.cpython-313.pyc b/entities/common/__pycache__/ge_water_heater.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7ee02b02599190a5b3ee3d81344eba6d42076b4 GIT binary patch literal 2521 zcma(TOK%fLc-G#v*Nz`?!Xr(JaDqc{frJuJR8d+SV-jqVs98`CwKQ7R6K9LPYi7r- z(o=frnI0nfm`bfwZaw$Zo_cl4!RjCcQrkmrLFNbaoAo13Y*6R$%{Sj;_IuAZ!{H!; z_3e%6%>@Oazr~@m)Hc%m6_6dIAx+95+S(;5!P%4ZWWCgzm8qOnsFL+jUtI9LIe%8A zs*B6HKsHE&F0SN;vLPDEhG`hiKFyztWTP|+xT*zm!`T>(#Zg{bPjpN;+^0}}^t#kK z(-UK83TeS{qz&a`c{PvLKRA@^U|Q%Sg`O%($?(1)ttMseO<7x9zHJ(e%lAx&adoZ2 zOxw~+Nr}fk*BPP9M8J&2OtvM64=j_dZQLj23ZXiyQj+v=e})#)c7gCwKvszE0J%&o zCS;v`FMJF%eYe+EQg>JPh0SnOmq_bULm1OpavN#>2*?h~BN0~W(Io2CJXFprnm6ye z=F#MF^rd{gElm9`UCsL-V+gp)5pJNX^Kr_$~6VIauY!c z8A@#mWZ5C54M%9l`RzHwwl>V-oKba{UB-r8uGkjj&6z6_ylI!o92`t$5(gy9W!n;T z*EHNRZML$-hgv9RTNT152gYKyM`Rd2-4va)jsA&D*44=-;OPJQMo)k+Jt@Fx04+!8 zdw}vtH?p`SN;xsul%rDab?=Mo&Hy-Kg{UPr!gm1ajc^3|{sCI|0QuNzb2>%b!;PSM zC4rKjRW9MAmxh4F!w#XQUcxrTW!<3TKTYFb-)#p<6=we-6?DtsM?RtD4kR1d*!hU?c-TQSB zv16;#KH$H%?SCBnKL=v2!(4&-Dv>_FWlJv;K0erG!4o-i#E{!YuSQNaMiP4?iF#~$ z_p@Erm|57HS!m4M+?%=iZ06JY`CIj9s;;K`9qu;K6kODs;z7wxAh-CE9xAGr2k)eo z*E6fjnY;J)w)YQ!cQnaJ zd)|-&-hkl(bK2n-2HVyzc>xTg-DY_TFUT-gF&62-mHMJcRM%s$Obyq8M+O z)L|tPhKmhsKgI>y07R`&(T{A4dtrdKT0m40Ju5&$0G|L!0WJ%01pq&ZvBPv`8dztH znh&dtI2hBbLO}GfI}oUdqYK0&c76iz9r{aNd8LF8Jd!e1)0zmjrWg4lF5$q}!FJ_c z?T5hI*lDRYFL-IGtDBa(%8SyI&6@LlSx77%N-VY{UV9YfQdK23sT{hc%VoG&nY~j0dc?hGvW%o86eP_VDKCeTEOlru|-8Ca!_LT zz(hjnyg2*uy|tyK%sTg{7Sk)>O)D=)bH&w>T1mre%2fmf2G=P39bMP8yy z&(NiRQThe?@FlwX0!_R?=UxS4jo`$e!HL@Pv*7f$|BZY`nyM|=&oBHo|HqBrZ!{5{ H-GKfF*5gzm literal 0 HcmV?d00001 diff --git a/entities/common/__pycache__/options_converter.cpython-313.pyc b/entities/common/__pycache__/options_converter.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7413588c20369625f2f585a8d5ddded48a15143 GIT binary patch literal 1097 zcmZWn&ubGw7@gT)*|bU1h6dW4Vr>fnX)eu45kwHIQV&Z{28Ot9(v|FP{AN>#r+Sfs z7cYT&^Vr<_r|7K=f`X^sf-Cgu_wBA#(>c6-^S*Cp-n`kQTwY)}-rn6h+~SP=pvj+M z99IehAy{%>4VDycqc}bI|uJi}tSQ$@!87uqQ zF!rOcJ8-kc49$F!bdXF~hXHpOXbuOxV>nvJ>{!&Yt2ug|y)xQ)I}{wFV`1IiH5`*F zP(;N>Wg%Z-I994JIt3{8?PgIeXAb1UC_M5Y_TW;-?UV&xJc7{0>t7GzR7@;{32U)U zHs$+M*5t~N-k>L`p`jkN+$ik%{gykDaTEwQ3WibWg|Te)J#iQXUJECF?0XVBgCGi9 zQJR9p*4)I-aI6Zss@CQ$?&KAE3t2!oX5T9-C&dIQQ|3{3F!J0ydXm_j^8>!gU=ejF zAxt$W6R#pjTGeR45`I**2WUS#OUM}fu&-|4@N%<>9ObqyDyt_YSV7@GKa^sciip8uL#j-xqiw{BRQZz<9ybme3AwNm}ie*fmPy`H*DgV=AD zl`TZr4Ll*#f)GLUax@@b65{o!JIE^)A$mT@c;Mr!Mp(9o5DoF?83Vlt(50Y4KzD>1 z0sXJAj-YA+v+TycE4nde`eGD&Qi#+*W`(i`$}k1mLOw=#$1cqBOyg$tboJEzxSlYi ziO#I0RGb;ytexpIRBPw@FZz&F|KyTXKRY~^)8L9|b_yM^-!zpKkB9iMnbp*OYBfEd nqWbGe+4r(Q?!yKa=$$0pZKiY1zp?EvZ2QV6@cK`NkS_ZTG* bool: + return bool(value) + def true_value(self) -> Any: + return True + def false_value(self) -> Any: + return False + +class ErdOnOffBoolConverter(BoolConverter): + def boolify(self, value: ErdOnOff) -> bool: + return value.boolify() + def true_value(self) -> Any: + return ErdOnOff.ON + def false_value(self) -> Any: + return ErdOnOff.OFF \ No newline at end of file diff --git a/entities/common/ge_climate.py b/entities/common/ge_climate.py new file mode 100644 index 0000000..3c9aa5d --- /dev/null +++ b/entities/common/ge_climate.py @@ -0,0 +1,201 @@ +import logging +from typing import List, Optional + +from homeassistant.components.climate import ClimateEntity +from homeassistant.const import ( + ATTR_TEMPERATURE, + UnitOfTemperature, +) +from homeassistant.components.climate import ClimateEntityFeature, HVACMode +from homeassistant.components.water_heater import WaterHeaterEntityFeature +from gehomesdk import ErdCode, ErdCodeType, ErdMeasurementUnits, ErdOnOff +from ...const import DOMAIN +from ...devices import ApplianceApi +from .ge_erd_entity import GeEntity +from .options_converter import OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +#by default, we'll support target temp and fan mode (derived classes can override) +GE_CLIMATE_SUPPORT = ( + ClimateEntityFeature.TARGET_TEMPERATURE | + ClimateEntityFeature.FAN_MODE | + ClimateEntityFeature.TURN_ON | + ClimateEntityFeature.TURN_OFF +) + +class GeClimate(GeEntity, ClimateEntity): + """GE Climate Base Entity (Window AC, Portable AC, etc)""" + def __init__( + self, + api: ApplianceApi, + hvac_mode_converter: OptionsConverter, + fan_mode_converter: OptionsConverter, + fan_only_fan_mode_converter: OptionsConverter = None, + power_status_erd_code: ErdCodeType = ErdCode.AC_POWER_STATUS, + current_temperature_erd_code: ErdCodeType = ErdCode.AC_AMBIENT_TEMPERATURE, + target_temperature_erd_code: ErdCodeType = ErdCode.AC_TARGET_TEMPERATURE, + hvac_mode_erd_code: ErdCodeType = ErdCode.AC_OPERATION_MODE, + fan_mode_erd_code: ErdCodeType = ErdCode.AC_FAN_SETTING + + ): + super().__init__(api) + self._hvac_mode_converter = hvac_mode_converter + self._fan_mode_converter = fan_mode_converter + self._fan_only_fan_mode_converter = (fan_only_fan_mode_converter + if fan_only_fan_mode_converter is not None + else fan_mode_converter + ) + self._power_status_erd_code = api.appliance.translate_erd_code(power_status_erd_code) + self._current_temperature_erd_code = api.appliance.translate_erd_code(current_temperature_erd_code) + self._target_temperature_erd_code = api.appliance.translate_erd_code(target_temperature_erd_code) + self._hvac_mode_erd_code = api.appliance.translate_erd_code(hvac_mode_erd_code) + self._fan_mode_erd_code = api.appliance.translate_erd_code(fan_mode_erd_code) + + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.serial_or_mac}_climate" + + @property + def name(self) -> Optional[str]: + return f"{self.serial_or_mac} Climate" + + @property + def power_status_erd_code(self): + return self._power_status_erd_code + + @property + def target_temperature_erd_code(self): + return self._target_temperature_erd_code + + @property + def current_temperature_erd_code(self): + return self._current_temperature_erd_code + + @property + def hvac_mode_erd_code(self): + return self._hvac_mode_erd_code + + @property + def fan_mode_erd_code(self): + return self._fan_mode_erd_code + + @property + def temperature_unit(self): + return UnitOfTemperature.FAHRENHEIT + + @property + def supported_features(self): + return GE_CLIMATE_SUPPORT + + @property + def is_on(self) -> bool: + return self.appliance.get_erd_value(self.power_status_erd_code) == ErdOnOff.ON + + @property + def target_temperature(self) -> Optional[float]: + measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + if measurement_system == ErdMeasurementUnits.METRIC: + targ = float(self.appliance.get_erd_value(self.target_temperature_erd_code)) + targ = round(((targ - 32.0) * (5/9)) / 2) * 2 + return (9 * targ) / 5 + 32 + return float(self.appliance.get_erd_value(self.target_temperature_erd_code)) + + @property + def current_temperature(self) -> Optional[float]: + try: + measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + temp_raw = self.appliance.get_erd_value(self.current_temperature_erd_code) + temp_float = float(temp_raw) + + if measurement_system == ErdMeasurementUnits.METRIC: + temp_c = round((temp_float - 32.0) * (5 / 9)) + return (9 * temp_c) / 5 + 32 + + return temp_float + + except KeyError as e: + _LOGGER.warning(f"Missing ERD value for current temperature: {self.current_temperature_erd_code} ({e})") + return None + + @property + def min_temp(self) -> float: + return self._convert_temp(64) + + @property + def max_temp(self) -> float: + return self._convert_temp(86) + + @property + def hvac_mode(self): + if not self.is_on: + return HVACMode.OFF + + return self._hvac_mode_converter.to_option_string(self.appliance.get_erd_value(self.hvac_mode_erd_code)) + + @property + def hvac_modes(self) -> List[str]: + return [HVACMode.OFF] + self._hvac_mode_converter.options + + @property + def fan_mode(self): + if self.hvac_mode == HVACMode.FAN_ONLY: + return self._fan_only_fan_mode_converter.to_option_string(self.appliance.get_erd_value(self.fan_mode_erd_code)) + return self._fan_mode_converter.to_option_string(self.appliance.get_erd_value(self.fan_mode_erd_code)) + + @property + def fan_modes(self) -> List[str]: + if self.hvac_mode == HVACMode.FAN_ONLY: + return self._fan_only_fan_mode_converter.options + return self._fan_mode_converter.options + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + _LOGGER.debug(f"Setting HVAC mode from {self.hvac_mode} to {hvac_mode}") + if hvac_mode != self.hvac_mode: + if hvac_mode == HVACMode.OFF: + await self.appliance.async_set_erd_value(self.power_status_erd_code, ErdOnOff.OFF) + else: + if not self.is_on: + await self.appliance.async_set_erd_value(self.power_status_erd_code, ErdOnOff.ON) + await self.appliance.async_set_erd_value( + self.hvac_mode_erd_code, + self._hvac_mode_converter.from_option_string(hvac_mode) + ) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + _LOGGER.debug(f"Setting Fan mode from {self.fan_mode} to {fan_mode}") + if fan_mode != self.fan_mode: + converter = (self._fan_only_fan_mode_converter + if self.hvac_mode == HVACMode.FAN_ONLY + else self._fan_mode_converter + ) + + await self.appliance.async_set_erd_value( + self.fan_mode_erd_code, + converter.from_option_string(fan_mode) + ) + + async def async_set_temperature(self, **kwargs) -> None: + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + temperature = int(temperature) + + _LOGGER.debug(f"Setting temperature from {self.target_temperature} to {temperature}") + if self.target_temperature != temperature: + await self.appliance.async_set_erd_value(self.target_temperature_erd_code, temperature) + + async def async_turn_on(self): + await self.appliance.async_set_erd_value(self.power_status_erd_code, ErdOnOff.ON) + + async def async_turn_off(self): + await self.appliance.async_set_erd_value(self.power_status_erd_code, ErdOnOff.OFF) + + def _convert_temp(self, temperature_f: int): + if self.temperature_unit == UnitOfTemperature.FAHRENHEIT: + return float(temperature_f) + else: + return (temperature_f - 32.0) * (5/9) + + def _get_icon(self) -> Optional[str]: + return "mdi:air-conditioner" diff --git a/entities/common/ge_entity.py b/entities/common/ge_entity.py new file mode 100644 index 0000000..6104d20 --- /dev/null +++ b/entities/common/ge_entity.py @@ -0,0 +1,85 @@ +from datetime import timedelta +from typing import Optional, Dict, Any + +from gehomesdk import GeAppliance +from ...devices import ApplianceApi + +class GeEntity: + """Base class for all GE Entities""" + should_poll = False + + def __init__(self, api: ApplianceApi): + self._api = api + self._added = False + + @property + def unique_id(self) -> str: + raise NotImplementedError + + @property + def api(self) -> ApplianceApi: + return self._api + + @property + def device_info(self) -> Optional[Dict[str, Any]]: + return self.api.device_info + + @property + def serial_number(self): + return self.api.serial_number + + @property + def available(self) -> bool: + return self.api.available + + @property + def appliance(self) -> GeAppliance: + return self.api.appliance + + @property + def mac_addr(self) -> str: + return self.api.mac_addr + + @property + def serial_or_mac(self) -> str: + return self.api.serial_or_mac + + @property + def name(self) -> Optional[str]: + raise NotImplementedError + + @property + def icon(self) -> Optional[str]: + return self._get_icon() + + @property + def device_class(self) -> Optional[str]: + return self._get_device_class() + + @property + def added(self) -> bool: + return self._added + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self._added = True + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + self._added = False + + def _stringify(self, value: any, **kwargs) -> Optional[str]: + if isinstance(value, timedelta): + return str(value)[:-3] if value else "" + if value is None: + return None + return self.appliance.stringify_erd_value(value, **kwargs) + + def _boolify(self, value: any) -> Optional[bool]: + return self.appliance.boolify_erd_value(value) + + def _get_icon(self) -> Optional[str]: + return None + + def _get_device_class(self) -> Optional[str]: + return None diff --git a/entities/common/ge_erd_binary_sensor.py b/entities/common/ge_erd_binary_sensor.py new file mode 100644 index 0000000..55afc01 --- /dev/null +++ b/entities/common/ge_erd_binary_sensor.py @@ -0,0 +1,38 @@ +from typing import Optional + +from homeassistant.components.binary_sensor import BinarySensorEntity + +from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass +from ...devices import ApplianceApi +from .ge_erd_entity import GeErdEntity + + +class GeErdBinarySensor(GeErdEntity, BinarySensorEntity): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None, icon_on_override: str = None, icon_off_override: str = None, device_class_override: str = None): + super().__init__(api, erd_code, erd_override=erd_override, icon_override=icon_on_override, device_class_override=device_class_override) + self._icon_on_override = icon_on_override + self._icon_off_override = icon_off_override + + """GE Entity for binary sensors""" + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._boolify(self.appliance.get_erd_value(self.erd_code)) + + def _get_icon(self): + if self._icon_on_override and self.is_on: + return self._icon_on_override + if self._icon_off_override and not self.is_on: + return self._icon_off_override + + if self._erd_code_class == ErdCodeClass.DOOR or self.device_class == "door": + return "mdi:door-open" if self.is_on else "mdi:door-closed" + + return super()._get_icon() + + def _get_device_class(self) -> Optional[str]: + if self._device_class_override: + return self._device_class_override + if self._erd_code_class == ErdCodeClass.DOOR: + return "door" + return None diff --git a/entities/common/ge_erd_button.py b/entities/common/ge_erd_button.py new file mode 100644 index 0000000..ef28295 --- /dev/null +++ b/entities/common/ge_erd_button.py @@ -0,0 +1,17 @@ +from typing import Optional + +from homeassistant.components.button import ButtonEntity + +from gehomesdk import ErdCodeType +from ...devices import ApplianceApi +from .ge_erd_entity import GeErdEntity + + +class GeErdButton(GeErdEntity, ButtonEntity): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): + super().__init__(api, erd_code, erd_override=erd_override) + + """GE Entity for buttons""" + async def async_press(self) -> None: + """Handle the button press.""" + await self.appliance.async_set_erd_value(self.erd_code, True) diff --git a/entities/common/ge_erd_entity.py b/entities/common/ge_erd_entity.py new file mode 100644 index 0000000..6763253 --- /dev/null +++ b/entities/common/ge_erd_entity.py @@ -0,0 +1,153 @@ +from datetime import timedelta +from typing import Optional + +from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits + +from ...const import DOMAIN +from ...devices import ApplianceApi +from .ge_entity import GeEntity + + +class GeErdEntity(GeEntity): + """Parent class for GE entities tied to a specific ERD""" + + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_override: str = None, + icon_override: str = None, + device_class_override: str = None, + ): + super().__init__(api) + self._erd_code = api.appliance.translate_erd_code(erd_code) + self._erd_code_class = api.appliance.get_erd_code_class(self._erd_code) + self._erd_override = erd_override + self._icon_override = icon_override + self._device_class_override = device_class_override + + if not self._erd_code_class: + self._erd_code_class = ErdCodeClass.GENERAL + + @property + def erd_code(self) -> ErdCodeType: + return self._erd_code + + @property + def erd_code_class(self) -> ErdCodeClass: + return self._erd_code_class + + @property + def erd_string(self) -> str: + erd_code = self.erd_code + if isinstance(self.erd_code, ErdCode): + return erd_code.name + return erd_code + + @property + def name(self) -> Optional[str]: + erd_string = self.erd_string + + # override the name if specified + if self._erd_override != None: + erd_string = self._erd_override + + erd_title = " ".join(erd_string.split("_")).title() + return f"{self.serial_or_mac} {erd_title}" + + @property + def unique_id(self) -> Optional[str]: + return f"{DOMAIN}_{self.serial_or_mac}_{self.erd_string.lower()}" + + def _stringify(self, value: any, **kwargs) -> Optional[str]: + """Stringify a value""" + # perform special processing before passing over to the default method + if self.erd_code == ErdCode.CLOCK_TIME: + return value.strftime("%H:%M:%S") if value else None + if self.erd_code_class == ErdCodeClass.RAW_TEMPERATURE: + return f"{value}" + if self.erd_code_class == ErdCodeClass.NON_ZERO_TEMPERATURE: + return f"{value}" if value else "" + if self.erd_code_class == ErdCodeClass.TIMER or isinstance(value, timedelta): + return str(value)[:-3] if value else "Off" + if value is None: + return None + return self.appliance.stringify_erd_value(value, **kwargs) + + @property + def _measurement_system(self) -> Optional[ErdMeasurementUnits]: + """ + Get the measurement system this appliance is using. For now, uses the + temperature unit if available, otherwise assumes imperial. + """ + try: + value = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + except KeyError: + return ErdMeasurementUnits.Imperial + return value + + def _get_icon(self): + """Select an appropriate icon.""" + + if self._icon_override: + return self._icon_override + if not isinstance(self.erd_code, ErdCode): + return None + if self.erd_code_class == ErdCodeClass.CLOCK: + return "mdi:clock" + if self.erd_code_class == ErdCodeClass.COUNTER: + return "mdi:counter" + if self.erd_code_class == ErdCodeClass.DOOR: + return "mdi:door" + if self.erd_code_class == ErdCodeClass.TIMER: + return "mdi:timer-outline" + if self.erd_code_class == ErdCodeClass.LOCK_CONTROL: + return "mdi:lock-outline" + if self.erd_code_class == ErdCodeClass.SABBATH_CONTROL: + return "mdi:star-david" + if self.erd_code_class == ErdCodeClass.COOLING_CONTROL: + return "mdi:snowflake" + if self.erd_code_class == ErdCodeClass.OVEN_SENSOR: + return "mdi:stove" + if self.erd_code_class == ErdCodeClass.FRIDGE_SENSOR: + return "mdi:fridge-bottom" + if self.erd_code_class == ErdCodeClass.FREEZER_SENSOR: + return "mdi:fridge-top" + if self.erd_code_class == ErdCodeClass.DISPENSER_SENSOR: + return "mdi:cup-water" + if self.erd_code_class == ErdCodeClass.DISHWASHER_SENSOR: + return "mdi:dishwasher" + if self.erd_code_class == ErdCodeClass.WATERFILTER_SENSOR: + return "mdi:water" + if self.erd_code_class == ErdCodeClass.LAUNDRY_SENSOR: + return "mdi:washing-machine" + if self.erd_code_class == ErdCodeClass.LAUNDRY_WASHER_SENSOR: + return "mdi:washing-machine" + if self.erd_code_class == ErdCodeClass.LAUNDRY_DRYER_SENSOR: + return "mdi:tumble-dryer" + if self.erd_code_class == ErdCodeClass.ADVANTIUM_SENSOR: + return "mdi:microwave" + if self.erd_code_class == ErdCodeClass.FLOW_RATE: + return "mdi:water" + if self.erd_code_class == ErdCodeClass.LIQUID_VOLUME: + return "mdi:water" + if self.erd_code_class == ErdCodeClass.AC_SENSOR: + return "mdi:air-conditioner" + if self.erd_code_class == ErdCodeClass.TEMPERATURE_CONTROL: + return "mdi:thermometer" + if self.erd_code_class == ErdCodeClass.FAN: + return "mdi:fan" + if self.erd_code_class == ErdCodeClass.LIGHT: + return "mdi:lightbulb" + if self.erd_code_class == ErdCodeClass.OIM_SENSOR: + return "mdi:snowflake" + if self.erd_code_class == ErdCodeClass.WATERSOFTENER_SENSOR: + return "mdi:water" + if self.erd_code_class == ErdCodeClass.CCM_SENSOR: + return "mdi:coffee-maker" + if self.erd_code_class == ErdCodeClass.HUMIDITY: + return "mdi:water-percent" + if self.erd_code_class == ErdCodeClass.DEHUMIDIFIER_SENSOR: + return "mdi:air-humidifier" + + return None diff --git a/entities/common/ge_erd_light.py b/entities/common/ge_erd_light.py new file mode 100644 index 0000000..35b0422 --- /dev/null +++ b/entities/common/ge_erd_light.py @@ -0,0 +1,64 @@ +import logging + +from gehomesdk import ErdCodeType +from homeassistant.components.light import ( + ColorMode, + ATTR_BRIGHTNESS, + LightEntity +) + +from ...devices import ApplianceApi +from .ge_erd_entity import GeErdEntity + +_LOGGER = logging.getLogger(__name__) + + +def to_ge_level(level): + """Convert the given Home Assistant light level (0-255) to GE (0-100).""" + return int(round((level * 100) / 255)) + +def to_hass_level(level): + """Convert the given GE (0-100) light level to Home Assistant (0-255).""" + return int((level * 255) // 100) + +class GeErdLight(GeErdEntity, LightEntity): + """Lights for ERD codes.""" + + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None, color_mode = ColorMode.BRIGHTNESS): + super().__init__(api, erd_code, erd_override) + self._color_mode = color_mode + + @property + def supported_color_modes(self): + """Flag supported color modes.""" + return ColorMode.BRIGHTNESS + + @property + def color_mode(self): + """Return the color mode of the light.""" + return self._color_mode + + @property + def brightness(self): + """Return the brightness of the light.""" + return to_hass_level(self.appliance.get_erd_value(self.erd_code)) + + async def _set_brightness(self, brightness, **kwargs): + await self.appliance.async_set_erd_value(self.erd_code, to_ge_level(brightness)) + + @property + def is_on(self) -> bool: + """Return True if light is on.""" + return self.appliance.get_erd_value(self.erd_code) > 0 + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + brightness = kwargs.pop(ATTR_BRIGHTNESS, 255) + + _LOGGER.debug(f"Turning on {self.unique_id}") + await self._set_brightness(brightness, **kwargs) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + _LOGGER.debug(f"Turning off {self.unique_id}") + await self._set_brightness(0, **kwargs) diff --git a/entities/common/ge_erd_number.py b/entities/common/ge_erd_number.py new file mode 100644 index 0000000..91026dd --- /dev/null +++ b/entities/common/ge_erd_number.py @@ -0,0 +1,127 @@ +import logging +from typing import Optional +from gehomesdk.erd.erd_data_type import ErdDataType +from homeassistant.components.number import ( + NumberEntity, + NumberDeviceClass, +) +from homeassistant.const import UnitOfTemperature +from gehomesdk import ErdCodeType, ErdCodeClass +from .ge_erd_entity import GeErdEntity +from ...devices import ApplianceApi + +_LOGGER = logging.getLogger(__name__) + +class GeErdNumber(GeErdEntity, NumberEntity): + """GE Entity for numbers""" + + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_override: str = None, + icon_override: str = None, + device_class_override: str = None, + uom_override: str = None, + data_type_override: ErdDataType = None, + min_value: float = 1, + max_value: float = 100, + step_value: float = 1, + mode: str = "auto" + ): + super().__init__(api, erd_code, erd_override, icon_override, device_class_override) + self._uom_override = uom_override + self._data_type_override = data_type_override + self._native_min_value = min_value + self._native_max_value = max_value + self._native_step = step_value + self._mode = mode + + @property + def native_value(self): + try: + value = self.appliance.get_erd_value(self.erd_code) + return self._convert_value_from_device(value) + except KeyError: + return None + + @property + def native_unit_of_measurement(self) -> Optional[str]: + return self._get_uom() + + @property + def _data_type(self) -> ErdDataType: + if self._data_type_override is not None: + return self._data_type_override + + return self.appliance.get_erd_code_data_type(self.erd_code) + + @property + def native_min_value(self) -> float: + return self._convert_value_from_device(self._native_min_value) + + @property + def native_max_value(self) -> float: + return self._convert_value_from_device(self._native_max_value) + + @property + def native_step(self) -> float: + return self._native_step + + @property + def mode(self) -> float: + return self._mode + + def _convert_value_from_device(self, value): + """Convert to expected data type""" + + if self._data_type == ErdDataType.INT: + return int(round(value)) + else: + return value + + def _get_uom(self): + """Select appropriate units""" + + #if we have an override, just use it + if self._uom_override: + return self._uom_override + + if self.device_class == NumberDeviceClass.TEMPERATURE: + #NOTE: it appears that the API only sets temperature in Fahrenheit, + #so we'll hard code this UOM instead of using the device configured + #settings + return UnitOfTemperature.FAHRENHEIT + + return None + + def _get_device_class(self) -> Optional[str]: + if self._device_class_override: + return self._device_class_override + + if self.erd_code_class in [ + ErdCodeClass.RAW_TEMPERATURE, + ErdCodeClass.NON_ZERO_TEMPERATURE, + ]: + return NumberDeviceClass.TEMPERATURE + + return None + + def _get_icon(self): + if self.erd_code_class == ErdCodeClass.DOOR: + if self.state.lower().endswith("open"): + return "mdi:door-open" + if self.state.lower().endswith("closed"): + return "mdi:door-closed" + return super()._get_icon() + + async def async_set_native_value(self, value): + """Sets the ERD value, assumes that the data type is correct""" + + if self._data_type == ErdDataType.INT: + value = int(round(value)) + + try: + await self.appliance.async_set_erd_value(self.erd_code, value) + except: + _LOGGER.warning(f"Could not set {self.name} to {value}") diff --git a/entities/common/ge_erd_property_binary_sensor.py b/entities/common/ge_erd_property_binary_sensor.py new file mode 100644 index 0000000..d7504ce --- /dev/null +++ b/entities/common/ge_erd_property_binary_sensor.py @@ -0,0 +1,32 @@ +from typing import Optional + +import magicattr +from gehomesdk import ErdCodeType +from ...devices import ApplianceApi +from .ge_erd_binary_sensor import GeErdBinarySensor + +class GeErdPropertyBinarySensor(GeErdBinarySensor): + """GE Entity for property binary sensors""" + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str, erd_override: str = None, icon_on_override: str = None, icon_off_override: str = None, device_class_override: str = None): + super().__init__(api, erd_code, erd_override, icon_on_override, icon_off_override, device_class_override) + self.erd_property = erd_property + self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") + + @property + def is_on(self) -> Optional[bool]: + """Return True if entity is on.""" + try: + value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) + except KeyError: + return None + return self._boolify(value) + + @property + def unique_id(self) -> Optional[str]: + return f"{super().unique_id}_{self._erd_property_cleansed}" + + @property + def name(self) -> Optional[str]: + base_string = super().name + property_name = self._erd_property_cleansed.replace("_", " ").title() + return f"{base_string} {property_name}" diff --git a/entities/common/ge_erd_property_sensor.py b/entities/common/ge_erd_property_sensor.py new file mode 100644 index 0000000..70938d0 --- /dev/null +++ b/entities/common/ge_erd_property_sensor.py @@ -0,0 +1,50 @@ +from typing import Optional + +import magicattr +from gehomesdk import ErdCode, ErdCodeType, ErdMeasurementUnits, ErdDataType +from ...devices import ApplianceApi +from .ge_erd_sensor import GeErdSensor + + +class GeErdPropertySensor(GeErdSensor): + """GE Entity for sensors""" + def __init__( + self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str, + erd_override: str = None, icon_override: str = None, device_class_override: str = None, + state_class_override: str = None, uom_override: str = None, data_type_override: ErdDataType = None + ): + super().__init__( + api, erd_code, erd_override=erd_override, + icon_override=icon_override, device_class_override=device_class_override, + state_class_override=state_class_override, + uom_override=uom_override, + data_type_override=data_type_override + ) + self.erd_property = erd_property + self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") + + @property + def unique_id(self) -> Optional[str]: + return f"{super().unique_id}_{self._erd_property_cleansed}" + + @property + def name(self) -> Optional[str]: + base_string = super().name + property_name = self._erd_property_cleansed.replace("_", " ").title() + return f"{base_string} {property_name}" + + @property + def native_value(self): + try: + value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) + + # if it's a numeric data type, return it directly + if self._data_type in [ErdDataType.INT, ErdDataType.FLOAT]: + return value + + # otherwise, return a stringified version + # TODO: perhaps enhance so that there's a list of variables available + # for the stringify function to consume... + return self._stringify(value, temp_units=self._temp_units) + except KeyError: + return None \ No newline at end of file diff --git a/entities/common/ge_erd_select.py b/entities/common/ge_erd_select.py new file mode 100644 index 0000000..833dea2 --- /dev/null +++ b/entities/common/ge_erd_select.py @@ -0,0 +1,35 @@ + +import logging +from typing import Any, List, Optional + +from homeassistant.components.select import SelectEntity +from gehomesdk import ErdCodeType + +from ...devices import ApplianceApi +from .ge_erd_entity import GeErdEntity +from .options_converter import OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class GeErdSelect(GeErdEntity, SelectEntity): + """ERD-based selector entity""" + device_class = "select" + + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, converter: OptionsConverter, erd_override: str = None, icon_override: str = None, device_class_override: str = None): + super().__init__(api, erd_code, erd_override=erd_override, icon_override=icon_override, device_class_override=device_class_override) + self._converter = converter + + @property + def current_option(self): + return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) + + @property + def options(self) -> List[str]: + "Return a list of options" + return self._converter.options + + async def async_select_option(self, option: str) -> None: + _LOGGER.debug(f"Setting select from {self.current_option} to {option}") + """Change the selected option.""" + if option != self.current_option: + await self.appliance.async_set_erd_value(self.erd_code, self._converter.from_option_string(option)) diff --git a/entities/common/ge_erd_sensor.py b/entities/common/ge_erd_sensor.py new file mode 100644 index 0000000..8466dd9 --- /dev/null +++ b/entities/common/ge_erd_sensor.py @@ -0,0 +1,164 @@ +import logging +from typing import Optional +from gehomesdk.erd.erd_data_type import ErdDataType +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity, SensorStateClass + +from homeassistant.const import UnitOfTemperature +from gehomesdk import ErdCodeType, ErdCodeClass +from .ge_erd_entity import GeErdEntity +from ...devices import ApplianceApi + +_LOGGER = logging.getLogger(__name__) + +class GeErdSensor(GeErdEntity, SensorEntity): + """GE Entity for sensors""" + + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_override: str = None, + icon_override: str = None, + device_class_override: str = None, + state_class_override: str = None, + uom_override: str = None, + data_type_override: ErdDataType = None + ): + super().__init__(api, erd_code, erd_override, icon_override, device_class_override) + self._uom_override = uom_override + self._state_class_override = state_class_override + self._data_type_override = data_type_override + + @property + def native_value(self): + try: + value = self.appliance.get_erd_value(self.erd_code) + + # if it's a numeric data type, return it directly + if self._data_type in [ErdDataType.INT, ErdDataType.FLOAT]: + return self._convert_numeric_value_from_device(value) + + # otherwise, return a stringified version + # TODO: perhaps enhance so that there's a list of variables available + # for the stringify function to consume... + return self._stringify(value, temp_units=self._temp_units) + except KeyError: + return None + + @property + def native_unit_of_measurement(self) -> Optional[str]: + return self._get_uom() + + @property + def state_class(self) -> Optional[str]: + return self._get_state_class() + + @property + def _data_type(self) -> ErdDataType: + if self._data_type_override is not None: + return self._data_type_override + + return self.appliance.get_erd_code_data_type(self.erd_code) + + @property + def _temp_units(self) -> Optional[str]: + #based on testing, all API values are in Fahrenheit, so we'll redefine + #this property to be the configured temperature unit and set the native + #unit differently + return self.api.hass.config.units.temperature_unit + + #if self._measurement_system == ErdMeasurementUnits.METRIC: + # return UnitOfTemperature.CELSIUS + #return UnitOfTemperature.FAHRENHEIT + + def _convert_numeric_value_from_device(self, value): + """Convert to expected data type""" + + if self._data_type == ErdDataType.INT: + return int(round(value)) + else: + return value + + def _get_uom(self): + """Select appropriate units""" + + #if we have an override, just use it + if self._uom_override: + return self._uom_override + + if ( + self.erd_code_class + in [ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE] + or self.device_class == SensorDeviceClass.TEMPERATURE + ): + #NOTE: it appears that the API only sets temperature in Fahrenheit, + #so we'll hard code this UOM instead of using the device configured + #settings + return UnitOfTemperature.FAHRENHEIT + if ( + self.erd_code_class == ErdCodeClass.BATTERY + or self.device_class == SensorDeviceClass.BATTERY + ): + return "%" + if self.erd_code_class == ErdCodeClass.PERCENTAGE: + return "%" + if self.device_class == SensorDeviceClass.POWER_FACTOR: + return "%" + if self.erd_code_class == ErdCodeClass.HUMIDITY: + return "%" + if self.erd_code_class == ErdCodeClass.FLOW_RATE: + #if self._measurement_system == ErdMeasurementUnits.METRIC: + # return "lpm" + return "gpm" + if self.erd_code_class == ErdCodeClass.LIQUID_VOLUME: + #if self._measurement_system == ErdMeasurementUnits.METRIC: + # return "l" + return "gal" + return None + + def _get_device_class(self) -> Optional[str]: + if self._device_class_override: + return self._device_class_override + if self.erd_code_class in [ + ErdCodeClass.RAW_TEMPERATURE, + ErdCodeClass.NON_ZERO_TEMPERATURE, + ]: + return SensorDeviceClass.TEMPERATURE + if self.erd_code_class == ErdCodeClass.BATTERY: + return SensorDeviceClass.BATTERY + if self.erd_code_class == ErdCodeClass.POWER: + return SensorDeviceClass.POWER + if self.erd_code_class == ErdCodeClass.ENERGY: + return SensorDeviceClass.ENERGY + if self.erd_code_class == ErdCodeClass.HUMIDITY: + return SensorDeviceClass.HUMIDITY + + return None + + def _get_state_class(self) -> Optional[str]: + if self._state_class_override: + return self._state_class_override + + if self.device_class in [SensorDeviceClass.TEMPERATURE, SensorDeviceClass.ENERGY]: + return SensorStateClass.MEASUREMENT + if self.erd_code_class in [ErdCodeClass.FLOW_RATE, ErdCodeClass.PERCENTAGE, ErdCodeClass.HUMIDITY]: + return SensorStateClass.MEASUREMENT + if self.erd_code_class in [ErdCodeClass.LIQUID_VOLUME]: + return SensorStateClass.TOTAL_INCREASING + + return None + + def _get_icon(self): + if self.erd_code_class == ErdCodeClass.DOOR: + if self.state.lower().endswith("open"): + return "mdi:door-open" + if self.state.lower().endswith("closed"): + return "mdi:door-closed" + return super()._get_icon() + + async def set_value(self, value): + """Sets the ERD value, assumes that the data type is correct""" + try: + await self.appliance.async_set_erd_value(self.erd_code, value) + except: + _LOGGER.warning(f"Could not set {self.name} to {value}") \ No newline at end of file diff --git a/entities/common/ge_erd_switch.py b/entities/common/ge_erd_switch.py new file mode 100644 index 0000000..0fb3703 --- /dev/null +++ b/entities/common/ge_erd_switch.py @@ -0,0 +1,33 @@ +import logging + +from gehomesdk import ErdCodeType +from homeassistant.components.switch import SwitchEntity + +from ...devices import ApplianceApi +from .ge_erd_binary_sensor import GeErdBinarySensor +from .bool_converter import BoolConverter + +_LOGGER = logging.getLogger(__name__) + +class GeErdSwitch(GeErdBinarySensor, SwitchEntity): + """Switches for boolean ERD codes.""" + device_class = "switch" + + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, bool_converter: BoolConverter = BoolConverter(), erd_override: str = None, icon_on_override: str = None, icon_off_override: str = None, device_class_override: str = None): + super().__init__(api, erd_code, erd_override, icon_on_override, icon_off_override, device_class_override) + self._converter = bool_converter + + @property + def is_on(self) -> bool: + """Return True if switch is on.""" + return self._converter.boolify(self.appliance.get_erd_value(self.erd_code)) + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + _LOGGER.debug(f"Turning on {self.unique_id}") + await self.appliance.async_set_erd_value(self.erd_code, self._converter.true_value()) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + _LOGGER.debug(f"Turning off {self.unique_id}") + await self.appliance.async_set_erd_value(self.erd_code, self._converter.false_value()) diff --git a/entities/common/ge_erd_timer_sensor.py b/entities/common/ge_erd_timer_sensor.py new file mode 100644 index 0000000..3f5e905 --- /dev/null +++ b/entities/common/ge_erd_timer_sensor.py @@ -0,0 +1,30 @@ +import asyncio +from datetime import timedelta +from typing import Optional +import logging +import async_timeout + +from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits + +from .ge_erd_sensor import GeErdSensor +from ...devices import ApplianceApi + + +_LOGGER = logging.getLogger(__name__) + +class GeErdTimerSensor(GeErdSensor): + """GE Entity for timer sensors""" + + async def set_timer(self, duration: timedelta): + try: + await self.appliance.async_set_erd_value(self.erd_code, duration) + except: + _LOGGER.warning("Could not set timer value", exc_info=1) + + async def clear_timer(self): + try: + #There's a stupid issue in that if the timer has already expired, the beeping + #won't turn off... I don't see any way around it though. + await self.appliance.async_set_erd_value(self.erd_code, timedelta(seconds=0)) + except: + _LOGGER.warning("Could not clear timer value", exc_info=1) diff --git a/entities/common/ge_humidifier.py b/entities/common/ge_humidifier.py new file mode 100644 index 0000000..e102e3b --- /dev/null +++ b/entities/common/ge_humidifier.py @@ -0,0 +1,105 @@ +import abc +import logging +from typing import Coroutine, Any, Optional + +from homeassistant.components.humidifier import HumidifierEntity, HumidifierDeviceClass +from homeassistant.components.humidifier.const import HumidifierEntityFeature + +from gehomesdk import ErdCodeType, ErdOnOff + +from ...const import DOMAIN +from ...devices import ApplianceApi +from .ge_entity import GeEntity + +DEFAULT_TARGET_PRECISION = 5 + +_LOGGER = logging.getLogger(__name__) + +class GeHumidifier(GeEntity, HumidifierEntity, metaclass=abc.ABCMeta): + """GE Humidifier Abstract Entity """ + + def __init__( + self, + api: ApplianceApi, + device_class: HumidifierDeviceClass, + power_status_erd_code: ErdCodeType, + target_humidity_erd_code: ErdCodeType, + current_humidity_erd_code: ErdCodeType, + range_min: int, + range_max: int, + target_precision = DEFAULT_TARGET_PRECISION + ): + super().__init__(api) + self._device_class = device_class + self._power_status_erd_code = power_status_erd_code + self._target_humidity_erd_code = target_humidity_erd_code + self._current_humidity_erd_code = current_humidity_erd_code + self._range_min = range_min + self._range_max = range_max + self._target_precision = target_precision + + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.serial_or_mac}_{self._device_class}" + + @property + def name(self) -> Optional[str]: + return f"{self.serial_or_mac} {self._device_class.title()}" + + @property + def target_humidity(self) -> int | None: + return int(self.appliance.get_erd_value(self._target_humidity_erd_code)) + + @property + def current_humidity(self) -> int | None: + return int(self.appliance.get_erd_value(self._current_humidity_erd_code)) + + @property + def min_humidity(self) -> int: + return self._range_min + + @property + def max_humidity(self) -> int: + return self._range_max + + @property + def supported_features(self) -> HumidifierEntityFeature: + return HumidifierEntityFeature(HumidifierEntityFeature.MODES) + + @property + def is_on(self) -> bool: + return self.appliance.get_erd_value(self._power_status_erd_code) == ErdOnOff.ON + + @property + def device_class(self): + return self._device_class + + async def async_set_humidity(self, humidity: int) -> Coroutine[Any, Any, None]: + # round to target precision + target = round(humidity / self._target_precision) * self._target_precision + + # if it's the same, just exit + if self.target_humidity == target: + return + + _LOGGER.debug(f"Setting Target Humidity from {self.target_humidity} to {target}") + + # make sure we're on + if not self.is_on: + await self.async_turn_on() + + # set the target humidity + await self.appliance.async_set_erd_value( + self._target_humidity_erd_code, + target, + ) + + async def async_turn_on(self): + await self.appliance.async_set_erd_value( + self._power_status_erd_code, ErdOnOff.ON + ) + + async def async_turn_off(self): + await self.appliance.async_set_erd_value( + self._power_status_erd_code, ErdOnOff.OFF + ) \ No newline at end of file diff --git a/entities/common/ge_water_heater.py b/entities/common/ge_water_heater.py new file mode 100644 index 0000000..88b376a --- /dev/null +++ b/entities/common/ge_water_heater.py @@ -0,0 +1,42 @@ +import abc +import logging +from typing import Any, Dict, List, Optional + +from homeassistant.components.water_heater import WaterHeaterEntity +from homeassistant.const import UnitOfTemperature +from gehomesdk import ErdCode, ErdMeasurementUnits +from ...const import DOMAIN +from .ge_erd_entity import GeEntity + +_LOGGER = logging.getLogger(__name__) + +class GeAbstractWaterHeater(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): + """Mock temperature/operation mode supporting device as a water heater""" + + @property + def heater_type(self) -> str: + raise NotImplementedError + + @property + def operation_list(self) -> List[str]: + raise NotImplementedError + + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.serial_or_mac}_{self.heater_type}" + + @property + def name(self) -> Optional[str]: + return f"{self.serial_or_mac} {self.heater_type.title()}" + + @property + def temperature_unit(self): + #It appears that the GE API is alwasy Fehrenheit + #measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + #if measurement_system == ErdMeasurementUnits.METRIC: + # return UnitOfTemperature.CELSIUS + return UnitOfTemperature.FAHRENHEIT + + @property + def supported_features(self): + raise NotImplementedError diff --git a/entities/common/options_converter.py b/entities/common/options_converter.py new file mode 100644 index 0000000..5759f46 --- /dev/null +++ b/entities/common/options_converter.py @@ -0,0 +1,10 @@ +from typing import Any, List, Optional + +class OptionsConverter: + @property + def options(self) -> List[str]: + return [] + def from_option_string(self, value: str) -> Any: + return value + def to_option_string(self, value: Any) -> Optional[str]: + return str(value) diff --git a/entities/dehumidifier/__init__.py b/entities/dehumidifier/__init__.py new file mode 100644 index 0000000..f68fa50 --- /dev/null +++ b/entities/dehumidifier/__init__.py @@ -0,0 +1,2 @@ +from .dehumidifier import GeDehumidifier +from .dehumidifier_fan_speed_sensor import GeDehumidifierFanSpeedSensor \ No newline at end of file diff --git a/entities/dehumidifier/__pycache__/__init__.cpython-313.pyc b/entities/dehumidifier/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7af83e82b4ce931968ef3049fcc277329f7abf35 GIT binary patch literal 301 zcmY*VK}y6x5bVk1u&C<`R@jSRF3tlKFNzDh;3W@emZXymI5S};Ie7FDKEf{z=*83C zgnYmrSt0hJyQ=6a=$@9#6X1Hix$JtLUy1xxb_COk!5frNiXCi)7wd$xUAC28uA!Ah zeh|D;hO!OEH5S+YY=En(*jDJFW*6x2LbA!@4!#OwYgE^dvh$7j0p W=^5#iaLVml3L!q=>Jv78!TbkKbXK4Loh=TLoj0zV=z+@Q#!LIOBH8uZemf1 zf=f~5OJ$&nmjOV+Pm}o;XRxnhP)NK>P~ef-|9aT{gwf zgr@~8#L+yHjkkWYYdMbQ_nc#z2RPD;jw2~_5J};A{+zpY8ZEg*;chIQiceN6YCNef zE>*OmwxFqalZs?ShU}eepC#4XT0xyCD~17#{T)V1H#KwlnyQ#fSQTBwm%_z~YEdQo zyH(~CTvE-fqLbF&Fs<;4G+JkLT+aCk*3-HH00zV+^k{IlkT$q*I*dwv?-Z57mjWLqv zAiBlfVzF?_HR4ukT7lZp z84#R^8hiUO81fSH5apR{D<~Why3V!lWF3Qbv7Io|U3pi*3)1k{95;e_SC(^jomF?Y zB<(bz=e8u@k#H!T{lOQ79of#>evv2dk<>mu1Kl zS;a;9wo+bFF^wj2FgcmOF6R>2bSftUIhCBseIREOnRJT8lBsKn{A^CXK9y20;x1S35Oz^3EfSVKi1=l>Kw(Li4QkG~J?@kAD7MvGnGw_`I>TgKGyWdF{r9C5yRFEEW~Fnxa?ZF$kD z(+%;GC0_dUPUEs-T~_Mi%@xo3iJ>QzFT=;5?`@o)vChw|xYth(Ke_j%(D!_xabecF zFuTIS-JWPe=(mLakKb<$j9UZab>Y1gcD?uTmo^L}we_$IPi-;GifY*AV z8xE-ZY`7=`0bqf$RCL!EZ0(f;00mUq9$lgS76|F@x*;)68UyONGT6p{H$DXFFjYZ3 z*KZ_K(lZA4!Oo!XjAM~9A`o`9Dqww4UT9VwgB;uCp4UZ&sFo8j-AAv2!s^A3SDcDXLa$>Kra+2EVe0Z0K1o$Yce#h=?T@M81We!%A-Z~R z_1;EgpdLB7#-D_JL+G=FzJ@Sn31bkstC zZ|!gv4JX@KMIipq&T1DxP~95+sA*KT8Nlzranp#trUi}Nb|HdtThYqO&9Z94szHwI zve7R7IH)xGVS1~%hAd%djT^GDZsoKRf?) z=#$*@{^$GF_FZ|wC0@mP8?iwvHrR-bTe0!y$$BiY;$II(R`myZBYegRpZUxF#@V!W zHvPqcdRS_3Qk|3jZBRG-_G;j2jQNabu146;LSCRnSJQYDaiINyUP4zYsw|U$O!Hz1 z(nBV^EdOdrDYrO+EH7x-Fv}WL^D3}?vRtedfM7K!`IyFCvuJZQT!jwBT(&C;J_8Km z1}tkDlPzmNU!c;|B4jT#5MWxZAS))|-OVMiGAIqT+KYl(F3YmWU|Iofgu7xJ9}83i zooJ_S{1K+_e~-Rm54;K?k+uHuZ3OGK+d0|dZOZEj zd{}st{OHy;0>c;V+N;4_K<#ADwjz1`gyZ9iK{V>H`P&}z=Y+f|H{a0_3g z6aBJhsQHz4lfb>#jGjyQ17Oka!MFs|mYZRiuh7B2q43uz^%7nGC;G|PsP82@wt-?V k(d?_ho<`u%Ujv68P1XYg_kG{67nmIL;rzFVuD1UF09#vtod5s; literal 0 HcmV?d00001 diff --git a/entities/dehumidifier/__pycache__/dehumidifier_fan_options.cpython-313.pyc b/entities/dehumidifier/__pycache__/dehumidifier_fan_options.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5964f036f8e54df86a3f94bd851ea40f31ec345c GIT binary patch literal 2308 zcma)7O>7fK6rTO@dTpH8;6P$Xz(G!02gMf3Pk&O1EF4H|C#k&97R6#Q&f09Tciq{w zQCg{@sH)miFO38e0;(Qbq#lr3p`aexV=Gmvogf)aQx&Q0ft#gJm2&Ev^$$rvh>`u~ z&3kX&{CwZMUGL~{B4{5@44ifI2>nS1t!8T~>mPu!gd`*}afE5l#+ew4*%*hp7>{{B zwQ+G0ma<5k8?BCgCF5A`$0`rmAb{e2^jTNxTrr98GJIVw$R!yU#*A*gcIH zKW_K3xnP4rwNmJT6C*Co?p4tF;fE0?QI?8Z#2Yti)h0#Y=3;mJ(>lC@XQh z(L4NDa~`%!d`bYnI10z79_#>%6D-@BGk8g`>@LZUJ91*sK?bAB*-}AWP_wFnA8-AM z9aD&ifEBZZwM_G_taSyD^p@6dfmlNGmXV4uGf&TNwItxWwQcay99!B3ueikp!%@zQ z-ee3mj11O`rgk8tZ%=M}GOl$sZ?H362yH`th}W252n>MM7-i87W;Q5bdO}QhLb$}- zTZEgLj1qoo`q&h47$ycd)$C%BCA^{JvxHF%dN{wINa2jGWz}3bQ!-4wAZPSKQP&jB zG{QMWKC2g$Fbq^vRgCb$hP&Q0kh5t`)-5I(q2eO3H+2$vK6paSczOj!1jGgOm&f2zp>J4UwLJ$bd$Mu?((^-1u#_m%2ju{B9{MK z=_?zQVavYAE`iv}>3m7aw7~>N8}xfHSVDWz3Fa~IufB~?+Jn#r2%iFfV1xezle9yk zEv*F}-AMrT19#xw{D1n<}mKvdoSyMf?0a&0(T9geOBqMt8TJo7&d{2Kl_yc(Maz0M&|{{s>Wqf+~epLdziZcpQTPB%CeR&D6&kPvRu#?N_ncg zWcl4vI^XoTWjU*2!_2EtqjYeKMXUo&%|%SHA{_7z_kf1!_2Ygj=xfLH72*I0vQq|f zrcE^?r%fn~XG*4G$TA)T-#Yw}MP^L5XbDt2nM+-lGM9bpi0bPc z5;_~^x?r~*_?oLD=x%ZkXjk_LHs5uDBo~q>-z?QW&(047{5UC?`va z^-{&x8rS`s=;t++K6;S#j>UG_~p+xL|+4 Pdl_G4_|QKHdMoSyE*J(m literal 0 HcmV?d00001 diff --git a/entities/dehumidifier/__pycache__/dehumidifier_fan_speed_sensor.cpython-313.pyc b/entities/dehumidifier/__pycache__/dehumidifier_fan_speed_sensor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d5a6ef59a8627dda567dafac8e037a17574a252 GIT binary patch literal 1949 zcmaJ>L66f$6n>8F#7>;eLVyhm?0R8S;YzSlVO0qVG+h>?Y@rC_suC$9*O>&<#7<|N zmG(w^sOW)152)8xs#J07PbjBei}paUidw0sy#;04qMrK3PS`D#n8eTD%$s@R`QGlJX-7dTbXpQ zaG9>MrcDDLvMnhcd*dR#-S7HMzvWYQ$qj58MSjr!pcnaJz*oXxowA6sEwPEE#03nn z5;p0LjUJ7Sac8CDa-O&@x{;f>O6aP2JMlcRe$w`g!Kj1a7IM)5CfUF;w#4K)*!kH- zJJV2XxuM!hBWtVpWbw)2qv4}Ba;K*qI4#-vS-7L!QSL}gYv^`i7M90bu)J-sf&=Fy zW-Nj=6(7D#Up;Y;(g`V9To%i2&yO|An#9AC#)d#4(!hM|6@4!ZcDnnUblvwT@dO1s z!~1yTM*kZ%`eAox&fX@HMIt2f<(Q|BKU=(nl0-H~*U+4o9>7g}r|pLBz{jX3M{+Wb z>?n@vWSyL&Eo-UYgqst!y=20H&R8h`ZFneM#cZm)-=i$n2=N0yB825|eVoU0kJE1D zV};XBi%p}0iM+AB!jy&CC(M3f4hS=YCf3QA3c|(ro0E1o{%8rKw*2;z*XI%P;Dy~@ z7|A&@1AnMG{U9eiPD)g ztnxHV-y?W{+I8Ui#JB5x{U!H1gE&*Hex)ssqX!cDg&tpN*LQ1V>~o~aszq3Uo5>v{ zTsd%YUlZmr<1IKR&qGUM2hm|=W8IzH5OJ|hBO+A3?sob#P4i)#6;DSY36qzn75x9}RPSa?y_1eVjo@(M^ehJN87%GZh8C0zw zZhd#_NwxO4TKj47=M#@kY*w$`TOE{VzPa%Ag(u}xkISciT-_|6`NcT%;@8s52qeAo zv^+DE@%;}Ma{b~$Ed!4-`C3VSG$o<6Bqok#Wv?d9Dhl-nZkG~*!YAFZ+3yH!5b{~y z?TkGoLRvoKQO8HchFB;folImRmXR4&5Zy$FT91WTQ_%)HF1-5*GKX|{#Ep=_Px=w% z7`-?y6OVQ}gy1*L;bbl{|8Bw|n-T$q5ene<(Y*K)hIu%$Fu;G+8Y~_k95x4Uo_t=O zQCFnjiwB3Q0K+Vl=Z3iiYA`c9)DxJ8%AsK)fd-hzhQ$OWQaU zjozlB@_F;qgiI!so04fvHh2i>W{I@4chUr%XQGG&U))54374d2aO@eF&*1&v;L0Xk R*;Y)c^6l9_0bfake*vWq>E8eV literal 0 HcmV?d00001 diff --git a/entities/dehumidifier/const.py b/entities/dehumidifier/const.py new file mode 100644 index 0000000..1e5a8b3 --- /dev/null +++ b/entities/dehumidifier/const.py @@ -0,0 +1,3 @@ +SMART_DRY = "Smart Dry" +DEFAULT_MIN_HUMIDITY = 35 +DEFAULT_MAX_HUMIDITY = 80 \ No newline at end of file diff --git a/entities/dehumidifier/dehumidifier.py b/entities/dehumidifier/dehumidifier.py new file mode 100644 index 0000000..1ed2755 --- /dev/null +++ b/entities/dehumidifier/dehumidifier.py @@ -0,0 +1,74 @@ +"""GE Home Dehumidifier""" +import logging + +from homeassistant.components.humidifier import HumidifierDeviceClass +from homeassistant.components.humidifier.const import HumidifierEntityFeature + +from gehomesdk import ErdCode, DehumidifierTargetRange + +from ...devices import ApplianceApi +from ..common import GeHumidifier +from .const import * +from .dehumidifier_fan_options import DehumidifierFanSettingOptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class GeDehumidifier(GeHumidifier): + """GE Dehumidifier""" + + icon = "mdi:air-humidifier" + + def __init__(self, api: ApplianceApi): + + #try to get the range + range: DehumidifierTargetRange = api.try_get_erd_value(ErdCode.DHUM_TARGET_HUMIDITY_RANGE) + low = DEFAULT_MIN_HUMIDITY if range is None else range.min_humidity + high = DEFAULT_MAX_HUMIDITY if range is None else range.max_humidity + + #try to get the fan mode and determine feature + mode = api.try_get_erd_value(ErdCode.AC_FAN_SETTING) + self._has_fan = mode is not None + self._mode_converter = DehumidifierFanSettingOptionsConverter() + + #initialize the dehumidifier + super().__init__(api, + HumidifierDeviceClass.DEHUMIDIFIER, + ErdCode.AC_POWER_STATUS, + ErdCode.DHUM_TARGET_HUMIDITY, + ErdCode.DHUM_CURRENT_HUMIDITY, + low, + high + ) + + @property + def supported_features(self) -> HumidifierEntityFeature: + if self._has_fan: + return HumidifierEntityFeature(HumidifierEntityFeature.MODES) + else: + return HumidifierEntityFeature(0) + + @property + def mode(self) -> str | None: + if not self._has_fan: + raise NotImplementedError() + + return self._mode_converter.to_option_string( + self.appliance.get_erd_value(ErdCode.AC_FAN_SETTING) + ) + + @property + def available_modes(self) -> list[str] | None: + if not self._has_fan: + raise NotImplementedError() + + return self._mode_converter.options + + async def async_set_mode(self, mode: str) -> None: + if not self._has_fan: + raise NotImplementedError() + + """Change the selected mode.""" + _LOGGER.debug(f"Setting mode from {self.mode} to {mode}") + + new_state = self._mode_converter.from_option_string(mode) + await self.appliance.async_set_erd_value(ErdCode.AC_FAN_SETTING, new_state) diff --git a/entities/dehumidifier/dehumidifier_fan_options.py b/entities/dehumidifier/dehumidifier_fan_options.py new file mode 100644 index 0000000..6ef918d --- /dev/null +++ b/entities/dehumidifier/dehumidifier_fan_options.py @@ -0,0 +1,29 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdAcFanSetting +from ..common import OptionsConverter +from .const import SMART_DRY + +_LOGGER = logging.getLogger(__name__) + +class DehumidifierFanSettingOptionsConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [SMART_DRY] + [i.stringify() for i in [ErdAcFanSetting.LOW, ErdAcFanSetting.MED, ErdAcFanSetting.HIGH]] + + def from_option_string(self, value: str) -> Any: + try: + if value == SMART_DRY: + return ErdAcFanSetting.DEFAULT + return ErdAcFanSetting[value.upper()] + except: + _LOGGER.warning(f"Could not set fan setting to {value.upper()}") + return ErdAcFanSetting.DEFAULT + def to_option_string(self, value: ErdAcFanSetting) -> Optional[str]: + try: + if value is not None: + return SMART_DRY if value == ErdAcFanSetting.DEFAULT else value.stringify() + except: + pass + return SMART_DRY diff --git a/entities/dehumidifier/dehumidifier_fan_speed_sensor.py b/entities/dehumidifier/dehumidifier_fan_speed_sensor.py new file mode 100644 index 0000000..dd8424a --- /dev/null +++ b/entities/dehumidifier/dehumidifier_fan_speed_sensor.py @@ -0,0 +1,40 @@ +from ...devices import ApplianceApi +from ..common import GeErdSensor +from .dehumidifier_fan_options import DehumidifierFanSettingOptionsConverter +from gehomesdk import ErdCodeType, ErdCodeClass, ErdDataType, ErdAcFanSetting + +class GeDehumidifierFanSpeedSensor(GeErdSensor): + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_override: str = None, + icon_override: str = None, + device_class_override: str = None, + state_class_override: str = None, + uom_override: str = None, + data_type_override: ErdDataType = None + ): + + super().__init__( + api, + erd_code, + erd_override, + icon_override, + device_class_override, + state_class_override, + uom_override, + data_type_override + ) + + self._converter = DehumidifierFanSettingOptionsConverter() + + @property + def native_value(self): + try: + value: ErdAcFanSetting = self.appliance.get_erd_value(self.erd_code) + return self._converter.to_option_string(value) + except KeyError: + return None + + diff --git a/entities/dishwasher/__init__.py b/entities/dishwasher/__init__.py new file mode 100644 index 0000000..bef929d --- /dev/null +++ b/entities/dishwasher/__init__.py @@ -0,0 +1 @@ +from .ge_dishwasher_control_locked_switch import GeDishwasherControlLockedSwitch \ No newline at end of file diff --git a/entities/dishwasher/__pycache__/__init__.cpython-313.pyc b/entities/dishwasher/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7da060fe74b380fde812037e5e2ff1e3349ba006 GIT binary patch literal 256 zcmey&%ge<81dZ0J83920F^B^LOi;#W0U%>4Loh=yqc?*WV-ceQLpqZt^GimcuqNXz zdG}P8%;Jpl#Nv$9BIo?PlA`<^pZw(P)Rf@z%#!2`KTW1v%IT@`DR4#c$q-fXIbdb+ z#b9+s%s}&2GJFP-47VKhfojq+)Af@}i%arzfjV*v^7B&jN{aP?MrGvZrs@MZnI)O2 z#rgHR zy=C6TPG;b#u`r0@Y0Kal*DR*xja9nhiO#+)I+Ryf5OU^kFlUbr3Y}&!My5Fp`#f+E zf%BuPsy}*x@GuKIN(bIbMHKZ9Hr~uC0>I`bw3n4&->HUTZe$8)Tzi zS+B0(3=Bd2U1@X?bfG}c=Vhzpuwd77OHN;etV`Q1L>Kic15{1{jw;t|gG4y5S`!U8$>pCQc0*Ay6H9?47iQJDKM`+oMguyj;d`s5xj zH;4#Jh!mpx_PbqRm z5!G=_)Dns7NM6HUbMb8$0kS9s>G4Pzk~kF-57ea!T+!Z(Czwnujp%n-z}4C$ q$^R`Vvk#J2E%Q8HQJW;Dk({M8P5Ta|Uy%QuHMGTddw&2~T=6gWQ1gBO literal 0 HcmV?d00001 diff --git a/entities/dishwasher/ge_dishwasher_control_locked_switch.py b/entities/dishwasher/ge_dishwasher_control_locked_switch.py new file mode 100644 index 0000000..55923d8 --- /dev/null +++ b/entities/dishwasher/ge_dishwasher_control_locked_switch.py @@ -0,0 +1,12 @@ +from gehomesdk import ErdCode, ErdOperatingMode + +from ..common import GeErdSwitch + +# TODO: This is actually controlled through the 0x3007 ERD value (SOUND). +# The conversions are a pain in the butt, so this will be left for later. +class GeDishwasherControlLockedSwitch(GeErdSwitch): + @property + def is_on(self) -> bool: + mode: ErdOperatingMode = self.appliance.get_erd_value(ErdCode.OPERATING_MODE) + return mode == ErdOperatingMode.CONTROL_LOCKED + diff --git a/entities/fridge/__init__.py b/entities/fridge/__init__.py new file mode 100644 index 0000000..2d14761 --- /dev/null +++ b/entities/fridge/__init__.py @@ -0,0 +1,5 @@ +from .ge_fridge import GeFridge +from .ge_freezer import GeFreezer +from .ge_dispenser import GeDispenser +from .convertable_drawer_mode_options import ConvertableDrawerModeOptionsConverter +from .ge_fridge_ice_control_switch import GeFridgeIceControlSwitch \ No newline at end of file diff --git a/entities/fridge/__pycache__/__init__.cpython-313.pyc b/entities/fridge/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6297c52734a78fe4f3401f2129a972f43a2c4bdc GIT binary patch literal 469 zcmYL_zfQw25XSAMY1%>&0|Nt6%g~_w+hK}AI(K%x&|mE@XQYU1b|s9@tEcnDr8 zQdpSSkUArAc3bdZf8X8t`&nLgI&DVl^|(L#q4U$2&8n@%@|?sQ8?!MFS-_F^jIM>X zz(L1qC#(l9x>nc2M&O}mwHr2r7PfjUX$-tCPB8;sHb-!UGERVW%V>ZXKr&k+7)qT( zrmgK?s_Y4{m^{p3h?522jf&weFQm%!x@QOXH-Xm?P`yAkA1`DP&29(nvXy|CS{ypW zFWc5zB-Tw~n+ov;i>@hkqkpo9En1|+7HS2@hM79}OBON$L3a#UXp2UVM&8+yc2=JH z)SSx1j~;cQQW{Njl|fc$pE71D1)q#8Bxry2R$mC2$wG*u{JCtN5d3itXShpg<`?xI YZQl;(ykgyo9agMgvHK5pfjiXr1%@$EBX+9 zh`tS6xtXMiNw0cm;tO;ZLaHY-=bZ05bAEE>*)&r?%a1Sl{^|+o1 z05Q(NnIVlFAS-g^yMg122QRf|lP~eYKS%%|S4m99$s3s&4tjTqf zk|s&Zjox~}{2L_|47GdN3b?HCNJB4hgZrxXN0O^_M+nLMKqFtpyVC9SxJq;-=VLC_ zTF3Q=-1D^?L*4G!JU=LphPjgDcdpk{@rICt*6nzmwA$@f(9hLHhmTWqB-_>Qii_%l~zwN!!Uk>v&Ze+6l{jp!Gm=)vyP_6mk-A`GkvU^DcE|nK1@M2 ue7ipd+u`n~DX^a6AhR<`hnxEoGu+ynY=qg|BokU6qj&G}Gq5A~boqZN4CAE$ literal 0 HcmV?d00001 diff --git a/entities/fridge/__pycache__/convertable_drawer_mode_options.cpython-313.pyc b/entities/fridge/__pycache__/convertable_drawer_mode_options.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..92d16b24ebcfa26b4aff80a8eda521103eb9c409 GIT binary patch literal 3596 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 BG(7+S literal 0 HcmV?d00001 diff --git a/entities/fridge/__pycache__/ge_freezer.cpython-313.pyc b/entities/fridge/__pycache__/ge_freezer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57f0a795e6ed17853124872a0357b3f0aa85a2d5 GIT binary patch literal 1703 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 List[str]: + return [self.to_option_string(i) for i in ErdConvertableDrawerMode if i not in self._excluded_options] + + def from_option_string(self, value: str) -> Any: + try: + v = value.split(" ")[0] + return ErdConvertableDrawerMode[v.upper()] + except: + _LOGGER.warning(f"Could not set drawer mode to {value.upper()}") + return ErdConvertableDrawerMode.NA + def to_option_string(self, value: ErdConvertableDrawerMode) -> Optional[str]: + try: + if value is not None: + v = value.stringify() + t = _TEMP_MAP.get(value, None) + + if t and self._units.temperature_unit == UnitOfTemperature.CELSIUS: + t = self._units.temperature(float(t), UnitOfTemperature.FAHRENHEIT) + t = round(t,1) + + if t: + return f"{v} ({t}{self._units.temperature_unit})" + return v + except: + pass + + return ErdConvertableDrawerMode.NA.stringify() + diff --git a/entities/fridge/ge_abstract_fridge.py b/entities/fridge/ge_abstract_fridge.py new file mode 100644 index 0000000..a024ca1 --- /dev/null +++ b/entities/fridge/ge_abstract_fridge.py @@ -0,0 +1,199 @@ +"""GE Home Sensor Entities - Abstract Fridge""" +import importlib +import sys +import os +import abc +import logging +from typing import Any, Dict, List, Optional + +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.util.unit_conversion import TemperatureConverter +from gehomesdk import ( + ErdCode, + ErdOnOff, + ErdFullNotFull, + FridgeDoorStatus, + FridgeSetPointLimits, + FridgeSetPoints, + FridgeIceBucketStatus, + IceMakerControlStatus +) +from ...const import DOMAIN +from ..common import GeAbstractWaterHeater +from .const import * + +_LOGGER = logging.getLogger(__name__) + +class GeAbstractFridge(GeAbstractWaterHeater): + """Mock a fridge or freezer as a water heater.""" + + # These values are from the Fisher & Paykel RF610AA in imperial units + # They're to be used as hardcoded limits when ErdCode.SETPOINT_LIMITS is unavailable. + temp_limits = {} + temp_limits["fridge_min"] = 32 + temp_limits["fridge_max"] = 46 + temp_limits["freezer_min"] = -6 + temp_limits["freezer_max"] = 7 + + @property + def heater_type(self) -> str: + raise NotImplementedError + + @property + def turbo_erd_code(self) -> str: + raise NotImplementedError + + @property + def turbo_mode(self) -> str: + raise NotImplementedError + + @property + def operation_list(self) -> List[str]: + try: + return [OP_MODE_NORMAL, OP_MODE_SABBATH, self.turbo_mode] + except: + _LOGGER.debug("Turbo mode not supported.") + return [OP_MODE_NORMAL, OP_MODE_SABBATH] + + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.serial_number}_{self.heater_type}" + + @property + def name(self) -> Optional[str]: + return f"{self.serial_or_mac} {self.heater_type.title()}" + + @property + def target_temps(self) -> FridgeSetPoints: + """Get the current temperature settings tuple.""" + return self.appliance.get_erd_value(ErdCode.TEMPERATURE_SETTING) + + @property + def target_temperature(self) -> int: + """Return the temperature we try to reach.""" + return getattr(self.target_temps, self.heater_type) + + @property + def current_temperature(self) -> int: + """Return the current temperature.""" + try: + current_temps = self.appliance.get_erd_value(ErdCode.CURRENT_TEMPERATURE) + current_temp = getattr(current_temps, self.heater_type) + if current_temp is None: + _LOGGER.exception(f"{self.name} has None for current_temperature (available: {self.available})!") + return current_temp + except: + _LOGGER.debug("Device doesn't report current temperature.") + return None + + async def async_set_temperature(self, **kwargs): + target_temp = kwargs.get(ATTR_TEMPERATURE) + if target_temp is None: + return + if not self.min_temp <= target_temp <= self.max_temp: + raise ValueError("Tried to set temperature out of device range") + + if self.heater_type == HEATER_TYPE_FRIDGE: + new_temp = FridgeSetPoints(fridge=target_temp, freezer=self.target_temps.freezer) + elif self.heater_type == HEATER_TYPE_FREEZER: + new_temp = FridgeSetPoints(fridge=self.target_temps.fridge, freezer=target_temp) + else: + raise ValueError("Invalid heater_type") + + await self.appliance.async_set_erd_value(ErdCode.TEMPERATURE_SETTING, new_temp) + + @property + def supported_features(self): + return GE_FRIDGE_SUPPORT + + @property + def setpoint_limits(self) -> FridgeSetPointLimits: + return self.appliance.get_erd_value(ErdCode.SETPOINT_LIMITS) + + @property + def min_temp(self): + """Return the minimum temperature if available, otherwise use hardcoded limits.""" + try: + return getattr(self.setpoint_limits, f"{self.heater_type}_min") + except: + _LOGGER.debug("No temperature setpoint limits available. Using hardcoded limits.") + return TemperatureConverter.convert(self.temp_limits[f"{self.heater_type}_min"], UnitOfTemperature.FAHRENHEIT, self.temperature_unit) + + @property + def max_temp(self): + """Return the maximum temperature if available, otherwise use hardcoded limits.""" + try: + return getattr(self.setpoint_limits, f"{self.heater_type}_max") + except: + _LOGGER.debug("No temperature setpoint limits available. Using hardcoded limits.") + return TemperatureConverter.convert(self.temp_limits[f"{self.heater_type}_max"], UnitOfTemperature.FAHRENHEIT, self.temperature_unit) + + @property + def current_operation(self) -> str: + """Get the current operation mode.""" + if self.appliance.get_erd_value(ErdCode.SABBATH_MODE): + return OP_MODE_SABBATH + try: + if self.appliance.get_erd_value(self.turbo_erd_code): + return self.turbo_mode + except: + _LOGGER.debug("Turbo mode not supported.") + return OP_MODE_NORMAL + + async def async_set_sabbath_mode(self, sabbath_on: bool = True): + """Set sabbath mode if it's changed""" + if self.appliance.get_erd_value(ErdCode.SABBATH_MODE) == sabbath_on: + return + await self.appliance.async_set_erd_value(ErdCode.SABBATH_MODE, sabbath_on) + + async def async_set_operation_mode(self, operation_mode): + """Set the operation mode.""" + if operation_mode not in self.operation_list: + raise ValueError("Invalid operation mode") + if operation_mode == self.current_operation: + return + sabbath_mode = operation_mode == OP_MODE_SABBATH + await self.async_set_sabbath_mode(sabbath_mode) + if not sabbath_mode: + await self.appliance.async_set_erd_value(self.turbo_erd_code, operation_mode == self.turbo_mode) + + @property + def door_status(self) -> FridgeDoorStatus: + """Shorthand to get door status.""" + return self.appliance.get_erd_value(ErdCode.DOOR_STATUS) + + @property + def ice_maker_state_attrs(self) -> Dict[str, Any]: + """Get state attributes for the ice maker, if applicable.""" + data = {} + + if self.api.has_erd_code(ErdCode.ICE_MAKER_BUCKET_STATUS): + erd_val: FridgeIceBucketStatus = self.appliance.get_erd_value(ErdCode.ICE_MAKER_BUCKET_STATUS) + ice_bucket_status = getattr(erd_val, f"state_full_{self.heater_type}") + if ice_bucket_status != ErdFullNotFull.NA: + data["ice_bucket"] = self._stringify(ice_bucket_status) + + if self.api.has_erd_code(ErdCode.ICE_MAKER_CONTROL): + erd_val: IceMakerControlStatus = self.appliance.get_erd_value(ErdCode.ICE_MAKER_CONTROL) + ice_control_status = getattr(erd_val, f"status_{self.heater_type}") + if ice_control_status != ErdOnOff.NA: + data["ice_maker"] = self._stringify(ice_control_status) + + return data + + @property + def door_state_attrs(self) -> Dict[str, Any]: + """Get state attributes for the doors.""" + return {} + + @property + def other_state_attrs(self) -> Dict[str, Any]: + """Other state attributes for the entity""" + return {} + + @property + def extra_state_attributes(self) -> Dict[str, Any]: + door_attrs = self.door_state_attrs + ice_maker_attrs = self.ice_maker_state_attrs + other_state_attrs = self.other_state_attrs + return {**door_attrs, **ice_maker_attrs, **other_state_attrs} diff --git a/entities/fridge/ge_dispenser.py b/entities/fridge/ge_dispenser.py new file mode 100644 index 0000000..394af9e --- /dev/null +++ b/entities/fridge/ge_dispenser.py @@ -0,0 +1,126 @@ +"""GE Home Sensor Entities - Dispenser""" + +import logging +from typing import List, Optional, Dict, Any + +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.util.unit_conversion import TemperatureConverter + +from gehomesdk import ( + ErdCode, + ErdHotWaterStatus, + ErdPresent, + ErdPodStatus, + ErdFullNotFull, + HotWaterStatus +) + +from ..common import GeAbstractWaterHeater +from .const import ( + HEATER_TYPE_DISPENSER, + OP_MODE_NORMAL, + OP_MODE_SABBATH, + GE_FRIDGE_SUPPORT +) + +_LOGGER = logging.getLogger(__name__) + +class GeDispenser(GeAbstractWaterHeater): + """Entity for in-fridge dispensers""" + + # These values are from FridgeHotWaterFragment.smali in the android app (in imperial units) + # However, the k-cup temperature max appears to be 190. Since there doesn't seem to be any + # Difference between normal heating and k-cup heating based on what I see in the app, + # we will just set the max temp to 190 instead of the 185 + _min_temp = 90 + _max_temp = 190 #185 + icon = "mdi:cup-water" + heater_type = HEATER_TYPE_DISPENSER + + @property + def hot_water_status(self) -> HotWaterStatus: + """Access the main status value conveniently.""" + return self.appliance.get_erd_value(ErdCode.HOT_WATER_STATUS) + + @property + def supports_k_cups(self) -> bool: + """Return True if the device supports k-cup brewing.""" + status = self.hot_water_status + return status.pod_status != ErdPodStatus.NA and status.brew_module != ErdPresent.NA + + @property + def operation_list(self) -> List[str]: + """Supported Operations List""" + ops_list = [OP_MODE_NORMAL, OP_MODE_SABBATH] + return ops_list + + async def async_set_temperature(self, **kwargs): + target_temp = kwargs.get(ATTR_TEMPERATURE) + if target_temp is None: + return + if not self.min_temp <= target_temp <= self.max_temp: + raise ValueError("Tried to set temperature out of device range") + + await self.appliance.async_set_erd_value(ErdCode.HOT_WATER_SET_TEMP, target_temp) + + async def async_set_sabbath_mode(self, sabbath_on: bool = True): + """Set sabbath mode if it's changed""" + if self.appliance.get_erd_value(ErdCode.SABBATH_MODE) == sabbath_on: + return + await self.appliance.async_set_erd_value(ErdCode.SABBATH_MODE, sabbath_on) + + async def async_set_operation_mode(self, operation_mode): + """Set the operation mode.""" + if operation_mode not in self.operation_list: + raise ValueError("Invalid operation mode") + if operation_mode == self.current_operation: + return + sabbath_mode = operation_mode == OP_MODE_SABBATH + await self.async_set_sabbath_mode(sabbath_mode) + + @property + def supported_features(self): + return GE_FRIDGE_SUPPORT + + @property + def current_operation(self) -> str: + """Get the current operation mode.""" + if self.appliance.get_erd_value(ErdCode.SABBATH_MODE): + return OP_MODE_SABBATH + return OP_MODE_NORMAL + + @property + def current_temperature(self) -> Optional[int]: + """Return the current temperature.""" + return self.hot_water_status.current_temp + + @property + def target_temperature(self) -> Optional[int]: + """Return the target temperature.""" + return self.appliance.get_erd_value(ErdCode.HOT_WATER_SET_TEMP) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return TemperatureConverter.convert(self._min_temp, UnitOfTemperature.FAHRENHEIT, self.temperature_unit) + + @property + def max_temp(self): + """Return the maximum temperature.""" + return TemperatureConverter.convert(self._max_temp, UnitOfTemperature.FAHRENHEIT, self.temperature_unit) + + @property + def extra_state_attributes(self) -> Dict[str, Any]: + data = {} + + data["target_temperature"] = self.target_temperature + if self.hot_water_status.status in [ErdHotWaterStatus.FAULT_LOCKED_OUT, ErdHotWaterStatus.FAULT_NEED_CLEARED]: + data["fault_status"] = self._stringify(self.hot_water_status.status) + if self.supports_k_cups: + data["pod_status"] = self._stringify(self.hot_water_status.pod_status) + if self.hot_water_status.time_until_ready: + data["time_until_ready"] = self._stringify(self.hot_water_status.time_until_ready) + if self.hot_water_status.tank_full != ErdFullNotFull.NA: + data["tank_status"] = self._stringify(self.hot_water_status.tank_full) + + return data diff --git a/entities/fridge/ge_freezer.py b/entities/fridge/ge_freezer.py new file mode 100644 index 0000000..005dba9 --- /dev/null +++ b/entities/fridge/ge_freezer.py @@ -0,0 +1,35 @@ +"""GE Home Sensor Entities - Freezer""" +import logging +from typing import Any, Dict, Optional + +from gehomesdk import ( + ErdCode, + ErdDoorStatus +) + +from .ge_abstract_fridge import ( + ATTR_DOOR_STATUS, + HEATER_TYPE_FREEZER, + OP_MODE_TURBO_FREEZE, + GeAbstractFridge +) + +_LOGGER = logging.getLogger(__name__) + +class GeFreezer(GeAbstractFridge): + """A freezer is basically a fridge.""" + + heater_type = HEATER_TYPE_FREEZER + turbo_erd_code = ErdCode.TURBO_FREEZE_STATUS + turbo_mode = OP_MODE_TURBO_FREEZE + icon = "mdi:fridge-top" + + @property + def door_state_attrs(self) -> Optional[Dict[str, Any]]: + try: + door_status = self.door_status.freezer + if door_status and door_status != ErdDoorStatus.NA: + return {ATTR_DOOR_STATUS: self._stringify(door_status)} + except: + _LOGGER.debug("Device does not report door status.") + return {} diff --git a/entities/fridge/ge_fridge.py b/entities/fridge/ge_fridge.py new file mode 100644 index 0000000..e24c3e0 --- /dev/null +++ b/entities/fridge/ge_fridge.py @@ -0,0 +1,62 @@ +"""GE Home Sensor Entities - Fridge""" +import logging +from typing import Any, Dict + +from gehomesdk import ( + ErdCode, + ErdDoorStatus, + ErdFilterStatus +) + +from .const import * +from .ge_abstract_fridge import ( + ATTR_DOOR_STATUS, + HEATER_TYPE_FRIDGE, + OP_MODE_TURBO_COOL, + GeAbstractFridge +) + +_LOGGER = logging.getLogger(__name__) + +class GeFridge(GeAbstractFridge): + heater_type = HEATER_TYPE_FRIDGE + turbo_erd_code = ErdCode.TURBO_COOL_STATUS + turbo_mode = OP_MODE_TURBO_COOL + icon = "mdi:fridge-bottom" + + @property + def other_state_attrs(self) -> Dict[str, Any]: + if(self.api.has_erd_code(ErdCode.WATER_FILTER_STATUS)): + filter_status: ErdFilterStatus = self.appliance.get_erd_value(ErdCode.WATER_FILTER_STATUS) + if filter_status == ErdFilterStatus.NA: + return {} + return {"water_filter_status": self._stringify(filter_status)} + return {} + + @property + def door_state_attrs(self) -> Dict[str, Any]: + """Get state attributes for the doors.""" + try: + data = {} + door_status = self.door_status + if not door_status: + return {} + door_right = door_status.fridge_right + door_left = door_status.fridge_left + drawer = door_status.drawer + + if door_right and door_right != ErdDoorStatus.NA: + data["right_door"] = door_status.fridge_right.name.title() + if door_left and door_left != ErdDoorStatus.NA: + data["left_door"] = door_status.fridge_left.name.title() + if drawer and drawer != ErdDoorStatus.NA: + data["drawer"] = door_status.drawer.name.title() + + if data: + all_closed = all(v == "Closed" for v in data.values()) + data[ATTR_DOOR_STATUS] = "Closed" if all_closed else "Open" + + return data + except: + _LOGGER.debug("Device does not report door status.") + return {} diff --git a/entities/fridge/ge_fridge_ice_control_switch.py b/entities/fridge/ge_fridge_ice_control_switch.py new file mode 100644 index 0000000..f86c59a --- /dev/null +++ b/entities/fridge/ge_fridge_ice_control_switch.py @@ -0,0 +1,47 @@ +import logging +from gehomesdk import ErdCode, IceMakerControlStatus, ErdOnOff + +from ...devices import ApplianceApi +from ..common import GeErdSwitch, BoolConverter + +_LOGGER = logging.getLogger(__name__) + +class GeFridgeIceControlSwitch(GeErdSwitch): + def __init__(self, api: ApplianceApi, control_type: str): + super().__init__(api, ErdCode.ICE_MAKER_CONTROL, BoolConverter()) + self._control_type = control_type + + @property + def control_status(self) -> IceMakerControlStatus: + return self.appliance.get_erd_value(ErdCode.ICE_MAKER_CONTROL) + + @property + def is_on(self) -> bool: + if self._control_type == "fridge": + return self.control_status.status_fridge == ErdOnOff.ON + else: + return self.control_status.status_freezer == ErdOnOff.ON + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + _LOGGER.debug(f"Turning on {self.unique_id}") + + old_status = self.control_status + if self._control_type == "fridge": + new_status = IceMakerControlStatus(ErdOnOff.ON, old_status.status_freezer) + else: + new_status = IceMakerControlStatus(old_status.status_fridge, ErdOnOff.ON) + + await self.appliance.async_set_erd_value(self.erd_code, new_status) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + _LOGGER.debug(f"Turning off {self.unique_id}") + + old_status = self.control_status + if self._control_type == "fridge": + new_status = IceMakerControlStatus(ErdOnOff.OFF, old_status.status_freezer) + else: + new_status = IceMakerControlStatus(old_status.status_fridge, ErdOnOff.OFF) + + await self.appliance.async_set_erd_value(self.erd_code, new_status) diff --git a/entities/hood/__init__.py b/entities/hood/__init__.py new file mode 100644 index 0000000..abba26b --- /dev/null +++ b/entities/hood/__init__.py @@ -0,0 +1,2 @@ +from .ge_hood_fan_speed import GeHoodFanSpeedSelect +from .ge_hood_light_level import GeHoodLightLevelSelect \ No newline at end of file diff --git a/entities/hood/__pycache__/__init__.cpython-313.pyc b/entities/hood/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3fda0e242f597fc0708eb32c248bfaa5a9f6f1c6 GIT binary patch literal 288 zcmey&%ge<81dZ0J8QDPkF^B^LOi;#W86aaSLoh=yqc?*WV-ceQh|T29DS5mAG)|#8D59DN)WTqDDgDlXGkI&4@EQycTE2zB1VUwGmQks)$ dR|N7u$o0kCK;i>4BO~Ko2IG4S=0zMpJ^(P^P{;rP literal 0 HcmV?d00001 diff --git a/entities/hood/__pycache__/ge_hood_fan_speed.cpython-313.pyc b/entities/hood/__pycache__/ge_hood_fan_speed.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..681117dd49ae585cc6f739e9ebd6b77ecce71649 GIT binary patch literal 4087 zcmbssU2hZF_0EUwaWak_oL#=MWC;1N`HCo^(1Zj+2m~j@D3feLaXT{h#LU+5xOc|v zrmG6dL$y4$tClYN#!dm75?GF+ag2c*-XPDkMZI zD##Nid&%mvz{t>Ulk5vxqC$8Z`^Ib!zK0gRk?(hIZcr&i6sTDMC4w zP+sMzpzL+0WC)1KR;i@MAl$&tt0R6N3bR;j2{CTW^cFlqFK-jw$+!@wOi(*-Uk!IHgv1+79=p2Yi6~Dbiw_)@2BBP zY_J>~T$Rth=9b}dYa>3KC;c0p>k~K3wgLIkjT+> z_$I>qRJTjCkg1gwI1p-=9V!}H71{C}Q>=PitZ`_a?lqqfr`^W9G8OZuj?kEz&E-lsr3AO{-KKBxj$ zwhT`dsHHA)5L9tP^tMbx-?4S5+|Kv3kdu5p^y@`uyE0<&9UyZ^=pYMRoS*uUGHRCc zS*2jwily5M29N@Qqc~|nu}vlJbCZCt1Q~&{fHs-De0gk|1>Q~50vLy)Rv)|Op~r#V zWT0IO6hhY3zvfz#_3db@|2^;)UOEQgE~)Zju=mSo=c7}<8~kMOH$&yOVCxar=Td&$RVMxw$d zL!uEJL*51g4LQr*gsKS2Z;TaK!{HgR3k|*z5z`xnbax@#`81lpgG_b0Ch=kbiUM&1 z0o6-S0@$S2h;44s>aiWQx^+7SlXV_|tJBuCa9cU7tnF_9=+1|CR(8jhBQZ=F2VY1e z(oqR_mcyNu@X>Pk=t}tbvUL1a)yD-}{~|}eaiZhiu72`I{)_-oiq*vl-gqn4oI-k0 z*EH6oX^UpIlt)_Dv>%qz`PxcY(-sVB*?9v}x(Ve8{ol>jQjwbAa`qBMqc8#7haSaM z)G_TxfT2k-jOb|q>ZjmqA>X;>?(;7R;OYV5Z>jQ}?>hIe?P2EQz3T+&bzy^D6@9+m-wRa& z=riF3qN?oo^*u7H1kjV%)03Ycdx_+~5zt;wW(J6j;knN`e%^L{uqvc{{i4$gi?v?Z zuli`f>0u@4Vg0P_vfk(mZvXxK2wcd7bkSgvPP0%&vw8;g>J8L8&ewkl0GrqC-F|0R z_p(r9J9`6-WKwKYj8i=>HQ|(6MfX->10RWA|+Q3j{AXy$ru3np~T(ip8td#*9JBNK5(FEKP57O@dq5}v95u8PU%?ZVdr(lU0KFvx%1u{@Q`AN8*Wi8w=#kTn@53^V>^B%<||OZs1|K zv0zTe&dbXFzv}}uJfuhkV{tUt6_9RHqsr!*9aK(v2 zl>eBxZ|TOZ(pfii3{w@R7_=6es48$A_gB*L73nFHo_~?~6*B)fa_B4at-m+5R+{!d lYuf*Ce5I-TZs3L3!SyX4I{AVCesO^lIMBf0*95U6=f9GgLhAqk literal 0 HcmV?d00001 diff --git a/entities/hood/__pycache__/ge_hood_light_level.cpython-313.pyc b/entities/hood/__pycache__/ge_hood_light_level.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a21c83b42f54295517e9205ea1a958bd851541b GIT binary patch literal 3769 zcmbtWT}&I<6~6OhdkkX?m?RrW!jL6kr)o$V($EG-NS1#mY;1@Y@OI?bgPHB%aqkQy z^kwNjRLw)xuG)0jKBa1SaleclootVJHxNg}@7|1U( zX`*23WXUv$xEwHo08T*lFmDm}WMv=jQFWwxk&YOKIIm-`5S@US3IjAQaZp0r@ZH;E|hF;sV@I%mi z18kfkluHuICpju4`IOkrPmyjeDKwLJq<2K>OZk&xDv*+JVK<-jfnK~)*^LI1{*(mc zHQiz|fFraPG%{#Hl@%h|67+S+APrBEK91J6lUNNqbI=Tj@4SsM!sPTm>tyg_y@uAn z|ISarasAw5GV6_yN%(-b;kMo-74lZSvA4F1Oc4WDI&F$eNS<8f(TbRj5tyBTAU)g? zwTCr?%*viFQRhij?gf}a;LQ4}-jn|CGaJ2~s{Fv2O;x=o?VWl4fBeql-cD6sJu^pg zz6rt`qqkYzJV0rmQXCl_9zir(nla}d6jarWk+v*Vwb1d3vciVCGG=~G z&vj)ME!$jBGv-3U%xihu>dI;Ayt$xtK}WZB&FY#5yt-Nwu8P2QFMOe}Owj4QF?7tYTh~(-IPvlwYGgv+>}mjgs*GwagKzn+C> zER4)Yk*ng9rmG_JEv1b`?Ld;k#Nin5*C0G5XZ9s2Fb9qtYMwdJQthM-%rPr^QxXAB zs`4cF@_p*aWxfi?YKlB6=~;X$oY^O|k~uEM-}(00TjrvXRr02-SemUM3KS>$6$h@0 zZ7MOJ3r^o6AeFL!8lM;*9!#*ngEY;9i)b@k^{MM53ONSc3dD0$;~WL#xjs-`pR!A@ z@YolZoWyMhfvk`+F9u_uMB1NB{c`qavp;{k7`ePIUw%>Mc~HxdA51)&SU+=P?bsh% ze&6zk(`(`NQ*Um{cVR&(5-moe&*bRmRuFc2@AygbVb4HBIN(flgyYO55YU-Z+esbw2FWM>ne4n%>QcBKL}A?)mBif&;7gb>9SjiQz5MI;DNieRFbfv{un0@!I= z&!}k|a>)HfTZ2b|g(~kuMl%dmeacY#ixMjf>?}6e&40_j;mz?{I?@( z$=}_6j^aO&(AT|VGa&3WFNSuyL3LCCmlRUIZqX^?#7Yt8Px@%UDZV5qzWnU?uy!yY z+|qHo6SibRx}Y;jqgi+rvswm?>oqh!_IG{^1goy2qyOa*?b+j-y<6o!qVkOY*l?bbm&FIjYT^gP)4o`1HXFir@ zozRGJbO`o{1u145`c))Xk>Ji0iw%kokOLNa4W(WreL(gH_jObvLFZctd?&bhUy56e zmkIKZ#-BU*ucQ T65zXE7dSA$>}LeIbIgALRhAr4 literal 0 HcmV?d00001 diff --git a/entities/hood/ge_hood_fan_speed.py b/entities/hood/ge_hood_fan_speed.py new file mode 100644 index 0000000..0dcfe73 --- /dev/null +++ b/entities/hood/ge_hood_fan_speed.py @@ -0,0 +1,46 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCodeType, ErdHoodFanSpeedAvailability, ErdHoodFanSpeed, ErdCode +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class HoodFanSpeedOptionsConverter(OptionsConverter): + def __init__(self, availability: ErdHoodFanSpeedAvailability): + super().__init__() + self.availability = availability + self.excluded_speeds = [] + if not availability.off_available: + self.excluded_speeds.append(ErdHoodFanSpeed.OFF) + if not availability.low_available: + self.excluded_speeds.append(ErdHoodFanSpeed.LOW) + if not availability.med_available: + self.excluded_speeds.append(ErdHoodFanSpeed.MEDIUM) + if not availability.high_available: + self.excluded_speeds.append(ErdHoodFanSpeed.HIGH) + if not availability.boost_available: + self.excluded_speeds.append(ErdHoodFanSpeed.BOOST) + + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdHoodFanSpeed if i not in self.excluded_speeds] + def from_option_string(self, value: str) -> Any: + try: + return ErdHoodFanSpeed[value.upper()] + except: + _LOGGER.warning(f"Could not set hood fan speed to {value.upper()}") + return ErdHoodFanSpeed.OFF + def to_option_string(self, value: ErdHoodFanSpeed) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdHoodFanSpeed.OFF.stringify() + +class GeHoodFanSpeedSelect(GeErdSelect): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): + self._availability: ErdHoodFanSpeedAvailability = api.try_get_erd_value(ErdCode.HOOD_FAN_SPEED_AVAILABILITY) + super().__init__(api, erd_code, HoodFanSpeedOptionsConverter(self._availability)) diff --git a/entities/hood/ge_hood_light_level.py b/entities/hood/ge_hood_light_level.py new file mode 100644 index 0000000..a44dccd --- /dev/null +++ b/entities/hood/ge_hood_light_level.py @@ -0,0 +1,42 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCodeType, ErdHoodLightLevelAvailability, ErdHoodLightLevel, ErdCode +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class HoodLightLevelOptionsConverter(OptionsConverter): + def __init__(self, availability: ErdHoodLightLevelAvailability): + super().__init__() + self.availability = availability + self.excluded_levels = [] + if not availability.off_available: + self.excluded_levels.append(ErdHoodLightLevel.OFF) + if not availability.dim_available: + self.excluded_levels.append(ErdHoodLightLevel.DIM) + if not availability.high_available: + self.excluded_levels.append(ErdHoodLightLevel.HIGH) + + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdHoodLightLevel if i not in self.excluded_levels] + def from_option_string(self, value: str) -> Any: + try: + return ErdHoodLightLevel[value.upper()] + except: + _LOGGER.warning(f"Could not set hood light level to {value.upper()}") + return ErdHoodLightLevel.OFF + def to_option_string(self, value: ErdHoodLightLevel) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdHoodLightLevel.OFF.stringify() + +class GeHoodLightLevelSelect(GeErdSelect): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): + self._availability: ErdHoodLightLevelAvailability = api.try_get_erd_value(ErdCode.HOOD_LIGHT_LEVEL_AVAILABILITY) + super().__init__(api, erd_code, HoodLightLevelOptionsConverter(self._availability)) diff --git a/entities/opal_ice_maker/__init__.py b/entities/opal_ice_maker/__init__.py new file mode 100644 index 0000000..5ec3f31 --- /dev/null +++ b/entities/opal_ice_maker/__init__.py @@ -0,0 +1 @@ +from .oim_light_level_options import OimLightLevelOptionsConverter \ No newline at end of file diff --git a/entities/opal_ice_maker/__pycache__/__init__.cpython-313.pyc b/entities/opal_ice_maker/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..976593894ec78e367d3704b08cb4f04a668015e2 GIT binary patch literal 246 zcmXwzF=_)b5Jjb3Fveg=mnz`K#WvOf5?nZ>NlaY$0E5{~b_YwEMUo9Ja|jO>7%Q6rTO@znyjBmj0w|HouM!jjNIpBU;obN>kZsQoJpfT3WgG+F82ZHM8rW zaB1lQR4%B8(Dqiu0V?&tfdkx%IB>NSq?7?vB!p0ImQqCNiT8GG(*`Lc?VE4kym|BH zd+*IoEEXm(K6qn%yd!F@SZV5QWPTiZh?%vOMLp0u{0%6=l>3IbYUK z{n-EwWP>yaI#KcELfJ441NJL{TqGN%(QJ&yWKtmcBi&>En@31NI>mYaG=7kb5G8n+ zD4{~^6vT#u_E))7cpKqTN?_umZaYkvHP={hx$fweSu{W$nWyDBtE^pGt7|L{Kg)V; zQLnBzi`uGYus~;d8=a;&7M-ov4ZUcVwAs48&7+cMkp&IR6wS~|4vTxv?K#U_)u^M< z5=PJ+J)R1nS%M?BTpM+1sj@WXm=8koWr8Ujri zG?C5@QSrO_s1l&DYA_XIM_;8$LahFLhwGjRfNsne`3w69i4Ym=O>PXNhbrU&x0K>(0Bl*vcBpPv^~xIX zDB7A)VVrIwud>W8rAwAs(W~iF!*;BiTC!?&%hXKAPFFQ`#j0s(_|Y9*v(r|+XsCKg zQ)|U*8ckbzO*Jr$s)2b_EjQ!#RDF#FI+%^`$Sb;(7BLxP zWEfbyg8to+N9uMBh1=UtA9U}QH6g)FSUz{|0Z;eCA@nea zPLAmTz-*vGA-x{qs#g*0#NOHY|BgQNIuGIoY4c)e@|VQu{rN9)pXENk&`O+aMo&I# z^E{}diBFbqEjN#yeYF3F!S4pYKl(^&9=Z5q^ex!1l}NS{$&G08xg7#eH(xnK?qy~Z z!b`ctJh-`B1%SD{%w3HWkogz82-a|PyI~A--m8dwFPEMb%Ds1E1q_j;5ict=0UJ^R zg$5%o@pK504B{nXx zKKwLD;_{X>+LA`Mq~k5=_@FipPZGr|LSk^5LsRn1~e zQ&kpL)tXgq7>J{)`fj6WbXFu)t?1Nt3?2I3g5_YHT2Num8pRYa0jh-_1VoWhI*bCl zgJR#%F%WFO3MEx^^paY1;IX;da5P(0=>)9pz(?E?q!J<&_$=C45Fe7qzF3>*e6Qc# zceix!&<+8(BM^U|t89xt-{hA List[str]: + return [i.stringify() for i in ErdOimLightLevel] + def from_option_string(self, value: str) -> Any: + try: + return ErdOimLightLevel[value.upper()] + except: + _LOGGER.warning(f"Could not set hood light level to {value.upper()}") + return ErdOimLightLevel.OFF + def to_option_string(self, value: ErdOimLightLevel) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdOimLightLevel.OFF.stringify() diff --git a/entities/oven/__init__.py b/entities/oven/__init__.py new file mode 100644 index 0000000..6ef1066 --- /dev/null +++ b/entities/oven/__init__.py @@ -0,0 +1,4 @@ +from .ge_oven import GeOven +from .ge_oven_light_level_select import GeOvenLightLevelSelect +from .ge_oven_warming_state_select import GeOvenWarmingStateSelect +from .const import UPPER_OVEN, LOWER_OVEN \ No newline at end of file diff --git a/entities/oven/__pycache__/__init__.cpython-313.pyc b/entities/oven/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce0afaa079e00db0bfc97d0fc0f8fb2d6487aa3a GIT binary patch literal 414 zcmXw#PfNov7{-%!{o|l`5fSyUi-%t9RhaA`452OSu;`9q)Yt{uW+d6f<35BR!cXGW zb6|Mz2y2)kqx0EBFLfwBUP^y z>X#ba2@MaTzFHdt0F6Ti`QZo(z)=*0V<-g25exk&3x zO2QbLfN?YlIWz?mXc{I_stTlj;i3Bis4~)5L!c+Ce!@3hc#!Zd7ak&f+l8})zjWbA z!e6=Y1mRg19w2y5^7wB}&~?buB$e=>bWv873a9Xy=-Kt+hfqq_-3;T~b@ET8r&P zP^;C?jaHD(Zlw2hy1sMvN-2%j1KZ_RLllMh&58_lG_HvyHveOZM^1)cQ zPH!;YAIwT({zyK^?;qfYX1k2+$xv&OC=R)BoRNyu)viIAyp)r^L-d`Ag4F#{L3Dhc zc;pY+3$a8u0mmq(>lmOoMUF!Q<{vZ?80wRKJTqLPy$xouw7EeT8nyQ{!TU&@1{yK$z-^q_8# zbF7-GVr=N-*GXR?zrf<(k>y`K ze?(T>p$_-#9k*q3Th>nH-A>acU#{7X+=aPzuoFp=gsD{vBs<*47KnBdOBRTAxR0og z&bHak)G{5McxnNzGxM1RQk~f^Ebye0oVP%{lbECF(Mgg>OWnm%?eHBt-41l<<&K;F zdFRKS4#T};QZ|$NrQpa_n_0C!%{Y3|W)`jGuN}Q)GfUPd-#B{4W@fCp=Z>DWnOQ6S zxufT8X8v~Dkqb7na9jM4rfnvDyY@h>r<;zRvza+-{)MBv&&fJ^*=Clll|4s0Eeh}F Sq6^-85KZ`O5JfMDz5fHgZw|Kr literal 0 HcmV?d00001 diff --git a/entities/oven/__pycache__/ge_oven.cpython-313.pyc b/entities/oven/__pycache__/ge_oven.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27279a6482d7ca3c1025c4262bb666870642a21c GIT binary patch literal 12543 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 cOTL~tT str: + return f"{DOMAIN}_{self.serial_or_mac}_{self.oven_select.lower()}" + + @property + def name(self) -> Optional[str]: + if self._two_cavity: + oven_title = self.oven_select.replace("_", " ").title() + else: + oven_title = "Oven" + + return f"{self.serial_or_mac} {oven_title}" + + @property + def temperature_unit(self): + measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + if measurement_system == ErdMeasurementUnits.METRIC: + return UnitOfTemperature.CELSIUS + return UnitOfTemperature.FAHRENHEIT + + @property + def oven_select(self) -> str: + return self._oven_select + + def get_erd_code(self, suffix: str) -> ErdCode: + """Return the appropriate ERD code for this oven_select""" + return ErdCode[f"{self.oven_select}_{suffix}"] + + @property + def remote_enabled(self) -> bool: + """Returns whether the oven is remote enabled""" + value = self.get_erd_value("REMOTE_ENABLED") + return value == True + + @property + def current_temperature(self) -> Optional[int]: + #DISPLAY_TEMPERATURE appears to be out of line with what's + #actually going on in the oven, RAW_TEMPERATURE seems to be + #accurate. However, it appears some devices don't have + #the raw temperature. So, we'll allow an override to handle + #that situation (see constructor) + #current_temp = self.get_erd_value("DISPLAY_TEMPERATURE") + #if current_temp: + # return current_temp + return self.get_erd_value(self._temperature_erd_code) + + @property + def current_operation(self) -> Optional[str]: + cook_setting = self.current_cook_setting + cook_mode = cook_setting.cook_mode + # TODO: simplify this lookup nonsense somehow + current_state = OVEN_COOK_MODE_MAP.inverse[cook_mode] + try: + return COOK_MODE_OP_MAP[current_state] + except KeyError: + _LOGGER.debug(f"Unable to map {current_state} to an operation mode") + return OP_MODE_COOK_UNK + + @property + def operation_list(self) -> List[str]: + #lookup all the available cook modes + erd_code = self.get_erd_code("AVAILABLE_COOK_MODES") + cook_modes: Set[ErdOvenCookMode] = self.appliance.get_erd_value(erd_code) + _LOGGER.debug(f"Available Cook Modes: {cook_modes}") + + #get the extended cook modes and add them to the list + ext_erd_code = self.get_erd_code("EXTENDED_COOK_MODES") + ext_cook_modes: Set[ErdOvenCookMode] = self.api.try_get_erd_value(ext_erd_code) + _LOGGER.debug(f"Extended Cook Modes: {ext_cook_modes}") + if ext_cook_modes: + cook_modes = cook_modes.union(ext_cook_modes) + + #make sure that we limit them to the list of known codes + cook_modes = cook_modes.intersection(COOK_MODE_OP_MAP.keys()) + + _LOGGER.debug(f"Final Cook Modes: {cook_modes}") + op_modes = [o for o in (COOK_MODE_OP_MAP[c] for c in cook_modes) if o] + op_modes = [OP_MODE_OFF] + op_modes + return op_modes + + @property + def current_cook_setting(self) -> OvenCookSetting: + """Get the current cook mode.""" + erd_code = self.get_erd_code("COOK_MODE") + return self.appliance.get_erd_value(erd_code) + + @property + def target_temperature(self) -> Optional[int]: + """Return the temperature we try to reach.""" + cook_mode = self.current_cook_setting + if cook_mode.temperature: + return cook_mode.temperature + return None + + @property + def min_temp(self) -> int: + """Return the minimum temperature.""" + min_temp, _ = self.appliance.get_erd_value(ErdCode.OVEN_MODE_MIN_MAX_TEMP) + return min_temp + + @property + def max_temp(self) -> int: + """Return the maximum temperature.""" + _, max_temp = self.appliance.get_erd_value(ErdCode.OVEN_MODE_MIN_MAX_TEMP) + return max_temp + + async def async_set_operation_mode(self, operation_mode: str): + """Set the operation mode.""" + + erd_cook_mode = COOK_MODE_OP_MAP.inverse[operation_mode] + # Pick a temperature to set. If there's not one already set, default to + # good old 350F. + if operation_mode == OP_MODE_OFF: + target_temp = 0 + elif self.target_temperature: + target_temp = self.target_temperature + elif self.temperature_unit == UnitOfTemperature.FAHRENHEIT: + target_temp = 350 + else: + target_temp = 177 + + new_cook_mode = OvenCookSetting(OVEN_COOK_MODE_MAP[erd_cook_mode], target_temp) + erd_code = self.get_erd_code("COOK_MODE") + await self.appliance.async_set_erd_value(erd_code, new_cook_mode) + + async def async_set_temperature(self, **kwargs): + """Set the cook temperature""" + + target_temp = kwargs.get(ATTR_TEMPERATURE) + if target_temp is None: + return + + current_op = self.current_operation + if current_op != OP_MODE_OFF: + erd_cook_mode = COOK_MODE_OP_MAP.inverse[current_op] + else: + erd_cook_mode = ErdOvenCookMode.BAKE_NOOPTION + + new_cook_mode = OvenCookSetting(OVEN_COOK_MODE_MAP[erd_cook_mode], target_temp) + erd_code = self.get_erd_code("COOK_MODE") + await self.appliance.async_set_erd_value(erd_code, new_cook_mode) + + def get_erd_value(self, suffix: str) -> Any: + erd_code = self.get_erd_code(suffix) + return self.appliance.get_erd_value(erd_code) + + @property + def display_state(self) -> Optional[str]: + erd_code = self.get_erd_code("CURRENT_STATE") + erd_value = self.appliance.get_erd_value(erd_code) + return self._stringify(erd_value, temp_units=self.temperature_unit) + + @property + def extra_state_attributes(self) -> Optional[Dict[str, Any]]: + probe_present = False + if self.api.has_erd_code(self.get_erd_code("PROBE_PRESENT")): + probe_present: bool = self.get_erd_value("PROBE_PRESENT") + data = { + "display_state": self.display_state, + "probe_present": probe_present, + "display_temperature": self.get_erd_value("DISPLAY_TEMPERATURE") + } + if self.api.has_erd_code(self.get_erd_code("RAW_TEMPERATURE")): + data["raw_temperature"] = self.get_erd_value("RAW_TEMPERATURE") + if probe_present: + data["probe_temperature"] = self.get_erd_value("PROBE_DISPLAY_TEMP") + + elapsed_time = None + cook_time_remaining = None + kitchen_timer = None + delay_time = None + if self.api.has_erd_code(self.get_erd_code("ELAPSED_COOK_TIME")): + elapsed_time = self.get_erd_value("ELAPSED_COOK_TIME") + if self.api.has_erd_code(self.get_erd_code("COOK_TIME_REMAINING")): + cook_time_remaining = self.get_erd_value("COOK_TIME_REMAINING") + if self.api.has_erd_code(self.get_erd_code("KITCHEN_TIMER")): + kitchen_timer = self.get_erd_value("KITCHEN_TIMER") + if self.api.has_erd_code(self.get_erd_code("DELAY_TIME_REMAINING")): + delay_time = self.get_erd_value("DELAY_TIME_REMAINING") + if elapsed_time: + data["cook_time_elapsed"] = self._stringify(elapsed_time) + if cook_time_remaining: + data["cook_time_remaining"] = self._stringify(cook_time_remaining) + if kitchen_timer: + data["cook_time_remaining"] = self._stringify(kitchen_timer) + if delay_time: + data["delay_time_remaining"] = self._stringify(delay_time) + return data diff --git a/entities/oven/ge_oven_light_level_select.py b/entities/oven/ge_oven_light_level_select.py new file mode 100644 index 0000000..ca7e0de --- /dev/null +++ b/entities/oven/ge_oven_light_level_select.py @@ -0,0 +1,66 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCodeType, ErdOvenLightLevelAvailability, ErdOvenLightLevel, ErdCode +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class OvenLightLevelOptionsConverter(OptionsConverter): + def __init__(self, availability: ErdOvenLightLevelAvailability): + super().__init__() + self.availability = availability + self.excluded_levels = [ErdOvenLightLevel.NOT_AVAILABLE] + + if not availability or not availability.dim_available: + self.excluded_levels.append(ErdOvenLightLevel.DIM) + + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdOvenLightLevel if i not in self.excluded_levels] + def from_option_string(self, value: str) -> Any: + try: + return ErdOvenLightLevel[value.upper()] + except: + _LOGGER.warning(f"Could not set Oven light level to {value.upper()}") + return ErdOvenLightLevel.OFF + def to_option_string(self, value: ErdOvenLightLevel) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdOvenLightLevel.OFF.stringify() + +class GeOvenLightLevelSelect(GeErdSelect): + + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): + self._availability: ErdOvenLightLevelAvailability = api.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT_AVAILABILITY) + + #check to see if we have a status + value: ErdOvenLightLevel = api.try_get_erd_value(erd_code) + self._has_status = value is not None and value != ErdOvenLightLevel.NOT_AVAILABLE + self._assumed_state = ErdOvenLightLevel.OFF + + super().__init__(api, erd_code, OvenLightLevelOptionsConverter(self._availability), erd_override=erd_override) + + @property + def assumed_state(self) -> bool: + return not self._has_status + + @property + def current_option(self): + if self.assumed_state: + return self._assumed_state + + return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + _LOGGER.debug(f"Setting select from {self.current_option} to {option}") + + new_state = self._converter.from_option_string(option) + await self.appliance.async_set_erd_value(self.erd_code, new_state) + self._assumed_state = new_state + \ No newline at end of file diff --git a/entities/oven/ge_oven_warming_state_select.py b/entities/oven/ge_oven_warming_state_select.py new file mode 100644 index 0000000..86da410 --- /dev/null +++ b/entities/oven/ge_oven_warming_state_select.py @@ -0,0 +1,56 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCodeType, ErdOvenWarmingState +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class OvenWarmingStateOptionsConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdOvenWarmingState] + def from_option_string(self, value: str) -> Any: + try: + return ErdOvenWarmingState[value.upper()] + except: + _LOGGER.warning(f"Could not set Oven warming state to {value.upper()}") + return ErdOvenWarmingState.OFF + def to_option_string(self, value: ErdOvenWarmingState) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdOvenWarmingState.OFF.stringify() + +class GeOvenWarmingStateSelect(GeErdSelect): + + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): + #check to see if we have a status + value: ErdOvenWarmingState = api.try_get_erd_value(erd_code) + self._has_status = value is not None and value != ErdOvenWarmingState.NOT_AVAILABLE + self._assumed_state = ErdOvenWarmingState.OFF + + super().__init__(api, erd_code, OvenWarmingStateOptionsConverter(self._availability), erd_override=erd_override) + + @property + def assumed_state(self) -> bool: + return not self._has_status + + @property + def current_option(self): + if self.assumed_state: + return self._assumed_state + + return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + _LOGGER.debug(f"Setting select from {self.current_option} to {option}") + + new_state = self._converter.from_option_string(option) + await self.appliance.async_set_erd_value(self.erd_code, new_state) + self._assumed_state = new_state + \ No newline at end of file diff --git a/entities/water_filter/__init__.py b/entities/water_filter/__init__.py new file mode 100644 index 0000000..1d37958 --- /dev/null +++ b/entities/water_filter/__init__.py @@ -0,0 +1 @@ +from .filter_position import GeErdFilterPositionSelect diff --git a/entities/water_filter/__pycache__/__init__.cpython-313.pyc b/entities/water_filter/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5069f7045a7a18691e6bd9f2efede2739203987 GIT binary patch literal 232 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 OmFc_d{(?YdEb?DMTM4HC literal 0 HcmV?d00001 diff --git a/entities/water_filter/filter_position.py b/entities/water_filter/filter_position.py new file mode 100644 index 0000000..53038af --- /dev/null +++ b/entities/water_filter/filter_position.py @@ -0,0 +1,63 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCodeType, ErdWaterFilterPosition, ErdCode, ErdWaterFilterMode +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class FilterPositionOptionsConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [i.name.title() for i in ErdWaterFilterPosition if i != ErdWaterFilterPosition.UNKNOWN] + def from_option_string(self, value: str) -> Any: + try: + return ErdWaterFilterPosition[value.upper()] + except: + _LOGGER.warning(f"Could not set filter position to {value.upper()}") + return ErdWaterFilterPosition.UNKNOWN + def to_option_string(self, value: Any) -> Optional[str]: + try: + if value is not None: + return value.name.title() + except: + pass + return ErdWaterFilterPosition.UNKNOWN.name.title() + +class GeErdFilterPositionSelect(GeErdSelect): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): + super().__init__(api, erd_code, FilterPositionOptionsConverter(), icon_override="mdi:valve") + + @property + def current_option(self): + """Return the current selected option""" + + #if we're transitioning or don't know what the mode is, don't allow changes + mode: ErdWaterFilterMode = self.appliance.get_erd_value(ErdCode.WH_FILTER_MODE) + if mode in [ErdWaterFilterMode.TRANSITION, ErdWaterFilterMode.UNKNOWN]: + return mode.name.title() + + return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) + + @property + def options(self) -> List[str]: + """Return a list of options""" + + #if we're transitioning or don't know what the mode is, don't allow changes + mode: ErdWaterFilterMode = self.appliance.get_erd_value(ErdCode.WH_FILTER_MODE) + if mode in [ErdWaterFilterMode.TRANSITION, ErdWaterFilterMode.UNKNOWN]: + return mode.name.title() + + return self._converter.options + + async def async_select_option(self, option: str) -> None: + value = self._converter.from_option_string(option) + if value in [ErdWaterFilterPosition.UNKNOWN, ErdWaterFilterPosition.READY]: + _LOGGER.debug("Cannot set position to ready/unknown") + return + if self.appliance.get_erd_value(self.erd_code) != ErdWaterFilterPosition.READY: + _LOGGER.debug("Cannot set position if not ready") + return + + return await super().async_select_option(option) diff --git a/entities/water_heater/__init__.py b/entities/water_heater/__init__.py new file mode 100644 index 0000000..c0fa79f --- /dev/null +++ b/entities/water_heater/__init__.py @@ -0,0 +1,2 @@ +from .heater_modes import WhHeaterModeConverter +from .ge_water_heater import GeWaterHeater \ No newline at end of file diff --git a/entities/water_heater/__pycache__/__init__.cpython-313.pyc b/entities/water_heater/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09bda0a749b2893faabf608da8419047f77ad71a GIT binary patch literal 279 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 literal 0 HcmV?d00001 diff --git a/entities/water_heater/__pycache__/heater_modes.cpython-313.pyc b/entities/water_heater/__pycache__/heater_modes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..409e63c752f4f78ac62811f5d71940e63c8d1ec4 GIT binary patch literal 1967 zcmaJ>%}*Og6rcU@dTnfMSP&r)SP)-s16ImM12k1r6oG(1>n)`cu2zm&8#nf@JF_Mt zm(U)n=2W4IlHQ`8h>V? zpo^Mk&O0k(Sw(RaYpkX_ox4%oKgzlPxb6b#Mw)aHY2LUz3TI#g2hvhxqGm(vOwDAUBT~62ro-vrbX{{PZJS+iM*bO67mbq?Va3)!<>Zsi; z4wh{=CFV4Qd5yzDT-5luCoZv&5ndB6qEE!p$~x@T#JB{0c?9mrJlF>oKUnH2D@1k^ zum?0R4yNUZk2Wu_ogS}toFgCPq!Kpl0ydKsESzoF#teny4?t|A701YgfjCL%DfEb2i11i~Sn4A-&Y0;;Y9qo^kr>$& zRC@@3r3P1xH@X^Jps!$TH<2=1-BAa1?fBhHBRG4D;Z6`1Ff*#oUz2S zK<`x#0 zp^0!9_QcH~sBD;poDSF#RzRw%V%DUzF@=F($9IeE z@BS2+f}jIsu%ry`1qNRb2Jm*)K}uD1v;ky ztVRr^sEk*Yj7xtfTIhD^!u7BS&<(-lkbo~S1Ahv4rJr1k!?tw>;lA?`J{t$kjL176 zHW96vG2$0foCxWEAc`2t`Vv$U$hh6dK+SjTYt6=VcG) zYxp@3enDhN@_FM3dct279>MckPi0*<6FEcIsb6PpTgWm!pzEI&64{C;r0b~+CU!Oh zPk;q(Igc%MUbE{w*MAvZ- z_>SR24nmluWN+kQcJASxo}HQc(gu6v$m(sk zxg>Z1bC!u2e#~jl!+HbbUY4`1n%KFHyN}aU@>?#TZ{s&0guO-N3W&16aojJc{&&<@ pLVeHBz;CGc4}ZgfzirRo_F!h;-@EBO5gWL%V*BO45X?@>e*vU$xM% str: + return "heater" + + @property + def supported_features(self): + return (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) + + @property + def temperature_unit(self): + return UnitOfTemperature.FAHRENHEIT + + @property + def current_temperature(self) -> Optional[int]: + return self.appliance.get_erd_value(ErdCode.WH_HEATER_TEMPERATURE) + + @property + def current_operation(self) -> Optional[str]: + erd_mode = self.appliance.get_erd_value(ErdCode.WH_HEATER_MODE) + return self._modes_converter.to_option_string(erd_mode) + + @property + def operation_list(self) -> List[str]: + return self._modes_converter.options + + @property + def target_temperature(self) -> Optional[int]: + """Return the temperature we try to reach.""" + return self.appliance.get_erd_value(ErdCode.WH_HEATER_TARGET_TEMPERATURE) + + @property + def min_temp(self) -> int: + """Return the minimum temperature.""" + min_temp, _ = self.appliance.get_erd_value(ErdCode.WH_HEATER_MIN_MAX_TEMPERATURE) + return min_temp + + @property + def max_temp(self) -> int: + """Return the maximum temperature.""" + _, max_temp = self.appliance.get_erd_value(ErdCode.WH_HEATER_MIN_MAX_TEMPERATURE) + return max_temp + + async def async_set_operation_mode(self, operation_mode: str): + """Set the operation mode.""" + + erd_mode = self._modes_converter.from_option_string(operation_mode) + + if (erd_mode != ErdWaterHeaterMode.UNKNOWN): + await self.appliance.async_set_erd_value(ErdCode.WH_HEATER_MODE, erd_mode) + + async def async_set_temperature(self, **kwargs): + """Set the water temperature""" + + target_temp = kwargs.get(ATTR_TEMPERATURE) + if target_temp is None: + return + + await self.appliance.async_set_erd_value(ErdCode.WH_HEATER_TARGET_TEMPERATURE, target_temp) + diff --git a/entities/water_heater/heater_modes.py b/entities/water_heater/heater_modes.py new file mode 100644 index 0000000..145bb20 --- /dev/null +++ b/entities/water_heater/heater_modes.py @@ -0,0 +1,26 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdWaterHeaterMode +from ..common import OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class WhHeaterModeConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdWaterHeaterMode] + def from_option_string(self, value: str) -> Any: + enum_val = value.upper().replace(" ","_") + try: + return ErdWaterHeaterMode[enum_val] + except: + _LOGGER.warning(f"Could not heater mode to {enum_val}") + return ErdWaterHeaterMode.UNKNOWN + def to_option_string(self, value: ErdWaterHeaterMode) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdWaterHeaterMode.UNKNOWN.stringify() diff --git a/entities/water_softener/__init__.py b/entities/water_softener/__init__.py new file mode 100644 index 0000000..7ae738e --- /dev/null +++ b/entities/water_softener/__init__.py @@ -0,0 +1 @@ +from .shutoff_position import GeErdShutoffPositionSelect \ No newline at end of file diff --git a/entities/water_softener/__pycache__/__init__.cpython-313.pyc b/entities/water_softener/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfe16ed1de96fb8ea91ccd3ed27816c01320e4e7 GIT binary patch literal 236 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 fu6Z7~wlZT(< List[str]: + return [i.name.title() + for i in ErdWaterSoftenerShutoffValveState + if i not in [ErdWaterSoftenerShutoffValveState.UNKNOWN, ErdWaterSoftenerShutoffValveState.TRANSITION]] + def from_option_string(self, value: str) -> Any: + try: + return ErdWaterSoftenerShutoffValveState[value.upper()] + except: + _LOGGER.warning(f"Could not set filter position to {value.upper()}") + return ErdWaterSoftenerShutoffValveState.UNKNOWN + def to_option_string(self, value: Any) -> Optional[str]: + try: + if value is not None: + return value.name.title() + except: + pass + return ErdWaterSoftenerShutoffValveState.UNKNOWN.name.title() + +class GeErdShutoffPositionSelect(GeErdSelect): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): + super().__init__(api, erd_code, FilterPositionOptionsConverter(), icon_override="mdi:valve") + + @property + def current_option(self): + """Return the current selected option""" + + #if we're transitioning or don't know what the mode is, don't allow changes + mode: ErdWaterSoftenerShutoffValveState = self.appliance.get_erd_value(ErdCode.WH_SOFTENER_SHUTOFF_VALVE_STATE) + if mode in [ErdWaterSoftenerShutoffValveState.TRANSITION, ErdWaterSoftenerShutoffValveState.UNKNOWN]: + return mode.name.title() + + return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) + + @property + def options(self) -> List[str]: + """Return a list of options""" + + #if we're transitioning or don't know what the mode is, don't allow changes + mode: ErdWaterSoftenerShutoffValveState = self.appliance.get_erd_value(ErdCode.WH_SOFTENER_SHUTOFF_VALVE_STATE) + if mode in [ErdWaterSoftenerShutoffValveState.TRANSITION, ErdWaterSoftenerShutoffValveState.UNKNOWN]: + return mode.name.title() + + return self._converter.options + + async def async_select_option(self, option: str) -> None: + value = self._converter.from_option_string(option) + if value in [ErdWaterSoftenerShutoffValveState.UNKNOWN, ErdWaterSoftenerShutoffValveState.TRANSITION]: + _LOGGER.debug("Cannot set position to transition/unknown") + return + if self.appliance.get_erd_value(ErdCode.WH_SOFTENER_SHUTOFF_VALVE_STATE) == ErdWaterSoftenerShutoffValveState.TRANSITION: + _LOGGER.debug("Cannot set position if in transition") + return + + return await super().async_select_option(option) diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..fe7946e --- /dev/null +++ b/exceptions.py @@ -0,0 +1,10 @@ +""" Home Assistant derived exceptions""" + +from homeassistant import exceptions as ha_exc + +class HaCannotConnect(ha_exc.HomeAssistantError): + """Error to indicate we cannot connect.""" +class HaAuthError(ha_exc.HomeAssistantError): + """Error to indicate authentication failure.""" +class HaAlreadyConfigured(ha_exc.HomeAssistantError): + """Error to indicate that the account is already configured""" \ No newline at end of file diff --git a/humidifier.py b/humidifier.py new file mode 100644 index 0000000..68aa896 --- /dev/null +++ b/humidifier.py @@ -0,0 +1,44 @@ +"""GE Home Humidifier Entities""" +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er + +from .entities import GeHumidifier +from .const import DOMAIN +from .devices import ApplianceApi +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): + """GE Home Water Heaters.""" + _LOGGER.debug('Adding GE "Humidifiers"') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeHumidifier) + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered humidifiers') + async_add_entities(entities) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/light.py b/light.py new file mode 100644 index 0000000..ba2a69c --- /dev/null +++ b/light.py @@ -0,0 +1,48 @@ +"""GE Home Select Entities""" +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er + +from .const import DOMAIN +from .entities import GeErdLight +from .devices import ApplianceApi +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +): + """GE Home lights.""" + _LOGGER.debug("Adding GE Home lights") + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f"Found {len(apis):d} appliance APIs") + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdLight) + and entity.erd_code in api.appliance._property_cache + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f"Found {len(entities):d} unregistered lights") + async_add_entities(entities) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..3573241 --- /dev/null +++ b/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "ge_home", + "name": "GE Home (SmartHQ)", + "config_flow": true, + "integration_type": "hub", + "iot_class": "cloud_push", + "documentation": "https://github.com/simbaja/ha_gehome", + "requirements": ["gehomesdk==2025.5.0","magicattr==0.1.6","slixmpp==1.8.3"], + "codeowners": ["@simbaja"], + "version": "2025.5.0" +} diff --git a/number.py b/number.py new file mode 100644 index 0000000..e691988 --- /dev/null +++ b/number.py @@ -0,0 +1,45 @@ +"""GE Home Number Entities""" +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er + +from .const import DOMAIN +from .devices import ApplianceApi +from .entities import GeErdNumber +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): + """GE Home numbers.""" + + _LOGGER.debug('Adding GE Number Entities') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdNumber) + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered numbers') + async_add_entities(entities) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/select.py b/select.py new file mode 100644 index 0000000..158af27 --- /dev/null +++ b/select.py @@ -0,0 +1,48 @@ +"""GE Home Select Entities""" +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er + +from .const import DOMAIN +from .devices import ApplianceApi +from .entities import GeErdSelect +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +): + """GE Home selects.""" + _LOGGER.debug("Adding GE Home selects") + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f"Found {len(apis):d} appliance APIs") + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdSelect) + and entity.erd_code in api.appliance._property_cache + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f"Found {len(entities):d} unregistered selects") + async_add_entities(entities) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/sensor.py b/sensor.py new file mode 100644 index 0000000..0bf7ba6 --- /dev/null +++ b/sensor.py @@ -0,0 +1,88 @@ +"""GE Home Sensor Entities""" +import logging +from typing import Callable +import voluptuous as vol +from datetime import timedelta + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_platform +from homeassistant.helpers import entity_registry as er + +from .const import ( + DOMAIN, + SERVICE_SET_TIMER, + SERVICE_CLEAR_TIMER, + SERVICE_SET_INT_VALUE +) +from .entities import GeErdSensor +from .devices import ApplianceApi +from .update_coordinator import GeHomeUpdateCoordinator + +ATTR_DURATION = "duration" +ATTR_VALUE = "value" + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): + """GE Home sensors.""" + _LOGGER.debug('Adding GE Home sensors') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + # Get the platform + platform = entity_platform.async_get_current_platform() + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdSensor) and entity.erd_code in api.appliance._property_cache + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered sensors') + async_add_entities(entities) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + + # register set_timer entity service + platform.async_register_entity_service( + SERVICE_SET_TIMER, + { + vol.Required(ATTR_DURATION): vol.All( + vol.Coerce(int), vol.Range(min=1, max=360) + ) + }, + set_timer) + + # register clear_timer entity service + platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, 'clear_timer') + + # register set_value entity service + platform.async_register_entity_service( + SERVICE_SET_INT_VALUE, + { + vol.Required(ATTR_VALUE): vol.All( + vol.Coerce(int), vol.Range(min=0) + ) + }, + set_int_value) + +async def set_timer(entity, service_call): + ts = timedelta(minutes=int(service_call.data['duration'])) + await entity.set_timer(ts) + +async def set_int_value(entity, service_call): + await entity.set_value(int(service_call.data['value'])) \ No newline at end of file diff --git a/services.yaml b/services.yaml new file mode 100644 index 0000000..1ca48ea --- /dev/null +++ b/services.yaml @@ -0,0 +1,47 @@ +# GE Home Services + +set_timer: + name: Set Timer + description: Sets a timer value (timespan) + target: + entity: + integration: "ge_home" + domain: "sensor" + fields: + duration: + name: Duration + description: Duration of the timer (minutes) + required: true + example: "90" + default: "30" + selector: + number: + min: 1 + max: 360 + unit_of_measurement: minutes + mode: slider +clear_timer: + name: Clear Timer + description: Clears a timer value (sets to zero) + target: + entity: + integration: "ge_home" + domain: "sensor" + +set_int_value: + name: Set Int Value + description: Sets an integer value (also can be used with ERD enums) + target: + entity: + integration: "ge_home" + domain: "sensor" + fields: + value: + name: Value + description: The value to set + required: true + selector: + number: + min: 0 + max: 65535 + \ No newline at end of file diff --git a/strings.json b/strings.json new file mode 100644 index 0000000..8e3c913 --- /dev/null +++ b/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Username", + "password": "Password", + "region": "Region" + } + } + }, + "error": { + "cannot_connect": "Can't connect to SmartHQ", + "invalid_auth": "Invalid authentication provided, please check credentials", + "unknown": "Unknown error occurred" + }, + "abort": { + "already_configured_account": "Account already configured!" + } + } +} diff --git a/switch.py b/switch.py new file mode 100644 index 0000000..7c339ae --- /dev/null +++ b/switch.py @@ -0,0 +1,44 @@ +"""GE Home Switch Entities""" +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er + +from .entities import GeErdSwitch +from .const import DOMAIN +from .devices import ApplianceApi +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): + """GE Home sensors.""" + _LOGGER.debug('Adding GE Home switches') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdSwitch) and entity.erd_code in api.appliance._property_cache + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered switches') + async_add_entities(entities) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/translations/en.json b/translations/en.json new file mode 100644 index 0000000..50680ea --- /dev/null +++ b/translations/en.json @@ -0,0 +1,29 @@ +{ + "title": "GE Home", + "config": { + "step": { + "init": { + "data": { + "username": "Username", + "password": "Password", + "region": "Region" + } + }, + "user": { + "data": { + "username": "Username", + "password": "Password", + "region": "Region" + } + } + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured_account": "Account is already configured" + } + } +} diff --git a/update_coordinator.py b/update_coordinator.py new file mode 100644 index 0000000..9bd27f4 --- /dev/null +++ b/update_coordinator.py @@ -0,0 +1,366 @@ +"""Data update coordinator for GE Home Appliances""" + +import asyncio +import async_timeout +import logging +from typing import Any, Callable, Dict, Iterable, Optional, Tuple, List + +from gehomesdk import ( + EVENT_APPLIANCE_INITIAL_UPDATE, + EVENT_APPLIANCE_UPDATE_RECEIVED, + EVENT_CONNECTED, + EVENT_DISCONNECTED, + EVENT_GOT_APPLIANCE_LIST, + ErdCodeType, + GeAppliance, + GeWebsocketClient, +) +from gehomesdk import GeAuthFailedError, GeGeneralServerError, GeNotAuthenticatedError +from .exceptions import HaAuthError, HaCannotConnect + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util.ssl import get_default_context +from .const import ( + DOMAIN, + EVENT_ALL_APPLIANCES_READY, + UPDATE_INTERVAL, + MIN_RETRY_DELAY, + MAX_RETRY_DELAY, + RETRY_OFFLINE_COUNT, + ASYNC_TIMEOUT, +) +from .devices import ApplianceApi, get_appliance_api_type + +PLATFORMS = [ + "binary_sensor", + "sensor", + "switch", + "water_heater", + "select", + "climate", + "light", + "button", + "number", + "humidifier" +] +_LOGGER = logging.getLogger(__name__) + + +class GeHomeUpdateCoordinator(DataUpdateCoordinator): + """Define a wrapper class to update GE Home data.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Set up the GeHomeUpdateCoordinator class.""" + super().__init__(hass, _LOGGER, name=DOMAIN) + + self._config_entry = config_entry + self._username = config_entry.data[CONF_USERNAME] + self._password = config_entry.data[CONF_PASSWORD] + self._region = config_entry.data[CONF_REGION] + self._appliance_apis = {} # type: Dict[str, ApplianceApi] + self._signal_remove_callbacks = [] # type: List[Callable] + + self._reset_initialization() + + def _reset_initialization(self): + self.client = None # type: Optional[GeWebsocketClient] + + # Mark all appliances as not initialized yet + for a in self.appliance_apis.values(): + a.appliance.initialized = False + + # Some record keeping to let us know when we can start generating entities + self._got_roster = False + self._init_done = False + self._retry_count = 0 + + def create_ge_client( + self, event_loop: Optional[asyncio.AbstractEventLoop] + ) -> GeWebsocketClient: + """ + Create a new GeClient object with some helpful callbacks. + + :param event_loop: Event loop + :return: GeWebsocketClient + """ + client = GeWebsocketClient( + self._username, + self._password, + self._region, + event_loop=event_loop, + ssl_context=get_default_context() + ) + client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update) + client.add_event_handler(EVENT_APPLIANCE_UPDATE_RECEIVED, self.on_device_update) + client.add_event_handler(EVENT_GOT_APPLIANCE_LIST, self.on_appliance_list) + client.add_event_handler(EVENT_DISCONNECTED, self.on_disconnect) + client.add_event_handler(EVENT_CONNECTED, self.on_connect) + return client + + @property + def appliances(self) -> Iterable[GeAppliance]: + return self.client.appliances.values() + + @property + def appliance_apis(self) -> Dict[str, ApplianceApi]: + return self._appliance_apis + + @property + def signal_ready(self) -> str: + """Event specific per entry to signal readiness""" + return f"{DOMAIN}-ready-{self._config_entry.entry_id}" + + @property + def initialized(self) -> bool: + return self._init_done + + @property + def online(self) -> bool: + """ + Indicates whether the services is online. If it's retried several times, it's assumed + that it's offline for some reason + """ + return self.connected or self._retry_count <= RETRY_OFFLINE_COUNT + + @property + def connected(self) -> bool: + """ + Indicates whether the coordinator is connected + """ + return self.client and self.client.connected + + def _get_appliance_api(self, appliance: GeAppliance) -> ApplianceApi: + api_type = get_appliance_api_type(appliance.appliance_type) + return api_type(self, appliance) + + def regenerate_appliance_apis(self): + """Regenerate the appliance_apis dictionary, adding elements as necessary.""" + for jid, appliance in self.client.appliances.keys(): + if jid not in self._appliance_apis: + self._appliance_apis[jid] = self._get_appliance_api(appliance) + + def maybe_add_appliance_api(self, appliance: GeAppliance): + mac_addr = appliance.mac_addr + if mac_addr not in self.appliance_apis: + _LOGGER.debug(f"Adding appliance api for appliance {mac_addr} ({appliance.appliance_type})") + api = self._get_appliance_api(appliance) + api.build_entities_list() + self.appliance_apis[mac_addr] = api + else: + # if we already have the API, switch out its appliance reference for this one + api = self.appliance_apis[mac_addr] + api.appliance = appliance + + def add_signal_remove_callback(self, cb: Callable): + self._signal_remove_callbacks.append(cb) + + async def get_client(self) -> GeWebsocketClient: + """Get a new GE Websocket client.""" + if self.client: + try: + self.client.clear_event_handlers() + await self.client.disconnect() + except Exception as err: + _LOGGER.warning(f"exception while disconnecting client {err}") + finally: + self._reset_initialization() + + self.client = self.create_ge_client(event_loop=self.hass.loop) + return self.client + + async def async_setup(self): + """Setup a new coordinator""" + _LOGGER.debug("Setting up coordinator") + + await self.hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) + + try: + await self.async_start_client() + except (GeNotAuthenticatedError, GeAuthFailedError): + raise HaAuthError("Authentication failure") + except GeGeneralServerError: + raise HaCannotConnect("Cannot connect (server error)") + except Exception: + raise HaCannotConnect("Unknown connection failure") + + return True + + async def async_start_client(self): + """Start a new GeClient in the HASS event loop.""" + try: + _LOGGER.debug("Creating and starting client") + await self.get_client() + await self.async_begin_session() + except: + _LOGGER.debug("could not start the client") + self.client = None + raise + + async def async_begin_session(self): + """Begins the ge_home session.""" + _LOGGER.debug("Beginning session") + session = async_get_clientsession(self.hass) + await self.client.async_get_credentials(session) + fut = asyncio.ensure_future(self.client.async_run_client(), loop=self.hass.loop) + _LOGGER.debug("Client running") + return fut + + async def async_reset(self): + """Resets the coordinator.""" + _LOGGER.debug("resetting the coordinator") + entry = self._config_entry + + # remove all the callbacks for this coordinator + for c in self._signal_remove_callbacks: + c() + self._signal_remove_callbacks.clear() + + unload_ok = await self.hass.config_entries.async_unload_platforms( + self._config_entry, PLATFORMS + ) + return unload_ok + + async def _kill_client(self): + """Kill the client. Leaving this in for testing purposes.""" + await asyncio.sleep(30) + _LOGGER.critical("Killing the connection. Popcorn time.") + await self.client.disconnect() + + @callback + def reconnect(self, log=False) -> None: + """Prepare to reconnect ge_home session.""" + if log: + _LOGGER.info("Will try to reconnect to ge_home service") + self.hass.loop.create_task(self.async_reconnect()) + + async def async_reconnect(self) -> None: + """Try to reconnect ge_home session.""" + self._retry_count += 1 + _LOGGER.info( + f"attempting to reconnect to ge_home service (attempt {self._retry_count})" + ) + + try: + with async_timeout.timeout(ASYNC_TIMEOUT): + await self.async_start_client() + except Exception as err: + _LOGGER.warning(f"could not reconnect: {err}, will retry in {self._get_retry_delay()} seconds") + self.hass.loop.call_later(self._get_retry_delay(), self.reconnect) + _LOGGER.debug("forcing a state refresh while disconnected") + try: + await self._refresh_ha_state() + except Exception as err: + _LOGGER.debug(f"error refreshing state: {err}") + + @callback + def shutdown(self, event) -> None: + """Close the connection on shutdown. + Used as an argument to EventBus.async_listen_once. + """ + _LOGGER.info("ge_home shutting down") + if self.client: + self.client.clear_event_handlers() + self.hass.loop.create_task(self.client.disconnect()) + + async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]): + """Let HA know there's new state.""" + self.last_update_success = True + appliance, _ = data + try: + api = self.appliance_apis[appliance.mac_addr] + except KeyError: + _LOGGER.info(f"Could not find appliance {appliance.mac_addr} in known device list.") + return + + self._update_entity_state(api.entities) + + async def _refresh_ha_state(self): + entities = [ + entity for api in self.appliance_apis.values() for entity in api.entities + ] + + self._update_entity_state(entities) + + def _update_entity_state(self, entities: List[Entity]): + from .entities import GeEntity + for entity in entities: + # if this is a GeEntity, check if it's been added + #if not, don't try to refresh this entity + if isinstance(entity, GeEntity): + gee: GeEntity = entity + if not gee.added: + _LOGGER.debug(f"Entity {entity} ({entity.unique_id}, {entity.entity_id}) not yet added, skipping update...") + continue + if entity.enabled: + try: + _LOGGER.debug(f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}") + entity.async_write_ha_state() + except: + _LOGGER.warning(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}", exc_info=1) + + @property + def all_appliances_updated(self) -> bool: + """True if all appliances have had an initial update.""" + return all([a.initialized for a in self.appliances]) + + async def on_appliance_list(self, _): + """When we get an appliance list, mark it and maybe trigger all ready.""" + _LOGGER.debug("Got roster update") + self.last_update_success = True + if not self._got_roster: + self._got_roster = True + # TODO: Probably should have a better way of confirming we're good to go... + await asyncio.sleep(5) + # After the initial roster update, wait a bit and hit go + await self.async_maybe_trigger_all_ready() + + async def on_device_initial_update(self, appliance: GeAppliance): + """When an appliance first becomes ready, let the system know and schedule periodic updates.""" + _LOGGER.debug(f"Got initial update for {appliance.mac_addr}") + self.last_update_success = True + self.maybe_add_appliance_api(appliance) + await self.async_maybe_trigger_all_ready() + _LOGGER.debug(f"Requesting updates for {appliance.mac_addr}") + while self.connected: + await asyncio.sleep(UPDATE_INTERVAL) + if self.connected and self.client.available: + await appliance.async_request_update() + + _LOGGER.debug(f"No longer requesting updates for {appliance.mac_addr}") + + async def on_disconnect(self, _): + """Handle disconnection.""" + _LOGGER.debug(f"Disconnected. Attempting to reconnect in {MIN_RETRY_DELAY} seconds") + self.last_update_success = False + self.hass.loop.call_later(MIN_RETRY_DELAY, self.reconnect, True) + + async def on_connect(self, _): + """Set state upon connection.""" + self.last_update_success = True + self._retry_count = 0 + + async def async_maybe_trigger_all_ready(self): + """See if we're all ready to go, and if so, let the games begin.""" + if self._init_done: + # Been here, done this + return + if self._got_roster and self.all_appliances_updated: + _LOGGER.debug("Ready to go, sending ready signal") + self._init_done = True + await self.client.async_event(EVENT_ALL_APPLIANCES_READY, None) + async_dispatcher_send( + self.hass, + self.signal_ready, + list(self.appliance_apis.values())) + + def _get_retry_delay(self) -> int: + delay = MIN_RETRY_DELAY * 2 ** (self._retry_count - 1) + return min(delay, MAX_RETRY_DELAY) diff --git a/water_heater.py b/water_heater.py new file mode 100644 index 0000000..9bb0a8e --- /dev/null +++ b/water_heater.py @@ -0,0 +1,46 @@ +"""GE Home Sensor Entities""" +import async_timeout +import logging +from typing import Callable + +from homeassistant.components.water_heater import WaterHeaterEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er + +from .entities import GeAbstractWaterHeater +from .const import DOMAIN +from .devices import ApplianceApi +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): + """GE Home Water Heaters.""" + _LOGGER.debug('Adding GE "Water Heaters"') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeAbstractWaterHeater) + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered water heaters') + async_add_entities(entities) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered))