From 7258a31644c417ddfcef58d95b02559433907ef4 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 30 Jun 2023 09:11:37 -0400 Subject: [PATCH] Initial release --- README.md | 15 +- custom_components/ge_appliances/__init__.py | 4 +- .../ge_appliances/api/__init__.py | 9 + .../ge_appliances/api/clients/__init__.py | 27 + .../api/clients/async_helpers.py | 20 + .../api/clients/async_login_flows.py | 206 +++++++ .../ge_appliances/api/clients/base_client.py | 306 ++++++++++ .../ge_appliances/api/clients/const.py | 34 ++ .../api/clients/ge_client_xmpp.py | 109 ++++ .../ge_appliances/api/clients/states.py | 13 + .../api/clients/websocket_client.py | 528 ++++++++++++++++++ .../ge_appliances/api/clients/xmpp_client.py | 319 +++++++++++ .../ge_appliances/api/entry_points.py | 18 + .../ge_appliances/api/erd/__init__.py | 7 + .../api/erd/converters/__init__.py | 6 + .../api/erd/converters/abstract.py | 36 ++ .../api/erd/converters/dishwasher/__init__.py | 13 + .../dishwasher/cycle_name_converter.py | 10 + .../dishwasher/erd_cycle_count_converter.py | 27 + .../dishwasher/erd_cycle_state_converter.py | 17 + .../erd_dishwasher_door_status_converter.py | 15 + .../dishwasher/erd_error_converter.py | 25 + .../dishwasher/erd_reminders_converter.py | 26 + .../dishwasher/erd_user_setting_converter.py | 137 +++++ .../dishwasher/operating_mode_converter.py | 18 + .../api/erd/converters/primitives/__init__.py | 6 + .../primitives/erd_bool_converter.py | 25 + .../primitives/erd_bytes_converter.py | 21 + .../primitives/erd_int_converter.py | 26 + .../primitives/erd_signed_byte_converter.py | 37 ++ .../primitives/erd_string_converter.py | 36 ++ .../primitives/erd_time_span_converter.py | 59 ++ .../erd/converters/specialized/__init__.py | 10 + .../erd_appliance_type_converter.py | 28 + .../specialized/erd_clock_format_converter.py | 10 + .../specialized/erd_end_tone_converter.py | 15 + .../specialized/erd_locked_converter.py | 32 ++ .../erd_measurement_units_converter.py | 10 + .../specialized/erd_model_serial_converter.py | 15 + .../specialized/erd_on_off_converter.py | 13 + .../erd_software_version_converter.py | 13 + .../specialized/erd_sound_level_converter.py | 11 + .../specialized/erd_unit_type_converter.py | 17 + .../ge_appliances/api/erd/erd_code_class.py | 27 + .../ge_appliances/api/erd/erd_codes.py | 77 +++ .../api/erd/erd_configuration.py | 235 ++++++++ .../ge_appliances/api/erd/erd_data_type.py | 11 + .../ge_appliances/api/erd/erd_encoder.py | 153 +++++ .../ge_appliances/api/erd/values/__init__.py | 4 + .../api/erd/values/common/__init__.py | 9 + .../erd/values/common/erd_appliance_type.py | 36 ++ .../api/erd/values/common/erd_clock_format.py | 7 + .../api/erd/values/common/erd_end_tone.py | 7 + .../api/erd/values/common/erd_locked.py | 7 + .../values/common/erd_measurement_units.py | 6 + .../api/erd/values/common/erd_on_off.py | 11 + .../api/erd/values/common/erd_present.py | 7 + .../api/erd/values/common/erd_sound_level.py | 8 + .../api/erd/values/common/erd_unit_type.py | 15 + .../api/erd/values/dishwasher/__init__.py | 16 + .../api/erd/values/dishwasher/cycle_count.py | 7 + .../values/dishwasher/cycle_state_mapping.py | 25 + .../erd/values/dishwasher/erd_cycle_state.py | 44 ++ .../dishwasher/erd_dishwasher_door_status.py | 20 + .../values/dishwasher/erd_operating_mode.py | 19 + .../erd/values/dishwasher/erd_reminders.py | 8 + .../erd/values/dishwasher/erd_user_setting.py | 53 ++ .../api/erd/values/dishwasher/error_state.py | 6 + .../erd/values/dishwasher/operating_mode.py | 19 + .../dishwasher/operating_mode_mapping.py | 20 + .../ge_appliances/api/exception/__init__.py | 12 + .../api/exception/ge_auth_failed_error.py | 5 + .../exception/ge_client_disconnected_error.py | 5 + .../exception/ge_duplicate_appliance_error.py | 5 + .../api/exception/ge_exception.py | 3 + .../api/exception/ge_general_server_error.py | 5 + .../ge_needs_reauthentication_error.py | 5 + .../exception/ge_not_authenticated_error.py | 5 + .../api/exception/ge_request_error.py | 13 + .../exception/ge_set_erd_not_allowed_error.py | 10 + .../ge_unsupported_operation_error.py | 5 + .../ge_appliances/api/gather_data.py | 56 ++ .../ge_appliances/api/ge_appliance.py | 248 ++++++++ .../ge_appliances/binary_sensor.py | 7 +- custom_components/ge_appliances/button.py | 37 -- custom_components/ge_appliances/climate.py | 39 -- .../ge_appliances/config_flow.py | 6 +- custom_components/ge_appliances/const.py | 8 +- .../ge_appliances/devices/__init__.py | 67 +-- .../ge_appliances/devices/advantium.py | 45 -- .../ge_appliances/devices/base.py | 52 +- .../ge_appliances/devices/biac.py | 35 -- .../ge_appliances/devices/coffee_maker.py | 66 --- .../ge_appliances/devices/cooktop.py | 52 -- .../ge_appliances/devices/dishwasher.py | 208 ++++++- .../ge_appliances/devices/dryer.py | 65 --- .../ge_appliances/devices/dual_dishwasher.py | 61 -- .../ge_appliances/devices/espresso_maker.py | 34 -- .../ge_appliances/devices/fridge.py | 126 ----- .../ge_appliances/devices/hood.py | 52 -- .../ge_appliances/devices/microwave.py | 56 -- .../ge_appliances/devices/oim.py | 41 -- .../ge_appliances/devices/oven.py | 120 ---- .../ge_appliances/devices/pac.py | 31 - .../ge_appliances/devices/sac.py | 37 -- .../ge_appliances/devices/wac.py | 33 -- .../ge_appliances/devices/washer.py | 63 --- .../ge_appliances/devices/washer_dryer.py | 36 -- .../ge_appliances/devices/water_filter.py | 38 -- .../ge_appliances/devices/water_heater.py | 43 -- .../ge_appliances/devices/water_softener.py | 38 -- .../ge_appliances/entities/__init__.py | 10 - .../ge_appliances/entities/ac/__init__.py | 4 - .../entities/ac/fan_mode_options.py | 50 -- .../entities/ac/ge_biac_climate.py | 45 -- .../entities/ac/ge_pac_climate.py | 81 --- .../entities/ac/ge_sac_climate.py | 84 --- .../entities/ac/ge_wac_climate.py | 45 -- .../entities/advantium/__init__.py | 1 - .../ge_appliances/entities/advantium/const.py | 8 - .../entities/advantium/ge_advantium.py | 282 ---------- .../ge_appliances/entities/ccm/__init__.py | 5 - .../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 | 19 - .../entities/ccm/ge_ccm_cached_value.py | 20 - .../ge_ccm_pot_not_present_binary_sensor.py | 8 - .../ge_appliances/entities/common/__init__.py | 7 - .../entities/common/bool_converter.py | 2 +- .../entities/common/ge_climate.py | 194 ------- .../entities/common/ge_entity.py | 21 +- .../entities/common/ge_erd_binary_sensor.py | 46 +- .../entities/common/ge_erd_button.py | 17 - .../entities/common/ge_erd_entity.py | 105 +--- .../entities/common/ge_erd_light.py | 70 --- .../entities/common/ge_erd_number.py | 127 ----- .../common/ge_erd_property_binary_sensor.py | 46 +- .../entities/common/ge_erd_property_sensor.py | 49 +- .../entities/common/ge_erd_select.py | 35 -- .../entities/common/ge_erd_sensor.py | 139 ++--- .../entities/common/ge_erd_switch.py | 31 +- .../entities/common/ge_erd_timer_sensor.py | 30 - .../entities/common/ge_water_heater.py | 45 -- .../entities/common/options_converter.py | 0 .../entities/dishwasher/__init__.py | 0 .../ge_dishwasher_control_locked_switch.py | 2 +- .../ge_appliances/entities/fridge/__init__.py | 4 - .../ge_appliances/entities/fridge/const.py | 19 - .../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 -- .../ge_appliances/entities/hood/__init__.py | 2 - .../entities/hood/ge_hood_fan_speed.py | 46 -- .../entities/hood/ge_hood_light_level.py | 42 -- .../entities/opal_ice_maker/__init__.py | 1 - .../opal_ice_maker/oim_light_level_options.py | 26 - .../ge_appliances/entities/oven/__init__.py | 3 - .../ge_appliances/entities/oven/const.py | 50 -- .../ge_appliances/entities/oven/ge_oven.py | 229 -------- .../oven/ge_oven_light_level_select.py | 66 --- .../entities/water_filter/__init__.py | 1 - .../entities/water_filter/filter_position.py | 63 --- .../entities/water_heater/__init__.py | 2 - .../entities/water_heater/ge_water_heater.py | 89 --- .../entities/water_heater/heater_modes.py | 26 - .../entities/water_softener/__init__.py | 1 - .../water_softener/shutoff_position.py | 65 --- custom_components/ge_appliances/exceptions.py | 0 custom_components/ge_appliances/light.py | 40 -- custom_components/ge_appliances/manifest.json | 1 - custom_components/ge_appliances/number.py | 37 -- custom_components/ge_appliances/select.py | 40 -- custom_components/ge_appliances/sensor.py | 52 +- custom_components/ge_appliances/services.yaml | 47 -- custom_components/ge_appliances/strings.json | 49 ++ custom_components/ge_appliances/switch.py | 6 +- .../ge_appliances/translations/en.json | 51 +- .../ge_appliances/update_coordinator.py | 16 +- .../ge_appliances/water_heater.py | 38 -- 182 files changed, 4078 insertions(+), 4278 deletions(-) mode change 100644 => 100755 custom_components/ge_appliances/__init__.py create mode 100755 custom_components/ge_appliances/api/__init__.py create mode 100755 custom_components/ge_appliances/api/clients/__init__.py create mode 100755 custom_components/ge_appliances/api/clients/async_helpers.py create mode 100755 custom_components/ge_appliances/api/clients/async_login_flows.py create mode 100755 custom_components/ge_appliances/api/clients/base_client.py create mode 100755 custom_components/ge_appliances/api/clients/const.py create mode 100755 custom_components/ge_appliances/api/clients/ge_client_xmpp.py create mode 100755 custom_components/ge_appliances/api/clients/states.py create mode 100755 custom_components/ge_appliances/api/clients/websocket_client.py create mode 100755 custom_components/ge_appliances/api/clients/xmpp_client.py create mode 100755 custom_components/ge_appliances/api/entry_points.py create mode 100755 custom_components/ge_appliances/api/erd/__init__.py create mode 100755 custom_components/ge_appliances/api/erd/converters/__init__.py create mode 100755 custom_components/ge_appliances/api/erd/converters/abstract.py create mode 100755 custom_components/ge_appliances/api/erd/converters/dishwasher/__init__.py create mode 100755 custom_components/ge_appliances/api/erd/converters/dishwasher/cycle_name_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/dishwasher/erd_cycle_count_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/dishwasher/erd_cycle_state_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/dishwasher/erd_dishwasher_door_status_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/dishwasher/erd_error_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/dishwasher/erd_reminders_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/dishwasher/erd_user_setting_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/dishwasher/operating_mode_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/primitives/__init__.py create mode 100755 custom_components/ge_appliances/api/erd/converters/primitives/erd_bool_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/primitives/erd_bytes_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/primitives/erd_int_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/primitives/erd_signed_byte_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/primitives/erd_string_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/primitives/erd_time_span_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/specialized/__init__.py create mode 100755 custom_components/ge_appliances/api/erd/converters/specialized/erd_appliance_type_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/specialized/erd_clock_format_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/specialized/erd_end_tone_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/specialized/erd_locked_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/specialized/erd_measurement_units_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/specialized/erd_model_serial_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/specialized/erd_on_off_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/specialized/erd_software_version_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/specialized/erd_sound_level_converter.py create mode 100755 custom_components/ge_appliances/api/erd/converters/specialized/erd_unit_type_converter.py create mode 100755 custom_components/ge_appliances/api/erd/erd_code_class.py create mode 100755 custom_components/ge_appliances/api/erd/erd_codes.py create mode 100755 custom_components/ge_appliances/api/erd/erd_configuration.py create mode 100755 custom_components/ge_appliances/api/erd/erd_data_type.py create mode 100755 custom_components/ge_appliances/api/erd/erd_encoder.py create mode 100755 custom_components/ge_appliances/api/erd/values/__init__.py create mode 100755 custom_components/ge_appliances/api/erd/values/common/__init__.py create mode 100755 custom_components/ge_appliances/api/erd/values/common/erd_appliance_type.py create mode 100755 custom_components/ge_appliances/api/erd/values/common/erd_clock_format.py create mode 100755 custom_components/ge_appliances/api/erd/values/common/erd_end_tone.py create mode 100755 custom_components/ge_appliances/api/erd/values/common/erd_locked.py create mode 100755 custom_components/ge_appliances/api/erd/values/common/erd_measurement_units.py create mode 100755 custom_components/ge_appliances/api/erd/values/common/erd_on_off.py create mode 100755 custom_components/ge_appliances/api/erd/values/common/erd_present.py create mode 100755 custom_components/ge_appliances/api/erd/values/common/erd_sound_level.py create mode 100755 custom_components/ge_appliances/api/erd/values/common/erd_unit_type.py create mode 100755 custom_components/ge_appliances/api/erd/values/dishwasher/__init__.py create mode 100755 custom_components/ge_appliances/api/erd/values/dishwasher/cycle_count.py create mode 100755 custom_components/ge_appliances/api/erd/values/dishwasher/cycle_state_mapping.py create mode 100755 custom_components/ge_appliances/api/erd/values/dishwasher/erd_cycle_state.py create mode 100755 custom_components/ge_appliances/api/erd/values/dishwasher/erd_dishwasher_door_status.py create mode 100755 custom_components/ge_appliances/api/erd/values/dishwasher/erd_operating_mode.py create mode 100755 custom_components/ge_appliances/api/erd/values/dishwasher/erd_reminders.py create mode 100755 custom_components/ge_appliances/api/erd/values/dishwasher/erd_user_setting.py create mode 100755 custom_components/ge_appliances/api/erd/values/dishwasher/error_state.py create mode 100755 custom_components/ge_appliances/api/erd/values/dishwasher/operating_mode.py create mode 100755 custom_components/ge_appliances/api/erd/values/dishwasher/operating_mode_mapping.py create mode 100755 custom_components/ge_appliances/api/exception/__init__.py create mode 100755 custom_components/ge_appliances/api/exception/ge_auth_failed_error.py create mode 100755 custom_components/ge_appliances/api/exception/ge_client_disconnected_error.py create mode 100755 custom_components/ge_appliances/api/exception/ge_duplicate_appliance_error.py create mode 100755 custom_components/ge_appliances/api/exception/ge_exception.py create mode 100755 custom_components/ge_appliances/api/exception/ge_general_server_error.py create mode 100755 custom_components/ge_appliances/api/exception/ge_needs_reauthentication_error.py create mode 100755 custom_components/ge_appliances/api/exception/ge_not_authenticated_error.py create mode 100755 custom_components/ge_appliances/api/exception/ge_request_error.py create mode 100755 custom_components/ge_appliances/api/exception/ge_set_erd_not_allowed_error.py create mode 100755 custom_components/ge_appliances/api/exception/ge_unsupported_operation_error.py create mode 100755 custom_components/ge_appliances/api/gather_data.py create mode 100755 custom_components/ge_appliances/api/ge_appliance.py mode change 100644 => 100755 custom_components/ge_appliances/binary_sensor.py delete mode 100644 custom_components/ge_appliances/button.py delete mode 100644 custom_components/ge_appliances/climate.py mode change 100644 => 100755 custom_components/ge_appliances/config_flow.py mode change 100644 => 100755 custom_components/ge_appliances/const.py mode change 100644 => 100755 custom_components/ge_appliances/devices/__init__.py delete mode 100644 custom_components/ge_appliances/devices/advantium.py mode change 100644 => 100755 custom_components/ge_appliances/devices/base.py delete mode 100644 custom_components/ge_appliances/devices/biac.py delete mode 100644 custom_components/ge_appliances/devices/coffee_maker.py delete mode 100644 custom_components/ge_appliances/devices/cooktop.py mode change 100644 => 100755 custom_components/ge_appliances/devices/dishwasher.py delete mode 100644 custom_components/ge_appliances/devices/dryer.py delete mode 100644 custom_components/ge_appliances/devices/dual_dishwasher.py delete mode 100644 custom_components/ge_appliances/devices/espresso_maker.py delete mode 100644 custom_components/ge_appliances/devices/fridge.py delete mode 100644 custom_components/ge_appliances/devices/hood.py delete mode 100644 custom_components/ge_appliances/devices/microwave.py delete mode 100644 custom_components/ge_appliances/devices/oim.py delete mode 100644 custom_components/ge_appliances/devices/oven.py delete mode 100644 custom_components/ge_appliances/devices/pac.py delete mode 100644 custom_components/ge_appliances/devices/sac.py delete mode 100644 custom_components/ge_appliances/devices/wac.py delete mode 100644 custom_components/ge_appliances/devices/washer.py delete mode 100644 custom_components/ge_appliances/devices/washer_dryer.py delete mode 100644 custom_components/ge_appliances/devices/water_filter.py delete mode 100644 custom_components/ge_appliances/devices/water_heater.py delete mode 100644 custom_components/ge_appliances/devices/water_softener.py mode change 100644 => 100755 custom_components/ge_appliances/entities/__init__.py delete mode 100644 custom_components/ge_appliances/entities/ac/__init__.py delete mode 100644 custom_components/ge_appliances/entities/ac/fan_mode_options.py delete mode 100644 custom_components/ge_appliances/entities/ac/ge_biac_climate.py delete mode 100644 custom_components/ge_appliances/entities/ac/ge_pac_climate.py delete mode 100644 custom_components/ge_appliances/entities/ac/ge_sac_climate.py delete mode 100644 custom_components/ge_appliances/entities/ac/ge_wac_climate.py delete mode 100644 custom_components/ge_appliances/entities/advantium/__init__.py delete mode 100644 custom_components/ge_appliances/entities/advantium/const.py delete mode 100644 custom_components/ge_appliances/entities/advantium/ge_advantium.py delete mode 100644 custom_components/ge_appliances/entities/ccm/__init__.py delete mode 100644 custom_components/ge_appliances/entities/ccm/ge_ccm_brew_cups.py delete mode 100644 custom_components/ge_appliances/entities/ccm/ge_ccm_brew_settings.py delete mode 100644 custom_components/ge_appliances/entities/ccm/ge_ccm_brew_strength.py delete mode 100644 custom_components/ge_appliances/entities/ccm/ge_ccm_brew_temperature.py delete mode 100644 custom_components/ge_appliances/entities/ccm/ge_ccm_cached_value.py delete mode 100644 custom_components/ge_appliances/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py mode change 100644 => 100755 custom_components/ge_appliances/entities/common/__init__.py mode change 100644 => 100755 custom_components/ge_appliances/entities/common/bool_converter.py delete mode 100644 custom_components/ge_appliances/entities/common/ge_climate.py mode change 100644 => 100755 custom_components/ge_appliances/entities/common/ge_entity.py mode change 100644 => 100755 custom_components/ge_appliances/entities/common/ge_erd_binary_sensor.py delete mode 100644 custom_components/ge_appliances/entities/common/ge_erd_button.py mode change 100644 => 100755 custom_components/ge_appliances/entities/common/ge_erd_entity.py delete mode 100644 custom_components/ge_appliances/entities/common/ge_erd_light.py delete mode 100644 custom_components/ge_appliances/entities/common/ge_erd_number.py mode change 100644 => 100755 custom_components/ge_appliances/entities/common/ge_erd_property_binary_sensor.py mode change 100644 => 100755 custom_components/ge_appliances/entities/common/ge_erd_property_sensor.py delete mode 100644 custom_components/ge_appliances/entities/common/ge_erd_select.py mode change 100644 => 100755 custom_components/ge_appliances/entities/common/ge_erd_sensor.py mode change 100644 => 100755 custom_components/ge_appliances/entities/common/ge_erd_switch.py delete mode 100644 custom_components/ge_appliances/entities/common/ge_erd_timer_sensor.py delete mode 100644 custom_components/ge_appliances/entities/common/ge_water_heater.py mode change 100644 => 100755 custom_components/ge_appliances/entities/common/options_converter.py mode change 100644 => 100755 custom_components/ge_appliances/entities/dishwasher/__init__.py mode change 100644 => 100755 custom_components/ge_appliances/entities/dishwasher/ge_dishwasher_control_locked_switch.py delete mode 100644 custom_components/ge_appliances/entities/fridge/__init__.py delete mode 100644 custom_components/ge_appliances/entities/fridge/const.py delete mode 100644 custom_components/ge_appliances/entities/fridge/convertable_drawer_mode_options.py delete mode 100644 custom_components/ge_appliances/entities/fridge/ge_abstract_fridge.py delete mode 100644 custom_components/ge_appliances/entities/fridge/ge_dispenser.py delete mode 100644 custom_components/ge_appliances/entities/fridge/ge_freezer.py delete mode 100644 custom_components/ge_appliances/entities/fridge/ge_fridge.py delete mode 100644 custom_components/ge_appliances/entities/hood/__init__.py delete mode 100644 custom_components/ge_appliances/entities/hood/ge_hood_fan_speed.py delete mode 100644 custom_components/ge_appliances/entities/hood/ge_hood_light_level.py delete mode 100644 custom_components/ge_appliances/entities/opal_ice_maker/__init__.py delete mode 100644 custom_components/ge_appliances/entities/opal_ice_maker/oim_light_level_options.py delete mode 100644 custom_components/ge_appliances/entities/oven/__init__.py delete mode 100644 custom_components/ge_appliances/entities/oven/const.py delete mode 100644 custom_components/ge_appliances/entities/oven/ge_oven.py delete mode 100644 custom_components/ge_appliances/entities/oven/ge_oven_light_level_select.py delete mode 100644 custom_components/ge_appliances/entities/water_filter/__init__.py delete mode 100644 custom_components/ge_appliances/entities/water_filter/filter_position.py delete mode 100644 custom_components/ge_appliances/entities/water_heater/__init__.py delete mode 100644 custom_components/ge_appliances/entities/water_heater/ge_water_heater.py delete mode 100644 custom_components/ge_appliances/entities/water_heater/heater_modes.py delete mode 100644 custom_components/ge_appliances/entities/water_softener/__init__.py delete mode 100644 custom_components/ge_appliances/entities/water_softener/shutoff_position.py mode change 100644 => 100755 custom_components/ge_appliances/exceptions.py delete mode 100644 custom_components/ge_appliances/light.py mode change 100644 => 100755 custom_components/ge_appliances/manifest.json delete mode 100644 custom_components/ge_appliances/number.py delete mode 100644 custom_components/ge_appliances/select.py mode change 100644 => 100755 custom_components/ge_appliances/sensor.py delete mode 100644 custom_components/ge_appliances/services.yaml mode change 100644 => 100755 custom_components/ge_appliances/strings.json mode change 100644 => 100755 custom_components/ge_appliances/switch.py mode change 100644 => 100755 custom_components/ge_appliances/translations/en.json mode change 100644 => 100755 custom_components/ge_appliances/update_coordinator.py delete mode 100644 custom_components/ge_appliances/water_heater.py diff --git a/README.md b/README.md index d17d720..0d60b1f 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,4 @@ Custom component to allow control of [GE Appliances (SmartHQ)](https://www.geapp 4. Follow the prompts. ## Supported Devices -- Fridge -- Oven -- Dishwasher / F&P Dual Dishwasher -- Laundry (Washer/Dryer) -- Whole Home Water Filter -- Whole Home Water Softener -- Whole Home Water Heater -- A/C (Portable, Split, Window, Built-In) -- Range Hood -- Advantium -- Microwave -- Opal Ice Maker -- Coffee Maker / Espresso Maker -- Beverage Center +- Dishwasher \ No newline at end of file diff --git a/custom_components/ge_appliances/__init__.py b/custom_components/ge_appliances/__init__.py old mode 100644 new mode 100755 index 4c2a8ee..191ed10 --- a/custom_components/ge_appliances/__init__.py +++ b/custom_components/ge_appliances/__init__.py @@ -1,13 +1,13 @@ """The ge_appliances integration.""" import logging -from homeassistant.const import EVENT_HOMEASSISTANT_STOP import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_REGION, EVENT_HOMEASSISTANT_STOP 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 diff --git a/custom_components/ge_appliances/api/__init__.py b/custom_components/ge_appliances/api/__init__.py new file mode 100755 index 0000000..330ec3e --- /dev/null +++ b/custom_components/ge_appliances/api/__init__.py @@ -0,0 +1,9 @@ +"""GE Home SDK""" + +__version__ = "0.5.13" + + +from .clients import * +from .erd import * +from .exception import * +from .ge_appliance import GeAppliance diff --git a/custom_components/ge_appliances/api/clients/__init__.py b/custom_components/ge_appliances/api/clients/__init__.py new file mode 100755 index 0000000..8f00aaf --- /dev/null +++ b/custom_components/ge_appliances/api/clients/__init__.py @@ -0,0 +1,27 @@ +"""GE Client implementations""" + +import logging +from .const import ( + EVENT_ADD_APPLIANCE, + EVENT_APPLIANCE_INITIAL_UPDATE, + EVENT_APPLIANCE_STATE_CHANGE, + EVENT_APPLIANCE_AVAILABLE, + EVENT_APPLIANCE_UNAVAILABLE, + EVENT_APPLIANCE_UPDATE_RECEIVED, + EVENT_CONNECTED, + EVENT_DISCONNECTED, + EVENT_GOT_APPLIANCE_LIST, + EVENT_GOT_APPLIANCE_FEATURES, + EVENT_STATE_CHANGED +) +from .const import LOGIN_REGIONS +from .base_client import GeBaseClient +from .websocket_client import GeWebsocketClient +from .async_login_flows import async_get_oauth2_token, async_refresh_oauth2_token + +_LOGGER = logging.getLogger(__name__) + +try: + from .xmpp_client import GeXmppClient +except ImportError: + _LOGGER.info("XMPP client not avaible. You may need to install slximpp.") diff --git a/custom_components/ge_appliances/api/clients/async_helpers.py b/custom_components/ge_appliances/api/clients/async_helpers.py new file mode 100755 index 0000000..3da8e80 --- /dev/null +++ b/custom_components/ge_appliances/api/clients/async_helpers.py @@ -0,0 +1,20 @@ + +import asyncio +from typing import AsyncIterator + + +async def CancellableAsyncIterator(async_iterator: AsyncIterator, cancellation_event: asyncio.Event) -> AsyncIterator: + cancellation_task = asyncio.create_task(cancellation_event.wait()) + result_iter = async_iterator.__aiter__() + while not cancellation_event.is_set(): + done, pending = await asyncio.wait( + [cancellation_task, asyncio.create_task(result_iter.__anext__())], + return_when=asyncio.FIRST_COMPLETED + ) + for done_task in done: + if done_task == cancellation_task: + for pending_task in pending: + await pending_task + break + else: + yield done_task.result() diff --git a/custom_components/ge_appliances/api/clients/async_login_flows.py b/custom_components/ge_appliances/api/clients/async_login_flows.py new file mode 100755 index 0000000..ae78b9c --- /dev/null +++ b/custom_components/ge_appliances/api/clients/async_login_flows.py @@ -0,0 +1,206 @@ +from http.cookies import SimpleCookie +from aiohttp import BasicAuth, ClientSession +from lxml import etree +from urllib.parse import urlparse, parse_qs +import logging + +from ..exception import * +from .const import ( + LOGIN_COOKIE_DOMAIN, + LOGIN_REGION_COOKIE_NAME, + LOGIN_REGIONS, + LOGIN_URL, + OAUTH2_CLIENT_ID, + OAUTH2_CLIENT_SECRET, + OAUTH2_REDIRECT_URI +) + +try: + import re2 as re +except ImportError: + import re + +_LOGGER = logging.getLogger(__name__) + +def set_login_cookie(session: ClientSession, account_region: str): + c = SimpleCookie() + c[LOGIN_REGION_COOKIE_NAME] = LOGIN_REGIONS[account_region] + c[LOGIN_REGION_COOKIE_NAME]["domain"] = LOGIN_COOKIE_DOMAIN + c[LOGIN_REGION_COOKIE_NAME]["path"] = "/" + c[LOGIN_REGION_COOKIE_NAME]["httponly"] = True + c[LOGIN_REGION_COOKIE_NAME]["secure"] = True + + session.cookie_jar.update_cookies(c) + +async def async_get_authorization_code(session: ClientSession, account_username: str, account_password: str, account_region: str): + params = { + 'client_id': OAUTH2_CLIENT_ID, + 'response_type': 'code', + 'access_type': 'offline', + 'redirect_uri': OAUTH2_REDIRECT_URI, + } + + set_login_cookie(session, account_region) + + async with session.get(f'{LOGIN_URL}/oauth2/auth', params=params) as resp: + if 400 <= resp.status < 500: + raise GeAuthFailedError(await resp.text()) + if resp.status >= 500: + raise GeGeneralServerError(await resp.text()) + resp_text = await resp.text() + + email_regex = ( + r'^\s*(\w+(?:(?:-\w+)|(?:\.\w+)|(?:\+\w+))*\@' + r'[A-Za-z0-9]+(?:(?:\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9][A-Za-z0-9]+)\s*$' + ) + clean_username = re.sub(email_regex, r'\1', account_username) + + etr = etree.HTML(resp_text) + post_data = { + i.attrib['name']: i.attrib['value'] + for i in etr.xpath("//form[@id = 'frmsignin']//input") + if 'value' in i.keys() + } + post_data['username'] = clean_username + post_data['password'] = account_password + + async with session.post(f'{LOGIN_URL}/oauth2/g_authenticate', data=post_data, allow_redirects=False) as resp: + if 400 <= resp.status < 500: + raise GeAuthFailedError(f"Problem with request, code: {resp.status}") + if resp.status >= 500: + raise GeGeneralServerError(f"Server error, code: {resp.status}") + try: + if resp.status == 200: + #if we have an OK response, probably need to "authorize", but could also + #be an authentication issue + code = await async_handle_ok_response(session, await resp.text()) + else: + #assume response has a location header from which we can get a code + code = parse_qs(urlparse(resp.headers['Location']).query)['code'][0] + except Exception as exc: + resp_text = await resp.text() + _LOGGER.exception(f"There was a problem getting the authorization code, response details: {resp.__dict__}") + raise GeAuthFailedError(f'Could not obtain authorization code') from exc + return code + +async def async_handle_ok_response(session: ClientSession, resp_text: str) -> str: + """Handles an OK 200 response from the login process""" + + #parse the response into html + etr = etree.HTML(resp_text) + post_data = {} + + try: + #first try to pull all the form values + post_data = { + i.attrib['name']: i.attrib['value'] + for i in etr.xpath("//form[@id = 'frmsignin']//input") + if 'value' in i.keys() + } + except: + pass + + #if we have an authorized key, try to authorize the application + if "authorized" in post_data: + code = await async_authorize_application(session, post_data) + return code + + #try to get the error based on the known responses + try: + reason = etr.find(".//div[@id='alert_pane']").text.translate({ord(c):"" for c in "\t\n"}) + raise GeAuthFailedError(f"Authentication failed, reason: {reason}") + except GeAuthFailedError: + raise #re-raise only auth failed errors, all others are irrelevant at this point + except: + pass + + #throw an exception by default + raise GeAuthFailedError("Authentication failed for unknown reason, please review response text for clues.") + +async def async_authorize_application(session: ClientSession, post_data: dict) -> str: + """Authorizes the application if needed""" + + _LOGGER.info( + "The application requires authentication and will attempt to grant consent automatically. " + \ + "Visit https://accounts.brillion.geappliances.com/consumer/active/applications to deauthorize.") + + post_data["authorized"] = "yes" + + async with session.post(f'{LOGIN_URL}/oauth2/code', data=post_data, allow_redirects=False) as resp: + if 400 <= resp.status < 500: + raise GeAuthFailedError(f"Problem with request, code: {resp.status}") + if resp.status >= 500: + raise GeGeneralServerError(f"Server error, code: {resp.status}") + try: + #oauth2/code appears to give the same header as we expect in the normal case, so let's try to use it + code = parse_qs(urlparse(resp.headers['Location']).query)['code'][0] + except Exception as exc: + resp_text = await resp.text() + _LOGGER.exception(f"There was a problem authorizing the application, response details: {resp.__dict__}") + raise GeAuthFailedError(f'Could not authorize application') from exc + return code + +async def async_get_oauth2_token(session: ClientSession, account_username: str, account_password: str, account_region: str): + """Hackily get an oauth2 token until I can be bothered to do this correctly""" + + #attempt to get the authorization code + code = await async_get_authorization_code(session, account_username, account_password, account_region) + + #get the token + post_data = { + 'code': code, + 'client_id': OAUTH2_CLIENT_ID, + 'client_secret': OAUTH2_CLIENT_SECRET, + 'redirect_uri': OAUTH2_REDIRECT_URI, + 'grant_type': 'authorization_code', + } + try: + auth = BasicAuth(OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET) + async with session.post(f'{LOGIN_URL}/oauth2/token', data=post_data, auth=auth) as resp: + if 400 <= resp.status < 500: + raise GeAuthFailedError(f"Problem with request, code: {resp.status}") + if resp.status >= 500: + raise GeGeneralServerError(f"Server error, code: {resp.status}") + oauth_token = await resp.json() + try: + access_token = oauth_token['access_token'] + return oauth_token + except KeyError: + raise GeAuthFailedError(f'Failed to get a token: {oauth_token}') + except Exception as exc: + resp_text = await resp.text() + _LOGGER.exception(f"Could not get OAuth token, response details: {resp.__dict__}") + raise GeAuthFailedError(f'Could not get OAuth token') from exc + except Exception as exc: + raise GeAuthFailedError(f'Could not get OAuth token') from exc + +async def async_refresh_oauth2_token(session: ClientSession, refresh_token: str): + """ Refreshes an OAuth2 Token based on a refresh token """ + + post_data = { + 'redirect_uri': OAUTH2_REDIRECT_URI, + 'client_id': OAUTH2_CLIENT_ID, + 'client_secret': OAUTH2_CLIENT_SECRET, + 'grant_type': 'refresh_token', + 'refresh_token': refresh_token + } + try: + auth = BasicAuth(OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET) + async with session.post(f'{LOGIN_URL}/oauth2/token', data=post_data, auth=auth) as resp: + if 400 <= resp.status < 500: + raise GeAuthFailedError(f"Problem with request, code: {resp.status}") + if resp.status >= 500: + raise GeGeneralServerError(f"Server error, code: {resp.status}") + oauth_token = await resp.json() + try: + access_token = oauth_token['access_token'] + return oauth_token + except KeyError: + raise GeAuthFailedError(f'Failed to get a token: {oauth_token}') + except Exception as exc: + resp_text = await resp.text() + _LOGGER.exception(f"Could not refresh OAuth token, response details: {resp.__dict__}") + raise GeAuthFailedError(f'Could not refresh OAuth token') from exc + except Exception as exc: + raise GeAuthFailedError(f'Could not refresh OAuth token') from exc + diff --git a/custom_components/ge_appliances/api/clients/base_client.py b/custom_components/ge_appliances/api/clients/base_client.py new file mode 100755 index 0000000..f81c21f --- /dev/null +++ b/custom_components/ge_appliances/api/clients/base_client.py @@ -0,0 +1,306 @@ +"""Base client for GE ERD APIs""" + +import abc +from ...api.clients.async_login_flows import async_get_oauth2_token, async_refresh_oauth2_token +from aiohttp import ClientSession +import asyncio +from collections import defaultdict +from datetime import datetime, timedelta +import logging +from typing import Any, Callable, Dict, List, Optional, Tuple + +from ..erd import ErdCode, ErdCodeType +from ..exception import * +from ..ge_appliance import GeAppliance +from .const import ( + EVENT_APPLIANCE_INITIAL_UPDATE, + EVENT_APPLIANCE_AVAILABLE, + EVENT_APPLIANCE_UNAVAILABLE, + EVENT_CONNECTED, + EVENT_DISCONNECTED, + EVENT_STATE_CHANGED, + MAX_RETRIES, + RETRY_INTERVAL, +) +from .states import GeClientState + +_LOGGER = logging.getLogger(__name__) + +class GeBaseClient(metaclass=abc.ABCMeta): + """Abstract base class for GE ERD APIs""" + + client_priority = 0 # Priority of this client class. Higher is better. + + def __init__(self, username: str, password: str, region: str = "US", event_loop: Optional[asyncio.AbstractEventLoop] = None): + self.account_username = username + self.account_password = password + self.account_region = region + self._credentials = None # type: Optional[Dict] + self._session = None # type: Optional[ClientSession] + + self._access_token = None + self._refresh_token = None + self._token_expiration_time = datetime.now() + + self._state = GeClientState.INITIALIZING + self._disconnect_requested = asyncio.Event() + self._retries_since_last_connect = -1 + self._has_successful_connect = False + self._loop = event_loop + self._appliances = {} # type: Dict[str, GeAppliance] + self._initialize_event_handlers() + + @property + def credentials(self) -> Optional[Dict]: + return self._credentials + + @credentials.setter + def credentials(self, credentials: Dict): + self._credentials = credentials + + @property + def appliances(self) -> Dict[str, GeAppliance]: + return self._appliances + + @property + def user_id(self) -> Optional[str]: + try: + return self.credentials['userId'] + except (TypeError, KeyError): + raise GeNotAuthenticatedError + + @property + def state(self) -> GeClientState: + return self._state + + @property + def loop(self) -> asyncio.AbstractEventLoop: + if self._loop is None: + self._loop = asyncio.get_event_loop() + return self._loop + + @property + def connected(self) -> bool: + """ Indicates whether the client is in a connected state """ + return self._state not in [GeClientState.DISCONNECTING, GeClientState.DISCONNECTED] + + @property + def available(self) -> bool: + """ Indicates whether the client is available for sending/receiving commands """ + return self._state == GeClientState.CONNECTED + + @property + def event_handlers(self) -> Dict[str, List[Callable]]: + return self._event_handlers + + async def async_event(self, event: str, *args, **kwargs): + _LOGGER.debug(f"received event: {event}, processing callbacks...") + """Trigger event callbacks sequentially""" + for cb in self.event_handlers[event]: + _LOGGER.debug(f"processing callback: {cb}") + asyncio.ensure_future(cb(*args, **kwargs), loop=self.loop) + + def add_event_handler(self, event: str, callback: Callable, disposable: bool = False): + if disposable: + raise NotImplementedError('Support for disposable callbacks not yet implemented') + self.event_handlers[event].append(callback) + + def remove_event_handler(self, event: str, callback: Callable): + try: + self.event_handlers[event].remove(callable) + except: + _LOGGER.warn(f"could not remove event handler {event}-{callable}") + + def clear_event_handlers(self): + self._initialize_event_handlers() + + async def async_get_credentials_and_run(self, session: ClientSession): + """Do a full login flow and run the client.""" + await self.async_get_credentials(session) + await self.async_run_client() + + async def async_run_client(self): + #reset the disconnect event + self._disconnect_requested.clear() + + _LOGGER.info('Starting GE Appliances client') + while not self._disconnect_requested.is_set(): + if self._retries_since_last_connect > MAX_RETRIES: + _LOGGER.debug(f'Tried auto-reconnecting {MAX_RETRIES} times, giving up.') + break + try: + await self._async_run_client() + except GeNeedsReauthenticationError: + _LOGGER.info('Reauthentication needed') + except GeRequestError as err: + _LOGGER.info(f'Error executing request {err}') + except Exception as err: + if not self._has_successful_connect: + _LOGGER.warn(f'Unhandled exception on first connect attempt: {err}, disconnecting') + break + _LOGGER.exception(err) + _LOGGER.info(f'Unhandled exception while running client, ignoring and restarting') + finally: + if not self._disconnect_requested.is_set(): + await self._set_state(GeClientState.DROPPED) + await self._set_state(GeClientState.WAITING) + _LOGGER.debug('Waiting before reconnecting') + await asyncio.sleep(RETRY_INTERVAL) + _LOGGER.debug('Refreshing authentication before reconnecting') + try: + await self.async_do_refresh_login_flow() + except Exception as err: + #if there was an error refreshing the authentication, break the loop and kill the client + _LOGGER.warn(f'Error refreshing authentication: {err}') + break + self._retries_since_last_connect += 1 + + #initiate the disconnection + await self.disconnect() + + @abc.abstractmethod + async def _async_run_client(self): + """ Internal method to run the client """ + + @abc.abstractmethod + async def async_set_erd_value(self, appliance: GeAppliance, erd_code: ErdCodeType, erd_value: Any): + """ + Send a new erd value to the appliance + :param appliance: The appliance being updated + :param erd_code: The ERD code to update + :param erd_value: The new value to set + """ + pass + + @abc.abstractmethod + async def async_request_update(self, appliance: GeAppliance): + """Request the appliance send a full state update""" + pass + + async def async_request_features(self, appliance: GeAppliance): + """Request the appliance send a features state update""" + pass + + async def async_request_message(self, appliance: GeAppliance): + """Request notification history""" + pass + + async def async_get_credentials(self, session: ClientSession): + """Get updated credentials""" + self._session = session + await self.async_do_full_login_flow() + + async def async_do_full_login_flow(self) -> Dict[str, str]: + """Do the full login flow for this client""" + self.credentials = await self._async_do_full_login_flow() + return self.credentials + + @abc.abstractmethod + async def _async_do_full_login_flow(self) -> Dict[str, str]: + """Internal full login flow""" + pass + + async def async_do_refresh_login_flow(self) -> Dict[str, str]: + """Do the refresh login flow for this client""" + self.credentials = await self._async_do_refresh_login_flow() + return self.credentials + + @abc.abstractmethod + async def _async_do_refresh_login_flow(self) -> Dict[str, str]: + """Internal refresh login flow""" + pass + + async def _async_get_oauth2_token(self): + """Get the OAuth2 token based on the username and password""" + + await self._set_state(GeClientState.AUTHORIZING_OAUTH) + + oauth_token = await async_get_oauth2_token( + self._session, + self.account_username, + self.account_password, + self.account_region) + + try: + self._access_token = oauth_token['access_token'] + self._token_expiration_time = datetime.now() + timedelta(seconds=(oauth_token['expires_in'] - 120)) + self._refresh_token = oauth_token['refresh_token'] + except KeyError: + raise GeAuthFailedError(f'Failed to get a token: {oauth_token}') + + async def _async_refresh_oauth2_token(self): + """ Refreshes an OAuth2 Token based on a refresh token """ + + await self._set_state(GeClientState.AUTHORIZING_OAUTH) + + oauth_token = await async_refresh_oauth2_token(self._session, self._refresh_token) + + try: + self._access_token = oauth_token['access_token'] + self._token_expiration_time = datetime.now() + timedelta(seconds=(oauth_token['expires_in'] - 120)) + self._refresh_token = oauth_token.get('refresh_token', self._refresh_token) + except KeyError: + raise GeAuthFailedError(f'Failed to get a token: {oauth_token}') + + async def _maybe_trigger_appliance_init_event(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]): + """ + Trigger the appliance_got_type event if appropriate + + :param data: GeAppliance updated and the updates + """ + appliance, state_changes = data + if ErdCode.APPLIANCE_TYPE in state_changes and not appliance.initialized: + _LOGGER.debug(f'Got initial appliance type for {appliance:s}') + appliance.initialized = True + await self.async_event(EVENT_APPLIANCE_INITIAL_UPDATE, appliance) + + async def _set_appliance_availability(self, appliance: GeAppliance, available: bool): + if available and not appliance.available: + appliance.set_available() + await self.async_event(EVENT_APPLIANCE_AVAILABLE, appliance) + elif not available and appliance.available: + appliance.set_unavailable() + await self.async_event(EVENT_APPLIANCE_UNAVAILABLE, appliance) + + async def _set_appliance_features(self, appliance: GeAppliance, features: List[str]): + appliance.features = features + + async def _set_state(self, new_state: GeClientState) -> bool: + """ Indicate that the state changed and raise an event """ + if self._state != new_state: + old_state = self._state + self._state = new_state + await self.async_event(EVENT_STATE_CHANGED, old_state, new_state) + return True + return False + + def _initialize_event_handlers(self): + self._event_handlers = defaultdict(list) # type: Dict[str, List[Callable]] + self.add_event_handler(EVENT_STATE_CHANGED, self._on_state_change) + pass + + async def _on_state_change(self, old_state: GeClientState, new_state: GeClientState): + _LOGGER.debug(f'Client changed state: {old_state} to {new_state}') + + if new_state == GeClientState.CONNECTED: + await self.async_event(EVENT_CONNECTED, None) + if new_state == GeClientState.DISCONNECTED: + await self.async_event(EVENT_DISCONNECTED, None) + + async def disconnect(self): + """Disconnect and cleanup.""" + if not self._disconnect_requested.is_set(): + _LOGGER.info("Disconnecting") + await self._set_state(GeClientState.DISCONNECTING) + self._disconnect_requested.set() + await self._disconnect() + await self._set_state(GeClientState.DISCONNECTED) + + async def _set_connected(self): + self._retries_since_last_connect = -1 + self._has_successful_connect = True + await self._set_state(GeClientState.CONNECTED) + + @abc.abstractmethod + async def _disconnect(self) -> None: + pass diff --git a/custom_components/ge_appliances/api/clients/const.py b/custom_components/ge_appliances/api/clients/const.py new file mode 100755 index 0000000..be12b20 --- /dev/null +++ b/custom_components/ge_appliances/api/clients/const.py @@ -0,0 +1,34 @@ +"""Constants""" + +# OAuth2 credentials +OAUTH2_APP_ID = "com.ge.kitchen.wca.prd.android" +OAUTH2_CLIENT_ID = "564c31616c4f7474434b307435412b4d2f6e7672" +OAUTH2_CLIENT_SECRET = "6476512b5246446d452f697154444941387052645938466e5671746e5847593d" +OAUTH2_REDIRECT_URI = "brillion.4e617a766474657344444e562b5935566e51324a://oauth/redirect" + +LOGIN_URL = "https://accounts.brillion.geappliances.com" +API_URL = "https://api.brillion.geappliances.com" +SECURE_URL = "https://secure.brillion.geappliances.com" + +LOGIN_REGIONS = { + "US": "us-east-1", + "EU": "eu-west-1" +} +LOGIN_REGION_COOKIE_NAME = "abgea_region" +LOGIN_COOKIE_DOMAIN = "accounts.brillion.geappliances.com" + +MAX_RETRIES = 3 +RETRY_INTERVAL = 2 + +EVENT_ADD_APPLIANCE = "add_appliance" +EVENT_APPLIANCE_INITIAL_UPDATE = "appliance_got_type" +EVENT_APPLIANCE_STATE_CHANGE = "appliance_state_change" +EVENT_APPLIANCE_AVAILABLE = "appliance_available" +EVENT_APPLIANCE_UNAVAILABLE = "appliance_unavailable" +EVENT_APPLIANCE_UPDATE_RECEIVED = "appliance_update_received" +EVENT_APPLIANCE_NOTIFICATION_RECEIVED = "appliance_notification_received" +EVENT_CONNECTED = "connected" +EVENT_DISCONNECTED = "disconnected" +EVENT_GOT_APPLIANCE_LIST = "got_appliance_list" +EVENT_GOT_APPLIANCE_FEATURES = "got_appliance_features" +EVENT_STATE_CHANGED = "state_changed" diff --git a/custom_components/ge_appliances/api/clients/ge_client_xmpp.py b/custom_components/ge_appliances/api/clients/ge_client_xmpp.py new file mode 100755 index 0000000..1c605d6 --- /dev/null +++ b/custom_components/ge_appliances/api/clients/ge_client_xmpp.py @@ -0,0 +1,109 @@ +import asyncio +from collections import defaultdict +import os +import logging +import socket +import slixmpp +import ssl +from typing import Any, Dict, List, Callable + +try: + import ujson as json +except ImportError: + import json + +_LOGGER = logging.getLogger(__name__) + +# If this isn't done, it'll throw a not implemented exception +if os.name == 'nt': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +XMPP_EVENT_MESSAGE = 'message' +XMPP_EVENT_SESSION_START = 'session_start' +XMPP_EVENT_PRESENCE_AVAILABLE = 'presence_available' +XMPP_EVENT_PRESENCE_UNAVAILABLE = 'presence_unavailable' + +class GeClientXMPP(slixmpp.ClientXMPP): + def __init__(self, jid, password): + super().__init__(jid, password) + + self.add_event_handler(XMPP_EVENT_MESSAGE, self.on_message) + self.add_event_handler(XMPP_EVENT_SESSION_START, self.on_start) + self.add_event_handler(XMPP_EVENT_PRESENCE_AVAILABLE, self.on_presence_available) + self.add_event_handler(XMPP_EVENT_PRESENCE_UNAVAILABLE, self.on_presence_unavailable) + self.ssl_context.verify_mode = ssl.CERT_NONE + + self._event_handlers = defaultdict(list) + + @property + def event_handlers(self) -> Dict[str, List[Callable]]: + return self._event_handlers + + async def async_event(self, event: str, *args, **kwargs): + """Trigger event callbacks sequentially""" + for cb in self.event_handlers[event]: + asyncio.ensure_future(cb(*args, **kwargs), loop=self.loop) + + def add_external_event_handler(self, event: str, callback: Callable, disposable: bool = False): + if disposable: + raise NotImplementedError('Support for disposable callbacks not yet implemented') + self.event_handlers[event].append(callback) + + async def on_presence_available(self, evt: slixmpp.ElementBase): + await self.async_event(XMPP_EVENT_PRESENCE_AVAILABLE, evt) + + async def on_presence_unavailable(self, evt: slixmpp.ElementBase): + await self.async_event(XMPP_EVENT_PRESENCE_UNAVAILABLE, evt) + + async def on_start(self, evt): + await self.async_event(XMPP_EVENT_SESSION_START, evt) + + async def on_message(self, evt): + await self.async_event(XMPP_EVENT_MESSAGE, evt) + + async def _connect_routine(self): + """ + Override the _connect_routine method from xmlstream.py. + This is to address the bug corrected in open PR https://github.com/poezio/slixmpp/pull/19. + """ + self.event_when_connected = "connected" + + if self.connect_loop_wait > 0: + self.event('reconnect_delay', self.connect_loop_wait) + await asyncio.sleep(self.connect_loop_wait, loop=self.loop) + + record = await self.pick_dns_answer(self.default_domain) + if record is not None: + host, address, dns_port = record + port = self.address[1] if self.address[1] else dns_port + self.address = (address, port) + self._service_name = host + else: + # No DNS records left, stop iterating + # and try (host, port) as a last resort + self.dns_answers = None + + if self.use_ssl: + ssl_context = self.get_ssl_context() + else: + ssl_context = None + + if self._current_connection_attempt is None: + return + try: + await self.loop.create_connection( + lambda: self, self.address[0], self.address[1], ssl=ssl_context, + server_hostname=self.default_domain if self.use_ssl else None + ) + self.connect_loop_wait = 0 + except socket.gaierror: + self.event('connection_failed', 'No DNS record available for %s' % self.default_domain) + except OSError as e: + _LOGGER.debug('Connection failed: %s', e) + self.event("connection_failed", e) + if self._current_connection_attempt is None: + return + self.connect_loop_wait = self.connect_loop_wait * 2 + 1 + self._current_connection_attempt = asyncio.ensure_future( + self._connect_routine(), loop=self.loop, + ) diff --git a/custom_components/ge_appliances/api/clients/states.py b/custom_components/ge_appliances/api/clients/states.py new file mode 100755 index 0000000..074b5ee --- /dev/null +++ b/custom_components/ge_appliances/api/clients/states.py @@ -0,0 +1,13 @@ +import enum + +@enum.unique +class GeClientState(enum.Enum): + INITIALIZING = enum.auto() + AUTHORIZING_OAUTH = enum.auto() + AUTHORIZING_CLIENT = enum.auto() + CONNECTING = enum.auto() + CONNECTED = enum.auto() + DROPPED = enum.auto() + WAITING = enum.auto() + DISCONNECTING = enum.auto() + DISCONNECTED = enum.auto() diff --git a/custom_components/ge_appliances/api/clients/websocket_client.py b/custom_components/ge_appliances/api/clients/websocket_client.py new file mode 100755 index 0000000..b4101c1 --- /dev/null +++ b/custom_components/ge_appliances/api/clients/websocket_client.py @@ -0,0 +1,528 @@ +import asyncio +import logging +import websockets +from typing import Any, Dict, List, Optional, Tuple + +from ..erd import ErdCode, ErdCodeType +from ..exception import * +from ..ge_appliance import GeAppliance + +from .async_helpers import CancellableAsyncIterator +from .base_client import GeBaseClient +from .const import ( + API_URL, + EVENT_ADD_APPLIANCE, + EVENT_APPLIANCE_STATE_CHANGE, + EVENT_APPLIANCE_UPDATE_RECEIVED, + EVENT_GOT_APPLIANCE_LIST, + EVENT_GOT_APPLIANCE_FEATURES, +) +from .states import GeClientState + +try: + import ujson as json +except ImportError: + import json + +ALL_ERD = "allErd" +API_HOST = API_URL[8:] # Drop the https:// +LIST_APPLIANCES = "List-appliances" +REQUEST_FEATURES = "Request-features" +SET_ERD = "setErd" + +KEEPALIVE_TIMEOUT = 30 +LIST_APPLIANCES_FREQUENCY = 600 + +_LOGGER = logging.getLogger(__name__) + +class GeWebsocketClient(GeBaseClient): + """ + Client for GE's Websocket pseudo-MQTT API. + """ + client_priority = 2 # This should be the primary client + + def __init__(self, username: str, password: str, region: str = "US", event_loop: Optional[asyncio.AbstractEventLoop] = None, keepalive: Optional[int] = KEEPALIVE_TIMEOUT, list_frequency: Optional[int] = LIST_APPLIANCES_FREQUENCY): + super().__init__(username, password, region, event_loop) + self._endpoint = None # type: Optional[str] + self._socket = None # type: Optional[websockets.client.WebSocketClientProtocol] + self._pending_erds = {} # type: Dict[Tuple[str, str], str] + self._keepalive_timeout = keepalive + self._keepalive_fut = None # type: Optional[asyncio.Future] + self._list_frequency = list_frequency + self._list_fut = None # type: Optional[asyncio.Future] + + @property + def available(self) -> bool: + """ Indicates whether the client is available for sending/receiving commands """ + return self._socket and not self._socket.closed + + async def _async_do_full_login_flow(self) -> Dict[str,str]: + """Perform a complete login flow, returning credentials.""" + + _LOGGER.debug('Getting OAuth2 token') + await self._async_get_oauth2_token() + + _LOGGER.debug('Getting WS credentials') + return await self._async_get_wss_credentials() + + async def _async_do_refresh_login_flow(self) -> Dict[str, str]: + """Perform a refresh login flow, returning credentials""" + + _LOGGER.debug('Refreshing OAuth2 token') + await self._async_refresh_oauth2_token() + + _LOGGER.debug('Getting WS credentials') + return await self._async_get_wss_credentials() + + async def _async_get_wss_credentials(self) -> Dict[str,str]: + """Get WSS credentials""" + + await self._set_state(GeClientState.AUTHORIZING_CLIENT) + + uri = f'{API_URL}/v1/websocket' + auth_header = { 'Authorization': 'Bearer ' + self._access_token } + async with self._session.get(uri, headers=auth_header) as resp: + if 400 <= resp.status < 500: + raise GeAuthFailedError(await resp.text()) + if resp.status >= 500: + raise GeGeneralServerError(await resp.text()) + return await resp.json() + + @property + def endpoint(self) -> str: + try: + return self.credentials['endpoint'] + except (TypeError, KeyError): + raise GeNotAuthenticatedError + + @property + def websocket(self): + return self._socket + + def _initialize_event_handlers(self): + super()._initialize_event_handlers() + self.add_event_handler(EVENT_APPLIANCE_STATE_CHANGE, self._maybe_trigger_appliance_init_event) + + async def _async_run_client(self): + """Run the client.""" + try: + await self._set_state(GeClientState.CONNECTING) + async with websockets.connect(self.endpoint, compression=None) as socket: + self._socket = socket + self._setup_futures() + await self._subscribe_all() + await self._set_connected() + await self._get_appliance_list() + try: + async for message in CancellableAsyncIterator(socket, self._disconnect_requested): + try: + await self._process_message(message) + except GeRequestError as err: + _LOGGER.exception("Could not process request") + except websockets.WebSocketException: + _LOGGER.error("Unknown error reading socket") + except RuntimeError as err: + #do nothing if it's a StopAsyncIteration, we just stopped the iteration + #as part of the disconnect + if not isinstance(err.__cause__, StopAsyncIteration): + raise + finally: + self._teardown_futures() + await self._disconnect() + + async def async_send_command(self, appliance: GeAppliance, cmd: str, data=[]): + ''' + Send command via websocket + ''' + mac_addr = appliance.mac_addr + + request_body = { + "kind": "appliance#control", + "userId": self.user_id, + "applianceId": appliance.mac_addr, + "command": cmd, + "data": data, + "ackTimeout": 10, + "delay": 0, + } + + msg_dict = { + "kind": "websocket#api", + "action": "api", + "host": API_HOST, + "method": "POST", + "path": f"/v1/appliance/{mac_addr}/control/{cmd}", + "id": "", + "body": request_body, + } + await self._send_dict(msg_dict) + + async def async_set_erd_value(self, appliance: GeAppliance, erd_code: ErdCodeType, erd_value: Any): + if isinstance(erd_code, ErdCode): + raw_erd_code = erd_code.value + else: + raw_erd_code = erd_code + raw_erd_code = raw_erd_code.upper().replace("0X", "0x") + + mac_addr = appliance.mac_addr + + request_body = { + "kind": "appliance#erdListEntry", + "userId": self.user_id, + "applianceId": appliance.mac_addr, + "erd": raw_erd_code, + "value": erd_value, + "ackTimeout": 10, + "delay": 0, + } + + msg_dict = { + "kind": "websocket#api", + "action": "api", + "host": API_HOST, + "method": "POST", + "path": f"/v1/appliance/{mac_addr}/erd/{raw_erd_code}", + "id": f"{mac_addr}-{SET_ERD}-{raw_erd_code}", + "body": request_body, + } + self._pending_erds[(mac_addr, raw_erd_code)] = erd_value + await self._send_dict(msg_dict) + + async def async_request_update(self, appliance: GeAppliance): + """Request an appliance send a full update.""" + _LOGGER.debug(f"Requesting update for client {appliance.mac_addr}") + msg_dict = { + "kind": "websocket#api", + "action": "api", + "host": API_HOST, + "method": "GET", + "path": f"/v1/appliance/{appliance.mac_addr}/erd", + "id": f"{appliance.mac_addr}-{ALL_ERD}" + } + await self._send_dict(msg_dict) + + async def async_request_message(self, appliance: GeAppliance): + """Request an appliance get notification history""" + _LOGGER.debug(f"Requesting notification history for client {appliance.mac_addr}") + msg_dict = { + "kind": "websocket#api", + "action": "api", + "host": API_HOST, + "method": "GET", + "path": f"/v1/appliance/{appliance.mac_addr}/message", + "id": f"{appliance.mac_addr}" + } + await self._send_dict(msg_dict) + + async def async_request_features(self, appliance: GeAppliance): + """Request an appliance send features.""" + _LOGGER.debug(f"Requesting features for client {appliance.mac_addr}") + msg_dict = { + "kind": "websocket#api", + "action": "api", + "host": API_HOST, + "method": "GET", + "path": f"/v1/appliance/{appliance.mac_addr}/feature", + "id": f"{REQUEST_FEATURES}" + } + await self._send_dict(msg_dict) + + def _setup_futures(self): + if self._keepalive_timeout: + self._keepalive_fut = asyncio.ensure_future(self._keep_alive(self._keepalive_timeout), loop=self.loop) + if self._list_frequency: + self._list_fut = asyncio.ensure_future(self._refresh_appliances(self._list_frequency), loop=self.loop) + + def _teardown_futures(self): + if self._keepalive_fut is not None: + self._keepalive_fut.cancel() + if self._list_fut is not None: + self._list_fut.cancel() + + async def _disconnect(self): + """Disconnect and cleanup.""" + if self._socket and not self._socket.closed: + await self._socket.close() + self._socket = None + + async def _process_pending_erd(self, message_id: str): + id_parts = message_id.split("-") + if id_parts[1] != SET_ERD: + raise ValueError("Invalid message id") + mac_addr = id_parts[0] + raw_erd_code = id_parts[2] + erd_value = self._pending_erds.get((mac_addr, raw_erd_code)) + if erd_value is not None: + _LOGGER.debug(f"") + try: + await self._update_appliance_state(mac_addr, {raw_erd_code: erd_value}) + except KeyError: + pass + + async def _process_message(self, message: str): + """ + Process an incoming message. + """ + message_dict = json.loads(message) # type: Dict + try: + kind = message_dict['kind'] + except KeyError: + _LOGGER.debug(f"Could not get message kind, skipping message: {message_dict}") + return + + #if we have a response that indicates success, check it + if message_dict.get("success", True) != True or message_dict.get("code", 200) != 200: + if message_dict.get("code") in [401,403] or message_dict.get("reason") == "Access token expired": + raise GeNeedsReauthenticationError + raise GeRequestError(message, message_dict.get("code"), message_dict.get("reason")) + + if kind.lower() == "publish#erd": + await self._process_erd_update(message_dict) + elif kind.lower() == "websocket#api": + try: + message_id = message_dict["id"] + except KeyError: + return + if message_id == LIST_APPLIANCES: + await self._process_appliance_list(message_dict) + elif message_id == REQUEST_FEATURES: + await self._process_appliance_features(message_dict) + elif f"-{SET_ERD}-" in message_id: + await self._process_pending_erd(message_id) + elif f"-{ALL_ERD}" in message_id: + await self._process_cache_update(message_dict) + else: + _LOGGER.debug(f"Unknown message received: {message_dict}") + + #added per #867 in websocket project + await asyncio.sleep(0) + + async def _process_appliance_list(self, message_dict: Dict): + """ + Process the appliance list. + + These messages should take the form:: + + {"kind": "websocket#api", + "id": "List-appliances", + "request":{...}, + "success": True, + "code": 200, + "body": { + "kind": "appliance#applianceList", + "userId": "USER_ID", + "items":[ + {"applianceId": "MAC_ADDR_1", + "type": "TYPE_1", + "brand": "Unknown", + "jid":"_", + "nickname":"NICKNAME", + "online":"ONLINE" + }, + ..., + ], + } + + :param message_dict: + """ + body = message_dict["body"] + if body.get("kind") != "appliance#applianceList": + raise ValueError("Not an applianceList") + items = body["items"] + for item in items: + mac_addr = item["applianceId"].upper() + online = item['online'].upper() == "ONLINE" + + #if we already have the appliance, just update it's online status + if mac_addr in self.appliances: + await self._set_appliance_availability(self.appliances[mac_addr], online) + continue + + await self._add_appliance(mac_addr, online) + await self.async_event(EVENT_GOT_APPLIANCE_LIST, items) + + async def _process_appliance_features(self, message_dict: Dict): + """ + Process the appliance features. + + These messages should take the form:: + + {"kind": "websocket#api", + "id": "Request-features", + "request":{...}, + "success": True, + "code": 200, + "body": { + "kind": "appliance#applianceFeature", + "userId": "USER_ID", + "applianceId": "APPLIANCE_MAC", + "features":[ + "CLOTHES_WASHER_V1_SMART_DISPENSE", + "CLOTHES_WASHER_V1_WASHER_LINK", + "MORE_FEATURES_IF_AVAILABLE" + ], + }, + } + + :param message_dict: + """ + body = message_dict["body"] + if body.get("kind") != "appliance#applianceFeature": + raise ValueError("Not an applianceFeature") + items = body["features"] + mac_addr = body["applianceId"].upper() + _LOGGER.debug(f'Received features {items} for {mac_addr}') + if mac_addr in self.appliances: + await self._set_appliance_features(self.appliances[mac_addr], items) + + await self.async_event(EVENT_GOT_APPLIANCE_FEATURES, items) + + async def _process_cache_update(self, message_dict: Dict): + """ + Process an appliance's full cache update. + + These messages should take the form:: + + { + "body": { + "applianceId": "MAC_ADDRESS", + "items": [ + {"erd": "ERD_CODE_1", "time": "UPDATE_TIMESTAMP_1", "value": "VALUE_1"}, + ..., + {"erd": "ERD_CODE_N", "time": "UPDATE_TIMESTAMP_N", "value": "VALUE_N"}, + ], + "kind": "appliance#erdList", + "userId": "USER_ID" + }, + "code": 200, + "id": "-allErd", + "kind": "websocket#api", + "request": {...}, + "success": True, + } + """ + body = message_dict["body"] + if body.get("kind") != "appliance#erdList": + raise ValueError("Not an erdList") + mac_addr = body["applianceId"].upper() + updates = {i["erd"]: i["value"] for i in body["items"]} + await self._update_appliance_state(mac_addr, updates) + + async def _process_erd_update(self, message_dict: Dict): + """ + Process an ERD update (pseudo-HTTP PUBLISH). + + These messages should be in the form:: + + { + "item": { + "applianceId": "MAC_ADDRESS", + "erd": "ERD_CODE", + "time": "UPDATE_TIMESTAMP", + "value": "SOME_VALUE", + }, + "resource": "/appliance//erd/", + "kind": "publish#erd", + "userId":"USER_ID" + } + + :param message_dict: dict, the json-decoded message. + """ + item = message_dict["item"] + mac_addr = item["applianceId"].upper() + update = {item['erd']: item['value']} + await self._update_appliance_state(mac_addr, update) + + async def _update_appliance_state(self, mac_addr: str, updates: Dict[ErdCodeType, str]): + """Update appliance state, performing callbacks if necessary.""" + try: + appliance = self.appliances[mac_addr] + except KeyError: + return + state_changes = appliance.update_erd_values(updates) + if state_changes: + await self.async_event(EVENT_APPLIANCE_STATE_CHANGE, [appliance, state_changes]) + await self.async_event(EVENT_APPLIANCE_UPDATE_RECEIVED, [appliance, updates]) + + async def _send_dict(self, msg_dict: Dict[str, Any]): + """JSON encode a dictionary and send it.""" + payload = json.dumps(msg_dict) + try: + #if there's no socket, assume the connection is closed + if not self.websocket: + raise websockets.ConnectionClosedOK(1001, 'Socket disconnected') + + #send the payload + await self.websocket.send(payload) + + #added per #867 in websocket project + await asyncio.sleep(0) + + except websockets.ConnectionClosed: + _LOGGER.info("Tried to send a message, but connection already closed.") + + async def _keep_alive(self, keepalive: int = KEEPALIVE_TIMEOUT): + """Send periodic pings to keep the connection alive.""" + while self.available: + await asyncio.sleep(keepalive) + if self.available: + _LOGGER.debug("Sending keepalive ping") + await self._send_ping() + + async def _refresh_appliances(self, frequency: int = LIST_APPLIANCES_FREQUENCY): + """Refresh the appliances list to detect changes over time.""" + while self.available: + await asyncio.sleep(frequency) + if self.available: + _LOGGER.debug("Refreshing appliance list/state") + await self._get_appliance_list() + + async def _subscribe_all(self): + """Subscribe to all appliances.""" + msg_dict = {"kind": "websocket#subscribe", "action": "subscribe", "resources": ["/appliance/*/erd/*"]} + await self._send_dict(msg_dict) + + async def _subscribe_appliances(self, appliances: List[GeAppliance]): + """Subscribe to a list of appliances.""" + msg_dict = { + "kind": "websocket#subscribe", + "action": "subscribe", + "resources": [f"/appliance/{appliance.mac_addr}/erd/*" for appliance in appliances] + } + await self._send_dict(msg_dict) + + async def _get_appliance_list(self): + """Request the list of appliances on this account.""" + msg_dict = { + "kind": "websocket#api", + "action": "api", + "host": API_HOST, + "method": "GET", + "path": "/v1/appliance", + "id": LIST_APPLIANCES, + } + await self._send_dict(msg_dict) + + async def _send_ping(self): + """Send a ping.""" + msg_dict = { + "kind": "websocket#ping", + "id": "keepalive-ping", + "action": "ping", + } + await self._send_dict(msg_dict) + + async def _add_appliance(self, mac_addr: str, set_online: bool = True): + """Add an appliance to the registry and request an update.""" + mac_addr = mac_addr.upper() + if mac_addr in self.appliances: + raise GeDuplicateApplianceError(f'Trying to add duplicate appliance {mac_addr}') + new_appliance = GeAppliance(mac_addr, self) + if set_online: + new_appliance.set_available() + + _LOGGER.debug(f'Adding appliance {mac_addr}') + self.appliances[mac_addr] = new_appliance + await self.async_event(EVENT_ADD_APPLIANCE, new_appliance) + await self.async_request_update(new_appliance) + await self.async_request_features(new_appliance) diff --git a/custom_components/ge_appliances/api/clients/xmpp_client.py b/custom_components/ge_appliances/api/clients/xmpp_client.py new file mode 100755 index 0000000..90042d5 --- /dev/null +++ b/custom_components/ge_appliances/api/clients/xmpp_client.py @@ -0,0 +1,319 @@ +import asyncio +import logging +from typing import Any, Dict, Optional, Union + +import slixmpp +from lxml import etree +from ..erd import ErdCode, ErdCodeType +from ..exception import * +from ..ge_appliance import GeAppliance +from .base_client import GeBaseClient +from .const import * +from .ge_client_xmpp import ( + GeClientXMPP, + XMPP_EVENT_MESSAGE, + XMPP_EVENT_PRESENCE_AVAILABLE, + XMPP_EVENT_PRESENCE_UNAVAILABLE, + XMPP_EVENT_SESSION_START +) +from .states import GeClientState + +try: + import ujson as json +except ImportError: + import json + +_LOGGER = logging.getLogger(__name__) + +def _first_or_none(lst: list) -> Any: + try: + return lst[0] + except IndexError: + return None + +class GeXmppClient(GeBaseClient): + client_priority = 1 + + def __init__(self, username: str, password: str, region: str = "US", event_loop: Optional[asyncio.AbstractEventLoop] = None): + super().__init__(username, password, region, event_loop=event_loop) + self._client = None # type: Optional[GeClientXMPP] + + def _get_xmpp_client(self) -> GeClientXMPP: + client = GeClientXMPP(self.credentials['jid'], self.credentials['password']) + client.add_external_event_handler(XMPP_EVENT_SESSION_START, self._on_start) + client.add_external_event_handler(XMPP_EVENT_MESSAGE, self._on_message) + client.add_external_event_handler(XMPP_EVENT_PRESENCE_AVAILABLE, self._on_presence_available) + client.add_external_event_handler(XMPP_EVENT_PRESENCE_UNAVAILABLE, self._on_presence_unavailable) + + return client + + async def _async_do_full_login_flow(self) -> Dict[str,str]: + """Perform a complete login flow, returning credentials.""" + + _LOGGER.debug('Getting OAuth2 token') + await self._async_get_oauth2_token() + + _LOGGER.debug('Getting mobile device token') + mdt = await self._async_get_mobile_device_token() + + _LOGGER.debug('Getting GE token') + ge_token = await self._async_get_ge_token(mdt) + + _LOGGER.debug('Getting XMPP credentials') + return await self._async_get_xmpp_credentials(ge_token) + + async def _async_do_refresh_login_flow(self) -> Dict[str, str]: + """Perform a refresh login flow, returning credentials""" + + _LOGGER.debug('Refreshing OAuth2 token') + await self._async_refresh_oauth2_token() + + _LOGGER.debug('Getting mobile device token') + mdt = await self._async_get_mobile_device_token() + + _LOGGER.debug('Getting GE token') + ge_token = await self._async_get_ge_token(mdt) + + _LOGGER.debug('Getting XMPP credentials') + return await self._async_get_xmpp_credentials(ge_token) + + async def _async_get_mobile_device_token(self) -> str: + """Get a mobile device token""" + await self._set_state(GeClientState.AUTHORIZING_CLIENT) + + mdt_data = { + 'kind': 'mdt#login', + 'app': OAUTH2_APP_ID, + 'os': 'google_android' + } + auth_header = { 'Authorization': 'Bearer ' + self._access_token } + + async with self._session.post(f'{API_URL}/v1/mdt', json=mdt_data, headers=auth_header) as resp: + if resp.status != 200: + raise GeAuthFailedError(await resp.text()) + results = await resp.json() + try: + return results['mdt'] + except KeyError: + raise GeAuthFailedError(f'Failed to get a mobile device token: {results}') + + async def _async_get_ge_token(self, mobile_device_token: str) -> str: + """Get the GE token that we can use to get XMPP credentials""" + params = { + 'client_id': OAUTH2_CLIENT_ID, + 'client_secret': OAUTH2_CLIENT_SECRET, + 'mdt': mobile_device_token + } + auth_header = { 'Authorization': 'Bearer ' + self._access_token } + + async with self._session.post(f'{LOGIN_URL}/oauth2/getoken', params=params, headers=auth_header) as resp: + if 400 <= resp.status < 500: + raise GeAuthFailedError(await resp.text()) + if resp.status >= 500: + raise GeGeneralServerError(await resp.text()) + results = await resp.json() + + try: + return results['access_token'] + except KeyError: + raise GeAuthFailedError(f'Failed to get a GE token: {results}') + + async def _async_get_xmpp_credentials(self, ge_token: str) -> Dict: + """Get XMPP credentials""" + uri = f'{API_URL}/v1/mdt/credentials' + headers = {'Authorization': f'Bearer {ge_token}'} + async with self._session.get(uri, headers=headers) as resp: + if 400 <= resp.status < 500: + raise GeAuthFailedError(await resp.text()) + if resp.status >= 500: + raise GeGeneralServerError(await resp.text()) + return await resp.json() + + async def _async_run_client(self): + """Run the client.""" + try: + await self._set_state(GeClientState.CONNECTING) + address = (self._credentials['address'], self._credentials['port']) + self._client = self._get_xmpp_client() + self._client.connect(address=address) + #run the loop + await asyncio.ensure_future(self._client.disconnected, loop=self.loop) + #TODO: better exception handling + except Exception as err: + _LOGGER.error(f"Exception while processing XMPP loop: {err}") + finally: + self._disconnect() + + async def _disconnect(self) -> None: + if self._client: + await self._client.disconnect() + self._client = None + + async def _add_appliance(self, jid: str): + """Add an appliance to the registry and request an update.""" + mac_addr = jid.split('_')[0] + if jid in self.appliances: + raise GeDuplicateApplianceError(f'Attempted to add duplicate appliances {mac_addr} ({jid})') + new_appliance = GeAppliance(mac_addr, self) + + _LOGGER.info(f'Adding appliance {jid}') + self.appliances[jid] = new_appliance + await self.async_event(EVENT_ADD_APPLIANCE, new_appliance) + await new_appliance.async_request_update() + + async def _maybe_add_appliance(self, jid: str): + """Add an appliance, suppressing the error if it already exists.""" + try: + await self._add_appliance(jid) + except GeDuplicateApplianceError as err: + _LOGGER.warn(f'{err}') + pass + + async def _on_presence_available(self, evt: slixmpp.ElementBase): + """Perform actions when notified of an available JID.""" + await asyncio.sleep(2) # Wait 2 seconds to give it time to register + jid = slixmpp.JID(evt['from']).bare + + if jid == self._client.boundjid.bare: + return + try: + await self._set_appliance_availability(self.appliances[jid], True) + except KeyError: + await self._add_appliance(jid) + self.appliances[jid].set_available() + + async def _on_presence_unavailable(self, evt): + """When appliance is no longer available, mark it as such.""" + jid = slixmpp.JID(evt['from']).bare + + if jid == self._client.boundjid.bare: + return + try: + self._set_appliance_availability(self.appliances[jid], False) + except KeyError: + pass + + async def _on_message(self, event): + """Handle incoming messages.""" + + msg = str(event) + msg_from = slixmpp.JID(event['from']).bare + try: + message_data = self._extract_message_json(msg) + except ValueError: + _LOGGER.info(f"From: {msg_from}: Not a GE message") + return + try: + appliance = self.appliances[msg_from] + state_changes = appliance.update_erd_values(message_data) + if state_changes: + await self.async_event(EVENT_APPLIANCE_STATE_CHANGE, [appliance, state_changes]) + await self.async_event(EVENT_APPLIANCE_UPDATE_RECEIVED, [appliance, message_data]) + except KeyError: + _LOGGER.warning('Received json message from unregistered appliance') + + async def _on_start(self, event): + _LOGGER.debug('Starting session: ' + str(event)) + self._client.send_presence('available') + await asyncio.sleep(5) + await self._set_connected() + await self.async_event(EVENT_GOT_APPLIANCE_LIST, None) + + def _complete_jid(self, jid) -> str: + """Make a complete jid from a username""" + if "@" in jid: + return jid + return f"{jid}@{self._client.boundjid.host}" + + def _get_appliance_jid(self, appliance: GeAppliance) -> str: + return self._complete_jid(f"{appliance.mac_addr}_{self.user_id}") + + def _send_raw_message(self, mto: slixmpp.JID, mbody: str, mtype: str = 'chat', msg_id: Optional[str] = None): + """TODO: Use actual xml for this instead of hacking it. Then again, this is what GE does in the app.""" + try: + if not self._client: + raise RuntimeError('Client connection is not available') + + if msg_id is None: + msg_id = self._client.new_id() + raw_message = ( + f'' + f'{mbody}' + f'' + ) + + self._client.send_raw(raw_message) + except Exception as err: + raise GeRequestError from err + + def _send_request( + self, appliance: GeAppliance, method: str, uri: str, key: Optional[str] = None, + value: Optional[str] = None, message_id: Optional[str] = None): + """ + Send a pseudo-http request to the appliance + :param appliance: GeAppliance, the appliance to send the request to + :param method: str, Usually "GET" or "POST" + :param uri: str, the "endpoint" for the request, usually of the form "/UUID/erd/{erd_code}" + :param key: The json key to set in a POST request. Usually a four-character hex string with leading "0x". + :param value: The value to set, usually encoded as a hex string without a leading "0x" + :param message_id: + """ + if method.lower() != 'post' and (key is not None or value is not None): + raise RuntimeError('Values can only be set in a POST request') + + if message_id is None: + message_id = self._client.new_id() + message_body = self._format_request(message_id, uri, method, key, value) + jid = slixmpp.JID(self._get_appliance_jid(appliance)) + self._send_raw_message(jid, message_body) + + async def async_request_update(self, appliance: GeAppliance): + """ + Request a full update from the appliance. + TODO: This doesn't seem to do a full request. Need to investigate if this is possible. + """ + self._send_request(appliance, 'GET', '/UUID/cache') + + async def async_set_erd_value(self, appliance: GeAppliance, erd_code: ErdCodeType, erd_value: Any): + """ + Send a new erd value to the appliance + :param appliance: GeAppliance, the appliance to update + :param erd_code: The ERD code to update + :param erd_value: The new value to set + """ + if isinstance(erd_code, ErdCode): + raw_erd_code = erd_code.value + else: + raw_erd_code = erd_code + + uri = f'/UUID/erd/{raw_erd_code}' + self._send_request(appliance, 'POST', uri, raw_erd_code, erd_value) + + @staticmethod + def _extract_message_json(message: str) -> Dict: + """The appliances send messages that don't play nice with slixmpp, so let's do this.""" + etr = etree.XML(message) + json_elem = _first_or_none(etr.xpath('//json')) + if json_elem is None: + raise ValueError('Not a GE appliance message') + + data = _first_or_none(json_elem.xpath('text()')) + data = json.loads(data) + return data + + @staticmethod + def _format_request( + msg_id: Union[int, str], uri: str, method: str, key: Optional[str] = None, value: Optional[str] = None) -> str: + """Format a XMPP pseudo-HTTP request.""" + if method.lower() == 'post': + post_body = f"{json.dumps({key:value})}" + else: + post_body = "" + message = ( + f"{msg_id}" + f"{method}" + f"{uri}" + f"{post_body}" + "" # [sic.] + ) + return message diff --git a/custom_components/ge_appliances/api/entry_points.py b/custom_components/ge_appliances/api/entry_points.py new file mode 100755 index 0000000..1ffb239 --- /dev/null +++ b/custom_components/ge_appliances/api/entry_points.py @@ -0,0 +1,18 @@ +import argparse +from asyncio import gather + +from .gather_data import gather_appliance_data + +def appliance_data(): + parser = argparse.ArgumentParser() + + parser.add_argument('-u', '--username', type=str, required=True, + help="Your SmartHQ Username") + parser.add_argument('-p', '--password', type=str, required=True, + help="Your SmartHQ Password") + parser.add_argument('-r', '--region', type=str, choices=["US","EU"], default="US", + help="Your SmartHQ Region") + + args = parser.parse_args() + + gather_appliance_data(args.username, args.password, args.region) diff --git a/custom_components/ge_appliances/api/erd/__init__.py b/custom_components/ge_appliances/api/erd/__init__.py new file mode 100755 index 0000000..f442c36 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/__init__.py @@ -0,0 +1,7 @@ +"""GE ERD Definitions""" + +from .values import * +from .erd_code_class import ErdCodeClass +from .erd_data_type import ErdDataType +from .erd_codes import ErdCode, ErdCodeType +from .erd_encoder import ErdEncoder diff --git a/custom_components/ge_appliances/api/erd/converters/__init__.py b/custom_components/ge_appliances/api/erd/converters/__init__.py new file mode 100755 index 0000000..c125333 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/__init__.py @@ -0,0 +1,6 @@ +"""GE ERD Converters""" + +from .abstract import * +from .primitives import * +from .specialized import * +from .dishwasher import * diff --git a/custom_components/ge_appliances/api/erd/converters/abstract.py b/custom_components/ge_appliances/api/erd/converters/abstract.py new file mode 100755 index 0000000..b962db2 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/abstract.py @@ -0,0 +1,36 @@ +from abc import ABC, abstractmethod +from typing import TypeVar, Generic, Optional + +from ....api.erd.erd_codes import ErdCodeType +from ....api.exception import GeSetErdNotAllowedError + +T = TypeVar('T') + +class ErdValueConverter(Generic[T], ABC): + def __init__(self, erd_code: ErdCodeType = "Unknown", can_decode: bool = True, can_encode: bool = True): + self.erd_code = erd_code + self._can_decode = can_decode + self._can_encode = can_encode + + @abstractmethod + def erd_encode(self, value: T) -> str: + pass + @abstractmethod + def erd_decode(self, value: str) -> T: + pass + @property + def can_decode(self) -> bool: + return self._can_decode + @property + def can_encode(self) -> bool: + return self._can_encode + +class ErdReadWriteConverter(Generic[T], ABC): + def __init__(self, erd_code: ErdCodeType = "Unknown"): + ErdValueConverter.__init__(self, erd_code, True, True) + +class ErdReadOnlyConverter(ErdValueConverter[T]): + def __init__(self, erd_code: ErdCodeType = "Unknown"): + ErdValueConverter.__init__(self, erd_code, True, False) + def erd_encode(self, value: T) -> str: + raise GeSetErdNotAllowedError(self.erd_code) diff --git a/custom_components/ge_appliances/api/erd/converters/dishwasher/__init__.py b/custom_components/ge_appliances/api/erd/converters/dishwasher/__init__.py new file mode 100755 index 0000000..ad52524 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/dishwasher/__init__.py @@ -0,0 +1,13 @@ +from .erd_cycle_state_converter import ErdCycleStateConverter +from .erd_reminders_converter import ErdRemindersSettingConverter +from .cycle_name_converter import CycleNameConverter +from .operating_mode_converter import OperatingModeConverter +from .erd_error_converter import ErdErrorStateConverter +from .erd_dishwasher_door_status_converter import ErdDishwasherDoorStatusConverter +from .erd_cycle_count_converter import ErdCycleCountSettingConverter +from .erd_user_setting_converter import ( ErdUserSettingConverter, + ErdUserCycleSettingConverter, + ErdUserTemperatureSettingConverter, + ErdUserDryingSettingConverter, + ErdUserZoneSettingConverter + ) \ No newline at end of file diff --git a/custom_components/ge_appliances/api/erd/converters/dishwasher/cycle_name_converter.py b/custom_components/ge_appliances/api/erd/converters/dishwasher/cycle_name_converter.py new file mode 100755 index 0000000..1208bbe --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/dishwasher/cycle_name_converter.py @@ -0,0 +1,10 @@ +from ..primitives import ErdReadOnlyStringConverter + +class CycleNameConverter(ErdReadOnlyStringConverter): + def erd_decode(self, value: str) -> str: + name = super().erd_decode(value) + if name.startswith("AutoSense"): + name = "AutoSense" + +# return name + return "_".join(name.split()).lower() diff --git a/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_cycle_count_converter.py b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_cycle_count_converter.py new file mode 100755 index 0000000..a1fdefe --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_cycle_count_converter.py @@ -0,0 +1,27 @@ +import logging +from ..abstract import ErdReadOnlyConverter +from ..primitives import * +#NOTE: reset is actually the number of power cycles + +from .....api.erd.values.dishwasher import ErdCycleCount + +_LOGGER = logging.getLogger(__name__) + +class ErdCycleCountSettingConverter(ErdReadOnlyConverter[ErdCycleCount]): + def erd_decode(self, value: str) -> ErdCycleCount: + if not value: + return ErdCycleCount() + + try: + #convert to int + i = erd_decode_int(value) + + return ErdCycleCount( + started = (i & 0xFFFF00000000) >> 32, + completed = (i & 0xFFFF0000 ) >> 16, + reset = (i & 0xFFFF), + raw_value=value + ) + except Exception as ex: + _LOGGER.exception("Could not construct cycle counts, using default.") + return ErdCycleCount(raw_value=value) diff --git a/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_cycle_state_converter.py b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_cycle_state_converter.py new file mode 100755 index 0000000..01cc360 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_cycle_state_converter.py @@ -0,0 +1,17 @@ +import logging + +from .....api.erd.converters.abstract import ErdReadOnlyConverter +from .....api.erd.converters.primitives import * +from .....api.erd.values.dishwasher import ErdCycleState, ErdCycleStateRaw, CYCLE_STATE_RAW_MAP + +_LOGGER = logging.getLogger(__name__) + +class ErdCycleStateConverter(ErdReadOnlyConverter[ErdCycleState]): + def erd_decode(self, value: str) -> ErdCycleState: + """ Decodes the dishwasher cycle state """ + try: + raw = ErdCycleStateRaw(erd_decode_int(value)) + _LOGGER.debug(f'raw cycle state value: {raw}') + return ErdCycleState(CYCLE_STATE_RAW_MAP[raw]) + except (ValueError, KeyError): + return ErdCycleState.NA diff --git a/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_dishwasher_door_status_converter.py b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_dishwasher_door_status_converter.py new file mode 100755 index 0000000..58728d8 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_dishwasher_door_status_converter.py @@ -0,0 +1,15 @@ +import logging + +from .....api.erd.converters.abstract import ErdReadOnlyConverter +from .....api.erd.converters.primitives import * +from .....api.erd.values.dishwasher import ErdDishwasherDoorStatus + +_LOGGER = logging.getLogger(__name__) + +class ErdDishwasherDoorStatusConverter(ErdReadOnlyConverter[ErdDishwasherDoorStatus]): + def erd_decode(self, value: str) -> ErdDishwasherDoorStatus: + """ Decodes the dishwasher door state """ + try: + return ErdDishwasherDoorStatus(erd_decode_int(value)) + except (ValueError, KeyError): + return ErdDishwasherDoorStatus.NA diff --git a/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_error_converter.py b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_error_converter.py new file mode 100755 index 0000000..b49bc08 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_error_converter.py @@ -0,0 +1,25 @@ +import logging +from ..abstract import ErdReadOnlyConverter +from ..primitives import * + +from .....api.erd.values.dishwasher import ErdErrorState + +_LOGGER = logging.getLogger(__name__) + +class ErdErrorStateConverter(ErdReadOnlyConverter[ErdErrorState]): + def erd_decode(self, value: str) -> ErdErrorState: + if not value: + return ErdErrorState() + + try: + #convert to int + i = erd_decode_int(value) + + return ErdErrorState( + id = i & 0xF, + active = bool((i & 0xF0) >> 8), + raw_value=value + ) + except Exception as ex: + _LOGGER.exception("Could not construct error state, using default.") + return ErdErrorState(raw_value=value) diff --git a/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_reminders_converter.py b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_reminders_converter.py new file mode 100755 index 0000000..969ad2d --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_reminders_converter.py @@ -0,0 +1,26 @@ +import logging +from ..abstract import ErdReadOnlyConverter +from ..primitives import * + +from .....api.erd.values.dishwasher import ErdReminders + +_LOGGER = logging.getLogger(__name__) + +class ErdRemindersSettingConverter(ErdReadOnlyConverter[ErdReminders]): + def erd_decode(self, value: str) -> ErdReminders: + if not value: + return ErdReminders() + + try: + #convert to int + i = erd_decode_int(value) + + return ErdReminders( + clean_filter = bool((i & 0x01)), + add_rinse_aid = bool((i & 0x02 ) >> 1), + sanitized = bool((i & 0x04) >> 2), + raw_value=value + ) + except Exception as ex: + _LOGGER.exception("Could not construct reminders, using default.") + return ErdReminders(raw_value=value) diff --git a/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_user_setting_converter.py b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_user_setting_converter.py new file mode 100755 index 0000000..0660a9a --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/dishwasher/erd_user_setting_converter.py @@ -0,0 +1,137 @@ +import logging +from .....api.erd.values.dishwasher.erd_user_setting import ( + ErdUserSetting, + UserSetting, + UserCycleSetting, + UserWashTempSetting, + UserDryOptionSetting, + UserWashZoneSetting +) +from ..abstract import ErdReadWriteConverter +from ..primitives import * + +from .....api.erd.values.dishwasher import ErdUserSetting + +_LOGGER = logging.getLogger(__name__) + +class ErdUserSettingConverter(ErdReadWriteConverter[ErdUserSetting]): + def erd_decode(self, value: str) -> ErdUserSetting: + if not value: + return ErdUserSetting() + + try: + #convert to int + i = erd_decode_int(value) + + return ErdUserSetting( + bottle_jet = UserSetting(i & 1), + cycle_mode = UserCycleSetting((i & 14) >> 1), #missing brackets added by Nick + sabbath = UserSetting((i & 64) >> 6), + presoak = UserSetting((i & 256) >> 8), + lock_control = UserSetting((i & 512) >> 9), + dry_option = UserDryOptionSetting((i & 3072) >> 10), + wash_temp = UserWashTempSetting((i & 12288) >> 12), + rinse_aid = UserSetting((i & 32768) >> 15), + delay_hours = (i & 983040) >> 16, + wash_zone = UserWashZoneSetting((i & 3145728) >> 20), + demo_mode = UserSetting((i & 4194304) >> 22), + mute = UserSetting((i & 8388608) >> 23), + raw_value=value + ) + except Exception as ex: + _LOGGER.exception("Could not construct user setting, using default.") + return ErdUserSetting(raw_value=value) + + def erd_encode(self, value: int) -> str: + """ + return the Dishwasher user setting value + """ + return '{:06X}'.format(value) + +class ErdUserCycleSettingConverter(ErdReadWriteConverter[UserCycleSetting]): + def erd_decode(self, value: str) -> UserCycleSetting: + if not value: + return UserCycleSetting() + + try: + #convert to int + i = erd_decode_int(value) + + return UserCycleSetting(i) + except Exception as ex: + _LOGGER.exception("Could not construct user cycle setting, using default.") + return UserCycleSetting(raw_value=value) + + def erd_encode(self, value: UserCycleSetting) -> str: + """ + return the Dishwasher cycle setting value + """ + return ( + '{:02d}'.format(value.value) + ) + +class ErdUserTemperatureSettingConverter(ErdReadWriteConverter[UserWashTempSetting]): + def erd_decode(self, value: str) -> UserWashTempSetting: + if not value: + return UserWashTempSetting() + + try: + #convert to int + i = erd_decode_int(value) + + return UserWashTempSetting(i) + except Exception as ex: + _LOGGER.exception("Could not construct user cycle setting, using default.") + return UserWashTempSetting(raw_value=value) + + def erd_encode(self, value: UserWashTempSetting) -> str: + """ + return the Dishwasher temperature setting value + """ + return ( + '{:02d}'.format(value.value) + ) + +class ErdUserDryingSettingConverter(ErdReadWriteConverter[UserDryOptionSetting]): + def erd_decode(self, value: str) -> UserDryOptionSetting: + if not value: + return UserDryOptionSetting() + + try: + #convert to int + i = erd_decode_int(value) + + return UserDryOptionSetting(i) + except Exception as ex: + _LOGGER.exception("Could not construct user cycle setting, using default.") + return UserDryOptionSetting(raw_value=value) + + def erd_encode(self, value: UserDryOptionSetting) -> str: + """ + return the Dishwasher drying setting value + """ + return ( + '{:02d}'.format(value.value) + ) + +class ErdUserZoneSettingConverter(ErdReadWriteConverter[UserWashZoneSetting]): + def erd_decode(self, value: str) -> UserWashZoneSetting: + if not value: + return UserWashZoneSetting() + + try: + #convert to int + i = erd_decode_int(value) + + return UserWashZoneSetting(i) + except Exception as ex: + _LOGGER.exception("Could not construct user cycle setting, using default.") + return UserWashZoneSetting(raw_value=value) + + def erd_encode(self, value: UserWashZoneSetting) -> str: + """ + return the Dishwasher wash zone setting value + """ + return ( + '{:02d}'.format(value.value) + ) diff --git a/custom_components/ge_appliances/api/erd/converters/dishwasher/operating_mode_converter.py b/custom_components/ge_appliances/api/erd/converters/dishwasher/operating_mode_converter.py new file mode 100755 index 0000000..5010e5c --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/dishwasher/operating_mode_converter.py @@ -0,0 +1,18 @@ +import logging + +from .....api.erd.converters.abstract import ErdReadOnlyConverter +from .....api.erd.converters.primitives import * +from .....api.erd.values.dishwasher import ErdOperatingMode, OperatingMode, OPERATING_MODE_MAP + +_LOGGER = logging.getLogger(__name__) + +class OperatingModeConverter(ErdReadOnlyConverter[OperatingMode]): + def erd_decode(self, value: str) -> OperatingMode: + """Decode the dishwasher operating state """ + try: + operating_mode = ErdOperatingMode(erd_decode_int(value)) + _LOGGER.debug(f'raw operating mode value: {operating_mode}') +# return OPERATING_MODE_MAP[om] + return operating_mode + except (KeyError, ValueError): + return "unknown" diff --git a/custom_components/ge_appliances/api/erd/converters/primitives/__init__.py b/custom_components/ge_appliances/api/erd/converters/primitives/__init__.py new file mode 100755 index 0000000..2c14b56 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/primitives/__init__.py @@ -0,0 +1,6 @@ +from .erd_bool_converter import erd_decode_bool, erd_encode_bool, ErdBoolConverter, ErdReadOnlyBoolConverter +from .erd_bytes_converter import erd_decode_bytes, erd_encode_bytes, ErdBytesConverter, ErdReadOnlyBytesConverter +from .erd_int_converter import erd_decode_int, erd_encode_int, ErdIntConverter, ErdReadOnlyIntConverter +from .erd_signed_byte_converter import erd_decode_signed_byte, erd_encode_signed_byte, ErdSignedByteConverter, ErdReadOnlySignedByteConverter +from .erd_string_converter import erd_decode_string, erd_encode_string, ErdStringConverter, ErdReadOnlyStringConverter +from .erd_time_span_converter import erd_decode_timespan, erd_encode_timespan, ErdTimeSpanConverter, ErdReadOnlyTimeSpanConverter diff --git a/custom_components/ge_appliances/api/erd/converters/primitives/erd_bool_converter.py b/custom_components/ge_appliances/api/erd/converters/primitives/erd_bool_converter.py new file mode 100755 index 0000000..820a817 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/primitives/erd_bool_converter.py @@ -0,0 +1,25 @@ +from typing import Optional +from ..abstract import ErdReadOnlyConverter, ErdReadWriteConverter + +def erd_decode_bool(value: any) -> Optional[bool]: + """ Decodes a raw value to a bool, FF is considered None """ + if value == "FF": + return None + return bool(int(value)) +def erd_encode_bool(value: Optional[bool]) -> str: + """ Encodes a raw value to a bool, None is encoded as FF """ + if value is None: + return "FF" + return "01" if value else "00" + +class ErdBoolConverter(ErdReadWriteConverter[Optional[bool]]): + def erd_decode(self, value: str) -> Optional[bool]: + """ Decodes a raw value to a bool, FF is considered None """ + return erd_decode_bool(value) + def erd_encode(self, value: Optional[bool]) -> str: + """ Encodes a raw value to a bool, None is encoded as FF """ + return erd_encode_bool(value) +class ErdReadOnlyBoolConverter(ErdReadOnlyConverter[Optional[bool]]): + def erd_decode(self, value: str) -> Optional[bool]: + """ Decodes a raw value to a bool, FF is considered None """ + return erd_decode_bool(value) diff --git a/custom_components/ge_appliances/api/erd/converters/primitives/erd_bytes_converter.py b/custom_components/ge_appliances/api/erd/converters/primitives/erd_bytes_converter.py new file mode 100755 index 0000000..db72f64 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/primitives/erd_bytes_converter.py @@ -0,0 +1,21 @@ +from ..abstract import ErdReadWriteConverter, ErdReadOnlyConverter + +def erd_decode_bytes(value: any) -> bytes: + """Decode a raw bytes ERD value sent as a hex encoded string.""" + return bytes.fromhex(value) +def erd_encode_bytes(value: bytes) -> str: + """Encode a raw bytes ERD value.""" + return value.hex('big') + +class ErdBytesConverter(ErdReadWriteConverter[bytes]): + def erd_decode(self, value: str) -> bytes: + """Decode a raw bytes ERD value sent as a hex encoded string.""" + return erd_decode_bytes(value) + def erd_encode(self, value: bytes) -> str: + """Encode a raw bytes ERD value.""" + return erd_encode_bytes(value) + +class ErdReadOnlyBytesConverter(ErdReadOnlyConverter[bytes]): + def erd_decode(self, value: str) -> bytes: + """Decode a raw bytes ERD value sent as a hex encoded string.""" + return erd_decode_bytes(value) diff --git a/custom_components/ge_appliances/api/erd/converters/primitives/erd_int_converter.py b/custom_components/ge_appliances/api/erd/converters/primitives/erd_int_converter.py new file mode 100755 index 0000000..f5b0a5a --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/primitives/erd_int_converter.py @@ -0,0 +1,26 @@ +from .....api.erd.converters.abstract import ErdReadWriteConverter, ErdReadOnlyConverter +from .....api.erd.erd_codes import ErdCodeType + +def erd_decode_int(value: str) -> int: + """Decode an integer value sent as a hex encoded string.""" + return int(value, 16) +def erd_encode_int(value: any, length: int = 2) -> str: + """Encode an integer value as a hex string.""" + value = int(value) + return value.to_bytes(length, 'big').hex() + +class ErdIntConverter(ErdReadWriteConverter[int]): + def __init__(self, erd_code: ErdCodeType = "Unknown", length: int = 2): + super().__init__(erd_code) + self.length = length + def erd_decode(self, value: str) -> int: + """Decode an integer value sent as a hex encoded string.""" + return erd_decode_int(value) + def erd_encode(self, value) -> str: + """Encode an integer value as a hex string.""" + return erd_encode_int(value, self.length) + +class ErdReadOnlyIntConverter(ErdReadOnlyConverter[int]): + def erd_decode(self, value: str) -> int: + """Decode an integer value sent as a hex encoded string.""" + return erd_decode_int(value) diff --git a/custom_components/ge_appliances/api/erd/converters/primitives/erd_signed_byte_converter.py b/custom_components/ge_appliances/api/erd/converters/primitives/erd_signed_byte_converter.py new file mode 100755 index 0000000..56f59aa --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/primitives/erd_signed_byte_converter.py @@ -0,0 +1,37 @@ +from ..abstract import ErdReadOnlyConverter, ErdReadWriteConverter + +def erd_decode_signed_byte(value: any) -> int: + """ + Convert a hex byte to a signed int. Copied from GE's hextodec method. + """ + val = int(value, 16) + if val > 128: + return val - 256 + return val +def erd_encode_signed_byte(value: int) -> str: + """ + Convert a hex byte to a signed int. Copied from GE's hextodec method. + """ + value = int(value) + if value < 0: + value = value + 256 + return value.to_bytes(1, "big").hex() + +class ErdSignedByteConverter(ErdReadWriteConverter[int]): + def erd_decode(self, value: str) -> int: + """ + Convert a hex byte to a signed int. Copied from GE's hextodec method. + """ + return erd_decode_signed_byte(value) + def erd_encode(self, value: int) -> str: + """ + Convert a hex byte to a signed int. Copied from GE's hextodec method. + """ + return erd_encode_signed_byte(value) + +class ErdReadOnlySignedByteConverter(ErdReadOnlyConverter[int]): + def erd_decode(self, value: str) -> int: + """ + Convert a hex byte to a signed int. Copied from GE's hextodec method. + """ + return erd_decode_signed_byte(value) diff --git a/custom_components/ge_appliances/api/erd/converters/primitives/erd_string_converter.py b/custom_components/ge_appliances/api/erd/converters/primitives/erd_string_converter.py new file mode 100755 index 0000000..43ea638 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/primitives/erd_string_converter.py @@ -0,0 +1,36 @@ +from ..abstract import ErdReadWriteConverter, ErdReadOnlyConverter + +def erd_decode_string(value: str) -> str: + """ + Decode an string value sent as a hex encoded string. + """ + raw_bytes = bytes.fromhex(value) + raw_bytes = raw_bytes.rstrip(b'\x00') + + return raw_bytes.decode('ascii') +def erd_encode_string(value: str) -> str: + """ + Encode an string value to a hex encoded string. + """ + raw_bytes = value.encode('ascii') + return bytes.hex(raw_bytes) + +class ErdStringConverter(ErdReadWriteConverter[str]): + def erd_decode(self, value: str) -> str: + """ + Decode an string value sent as a hex encoded string. + """ + return erd_decode_string(value) + def erd_encode(self, value: str) -> str: + """ + Encode an string value to a hex encoded string. + """ + return erd_encode_string(value) + +class ErdReadOnlyStringConverter(ErdReadOnlyConverter[str]): + def erd_decode(self, value: str) -> str: + """ + Decode an string value sent as a hex encoded string. + """ + return erd_decode_string(value) + \ No newline at end of file diff --git a/custom_components/ge_appliances/api/erd/converters/primitives/erd_time_span_converter.py b/custom_components/ge_appliances/api/erd/converters/primitives/erd_time_span_converter.py new file mode 100755 index 0000000..f1cda7d --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/primitives/erd_time_span_converter.py @@ -0,0 +1,59 @@ +import logging + +from datetime import timedelta +from typing import Optional + +from ..abstract import ErdReadWriteConverter, ErdReadOnlyConverter +from .....api.erd.erd_codes import ErdCodeType + +_LOGGER = logging.getLogger(__name__) + +def erd_decode_timespan(value: any, unit_of_measurement: str = 'minutes') -> Optional[timedelta]: + """ + Decodes a raw integer as a time span, 65535 is treated as None. + unit_of_measurements supported: hours, minutes, seconds; default = minutes. + """ + int_value = int(value, 16) + if int_value == 65535: + _LOGGER.debug('Got timespan value of 65535. Treating as None.') + return None + return int_value +# if unit_of_measurement == 'seconds': +# return timedelta(seconds=int_value) +# if unit_of_measurement == 'hours': +# return timedelta(hours=int_value) +# return timedelta(minutes=int_value) +def erd_encode_timespan(value: Optional[timedelta], unit_of_measurement: str = 'minutes', length: int = 2) -> str: + """ + Encodes a time span as an erd integer, None is encoded as 65535. + unit_of_measurements supported: hours, minutes, seconds; default = minutes. + """ + if value is None: + int_value = 65535 + else: + if unit_of_measurement == 'seconds': + int_value = value.seconds + if unit_of_measurement == 'hours': + int_value = value.seconds // 3600 + int_value = value.seconds // 60 + return int_value.to_bytes(length, 'big').hex() + +class ErdTimeSpanConverter(ErdReadWriteConverter[Optional[timedelta]]): + def __init__(self, erd_code: ErdCodeType = "Unknown", unit_of_measurement: str = 'minutes', length: int = 2): + super().__init__(erd_code) + self.length = length + self.unit_of_measurement = unit_of_measurement + def erd_decode(self, value: str) -> Optional[timedelta]: + """ Decodes a raw integer as a time span, 65535 is treated as None. """ + return erd_decode_timespan(value, self.unit_of_measurement) + def erd_encode(self, value: Optional[timedelta]) -> str: + """ Encodes a time span as an erd integer, None is encoded as 65535. """ + return erd_encode_timespan(value, self.unit_of_measurement, self.length) + +class ErdReadOnlyTimeSpanConverter(ErdReadOnlyConverter[Optional[timedelta]]): + def __init__(self, erd_code: ErdCodeType = "Unknown", unit_of_measurement: str = 'minutes'): + super().__init__(erd_code) + self.unit_of_measurement = unit_of_measurement + def erd_decode(self, value: str) -> Optional[timedelta]: + """ Decodes a raw integer as a time span, 65535 is treated as None. """ + return erd_decode_timespan(value, self.unit_of_measurement) \ No newline at end of file diff --git a/custom_components/ge_appliances/api/erd/converters/specialized/__init__.py b/custom_components/ge_appliances/api/erd/converters/specialized/__init__.py new file mode 100755 index 0000000..17230cf --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/specialized/__init__.py @@ -0,0 +1,10 @@ +from .erd_appliance_type_converter import ErdApplianceTypeConverter +from .erd_clock_format_converter import ErdClockFormatConverter +from .erd_end_tone_converter import ErdEndToneConverter +from .erd_measurement_units_converter import ErdMeasurementUnitsConverter +from .erd_model_serial_converter import ErdModelSerialConverter +from .erd_software_version_converter import ErdSoftwareVersionConverter +from .erd_sound_level_converter import ErdSoundLevelConverter +from .erd_unit_type_converter import ErdUnitTypeConverter +from .erd_on_off_converter import ErdOnOffConverter +from .erd_locked_converter import ErdLockedConverter, ErdLockedBoolConverter \ No newline at end of file diff --git a/custom_components/ge_appliances/api/erd/converters/specialized/erd_appliance_type_converter.py b/custom_components/ge_appliances/api/erd/converters/specialized/erd_appliance_type_converter.py new file mode 100755 index 0000000..9d89654 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/specialized/erd_appliance_type_converter.py @@ -0,0 +1,28 @@ + +import logging + +from ..abstract import ErdReadOnlyConverter +from ..primitives import * + +from .....api.erd.erd_codes import ErdCodeType +from .....api.erd.values import ErdApplianceType + +_LOGGER = logging.getLogger(__name__) +class ErdApplianceTypeConverter(ErdReadOnlyConverter[ErdApplianceType]): + def __init__(self, erd_code: ErdCodeType = "Unknown"): + super().__init__(erd_code) + self._unknowns = set() + + def erd_decode(self, value) -> ErdApplianceType: + try: + return ErdApplianceType(value) + except ValueError: + if not self._have_already_seen_unknown_appliance(value): + _LOGGER.info(f"Unknown appliance type found, value = {value}") + return ErdApplianceType.UNKNOWN + + def _have_already_seen_unknown_appliance(self, value): + is_duplicate = value in self._unknowns + if not is_duplicate: + self._unknowns.add(value) + return not is_duplicate diff --git a/custom_components/ge_appliances/api/erd/converters/specialized/erd_clock_format_converter.py b/custom_components/ge_appliances/api/erd/converters/specialized/erd_clock_format_converter.py new file mode 100755 index 0000000..9e55694 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/specialized/erd_clock_format_converter.py @@ -0,0 +1,10 @@ +from ..abstract import ErdReadWriteConverter +from ..primitives import * + +from .....api.erd.values import ErdClockFormat + +class ErdClockFormatConverter(ErdReadWriteConverter[ErdClockFormat]): + def erd_decode(self, value: str) -> ErdClockFormat: + return ErdClockFormat(value) + def erd_encode(self, value: ErdClockFormat) -> str: + return value.value diff --git a/custom_components/ge_appliances/api/erd/converters/specialized/erd_end_tone_converter.py b/custom_components/ge_appliances/api/erd/converters/specialized/erd_end_tone_converter.py new file mode 100755 index 0000000..7244036 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/specialized/erd_end_tone_converter.py @@ -0,0 +1,15 @@ +from ..abstract import ErdReadWriteConverter +from ..primitives import * + +from .....api.erd.values import ErdEndTone + +class ErdEndToneConverter(ErdReadWriteConverter[ErdEndTone]): + def erd_decode(self, value: str) -> ErdEndTone: + try: + return ErdEndTone(value) + except ValueError: + return ErdEndTone.NA + def erd_encode(self, value: ErdEndTone) -> str: + if value == ErdEndTone.NA: + raise ValueError("Invalid EndTone value") + return value.value diff --git a/custom_components/ge_appliances/api/erd/converters/specialized/erd_locked_converter.py b/custom_components/ge_appliances/api/erd/converters/specialized/erd_locked_converter.py new file mode 100755 index 0000000..4015e3c --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/specialized/erd_locked_converter.py @@ -0,0 +1,32 @@ +from typing import Optional +from ..abstract import ErdReadWriteConverter +from .....api.erd.values.common import ErdInterfaceLocked +#user interface locked state + +class ErdLockedConverter(ErdReadWriteConverter[ErdInterfaceLocked]): + def erd_decode(self, value: str) -> ErdInterfaceLocked: + try: + return ErdInterfaceLocked(int(value)) + except ValueError: + return ErdInterfaceLocked.DEFAULT + + def erd_encode(self, value: ErdInterfaceLocked) -> str: + try: + value = value.value + except AttributeError: + pass + return '{:02X}'.format(value) + +class ErdLockedBoolConverter(ErdReadWriteConverter[Optional[bool]]): + def erd_decode(self, value: str) -> bool: + try: + return bool(ErdInterfaceLocked(int(value)) == ErdInterfaceLocked.LOCKED) + except ValueError: + return False + + def erd_encode(self, value: ErdInterfaceLocked) -> str: + try: + value = value.value + except AttributeError: + pass + return '{:02X}'.format(value) diff --git a/custom_components/ge_appliances/api/erd/converters/specialized/erd_measurement_units_converter.py b/custom_components/ge_appliances/api/erd/converters/specialized/erd_measurement_units_converter.py new file mode 100755 index 0000000..4014049 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/specialized/erd_measurement_units_converter.py @@ -0,0 +1,10 @@ +from ..abstract import ErdReadWriteConverter +from ..primitives import * + +from .....api.erd.values import ErdMeasurementUnits + +class ErdMeasurementUnitsConverter(ErdReadWriteConverter[ErdMeasurementUnits]): + def erd_decode(self, value: str) -> ErdMeasurementUnits: + return ErdMeasurementUnits(int(value)) + def erd_encode(self, value: ErdMeasurementUnits) -> str: + return f'{value.value:02d}' diff --git a/custom_components/ge_appliances/api/erd/converters/specialized/erd_model_serial_converter.py b/custom_components/ge_appliances/api/erd/converters/specialized/erd_model_serial_converter.py new file mode 100755 index 0000000..277fefc --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/specialized/erd_model_serial_converter.py @@ -0,0 +1,15 @@ + +from ..abstract import ErdReadOnlyConverter +from ..primitives import * + +class ErdModelSerialConverter(ErdReadOnlyConverter[str]): + def erd_decode(self, value) -> str: + """ + Decode a serial/model number string value sent as a hex encoded string. + + TODO: I think the first byte is a checksum. I need to confirm this so we can have an encoder as well. + """ + raw_bytes = bytes.fromhex(value) + raw_bytes = raw_bytes.rstrip(b'\x00') + + return raw_bytes[1:].decode('ascii') diff --git a/custom_components/ge_appliances/api/erd/converters/specialized/erd_on_off_converter.py b/custom_components/ge_appliances/api/erd/converters/specialized/erd_on_off_converter.py new file mode 100755 index 0000000..b4e55e3 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/specialized/erd_on_off_converter.py @@ -0,0 +1,13 @@ +from ..abstract import ErdReadWriteConverter +from ..primitives import * +from .....api.erd.values.common import ErdOnOff + +class ErdOnOffConverter(ErdReadWriteConverter[ErdOnOff]): + def erd_decode(self, value: str) -> bool: + try: + return ErdOnOff(value) + except ValueError: + return ErdOnOff.NA + + def erd_encode(self, value: ErdOnOff) -> str: + return value.value diff --git a/custom_components/ge_appliances/api/erd/converters/specialized/erd_software_version_converter.py b/custom_components/ge_appliances/api/erd/converters/specialized/erd_software_version_converter.py new file mode 100755 index 0000000..0ef5d21 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/specialized/erd_software_version_converter.py @@ -0,0 +1,13 @@ +from textwrap import wrap + +from ..abstract import ErdReadOnlyConverter +from ..primitives import * + +class ErdSoftwareVersionConverter(ErdReadOnlyConverter[str]): + def erd_decode(self, value) -> str: + """ + Decode a software version string. + These are sent as four bytes, encoding each part of a four-element version string. + """ + vals = wrap(value, 2) + return '.'.join(str(erd_decode_int(val)) for val in vals) diff --git a/custom_components/ge_appliances/api/erd/converters/specialized/erd_sound_level_converter.py b/custom_components/ge_appliances/api/erd/converters/specialized/erd_sound_level_converter.py new file mode 100755 index 0000000..4ad5bcc --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/specialized/erd_sound_level_converter.py @@ -0,0 +1,11 @@ +from ..abstract import ErdReadWriteConverter +from ..primitives import * + +from .....api.erd.values import ErdSoundLevel + +class ErdSoundLevelConverter(ErdReadWriteConverter[ErdSoundLevel]): + def erd_decode(self, value: str) -> ErdSoundLevel: + sound_level = erd_decode_int(value) + return ErdSoundLevel(sound_level) + def erd_encode(self, value: ErdSoundLevel) -> str: + return erd_encode_int(value.value) diff --git a/custom_components/ge_appliances/api/erd/converters/specialized/erd_unit_type_converter.py b/custom_components/ge_appliances/api/erd/converters/specialized/erd_unit_type_converter.py new file mode 100755 index 0000000..804e26a --- /dev/null +++ b/custom_components/ge_appliances/api/erd/converters/specialized/erd_unit_type_converter.py @@ -0,0 +1,17 @@ +import logging + + +import logging +from ..abstract import ErdReadOnlyConverter +from ..primitives import * +from .....api.erd.values.common import ErdUnitType + +_LOGGER = logging.getLogger(__name__) + +class ErdUnitTypeConverter(ErdReadOnlyConverter[ErdUnitType]): + def erd_decode(self, value: str) -> ErdUnitType: + try: + return ErdUnitType(erd_decode_int(value)) + except: + _LOGGER.warning("Unknown Unit Type: {value}, using default") + return ErdUnitType.UNKNOWN diff --git a/custom_components/ge_appliances/api/erd/erd_code_class.py b/custom_components/ge_appliances/api/erd/erd_code_class.py new file mode 100755 index 0000000..1a80c39 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/erd_code_class.py @@ -0,0 +1,27 @@ +import enum +from enum import auto + +@enum.unique +class ErdCodeClass(enum.IntFlag): + NONE = auto() + GENERAL = auto() + CLOCK = auto() + TIMER = auto() + COUNTER = auto() + NON_ZERO_TEMPERATURE = auto() + RAW_TEMPERATURE = auto() + DOOR = auto() + BATTERY = auto() + LOCK_CONTROL = auto() + SABBATH_CONTROL = auto() + COOLING_CONTROL = auto() + TEMPERATURE_CONTROL = auto() + PERCENTAGE = auto() + FLOW_RATE = auto() + LIQUID_VOLUME = auto() + POWER = auto() + ENERGY = auto() + FAN = auto() + LIGHT = auto() + DISPENSER_SENSOR = auto() + DISHWASHER_SENSOR = auto() diff --git a/custom_components/ge_appliances/api/erd/erd_codes.py b/custom_components/ge_appliances/api/erd/erd_codes.py new file mode 100755 index 0000000..8cf4636 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/erd_codes.py @@ -0,0 +1,77 @@ +"""ERD Codes for GE appliances""" +import enum +from typing import Union + + +@enum.unique +class ErdCode(enum.Enum): + """ + ERD codes for GE kitchen appliance properties. + These were mostly lifted from ERD.smali in the the GE SmartHQ app v1.0.3.13 + """ + + APPLIANCE_TYPE = "0x0008" + CLOCK_FORMAT = "0x0006" + CLOCK_TIME = "0x0005" + MODEL_NUMBER = "0x0001" + SABBATH_MODE = "0x0009" + SERIAL_NUMBER = "0x0002" + SOUND_LEVEL = "0x000a" + TEMPERATURE_UNIT = "0x0007" + USER_INTERFACE_LOCKED = "0x0004" + UNIT_TYPE = "0x0035" + + # Low-level-type things + WIFI_MODULE_UPDATING = "0x0099" + WIFI_MODULE_SW_VERSION = "0x0100" + WIFI_MODULE_SW_VERSION_AVAILABLE = "0x0101" + ACM_UPDATING = "0x0102" + APPLIANCE_SW_VERSION = "0x0103" + APPLIANCE_SW_VERSION_AVAILABLE = "0x0104" + APPLIANCE_UPDATING = "0x0105" + LCD_SW_VERSION = "0x0106" + LCD_SW_VERSION_AVAILABLE = "0x0107" + LCD_UPDATING = "0x0108" + + # Dishwasher Codes + DISHWASHER_CYCLE_NAME = "0x301c" + DISHWASHER_CYCLE_STATE = "0x300e" + DISHWASHER_OPERATING_MODE = "0x3001" + DISHWASHER_PODS_REMAINING_VALUE = "0x301f" + DISHWASHER_REMINDERS = "0x3003" + DISHWASHER_USER_SETTING = "0x3007" + DISHWASHER_TIME_REMAINING = "0xd004" + DISHWASHER_ERROR = "0x3008" + DISHWASHER_CYCLE_COUNTS = "0x3009" + DISHWASHER_UNKNOWN_300C = "0x300c" + DISHWASHER_UNKNOWN_300F = "0x300f" + DISHWASHER_UNKNOWN_301d = "0x301d" + DISHWASHER_UNKNOWN_3035 = "0x3035" + DISHWASHER_DOOR_STATUS = "0x3037" + DISHWASHER_UNKNOWN_3045 = "0x3045" + DISHWASHER_UNKNOWN_304E = "0x304e" + DISHWASHER_UNKNOWN_3086 = "0x3086" + DISHWASHER_UNKNOWN_3100 = "0x3100" + #added by Nick + DISHWASHER_IS_CLEAN = "0xd003" + DISHWASHER_REMOTE_START_ENABLE = '0x3087' + DISHWASHER_UNKNOWN_3200 = "0x3200" + DISHWASHER_UNKNOWN_3210 = "0x3210" + DISHWASHER_UNKNOWN_3211 = "0x3211" + DISHWASHER_UNKNOWN_3212 = "0x3212" + DISHWASHER_UNKNOWN_3213 = "0x3213" + DISHWASHER_UNKNOWN_3214 = "0x3214" + DISHWASHER_UNKNOWN_3215 = "0x3215" + DISHWASHER_UNKNOWN_3216 = "0x3216" + DISHWASHER_UNKNOWN_3217 = "0x3217" + DISHWASHER_UNKNOWN_3218 = "0x3218" + DISHWASHER_UNKNOWN_3219 = "0x3219" + DISHWASHER_DELAY_START_MINUTES = "0x321a" + DISHWASHER_CYCLE = "0x321b" + DISHWASHER_TEMPERATURE = "0x321c" + DISHWASHER_DRYING = "0x321d" + DISHWASHER_WASH_ZONE = "0x321e" + DISHWASHER_STEAM = "0x321f" + DISHWASHER_BOTTLE_JETS = "0x3220" + +ErdCodeType = Union[ErdCode, str] diff --git a/custom_components/ge_appliances/api/erd/erd_configuration.py b/custom_components/ge_appliances/api/erd/erd_configuration.py new file mode 100755 index 0000000..4b32ac1 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/erd_configuration.py @@ -0,0 +1,235 @@ +from typing import Any + +from .converters import * +from .erd_code_class import ErdCodeClass +from .erd_codes import ErdCode +from .erd_data_type import ErdDataType + + +class ErdConfigurationEntry: + def __init__( + self, + erd_code: ErdCode, + converter: ErdValueConverter, + code_class: ErdCodeClass, + data_type: ErdDataType = ErdDataType.STRING, + ) -> None: + super().__init__() + self.erd_code = erd_code + self.converter = converter + self.code_class = code_class + self.converter.erd_code = self.erd_code + self.data_type = data_type + + @property + def can_decode(self) -> bool: + return self.converter.can_decode + + @property + def can_encode(self) -> bool: + return self.converter.can_encode + + def erd_decode(self, value: str) -> Any: + return self.converter.erd_decode(value) + + def erd_encode(self, value: Any) -> str: + return self.converter.erd_encode(value) + +_configuration = [ + #Universal + ErdConfigurationEntry( + ErdCode.APPLIANCE_TYPE, + ErdApplianceTypeConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.MODEL_NUMBER, + ErdModelSerialConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.SERIAL_NUMBER, + ErdModelSerialConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.SABBATH_MODE, + ErdBoolConverter(), + ErdCodeClass.SABBATH_CONTROL, + ), + ErdConfigurationEntry( + ErdCode.USER_INTERFACE_LOCKED, + ErdLockedBoolConverter(), + ErdCodeClass.LOCK_CONTROL, + ), + ErdConfigurationEntry( + ErdCode.ACM_UPDATING, + ErdReadOnlyBoolConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.APPLIANCE_UPDATING, + ErdReadOnlyBoolConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.LCD_UPDATING, + ErdReadOnlyBoolConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.CLOCK_FORMAT, + ErdClockFormatConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.SOUND_LEVEL, + ErdSoundLevelConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.TEMPERATURE_UNIT, + ErdMeasurementUnitsConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.APPLIANCE_SW_VERSION, + ErdSoftwareVersionConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.APPLIANCE_SW_VERSION_AVAILABLE, + ErdSoftwareVersionConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.LCD_SW_VERSION, + ErdSoftwareVersionConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.LCD_SW_VERSION_AVAILABLE, + ErdSoftwareVersionConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.WIFI_MODULE_UPDATING, + ErdReadOnlyBoolConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.WIFI_MODULE_SW_VERSION, + ErdSoftwareVersionConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.WIFI_MODULE_SW_VERSION_AVAILABLE, + ErdSoftwareVersionConverter(), + ErdCodeClass.GENERAL, + ), + ErdConfigurationEntry( + ErdCode.UNIT_TYPE, + ErdUnitTypeConverter(), + ErdCodeClass.GENERAL, + ), + + # Dishwasher + ErdConfigurationEntry( + ErdCode.DISHWASHER_CYCLE_NAME, + CycleNameConverter(), + ErdCodeClass.DISHWASHER_SENSOR, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_PODS_REMAINING_VALUE, + ErdIntConverter(), + ErdCodeClass.COUNTER, + ErdDataType.INT, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_TIME_REMAINING, + ErdReadOnlyTimeSpanConverter(), + ErdCodeClass.TIMER, + ErdDataType.TIMESPAN, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_CYCLE_STATE, + ErdCycleStateConverter(), + ErdCodeClass.DISHWASHER_SENSOR, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_OPERATING_MODE, + OperatingModeConverter(), + ErdCodeClass.DISHWASHER_SENSOR, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_REMINDERS, + ErdRemindersSettingConverter(), + ErdCodeClass.DISHWASHER_SENSOR, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_DOOR_STATUS, + ErdDishwasherDoorStatusConverter(), + ErdCodeClass.DOOR, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_USER_SETTING, + ErdUserSettingConverter(), + ErdCodeClass.DISHWASHER_SENSOR, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_DELAY_START_MINUTES, + ErdTimeSpanConverter(), + ErdCodeClass.TIMER, + ErdDataType.TIMESPAN, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_CYCLE, + ErdUserCycleSettingConverter(), + ErdCodeClass.DISHWASHER_SENSOR, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_CYCLE_COUNTS, + ErdCycleCountSettingConverter(), + ErdCodeClass.DISHWASHER_SENSOR, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_TEMPERATURE, + ErdUserTemperatureSettingConverter(), + ErdCodeClass.DISHWASHER_SENSOR, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_DRYING, + ErdUserDryingSettingConverter(), + ErdCodeClass.DISHWASHER_SENSOR, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_WASH_ZONE, + ErdUserZoneSettingConverter(), + ErdCodeClass.DISHWASHER_SENSOR, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_STEAM, + ErdBoolConverter(), + ErdDataType.BOOL, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_BOTTLE_JETS, + ErdBoolConverter(), + ErdDataType.BOOL, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_IS_CLEAN, + ErdReadOnlyBoolConverter(), + ErdDataType.BOOL, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_REMOTE_START_ENABLE, + ErdReadOnlyBoolConverter(), + ErdDataType.BOOL, + ), + ErdConfigurationEntry( + ErdCode.DISHWASHER_ERROR, + ErdErrorStateConverter(), + ErdCodeClass.DISHWASHER_SENSOR, + ), +] diff --git a/custom_components/ge_appliances/api/erd/erd_data_type.py b/custom_components/ge_appliances/api/erd/erd_data_type.py new file mode 100755 index 0000000..7cdb440 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/erd_data_type.py @@ -0,0 +1,11 @@ +import enum + +@enum.unique +class ErdDataType(enum.IntFlag): + STRING = enum.auto() + BOOL = enum.auto() + INT = enum.auto() + FLOAT = enum.auto() + DATE = enum.auto() + DATETIME = enum.auto() + TIMESPAN = enum.auto() diff --git a/custom_components/ge_appliances/api/erd/erd_encoder.py b/custom_components/ge_appliances/api/erd/erd_encoder.py new file mode 100755 index 0000000..91fc380 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/erd_encoder.py @@ -0,0 +1,153 @@ +import logging +from typing import Any + +from .converters import * +from .erd_code_class import ErdCodeClass +from .erd_codes import ErdCode, ErdCodeType +from .erd_configuration import ErdConfigurationEntry, _configuration +from .erd_data_type import ErdDataType +from ..exception import GeUnsupportedOperationError + +_LOGGER = logging.getLogger(__name__) + + +class ErdEncoder: + def __init__(self) -> None: + super().__init__() + self._registry = dict((k.erd_code, k) for k in _configuration) + + def translate_code(self, erd_code: ErdCodeType) -> ErdCodeType: + """ + Try to resolve an ERD codes from string to ErdCode if possible. If an ErdCode + object is passed in, it will be returned. + :param erd_code: ErdCode or str + :return: Either an ErdCode object matching the `erd_code` string, or, if resolution fails, + the `erd_code` string itself. + """ + if isinstance(erd_code, ErdCode): + return erd_code + + try: + return ErdCode[erd_code] + except KeyError: + pass + + try: + return ErdCode(erd_code.lower()) + except ValueError: + return erd_code + + def decode_value(self, erd_code: ErdCodeType, erd_value: str) -> Any: + """ + Decode and ERD Code raw value into something useful. If the erd_code is a string that + cannot be resolved to a known ERD Code, the value will be treated as raw byte string. + Unregistered ERD Codes will be translated as a byte string. + + :param erd_code: ErdCode or str, the ERD Code the value of which we want to decode + :param erd_value: The raw ERD code value, usually a hex string without leading "0x" + :return: The decoded value. + """ + if erd_value == '': + return None + + erd_code = self.translate_code(erd_code) + + if isinstance(erd_code, str): + return erd_decode_bytes(erd_value) + + try: + return self._registry[erd_code].erd_decode(erd_value) + except KeyError: + return erd_decode_bytes(erd_value) + except ValueError: + _LOGGER.error(f'Got ValueError {erd_code} - {erd_value}') + return erd_decode_bytes(erd_value) + + def get_code_class(self, erd_code: ErdCodeType) -> ErdCodeClass: + """ + Gets the code class for a given ErdCode. Returns GENERAL if not + available. + """ + erd_code = self.translate_code(erd_code) + if isinstance(erd_code, str): + return ErdCodeClass.GENERAL + + try: + return self._registry[erd_code].code_class + except KeyError: + return ErdCodeClass.GENERAL + + + def get_data_type(self, erd_code: ErdCodeType) -> ErdDataType: + """ + Gets the data typefor a given ErdCode. Returns STRING if not + available. + """ + erd_code = self.translate_code(erd_code) + if isinstance(erd_code, str): + return ErdDataType.STRING + + try: + return self._registry[erd_code].data_type + except KeyError: + return ErdDataType.STRING + + def encode_value(self, erd_code: ErdCodeType, value: Any) -> str: + """ + Encode an ERD Code value as a hex string. + Only ERD Codes registered with self.erd_encoders will processed. Otherwise an error will be returned. + + :param erd_code: ErdCode or str, the ERD Code the value of which we want to decode + :param value: The value to re-encode + :return: The encoded value as a hex string + """ + if value is None: + return '' + + erd_code = self.translate_code(erd_code) + + try: + return self._registry[erd_code].erd_encode(value) + except KeyError: + _LOGGER.error(f'Attempt to encode unregistered ERD code {erd_code}') + raise + + def can_decode(self, erd_code: ErdCodeType) -> bool: + """ + Indicates whether an ERD Code can be decoded. If the code + is not registered, defaults to true + """ + + erd_code = self.translate_code(erd_code) + + try: + return self._registry[erd_code].can_decode + except KeyError: + return True + + def can_encode(self, erd_code: ErdCodeType) -> bool: + """ + Indicates whether an ERD Code can be encoded. If the code + is not registered, defaults to false + """ + + erd_code = self.translate_code(erd_code) + + try: + return self._registry[erd_code].can_encode + except KeyError: + return False + + def can_boolify(self, erd_code: ErdCodeType) -> bool: + """ + Indicates whether an ERD Code can boolified. If the code + is not registered, defaults to false + """ + + erd_code = self.translate_code(erd_code) + + try: + return self._registry[erd_code].can_boolify + except KeyError: + return False + \ No newline at end of file diff --git a/custom_components/ge_appliances/api/erd/values/__init__.py b/custom_components/ge_appliances/api/erd/values/__init__.py new file mode 100755 index 0000000..08379ec --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/__init__.py @@ -0,0 +1,4 @@ +"""GE ERD Values""" + +from .common import * +from .dishwasher import * diff --git a/custom_components/ge_appliances/api/erd/values/common/__init__.py b/custom_components/ge_appliances/api/erd/values/common/__init__.py new file mode 100755 index 0000000..82f4f2f --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/common/__init__.py @@ -0,0 +1,9 @@ +from .erd_appliance_type import ErdApplianceType +from .erd_clock_format import ErdClockFormat +from .erd_end_tone import ErdEndTone +from .erd_measurement_units import ErdMeasurementUnits +from .erd_on_off import ErdOnOff +from .erd_present import ErdPresent +from .erd_sound_level import ErdSoundLevel +from .erd_unit_type import ErdUnitType +from .erd_locked import ErdInterfaceLocked diff --git a/custom_components/ge_appliances/api/erd/values/common/erd_appliance_type.py b/custom_components/ge_appliances/api/erd/values/common/erd_appliance_type.py new file mode 100755 index 0000000..0c652b5 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/common/erd_appliance_type.py @@ -0,0 +1,36 @@ +import enum + +@enum.unique +class ErdApplianceType(enum.Enum): + UNKNOWN = "FF" + WATER_HEATER = "00" + DRYER = "01" + WASHER = "02" + FRIDGE = "03" + MICROWAVE = "04" + ADVANTIUM = "05" + DISHWASHER = "06" + OVEN = "07" + ELECTRIC_RANGE = "08" + GAS_RANGE = "09" + AIR_CONDITIONER = "0A" + ELECTRIC_COOKTOP = "0B" + COOKTOP = "11" + PIZZA_OVEN = "0C" + GAS_COOKTOP = "0D" + SPLIT_AIR_CONDITIONER = "0E" + HOOD = "0F" + POE_WATER_FILTER = "10" + WATER_SOFTENER = "15" + PORTABLE_AIR_CONDITIONER = "16" + COMBINATION_WASHER_DRYER = "17" + ZONELINE = "14" + DELIVERY_BOX = "12" + CAFE_COFFEE_MAKER = "1A" + OPAL_ICE_MAKER = "1B" + DEHUMIDIFIER = "1D" + BUILT_IN_AIR_CONDITIONER = "1F" + WINE_CHILLER = "18" + BEVERAGE_CENTER = "19" + DUAL_DISHWASHER = "20" + ESPRESSO_MAKER = "21" \ No newline at end of file diff --git a/custom_components/ge_appliances/api/erd/values/common/erd_clock_format.py b/custom_components/ge_appliances/api/erd/values/common/erd_clock_format.py new file mode 100755 index 0000000..1edb8c3 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/common/erd_clock_format.py @@ -0,0 +1,7 @@ +import enum + +@enum.unique +class ErdClockFormat(enum.Enum): + TWELVE_HOUR = "00" + TWENTY_FOUR_HOUR = "01" + NO_DISPLAY = "02" diff --git a/custom_components/ge_appliances/api/erd/values/common/erd_end_tone.py b/custom_components/ge_appliances/api/erd/values/common/erd_end_tone.py new file mode 100755 index 0000000..9af16ca --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/common/erd_end_tone.py @@ -0,0 +1,7 @@ +import enum + +@enum.unique +class ErdEndTone(enum.Enum): + BEEP = "00" + REPEATED_BEEP = "01" + NA = "FF" diff --git a/custom_components/ge_appliances/api/erd/values/common/erd_locked.py b/custom_components/ge_appliances/api/erd/values/common/erd_locked.py new file mode 100755 index 0000000..c6474c8 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/common/erd_locked.py @@ -0,0 +1,7 @@ +import enum + +@enum.unique +class ErdInterfaceLocked(enum.Enum): + DEFAULT = 0 + LOCKED = 1 + UNLOCKED = 2 diff --git a/custom_components/ge_appliances/api/erd/values/common/erd_measurement_units.py b/custom_components/ge_appliances/api/erd/values/common/erd_measurement_units.py new file mode 100755 index 0000000..13062fb --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/common/erd_measurement_units.py @@ -0,0 +1,6 @@ +import enum + +@enum.unique +class ErdMeasurementUnits(enum.Enum): + IMPERIAL = 0 + METRIC = 1 diff --git a/custom_components/ge_appliances/api/erd/values/common/erd_on_off.py b/custom_components/ge_appliances/api/erd/values/common/erd_on_off.py new file mode 100755 index 0000000..e357930 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/common/erd_on_off.py @@ -0,0 +1,11 @@ +import enum +from typing import Optional + +@enum.unique +class ErdOnOff(enum.Enum): + ON = "01" + OFF = "00" + NA = "FF" + + def boolify(self) -> Optional[bool]: + return self == ErdOnOff.ON \ No newline at end of file diff --git a/custom_components/ge_appliances/api/erd/values/common/erd_present.py b/custom_components/ge_appliances/api/erd/values/common/erd_present.py new file mode 100755 index 0000000..f1329ac --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/common/erd_present.py @@ -0,0 +1,7 @@ +import enum + +@enum.unique +class ErdPresent(enum.Enum): + PRESENT = "01" + NOT_PRESENT = "00" + NA = "FF" \ No newline at end of file diff --git a/custom_components/ge_appliances/api/erd/values/common/erd_sound_level.py b/custom_components/ge_appliances/api/erd/values/common/erd_sound_level.py new file mode 100755 index 0000000..051be54 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/common/erd_sound_level.py @@ -0,0 +1,8 @@ +import enum + +@enum.unique +class ErdSoundLevel(enum.Enum): + OFF = 0 + LOW = 1 + STANDARD = 2 + HIGH = 3 \ No newline at end of file diff --git a/custom_components/ge_appliances/api/erd/values/common/erd_unit_type.py b/custom_components/ge_appliances/api/erd/values/common/erd_unit_type.py new file mode 100755 index 0000000..1043257 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/common/erd_unit_type.py @@ -0,0 +1,15 @@ +import enum + +@enum.unique +class ErdUnitType(enum.Enum): + UNKNOWN = 0 + TYPE_120V_CAFE = 1 + TYPE_120V_MONOGRAM = 2 + TYPE_UNKNOWN03 = 3 + TYPE_UNKNOWN04 = 4 + TYPE_UNKNOWN05 = 5 + TYPE_UNKNOWN06 = 6 + TYPE_UNKNOWN07 = 7 + TYPE_UNKNOWN08 = 8 + TYPE_240V_MONOGRAM = 9 + TYPE_240V_CAFE = 10 diff --git a/custom_components/ge_appliances/api/erd/values/dishwasher/__init__.py b/custom_components/ge_appliances/api/erd/values/dishwasher/__init__.py new file mode 100755 index 0000000..79b626e --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/dishwasher/__init__.py @@ -0,0 +1,16 @@ +from .erd_cycle_state import ErdCycleStateRaw, ErdCycleState +from .erd_operating_mode import ErdOperatingMode +from .erd_reminders import ErdReminders +from .operating_mode import OperatingMode +from .error_state import ErdErrorState +from .cycle_state_mapping import CYCLE_STATE_RAW_MAP +from .operating_mode_mapping import OPERATING_MODE_MAP +from .erd_dishwasher_door_status import ErdDishwasherDoorStatus +from .cycle_count import ErdCycleCount +from .erd_user_setting import ( + ErdUserSetting, + UserCycleSetting, + UserDryOptionSetting, + UserWashTempSetting, + UserWashZoneSetting, +) \ No newline at end of file diff --git a/custom_components/ge_appliances/api/erd/values/dishwasher/cycle_count.py b/custom_components/ge_appliances/api/erd/values/dishwasher/cycle_count.py new file mode 100755 index 0000000..824d0a9 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/dishwasher/cycle_count.py @@ -0,0 +1,7 @@ +from typing import NamedTuple, Optional + +class ErdCycleCount (NamedTuple): + started: int = 0 + completed: int = 0 + reset: int = 0 + raw_value: Optional[str] = None diff --git a/custom_components/ge_appliances/api/erd/values/dishwasher/cycle_state_mapping.py b/custom_components/ge_appliances/api/erd/values/dishwasher/cycle_state_mapping.py new file mode 100755 index 0000000..0af3a1d --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/dishwasher/cycle_state_mapping.py @@ -0,0 +1,25 @@ +from .erd_cycle_state import ErdCycleState, ErdCycleStateRaw + +CYCLE_STATE_RAW_MAP = { + ErdCycleStateRaw.PREWASH: ErdCycleState.PRE_WASH, + ErdCycleStateRaw.PREWASH1: ErdCycleState.PRE_WASH, + ErdCycleStateRaw.AUTO_HOT_START1: ErdCycleState.PRE_WASH, + ErdCycleStateRaw.AUTO_HOT_START2: ErdCycleState.PRE_WASH, + ErdCycleStateRaw.AUTO_HOT_START3: ErdCycleState.PRE_WASH, + ErdCycleStateRaw.END_PREWASH1: ErdCycleState.PRE_WASH, + ErdCycleStateRaw.SENSING: ErdCycleState.SENSING, + ErdCycleStateRaw.MAIN_WASH: ErdCycleState.MAIN_WASH, + ErdCycleStateRaw.DIVERTER_CAL: ErdCycleState.MAIN_WASH, + ErdCycleStateRaw.DRYING: ErdCycleState.DRYING, + ErdCycleStateRaw.SANITIZING: ErdCycleState.SANITIZING, + ErdCycleStateRaw.RINSING: ErdCycleState.RINSING, + ErdCycleStateRaw.TURBIDITY_CAL: ErdCycleState.RINSING, + ErdCycleStateRaw.FINAL_RINSE: ErdCycleState.RINSING, + ErdCycleStateRaw.FINAL_RINSE_FILL: ErdCycleState.RINSING, + ErdCycleStateRaw.PAUSE: ErdCycleState.PAUSE, + ErdCycleStateRaw.STATE_17: ErdCycleState.INACTIVE, + ErdCycleStateRaw.STATE_18: ErdCycleStateRaw.STATE_18, + ErdCycleStateRaw.CYCLE_INACTIVE: ErdCycleState.INACTIVE, + ErdCycleStateRaw.MAX: ErdCycleStateRaw.MAX, + ErdCycleStateRaw.INVALID: ErdCycleStateRaw.INVALID, +} diff --git a/custom_components/ge_appliances/api/erd/values/dishwasher/erd_cycle_state.py b/custom_components/ge_appliances/api/erd/values/dishwasher/erd_cycle_state.py new file mode 100755 index 0000000..34de973 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/dishwasher/erd_cycle_state.py @@ -0,0 +1,44 @@ +import enum + +@enum.unique +class ErdCycleStateRaw(enum.Enum): + NO_CHANGE = 0 + PREWASH = 1 + SENSING = 2 + MAIN_WASH = 3 + DRYING = 4 + SANITIZING = 5 + TURBIDITY_CAL = 6 + DIVERTER_CAL = 7 + PAUSE = 8 + RINSING = 9 + PREWASH1 = 10 + FINAL_RINSE = 11 + END_PREWASH1 = 12 + AUTO_HOT_START1 = 13 + AUTO_HOT_START2 = 14 + AUTO_HOT_START3 = 15 + FINAL_RINSE_FILL = 16 + STATE_17 = 17 + STATE_18 = 18 + CYCLE_INACTIVE = 26 + MAX = 27 + INVALID = 255 + +@enum.unique +class ErdCycleState(enum.Enum): + NA = -1 + PRE_WASH = 1 + SENSING = 2 + MAIN_WASH = 3 + DRYING = 4 + SANITIZING = 5 + RINSING = 6 + PAUSE = 7 + INACTIVE = 8 + + def stringify(self, **kwargs): +# if self == ErdCycleState.NA: +# return "N/A" +# return self.name.replace("_"," ").title() + return "_".join(self.name.split()).lower() diff --git a/custom_components/ge_appliances/api/erd/values/dishwasher/erd_dishwasher_door_status.py b/custom_components/ge_appliances/api/erd/values/dishwasher/erd_dishwasher_door_status.py new file mode 100755 index 0000000..1551ee1 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/dishwasher/erd_dishwasher_door_status.py @@ -0,0 +1,20 @@ +import enum +from logging import setLoggerClass +from typing import Optional + +@enum.unique +class ErdDishwasherDoorStatus(enum.Enum): + """Dishwasher door status""" + CLOSED = 1 + OPEN = 0 + NA = 255 + + def boolify(self) -> Optional[bool]: + if self == ErdDishwasherDoorStatus.NA: + return None + return self == ErdDishwasherDoorStatus.OPEN + + def stringify(self, **kwargs) -> Optional[str]: + if self == ErdDishwasherDoorStatus.NA: + return None + return self.name.title() diff --git a/custom_components/ge_appliances/api/erd/values/dishwasher/erd_operating_mode.py b/custom_components/ge_appliances/api/erd/values/dishwasher/erd_operating_mode.py new file mode 100755 index 0000000..c0eeb4c --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/dishwasher/erd_operating_mode.py @@ -0,0 +1,19 @@ +import enum + +@enum.unique +class ErdOperatingMode(enum.Enum): + LOW_POWER = 0 + POWER_UP = 1 + STANDBY = 2 + DELAY_START = 3 + PAUSE = 4 + CYCLE_ACTIVE = 5 + EOC = 6 + DOWNLOAD_MODE = 7 + SENSOR_CHECK_MODE = 8 + LOAD_ACTIVATION_MODE = 9 + MC_ONLY_MODE = 17 + WARNING_MODE = 18 + CONTROL_LOCKED = 19 + CSM_TRIPPED = 20 + INVALID = 255 diff --git a/custom_components/ge_appliances/api/erd/values/dishwasher/erd_reminders.py b/custom_components/ge_appliances/api/erd/values/dishwasher/erd_reminders.py new file mode 100755 index 0000000..0993d68 --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/dishwasher/erd_reminders.py @@ -0,0 +1,8 @@ +from typing import NamedTuple, Optional + +class ErdReminders (NamedTuple): + clean_filter: bool = False + add_rinse_aid: bool = False + sanitized: bool = False + raw_value: Optional[str] = None + diff --git a/custom_components/ge_appliances/api/erd/values/dishwasher/erd_user_setting.py b/custom_components/ge_appliances/api/erd/values/dishwasher/erd_user_setting.py new file mode 100755 index 0000000..0c6228c --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/dishwasher/erd_user_setting.py @@ -0,0 +1,53 @@ +import enum +from typing import NamedTuple, Optional + +@enum.unique +class UserSetting(enum.Enum): + DISABLE = 0 + ENABLE = 1 + INVALID = 255 + +@enum.unique +class UserCycleSetting(enum.Enum): + AUTO = 0 + INTENSE = 1 + NORMAL = 2 + DELICATE = 3 + THIRTY_MIN = 4 + ECO = 5 + RINSE = 6, + FP_UNKNOWN = 7 + +@enum.unique +class UserWashTempSetting(enum.Enum): + NORMAL = 0 + BOOST = 1 + SANITIZE = 2 + BOOST_AND_SANITIZE = 3 + +@enum.unique +class UserDryOptionSetting(enum.Enum): + DISABLED = 0 + POWER_DRY = 1 + MAX_DRY = 2 + +@enum.unique +class UserWashZoneSetting(enum.Enum): + BOTH = 0 + LOWER = 1 + UPPER = 2 + +class ErdUserSetting (NamedTuple): + mute: UserSetting = UserSetting.DISABLE + demo_mode: UserSetting = UserSetting.DISABLE + lock_control: UserSetting = UserSetting.DISABLE + sabbath: UserSetting = UserSetting.DISABLE + cycle_mode: UserCycleSetting = UserCycleSetting.AUTO + presoak: UserSetting = UserSetting.DISABLE + bottle_jet: UserSetting = UserSetting.DISABLE + wash_temp: UserWashTempSetting = UserWashTempSetting.NORMAL + rinse_aid: UserSetting = UserSetting.DISABLE + dry_option: UserDryOptionSetting = UserDryOptionSetting.DISABLED + wash_zone: UserWashZoneSetting = UserWashZoneSetting.BOTH + delay_hours: int = 0 + raw_value: Optional[str] = None diff --git a/custom_components/ge_appliances/api/erd/values/dishwasher/error_state.py b/custom_components/ge_appliances/api/erd/values/dishwasher/error_state.py new file mode 100755 index 0000000..05e549f --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/dishwasher/error_state.py @@ -0,0 +1,6 @@ +from typing import NamedTuple, Optional + +class ErdErrorState (NamedTuple): + id: int = 0 + active: bool = False + raw_value: Optional[str] = None diff --git a/custom_components/ge_appliances/api/erd/values/dishwasher/operating_mode.py b/custom_components/ge_appliances/api/erd/values/dishwasher/operating_mode.py new file mode 100755 index 0000000..dc797cc --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/dishwasher/operating_mode.py @@ -0,0 +1,19 @@ +import enum + +@enum.unique +class OperatingMode(enum.Enum): + STATUS_DASH = "status_dash" + STATUS_CYCLE_ACTIVE = "status_active" + STATUS_CYCLE_COMPLETE = "status_complete" + STATUS_OFF = "status_off" + STATUS_DELAY = "status_delay" + STATUS_PAUSED = "status_paused" + CONTROL_LOCKED = "control_locked" + STATUS_STANDBY = "status_standby" + STATUS_INVALID = "status_invalid" + + def stringify(self, **kwargs): + if self == OperatingMode.STATUS_DASH: + return "off" +# return self.name.replace("STATUS_","").replace("_"," ").title() + return self.name.replace("STATUS_","").lower() diff --git a/custom_components/ge_appliances/api/erd/values/dishwasher/operating_mode_mapping.py b/custom_components/ge_appliances/api/erd/values/dishwasher/operating_mode_mapping.py new file mode 100755 index 0000000..e70a6ca --- /dev/null +++ b/custom_components/ge_appliances/api/erd/values/dishwasher/operating_mode_mapping.py @@ -0,0 +1,20 @@ +from .erd_operating_mode import ErdOperatingMode +from .operating_mode import OperatingMode + +OPERATING_MODE_MAP = { + ErdOperatingMode.DOWNLOAD_MODE: OperatingMode.STATUS_OFF, + ErdOperatingMode.SENSOR_CHECK_MODE: OperatingMode.STATUS_OFF, + ErdOperatingMode.LOAD_ACTIVATION_MODE: OperatingMode.STATUS_OFF, + ErdOperatingMode.MC_ONLY_MODE: OperatingMode.STATUS_OFF, + ErdOperatingMode.WARNING_MODE: OperatingMode.STATUS_OFF, + ErdOperatingMode.CSM_TRIPPED: OperatingMode.STATUS_OFF, + ErdOperatingMode.INVALID: OperatingMode.STATUS_DASH, + ErdOperatingMode.STANDBY: OperatingMode.STATUS_DASH, + ErdOperatingMode.LOW_POWER: OperatingMode.STATUS_DASH, + ErdOperatingMode.POWER_UP: OperatingMode.STATUS_DASH, + ErdOperatingMode.PAUSE: OperatingMode.STATUS_PAUSED, + ErdOperatingMode.DELAY_START: OperatingMode.STATUS_DELAY, + ErdOperatingMode.CYCLE_ACTIVE: OperatingMode.STATUS_CYCLE_ACTIVE, + ErdOperatingMode.EOC: OperatingMode.STATUS_CYCLE_COMPLETE, + ErdOperatingMode.CONTROL_LOCKED: OperatingMode.CONTROL_LOCKED +} diff --git a/custom_components/ge_appliances/api/exception/__init__.py b/custom_components/ge_appliances/api/exception/__init__.py new file mode 100755 index 0000000..347b915 --- /dev/null +++ b/custom_components/ge_appliances/api/exception/__init__.py @@ -0,0 +1,12 @@ +""" GE Home Exceptions """ + +from .ge_exception import GeException +from .ge_unsupported_operation_error import GeUnsupportedOperationError +from .ge_set_erd_not_allowed_error import GeSetErdNotAllowedError +from .ge_auth_failed_error import GeAuthFailedError +from .ge_client_disconnected_error import GeClientDisconnectedError +from .ge_not_authenticated_error import GeNotAuthenticatedError +from .ge_general_server_error import GeGeneralServerError +from .ge_request_error import GeRequestError +from .ge_needs_reauthentication_error import GeNeedsReauthenticationError +from .ge_duplicate_appliance_error import GeDuplicateApplianceError \ No newline at end of file diff --git a/custom_components/ge_appliances/api/exception/ge_auth_failed_error.py b/custom_components/ge_appliances/api/exception/ge_auth_failed_error.py new file mode 100755 index 0000000..f5b3aa3 --- /dev/null +++ b/custom_components/ge_appliances/api/exception/ge_auth_failed_error.py @@ -0,0 +1,5 @@ +from .ge_exception import GeException + +class GeAuthFailedError(GeException): + """Error raised when the client failed to authenticate""" + pass diff --git a/custom_components/ge_appliances/api/exception/ge_client_disconnected_error.py b/custom_components/ge_appliances/api/exception/ge_client_disconnected_error.py new file mode 100755 index 0000000..95982d5 --- /dev/null +++ b/custom_components/ge_appliances/api/exception/ge_client_disconnected_error.py @@ -0,0 +1,5 @@ +from .ge_exception import GeException + +class GeClientDisconnectedError(GeException): + """Error raised when the client is disconnected""" + pass diff --git a/custom_components/ge_appliances/api/exception/ge_duplicate_appliance_error.py b/custom_components/ge_appliances/api/exception/ge_duplicate_appliance_error.py new file mode 100755 index 0000000..990f64d --- /dev/null +++ b/custom_components/ge_appliances/api/exception/ge_duplicate_appliance_error.py @@ -0,0 +1,5 @@ +from .ge_exception import GeException + +class GeDuplicateApplianceError(GeException): + """Error raised when a duplicate appliance is attempted to be added""" + pass diff --git a/custom_components/ge_appliances/api/exception/ge_exception.py b/custom_components/ge_appliances/api/exception/ge_exception.py new file mode 100755 index 0000000..2edbb57 --- /dev/null +++ b/custom_components/ge_appliances/api/exception/ge_exception.py @@ -0,0 +1,3 @@ +class GeException(Exception): + """ Base class for all other custom exceptions """ + pass \ No newline at end of file diff --git a/custom_components/ge_appliances/api/exception/ge_general_server_error.py b/custom_components/ge_appliances/api/exception/ge_general_server_error.py new file mode 100755 index 0000000..b60d36c --- /dev/null +++ b/custom_components/ge_appliances/api/exception/ge_general_server_error.py @@ -0,0 +1,5 @@ +from .ge_exception import GeException + +class GeGeneralServerError(GeException): + """Error raised when there is a server error (not 4xx http code)""" + pass diff --git a/custom_components/ge_appliances/api/exception/ge_needs_reauthentication_error.py b/custom_components/ge_appliances/api/exception/ge_needs_reauthentication_error.py new file mode 100755 index 0000000..3f91d02 --- /dev/null +++ b/custom_components/ge_appliances/api/exception/ge_needs_reauthentication_error.py @@ -0,0 +1,5 @@ +from .ge_exception import GeException + +class GeNeedsReauthenticationError(GeException): + """Error raised when the reauthentication is needed""" + pass \ No newline at end of file diff --git a/custom_components/ge_appliances/api/exception/ge_not_authenticated_error.py b/custom_components/ge_appliances/api/exception/ge_not_authenticated_error.py new file mode 100755 index 0000000..87b6b95 --- /dev/null +++ b/custom_components/ge_appliances/api/exception/ge_not_authenticated_error.py @@ -0,0 +1,5 @@ +from .ge_exception import GeException + +class GeNotAuthenticatedError(GeException): + """Error raised when the user is not Not Authenticated""" + pass \ No newline at end of file diff --git a/custom_components/ge_appliances/api/exception/ge_request_error.py b/custom_components/ge_appliances/api/exception/ge_request_error.py new file mode 100755 index 0000000..7671655 --- /dev/null +++ b/custom_components/ge_appliances/api/exception/ge_request_error.py @@ -0,0 +1,13 @@ +from .ge_exception import GeException + +class GeRequestError(GeException): + """ Exception raised when there is an error processing a message """ + def __init__(self, message: str, code, reason, *args: object) -> None: + super().__init__(*args) + self.code = code + self.reason = reason + self.message = message + + def __str__(self) -> str: + return f"There was an error while processing a message: Code={self.code}, Reason={self.reason}, Message={self.message}" + diff --git a/custom_components/ge_appliances/api/exception/ge_set_erd_not_allowed_error.py b/custom_components/ge_appliances/api/exception/ge_set_erd_not_allowed_error.py new file mode 100755 index 0000000..73a36b9 --- /dev/null +++ b/custom_components/ge_appliances/api/exception/ge_set_erd_not_allowed_error.py @@ -0,0 +1,10 @@ +from .ge_unsupported_operation_error import GeUnsupportedOperationError + +class GeSetErdNotAllowedError(GeUnsupportedOperationError): + """ Exception raised when ERD is not allowed to be set (readonly) """ + def __init__(self, erd_code: str, message="ERD cannot be set, it is readonly"): + self.erd_code = erd_code + self.message = message + + def __str__(self): + return f'{self.erd_code} -> {self.message}' diff --git a/custom_components/ge_appliances/api/exception/ge_unsupported_operation_error.py b/custom_components/ge_appliances/api/exception/ge_unsupported_operation_error.py new file mode 100755 index 0000000..9fb32c4 --- /dev/null +++ b/custom_components/ge_appliances/api/exception/ge_unsupported_operation_error.py @@ -0,0 +1,5 @@ +from .ge_exception import GeException + +class GeUnsupportedOperationError(GeException): + """ Exception raised when the operation is not supported """ + pass diff --git a/custom_components/ge_appliances/api/gather_data.py b/custom_components/ge_appliances/api/gather_data.py new file mode 100755 index 0000000..15259fd --- /dev/null +++ b/custom_components/ge_appliances/api/gather_data.py @@ -0,0 +1,56 @@ +""" +Gets the appliance data, continues to run until cancelled so that values can be observed. +""" + +import aiohttp +import asyncio +import logging +from datetime import timedelta +from typing import Any, Dict, Tuple + +from ..api import ( + EVENT_ADD_APPLIANCE, + EVENT_APPLIANCE_STATE_CHANGE, + EVENT_APPLIANCE_INITIAL_UPDATE, + ErdCodeType, + GeAppliance, + GeWebsocketClient +) + +_LOGGER = logging.getLogger(__name__) + +async def log_state_change(data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]): + """Log changes in appliance state""" + appliance, state_changes = data + updated_keys = ', '.join([str(s) for s in state_changes]) + _LOGGER.debug(f'Appliance state change detected in {appliance}. Updated keys: {updated_keys}') + +async def detect_appliance_type(appliance: GeAppliance): + """ + Detect the appliance type. + This should only be triggered once since the appliance type should never change. + + Also, let's turn on ovens! + """ + _LOGGER.debug(f'Appliance state change detected in {appliance}') + +async def do_periodic_update(appliance: GeAppliance): + """Request a full state update every minute forever""" + _LOGGER.debug(f'Registering update callback for {appliance:s}') + while True: + await asyncio.sleep(60 * 1) + _LOGGER.debug(f'Requesting update for {appliance:s}') + await appliance.async_request_update() + +def gather_appliance_data(username: str, password: str, region: str): + logging.basicConfig(level=logging.DEBUG, format='%(asctime)-15s %(levelname)-8s %(message)s') + + loop = asyncio.get_event_loop() + client = GeWebsocketClient(username, password, region, loop) + client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, detect_appliance_type) + client.add_event_handler(EVENT_APPLIANCE_STATE_CHANGE, log_state_change) + client.add_event_handler(EVENT_ADD_APPLIANCE, do_periodic_update) + + session = aiohttp.ClientSession() + asyncio.ensure_future(client.async_get_credentials_and_run(session), loop=loop) + loop.run_until_complete(asyncio.sleep(7400)) diff --git a/custom_components/ge_appliances/api/ge_appliance.py b/custom_components/ge_appliances/api/ge_appliance.py new file mode 100755 index 0000000..374f366 --- /dev/null +++ b/custom_components/ge_appliances/api/ge_appliance.py @@ -0,0 +1,248 @@ +"""Data model for GE kitchen appliances""" + +import enum +import logging +from weakref import WeakValueDictionary +from typing import Any, List, Dict, Optional, Set, TYPE_CHECKING, Union +from slixmpp import JID + +from .erd import ErdCode, ErdCodeType, ErdCodeClass, ErdApplianceType, ErdEncoder, ErdDataType +from .exception import * + +if TYPE_CHECKING: + from .clients import GeBaseClient + +try: + import ujson as json +except ImportError: + import json + +_LOGGER = logging.getLogger(__name__) + + +class GeAppliance: + """Base class shared by all appliances""" + + # Registry of initialized appliances + _appliance_cache = WeakValueDictionary() + + def __new__(cls, mac_addr: Union[str, JID], client: "GeBaseClient", *args, **kwargs): + if isinstance(mac_addr, JID): + mac_addr = str(mac_addr.user).split('_')[0] + try: + obj = cls._appliance_cache[mac_addr] # type: "GeAppliance" + except KeyError: + obj = super(GeAppliance, cls).__new__(cls) + obj.__init__(mac_addr, client) + cls._appliance_cache[obj.mac_addr] = obj + return obj + else: + if client.client_priority >= obj.client.client_priority: + obj.client = client + return obj + + def __init__(self, mac_addr: Union[str, JID], client: "GeBaseClient"): + if isinstance(mac_addr, JID): + mac_addr = str(mac_addr.user).split('_')[0] + self._available = False + self._mac_addr = mac_addr.upper() + self._message_id = 0 + self._property_cache = {} # type: Dict[ErdCodeType, Any] + self._features = [] + self.client = client + self.initialized = False + self._encoder = ErdEncoder() + + @property + def mac_addr(self) -> str: + return self._mac_addr.upper() + + @property + def known_properties(self) -> Set[ErdCodeType]: + return set(self._property_cache) + + @property + def features(self) -> List[str]: + return list(self._features) + + @features.setter + def features(self, value: List[str]): + """Sets the features for this appliance""" + self._features = list(value) + + async def get_messages(self): + await self.client.async_request_message(self) + + async def async_request_update(self): + """Request the appliance send a full state update""" + await self.client.async_request_update(self) + + async def async_request_features(self): + """Request the appliance send a full state update""" + await self.client.async_request_update(self) + + def set_available(self): + _LOGGER.debug(f'{self.mac_addr} marked available') + self._available = True + + def set_unavailable(self): + _LOGGER.debug(f'{self.mac_addr} marked unavailable') + self._available = False + + @property + def available(self) -> bool: + return self._available and self.initialized + + @property + def appliance_type(self) -> Optional[ErdApplianceType]: + return self._property_cache.get(ErdCode.APPLIANCE_TYPE) + + def translate_erd_code(self, erd_code: ErdCodeType) -> ErdCodeType: + """ + Translates a code to it's native value or a string if it is not known. + """ + return self._encoder.translate_code(erd_code) + + def decode_erd_value(self, erd_code: ErdCodeType, erd_value: str) -> Any: + """ + Decode and ERD Code raw value into something useful. If the erd_code is a string that + cannot be resolved to a known ERD Code, the value will be treated as raw byte string. + Unregistered ERD Codes will be translated as ints. + + :param erd_code: ErdCode or str, the ERD Code the value of which we want to decode + :param erd_value: The raw ERD code value, usually a hex string without leading "0x" + :return: The decoded value. + """ + return self._encoder.decode_value(erd_code, erd_value) + + def encode_erd_value(self, erd_code: ErdCodeType, value: Any) -> str: + """ + Encode an ERD Code value as a hex string. + Only ERD Codes registered with self.erd_encoders will processed. Otherwise an error will be returned. + + :param erd_code: ErdCode or str, the ERD Code the value of which we want to decode + :param value: The value to re-encode + :return: The encoded value as a hex string + """ + return self._encoder.encode_value(erd_code, value) + + def get_erd_value(self, erd_code: ErdCodeType) -> Any: + """ + Get the value of a property represented by an ERD Code + :param erd_code: ErdCode or str, The ERD code for the property to get + :return: The current cached value of that ERD code + """ + erd_code = self._encoder.translate_code(erd_code) + return self._property_cache[erd_code] + + def get_erd_code_class(self, erd_code: ErdCodeType) -> ErdCodeClass: + """ + Get the classification for a given ErdCode + """ + return self._encoder.get_code_class(erd_code) + + def get_erd_code_data_type(self, erd_code: ErdCodeType) -> ErdDataType: + """ + Get the data type for a given ErdCode + """ + return self._encoder.get_data_type(erd_code) + + async def async_set_erd_value(self, erd_code: ErdCodeType, value: Any): + """ + Send a new erd value to the appliance. + :param erd_code: The ERD code to update + :param value: The new value to set + """ + erd_value = self.encode_erd_value(erd_code, value) + await self.client.async_set_erd_value(self, erd_code, erd_value) + + def update_erd_value( + self, erd_code: ErdCodeType, erd_value: str) -> bool: + """ + Setter for ERD code values. + + :param erd_code: ERD code to update + :param erd_value: The new value to set, as returned by the appliance (usually a hex string) + :return: Boolean, True if the state changed, False if no value changed + """ + erd_code = self._encoder.translate_code(erd_code) + value = self.decode_erd_value(erd_code, erd_value) + + old_value = self._property_cache.get(erd_code) + + try: + state_changed = ((old_value is None) != (value is None)) or (old_value != value) + except ValueError: + _LOGGER.info('Unable to compare new and prior states.') + state_changed = False + + if state_changed: + _LOGGER.debug(f'Setting {erd_code} to {value}') + self._property_cache[erd_code] = value + + return state_changed + + def update_erd_values(self, erd_values: Dict[ErdCodeType, str]) -> Dict[ErdCodeType, Any]: + """ + Set one or more ERD codes value at once + + :param erd_values: Dictionary of erd codes and their new values as raw hex strings + :return: dictionary of new states + """ + state_changes = { + self._encoder.translate_code(k): self.decode_erd_value(k, v) + for k, v in erd_values.items() + if self.update_erd_value(k, v) + } + + return state_changes + + def stringify_erd_value(self, value: Any, **kwargs) -> Optional[str]: + """ + Stringifies a code value if possible. If it can't be stringified, returns none. + By default, enums are stringified using their title-cased name (after replacing + underscores) + """ + + try: + if value is None: + return None + + stringify_op = getattr(value, "stringify", None) + if callable(stringify_op): + return value.stringify(**kwargs) + elif isinstance(value, enum.Enum): +# return value.name.replace("_"," ").title() + return value.name.lower() + else: + return str(value) + except (ValueError, KeyError): + return None + + def boolify_erd_value(self, value: Any) -> Optional[bool]: + """ + Boolifies a code value if possible. If it can't be boolified, returns none + """ + + try: + if value is None: + return None + if isinstance(value, bool): + return value + + boolify_op = getattr(value, "boolify", None) + if callable(boolify_op): + return value.boolify() + else: + return None + except: + return None + + def __str__(self): + appliance_type = self.appliance_type + if appliance_type is None: + appliance_type = 'Unknown Type' + return f'{self.__class__.__name__}({self.mac_addr}) ({appliance_type})' + + def __format__(self, format_spec): + return str(self).__format__(format_spec) diff --git a/custom_components/ge_appliances/binary_sensor.py b/custom_components/ge_appliances/binary_sensor.py old mode 100644 new mode 100755 index fb808f8..0208a73 --- a/custom_components/ge_appliances/binary_sensor.py +++ b/custom_components/ge_appliances/binary_sensor.py @@ -1,12 +1,12 @@ -"""GE Home Sensor Entities""" +"""GE Appliances 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 homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN from .devices import ApplianceApi @@ -15,8 +15,9 @@ _LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home binary sensors.""" + """GE Appliances binary sensors.""" _LOGGER.debug('Adding GE Binary Sensor Entities') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/custom_components/ge_appliances/button.py b/custom_components/ge_appliances/button.py deleted file mode 100644 index cfbb843..0000000 --- a/custom_components/ge_appliances/button.py +++ /dev/null @@ -1,37 +0,0 @@ -"""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) - - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) \ No newline at end of file diff --git a/custom_components/ge_appliances/climate.py b/custom_components/ge_appliances/climate.py deleted file mode 100644 index 8255fb8..0000000 --- a/custom_components/ge_appliances/climate.py +++ /dev/null @@ -1,39 +0,0 @@ -"""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) - - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) \ No newline at end of file diff --git a/custom_components/ge_appliances/config_flow.py b/custom_components/ge_appliances/config_flow.py old mode 100644 new mode 100755 index 3116627..7181068 --- a/custom_components/ge_appliances/config_flow.py +++ b/custom_components/ge_appliances/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for GE Home integration.""" +"""Config flow for GE Appliances integration.""" import logging from typing import Dict, Optional @@ -7,7 +7,7 @@ import asyncio import async_timeout -from gehomesdk import ( +from .api import ( GeAuthFailedError, GeNotAuthenticatedError, GeGeneralServerError, @@ -55,7 +55,7 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": f"{data[CONF_USERNAME]:s}"} class GeHomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for GE Home.""" + """Handle a config flow for GE Appliances.""" VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH diff --git a/custom_components/ge_appliances/const.py b/custom_components/ge_appliances/const.py old mode 100644 new mode 100755 index 198b8aa..8faad31 --- a/custom_components/ge_appliances/const.py +++ b/custom_components/ge_appliances/const.py @@ -4,12 +4,8 @@ EVENT_ALL_APPLIANCES_READY = 'all_appliances_ready' -UPDATE_INTERVAL = 30 ASYNC_TIMEOUT = 30 -MIN_RETRY_DELAY = 15 MAX_RETRY_DELAY = 1800 +MIN_RETRY_DELAY = 15 RETRY_OFFLINE_COUNT = 5 - -SERVICE_SET_TIMER = "set_timer" -SERVICE_CLEAR_TIMER = "clear_timer" -SERVICE_SET_INT_VALUE = "set_int_value" +UPDATE_INTERVAL = 30 diff --git a/custom_components/ge_appliances/devices/__init__.py b/custom_components/ge_appliances/devices/__init__.py old mode 100644 new mode 100755 index b8a07ef..b3c717d --- a/custom_components/ge_appliances/devices/__init__.py +++ b/custom_components/ge_appliances/devices/__init__.py @@ -1,31 +1,10 @@ import logging from typing import Type -from gehomesdk.erd import ErdApplianceType +from ..api.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 .coffee_maker import CcmApi -from .dual_dishwasher import DualDishwasherApi -from .espresso_maker import EspressoMakerApi - _LOGGER = logging.getLogger(__name__) @@ -33,50 +12,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: """Get the appropriate appliance type""" _LOGGER.debug(f"Found device type: {appliance_type}") - if appliance_type == ErdApplianceType.OVEN: - return OvenApi - if appliance_type == ErdApplianceType.COOKTOP: - return CooktopApi - if appliance_type == ErdApplianceType.FRIDGE: - return FridgeApi - if appliance_type == ErdApplianceType.BEVERAGE_CENTER: - return FridgeApi - if appliance_type == ErdApplianceType.DISH_WASHER: + if appliance_type == ErdApplianceType.DISHWASHER: return DishwasherApi - if appliance_type == ErdApplianceType.DUAL_DISH_WASHER: - return DualDishwasherApi - if appliance_type == ErdApplianceType.WASHER: - return WasherApi - if appliance_type == ErdApplianceType.DRYER: - return DryerApi - if appliance_type == ErdApplianceType.COMBINATION_WASHER_DRYER: - return WasherDryerApi - if appliance_type == ErdApplianceType.POE_WATER_FILTER: - return WaterFilterApi - if appliance_type == ErdApplianceType.WATER_SOFTENER: - return WaterSoftenerApi - if appliance_type == ErdApplianceType.WATER_HEATER: - return WaterHeaterApi - if appliance_type == ErdApplianceType.ADVANTIUM: - return AdvantiumApi - if appliance_type == ErdApplianceType.AIR_CONDITIONER: - return WacApi - if appliance_type == ErdApplianceType.SPLIT_AIR_CONDITIONER: - return SacApi - if appliance_type == ErdApplianceType.PORTABLE_AIR_CONDITIONER: - return PacApi - if appliance_type == ErdApplianceType.BUILT_IN_AIR_CONDITIONER: - return BiacApi - if appliance_type == ErdApplianceType.HOOD: - return HoodApi - if appliance_type == ErdApplianceType.MICROWAVE: - return MicrowaveApi - if appliance_type == ErdApplianceType.OPAL_ICE_MAKER: - return OimApi - if appliance_type == ErdApplianceType.CAFE_COFFEE_MAKER: - return CcmApi - if appliance_type == ErdApplianceType.ESPRESSO_MAKER: - return EspressoMakerApi # Fallback return ApplianceApi diff --git a/custom_components/ge_appliances/devices/advantium.py b/custom_components/ge_appliances/devices/advantium.py deleted file mode 100644 index 23775ec..0000000 --- a/custom_components/ge_appliances/devices/advantium.py +++ /dev/null @@ -1,45 +0,0 @@ -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.UNIT_TYPE), - 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/custom_components/ge_appliances/devices/base.py b/custom_components/ge_appliances/devices/base.py old mode 100644 new mode 100755 index 179e3e9..6085a5e --- a/custom_components/ge_appliances/devices/base.py +++ b/custom_components/ge_appliances/devices/base.py @@ -2,13 +2,12 @@ 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.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from ..api import GeAppliance +from ..api.erd import ErdCode, ErdCodeType, ErdApplianceType from ..const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -84,11 +83,12 @@ 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) + def appliance_sw_version(self) -> str: + return self.try_get_erd_value(ErdCode.APPLIANCE_SW_VERSION) - return 'Appliance=' + str(appVer or 'Unknown') + '/Wifi=' + str(wifiVer or 'Unknown') + @property + def wifi_module_sw_version(self) -> str: + return self.try_get_erd_value(ErdCode.WIFI_MODULE_SW_VERSION) @property def name(self) -> str: @@ -97,19 +97,19 @@ def name(self) -> str: appliance_type = "Appliance" else: appliance_type = appliance_type.name.replace("_", " ").title() - return f"GE {appliance_type} {self.serial_or_mac}" + return f"{appliance_type} {self.serial_or_mac}" @property - def device_info(self) -> Dict: + def device_info(self) -> DeviceInfo: """Device info dictionary.""" - - return { - "identifiers": {(DOMAIN, self.serial_or_mac)}, - "name": self.name, - "manufacturer": "GE", - "model": self.model_number, - "sw_version": self.sw_version - } + return DeviceInfo( + identifiers={(DOMAIN, self.serial_or_mac)}, + manufacturer="GE", + model=self.model_number, + name=self.name, + sw_version=self.appliance_sw_version, +# hw_version=self.hw_version, + ) @property def entities(self) -> List[Entity]: @@ -123,17 +123,25 @@ 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), + GeErdSensor( + api=self, + erd_code=ErdCode.CLOCK_TIME, + name="Clock Time", + ), + GeErdSwitch( + api=self, + erd_code=ErdCode.SABBATH_MODE, + name="Sabbath Mode", + ), ] return entities def build_entities_list(self) -> None: """Build the entities list, adding anything new.""" - from ..entities import GeErdEntity, GeErdButton + from ..entities import GeErdEntity 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 + if not isinstance(e, GeErdEntity) or e.erd_code in self.appliance.known_properties ] for entity in entities: diff --git a/custom_components/ge_appliances/devices/biac.py b/custom_components/ge_appliances/devices/biac.py deleted file mode 100644 index 916b6cb..0000000 --- a/custom_components/ge_appliances/devices/biac.py +++ /dev/null @@ -1,35 +0,0 @@ -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/custom_components/ge_appliances/devices/coffee_maker.py b/custom_components/ge_appliances/devices/coffee_maker.py deleted file mode 100644 index d3f39c9..0000000 --- a/custom_components/ge_appliances/devices/coffee_maker.py +++ /dev/null @@ -1,66 +0,0 @@ -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/custom_components/ge_appliances/devices/cooktop.py b/custom_components/ge_appliances/devices/cooktop.py deleted file mode 100644 index 6fb3453..0000000 --- a/custom_components/ge_appliances/devices/cooktop.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -from typing import List -from gehomesdk.erd.erd_data_type import ErdDataType - -from homeassistant.const import DEVICE_CLASS_POWER_FACTOR -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: - cooktop_status: CooktopStatus = self.appliance.get_erd_value(ErdCode.COOKTOP_STATUS) - cooktop_entities.append(GeErdBinarySensor(self, ErdCode.COOKTOP_STATUS)) - - for (k, v) in cooktop_status.burners.items(): - if v.exists: - prop = self._camel_to_snake(k) - cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, prop+".on")) - cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, prop+".synchronized")) - if not v.on_off_only: - cooktop_entities.append(GeErdPropertySensor(self, ErdCode.COOKTOP_STATUS, prop+".power_pct", icon_override="mdi:fire", device_class_override=DEVICE_CLASS_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/custom_components/ge_appliances/devices/dishwasher.py b/custom_components/ge_appliances/devices/dishwasher.py old mode 100644 new mode 100755 index 5943513..f92404d --- a/custom_components/ge_appliances/devices/dishwasher.py +++ b/custom_components/ge_appliances/devices/dishwasher.py @@ -1,53 +1,203 @@ import logging from typing import List +from homeassistant.const import UnitOfTime from homeassistant.helpers.entity import Entity -from gehomesdk.erd import ErdCode, ErdApplianceType +from ..api.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor +from ..entities import ( + GeErdBinarySensor, + GeErdSensor, + GeErdPropertyBinarySensor, + GeErdPropertySensor, +) _LOGGER = logging.getLogger(__name__) class DishwasherApi(ApplianceApi): """API class for dishwasher objects""" - APPLIANCE_TYPE = ErdApplianceType.DISH_WASHER + APPLIANCE_TYPE = ErdApplianceType.DISHWASHER 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"), - 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), +# GeDishwasherControlLockedSwitch( +# api=self, +# ErdCode.USER_INTERFACE_LOCKED, +# ), + GeErdSensor( + api=self, + erd_code=ErdCode.DISHWASHER_CYCLE_NAME, + name="Cycle Name", + ), + GeErdSensor( + api=self, + erd_code=ErdCode.DISHWASHER_CYCLE_STATE, +# icon="mdi:state-machine", + name="Cycle State", + ), + GeErdSensor( + api=self, + erd_code=ErdCode.DISHWASHER_OPERATING_MODE, + name="Operating Mode", + ), + GeErdSensor( + api=self, + erd_code=ErdCode.DISHWASHER_PODS_REMAINING_VALUE, + name="Remaining Pods", + native_unit_of_measurement="pods", + ), + GeErdPropertyBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_REMINDERS, + erd_property="add_rinse_aid", +# icon="mdi:shimmer", + name="Rinse Aid Needed", + ), + GeErdPropertyBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_REMINDERS, + erd_property="clean_filter", +# icon="mdi:dishwasher-alert", + name="Filter Dirty", + ), + GeErdPropertyBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_REMINDERS, + erd_property="sanitized", +# icon="mdi:silverware-clean", + name="Sanitization Complete", + ), + GeErdSensor( + api=self, + erd_code=ErdCode.DISHWASHER_TIME_REMAINING, + name="Cycle Time Remaining", + native_unit_of_measurement=UnitOfTime.MINUTES, + ), + GeErdBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_DOOR_STATUS, + name="Door Status", + ), + GeErdBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_IS_CLEAN, + name="Cleaning Complete", + ), + GeErdBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_REMOTE_START_ENABLE, + name="Remote Start Enabled", + ), #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"), + GeErdPropertyBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_USER_SETTING, + erd_property="mute", +# icon="mdi:volume-mute", + name="Mute Enabled", + ), + GeErdPropertyBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_USER_SETTING, + erd_property="lock_control", +# icon="mdi:lock", + name="Control Lock Enabled", + ), + GeErdPropertyBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_USER_SETTING, + erd_property="sabbath", +# icon="mdi:star-david", + name="Sabbath Mode Enabled", + ), + GeErdPropertySensor( + api=self, + erd_code=ErdCode.DISHWASHER_USER_SETTING, + erd_property="cycle_mode", +# icon="mdi:state-machine", + name="Cycle Mode", + ), + GeErdPropertyBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_USER_SETTING, + erd_property="presoak", +# icon="mdi:water", + name="Presoak Enabled", + ), + GeErdPropertyBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_USER_SETTING, + erd_property="bottle_jet", +# icon="mdi:bottle-tonic-outline", + name="Bottle Jet Enabled", + ), + GeErdPropertySensor( + api=self, + erd_code=ErdCode.DISHWASHER_USER_SETTING, + erd_property="wash_temp", +# icon="mdi:coolant-temperature", + name="Wash Temp", + ), + GeErdPropertyBinarySensor( + api=self, + erd_code=ErdCode.DISHWASHER_USER_SETTING, + erd_property="rinse_aid", +# icon="mdi:shimmer", + name="Rinse Aid Enabled", + ), + GeErdPropertySensor( + api=self, + erd_code=ErdCode.DISHWASHER_USER_SETTING, + erd_property="dry_option", +# icon="mdi:fan", + name="Dry Option", + ), + GeErdPropertySensor( + api=self, + erd_code=ErdCode.DISHWASHER_USER_SETTING, + erd_property="wash_zone", +# icon="mdi:dock-top", + name="Wash Zone", + ), + GeErdPropertySensor( + api=self, + erd_code=ErdCode.DISHWASHER_USER_SETTING, + erd_property="delay_hours", +# icon="mdi:clock-fast", + name="Delay Start", + native_unit_of_measurement=UnitOfTime.HOURS, + ), #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") + GeErdPropertySensor( + api=self, + erd_code=ErdCode.DISHWASHER_CYCLE_COUNTS, + erd_property="started", +# icon="mdi:counter", + name="Cycles Started", + native_unit_of_measurement="cycles", + ), + GeErdPropertySensor( + api=self, + erd_code=ErdCode.DISHWASHER_CYCLE_COUNTS, + erd_property="completed", +# icon="mdi:counter", + name="Cycles Completed", + native_unit_of_measurement="cycles", + ), + GeErdPropertySensor( + api=self, + erd_code=ErdCode.DISHWASHER_CYCLE_COUNTS, + erd_property="reset", +# icon="mdi:counter", + name="Cycles Reset", + native_unit_of_measurement="cycles", + ) ] entities = base_entities + dishwasher_entities return entities diff --git a/custom_components/ge_appliances/devices/dryer.py b/custom_components/ge_appliances/devices/dryer.py deleted file mode 100644 index 9387f7e..0000000 --- a/custom_components/ge_appliances/devices/dryer.py +++ /dev/null @@ -1,65 +0,0 @@ -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"), - ] - - 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_TUMBLENEW_STATUS): - dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_TUMBLENEW_STATUS)]) - 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_STATUS): - dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_ECODRY_STATUS)]) - - return dryer_entities - diff --git a/custom_components/ge_appliances/devices/dual_dishwasher.py b/custom_components/ge_appliances/devices/dual_dishwasher.py deleted file mode 100644 index 63c8b29..0000000 --- a/custom_components/ge_appliances/devices/dual_dishwasher.py +++ /dev/null @@ -1,61 +0,0 @@ -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_RINSE_AGENT, erd_override="lower_rinse_agent", icon_override="mdi:shimmer"), - GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING, erd_override="lower_time_remaining"), - GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS, erd_override="lower_door_status"), - - #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound", erd_override="lower_setting", icon_override="mdi:volume-high"), - 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, "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_RINSE_AGENT, erd_override="upper_rinse_agent", icon_override="mdi:shimmer"), - GeErdSensor(self, ErdCode.DISHWASHER_UPPER_TIME_REMAINING, erd_override="upper_time_remaining"), - GeErdBinarySensor(self, ErdCode.DISHWASHER_UPPER_DOOR_STATUS, erd_override="upper_door_status"), - - #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "sound", erd_override="upper_setting", icon_override="mdi:volume-high"), - 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, "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/custom_components/ge_appliances/devices/espresso_maker.py b/custom_components/ge_appliances/devices/espresso_maker.py deleted file mode 100644 index efb184e..0000000 --- a/custom_components/ge_appliances/devices/espresso_maker.py +++ /dev/null @@ -1,34 +0,0 @@ -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/custom_components/ge_appliances/devices/fridge.py b/custom_components/ge_appliances/devices/fridge.py deleted file mode 100644 index 24f0657..0000000 --- a/custom_components/ge_appliances/devices/fridge.py +++ /dev/null @@ -1,126 +0,0 @@ -from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM -from homeassistant.const import DEVICE_CLASS_TEMPERATURE -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 -) - -_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) - - 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(ice_maker_control and ice_maker_control.status_fridge != ErdOnOff.NA): - fridge_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_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(ice_maker_control and ice_maker_control.status_freezer != ErdOnOff.NA): - freezer_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_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=DEVICE_CLASS_TEMPERATURE, data_type_override=ErdDataType.INT), - GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=DEVICE_CLASS_PROBLEM), - GeDispenser(self) - ]) - - entities = base_entities + common_entities + fridge_entities + freezer_entities + dispenser_entities - return entities diff --git a/custom_components/ge_appliances/devices/hood.py b/custom_components/ge_appliances/devices/hood.py deleted file mode 100644 index 439c775..0000000 --- a/custom_components/ge_appliances/devices/hood.py +++ /dev/null @@ -1,52 +0,0 @@ -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/custom_components/ge_appliances/devices/microwave.py b/custom_components/ge_appliances/devices/microwave.py deleted file mode 100644 index ec943fa..0000000 --- a/custom_components/ge_appliances/devices/microwave.py +++ /dev/null @@ -1,56 +0,0 @@ -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/custom_components/ge_appliances/devices/oim.py b/custom_components/ge_appliances/devices/oim.py deleted file mode 100644 index 2eebd39..0000000 --- a/custom_components/ge_appliances/devices/oim.py +++ /dev/null @@ -1,41 +0,0 @@ -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/custom_components/ge_appliances/devices/oven.py b/custom_components/ge_appliances/devices/oven.py deleted file mode 100644 index f335f8d..0000000 --- a/custom_components/ge_appliances/devices/oven.py +++ /dev/null @@ -1,120 +0,0 @@ -import logging -from typing import List -from gehomesdk.erd.erd_data_type import ErdDataType - -from homeassistant.const import DEVICE_CLASS_POWER_FACTOR -from homeassistant.helpers.entity import Entity -from gehomesdk import ( - ErdCode, - ErdApplianceType, - OvenConfiguration, - ErdCooktopConfig, - CooktopStatus, - ErdOvenLightLevel, - ErdOvenLightLevelAvailability -) - -from .base import ApplianceApi -from ..entities import ( - GeErdSensor, - GeErdTimerSensor, - GeErdBinarySensor, - GeErdPropertySensor, - GeErdPropertyBinarySensor, - GeOven, - GeOvenLightLevelSelect, - 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) - - 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) - - _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.UPPER_OVEN_COOK_MODE), - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING), - GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER), - GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET), - GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED), - - GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE), - 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)), - GeOven(self, UPPER_OVEN, True, self._temperature_code(has_upper_raw_temperature)), - ]) - if has_upper_raw_temperature: - oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_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)) - else: - oven_entities.extend([ - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE)), - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING)), - GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER)), - GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, self._single_name(ErdCode.UPPER_OVEN_USER_TEMP_OFFSET)), - GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE)), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED)), - 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))) - 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))) - - - if cooktop_config == ErdCooktopConfig.PRESENT: - cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) - if cooktop_status is not None: - cooktop_entities.append(GeErdBinarySensor(self, ErdCode.COOKTOP_STATUS)) - - for (k, v) in cooktop_status.burners.items(): - if v.exists: - prop = self._camel_to_snake(k) - cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, prop+".on")) - cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, prop+".synchronized")) - if not v.on_off_only: - cooktop_entities.append(GeErdPropertySensor(self, ErdCode.COOKTOP_STATUS, prop+".power_pct", icon_override="mdi:fire", device_class_override=DEVICE_CLASS_POWER_FACTOR, data_type_override=ErdDataType.INT)) - - return base_entities + oven_entities + cooktop_entities - - def _single_name(self, erd_code: ErdCode): - return erd_code.name.replace(UPPER_OVEN+"_","").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" \ No newline at end of file diff --git a/custom_components/ge_appliances/devices/pac.py b/custom_components/ge_appliances/devices/pac.py deleted file mode 100644 index fa2da9d..0000000 --- a/custom_components/ge_appliances/devices/pac.py +++ /dev/null @@ -1,31 +0,0 @@ -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/custom_components/ge_appliances/devices/sac.py b/custom_components/ge_appliances/devices/sac.py deleted file mode 100644 index a1dfad5..0000000 --- a/custom_components/ge_appliances/devices/sac.py +++ /dev/null @@ -1,37 +0,0 @@ -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/custom_components/ge_appliances/devices/wac.py b/custom_components/ge_appliances/devices/wac.py deleted file mode 100644 index 6208a82..0000000 --- a/custom_components/ge_appliances/devices/wac.py +++ /dev/null @@ -1,33 +0,0 @@ -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/custom_components/ge_appliances/devices/washer.py b/custom_components/ge_appliances/devices/washer.py deleted file mode 100644 index 9cc0372..0000000 --- a/custom_components/ge_appliances/devices/washer.py +++ /dev/null @@ -1,63 +0,0 @@ -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/custom_components/ge_appliances/devices/washer_dryer.py b/custom_components/ge_appliances/devices/washer_dryer.py deleted file mode 100644 index f701ff2..0000000 --- a/custom_components/ge_appliances/devices/washer_dryer.py +++ /dev/null @@ -1,36 +0,0 @@ -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/custom_components/ge_appliances/devices/water_filter.py b/custom_components/ge_appliances/devices/water_filter.py deleted file mode 100644 index d466f67..0000000 --- a/custom_components/ge_appliances/devices/water_filter.py +++ /dev/null @@ -1,38 +0,0 @@ -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), - 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/custom_components/ge_appliances/devices/water_heater.py b/custom_components/ge_appliances/devices/water_heater.py deleted file mode 100644 index fdbdf18..0000000 --- a/custom_components/ge_appliances/devices/water_heater.py +++ /dev/null @@ -1,43 +0,0 @@ -import logging -from typing import List - -from homeassistant.helpers.entity import Entity -from gehomesdk import ( - ErdCode, - ErdApplianceType, - ErdOnOff -) - -from custom_components.ge_appliances.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() - - 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) - ] - - entities = base_entities + wh_entities - return entities - diff --git a/custom_components/ge_appliances/devices/water_softener.py b/custom_components/ge_appliances/devices/water_softener.py deleted file mode 100644 index a730471..0000000 --- a/custom_components/ge_appliances/devices/water_softener.py +++ /dev/null @@ -1,38 +0,0 @@ -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), - 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/custom_components/ge_appliances/entities/__init__.py b/custom_components/ge_appliances/entities/__init__.py old mode 100644 new mode 100755 index 2306cae..8f3a5fc --- a/custom_components/ge_appliances/entities/__init__.py +++ b/custom_components/ge_appliances/entities/__init__.py @@ -1,12 +1,2 @@ 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 * \ No newline at end of file diff --git a/custom_components/ge_appliances/entities/ac/__init__.py b/custom_components/ge_appliances/entities/ac/__init__.py deleted file mode 100644 index aefb995..0000000 --- a/custom_components/ge_appliances/entities/ac/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -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/custom_components/ge_appliances/entities/ac/fan_mode_options.py b/custom_components/ge_appliances/entities/ac/fan_mode_options.py deleted file mode 100644 index 0bf78cd..0000000 --- a/custom_components/ge_appliances/entities/ac/fan_mode_options.py +++ /dev/null @@ -1,50 +0,0 @@ -import logging -from typing import Any, List, Optional - -from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_FAN_ONLY, -) -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.warn(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/custom_components/ge_appliances/entities/ac/ge_biac_climate.py b/custom_components/ge_appliances/entities/ac/ge_biac_climate.py deleted file mode 100644 index f3b7453..0000000 --- a/custom_components/ge_appliances/entities/ac/ge_biac_climate.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging -from typing import Any, List, Optional - -from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_FAN_ONLY, -) -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 [HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] - def from_option_string(self, value: str) -> Any: - try: - return { - HVAC_MODE_AUTO: ErdAcOperationMode.ENERGY_SAVER, - HVAC_MODE_COOL: ErdAcOperationMode.COOL, - HVAC_MODE_FAN_ONLY: ErdAcOperationMode.FAN_ONLY - }.get(value) - except: - _LOGGER.warn(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: HVAC_MODE_AUTO, - ErdAcOperationMode.AUTO: HVAC_MODE_AUTO, - ErdAcOperationMode.COOL: HVAC_MODE_COOL, - ErdAcOperationMode.FAN_ONLY: HVAC_MODE_FAN_ONLY - }.get(value) - except: - _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_COOL - -class GeBiacClimate(GeClimate): - """Class for Built-In AC units""" - def __init__(self, api: ApplianceApi): - super().__init__(api, BiacHvacModeOptionsConverter(), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) diff --git a/custom_components/ge_appliances/entities/ac/ge_pac_climate.py b/custom_components/ge_appliances/entities/ac/ge_pac_climate.py deleted file mode 100644 index ba8eb75..0000000 --- a/custom_components/ge_appliances/entities/ac/ge_pac_climate.py +++ /dev/null @@ -1,81 +0,0 @@ -import logging -from typing import Any, List, Optional - - -from homeassistant.const import ( - TEMP_FAHRENHEIT -) -from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, -) -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 = [HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] - if self._available_modes and self._available_modes.has_heat: - modes.append(HVAC_MODE_HEAT) - if self._available_modes and self._available_modes.has_dry: - modes.append(HVAC_MODE_DRY) - return modes - def from_option_string(self, value: str) -> Any: - try: - return { - HVAC_MODE_COOL: ErdAcOperationMode.COOL, - HVAC_MODE_HEAT: ErdAcOperationMode.HEAT, - HVAC_MODE_FAN_ONLY: ErdAcOperationMode.FAN_ONLY, - HVAC_MODE_DRY: ErdAcOperationMode.DRY - }.get(value) - except: - _LOGGER.warn(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: HVAC_MODE_COOL, - ErdAcOperationMode.HEAT: HVAC_MODE_HEAT, - ErdAcOperationMode.DRY: HVAC_MODE_DRY, - ErdAcOperationMode.FAN_ONLY: HVAC_MODE_FAN_ONLY - }.get(value) - except: - _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_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/custom_components/ge_appliances/entities/ac/ge_sac_climate.py b/custom_components/ge_appliances/entities/ac/ge_sac_climate.py deleted file mode 100644 index 7c7ed54..0000000 --- a/custom_components/ge_appliances/entities/ac/ge_sac_climate.py +++ /dev/null @@ -1,84 +0,0 @@ -import logging -from typing import Any, List, Optional - -from homeassistant.const import ( - TEMP_FAHRENHEIT -) -from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, -) -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 = [HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] - if self._available_modes and self._available_modes.has_heat: - modes.append(HVAC_MODE_HEAT) - modes.append(HVAC_MODE_AUTO) - if self._available_modes and self._available_modes.has_dry: - modes.append(HVAC_MODE_DRY) - return modes - def from_option_string(self, value: str) -> Any: - try: - return { - HVAC_MODE_AUTO: ErdAcOperationMode.AUTO, - HVAC_MODE_COOL: ErdAcOperationMode.COOL, - HVAC_MODE_HEAT: ErdAcOperationMode.HEAT, - HVAC_MODE_FAN_ONLY: ErdAcOperationMode.FAN_ONLY, - HVAC_MODE_DRY: ErdAcOperationMode.DRY - }.get(value) - except: - _LOGGER.warn(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: HVAC_MODE_AUTO, - ErdAcOperationMode.AUTO: HVAC_MODE_AUTO, - ErdAcOperationMode.COOL: HVAC_MODE_COOL, - ErdAcOperationMode.HEAT: HVAC_MODE_HEAT, - ErdAcOperationMode.DRY: HVAC_MODE_DRY, - ErdAcOperationMode.FAN_ONLY: HVAC_MODE_FAN_ONLY - }.get(value) - except: - _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_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/custom_components/ge_appliances/entities/ac/ge_wac_climate.py b/custom_components/ge_appliances/entities/ac/ge_wac_climate.py deleted file mode 100644 index a7b3980..0000000 --- a/custom_components/ge_appliances/entities/ac/ge_wac_climate.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging -from typing import Any, List, Optional - -from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_FAN_ONLY, -) -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 [HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] - def from_option_string(self, value: str) -> Any: - try: - return { - HVAC_MODE_AUTO: ErdAcOperationMode.ENERGY_SAVER, - HVAC_MODE_COOL: ErdAcOperationMode.COOL, - HVAC_MODE_FAN_ONLY: ErdAcOperationMode.FAN_ONLY - }.get(value) - except: - _LOGGER.warn(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: HVAC_MODE_AUTO, - ErdAcOperationMode.AUTO: HVAC_MODE_AUTO, - ErdAcOperationMode.COOL: HVAC_MODE_COOL, - ErdAcOperationMode.FAN_ONLY: HVAC_MODE_FAN_ONLY - }.get(value) - except: - _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_COOL - -class GeWacClimate(GeClimate): - """Class for Window AC units""" - def __init__(self, api: ApplianceApi): - super().__init__(api, WacHvacModeOptionsConverter(), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) diff --git a/custom_components/ge_appliances/entities/advantium/__init__.py b/custom_components/ge_appliances/entities/advantium/__init__.py deleted file mode 100644 index a4cfe30..0000000 --- a/custom_components/ge_appliances/entities/advantium/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .ge_advantium import GeAdvantium \ No newline at end of file diff --git a/custom_components/ge_appliances/entities/advantium/const.py b/custom_components/ge_appliances/entities/advantium/const.py deleted file mode 100644 index 7c487be..0000000 --- a/custom_components/ge_appliances/entities/advantium/const.py +++ /dev/null @@ -1,8 +0,0 @@ -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) - -SUPPORT_NONE = 0 -GE_ADVANTIUM_WITH_TEMPERATURE = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) -GE_ADVANTIUM = SUPPORT_OPERATION_MODE diff --git a/custom_components/ge_appliances/entities/advantium/ge_advantium.py b/custom_components/ge_appliances/entities/advantium/ge_advantium.py deleted file mode 100644 index 2d1372b..0000000 --- a/custom_components/ge_appliances/entities/advantium/ge_advantium.py +++ /dev/null @@ -1,282 +0,0 @@ -"""GE Home Sensor Entities - Advantium""" -import logging -from typing import Any, Dict, List, Mapping, Optional, Set -from random import randrange - -from gehomesdk import ( - ErdCode, - ErdUnitType, - ErdAdvantiumCookStatus, - ErdAdvantiumCookSetting, - AdvantiumOperationMode, - AdvantiumCookSetting, - ErdAdvantiumRemoteCookModeConfig, - ADVANTIUM_OPERATION_MODE_COOK_SETTING_MAPPING -) -from gehomesdk.erd.values.advantium.advantium_enums import CookAction, CookMode - -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -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 unit_type(self) -> Optional[ErdUnitType]: - try: - return self.appliance.get_erd_value(ErdCode.UNIT_TYPE) - 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.unit_type) - 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.unit_type - target_temp_f = temp_240v if unit_type in [ErdUnitType.TYPE_240V_MONOGRAM, ErdUnitType.TYPE_240V_CAFE] else temp_120v - if self.temperature_unit == TEMP_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/custom_components/ge_appliances/entities/ccm/__init__.py b/custom_components/ge_appliances/entities/ccm/__init__.py deleted file mode 100644 index 614d130..0000000 --- a/custom_components/ge_appliances/entities/ccm/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -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/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_cups.py b/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_cups.py deleted file mode 100644 index 5792f12..0000000 --- a/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_cups.py +++ /dev/null @@ -1,19 +0,0 @@ -from gehomesdk import ErdCode -from ...devices import ApplianceApi -from ..common import GeErdNumber -from .ge_ccm_cached_value import GeCcmCachedValue - -class GeCcmBrewCupsNumber(GeErdNumber, GeCcmCachedValue): - def __init__(self, api: ApplianceApi): - GeErdNumber.__init__(self, api = api, erd_code = ErdCode.CCM_BREW_CUPS, min_value=1, max_value=10, mode="box") - GeCcmCachedValue.__init__(self) - - self._set_value = None - - async def async_set_native_value(self, value): - GeCcmCachedValue.set_value(self, value) - self.schedule_update_ha_state() - - @property - def native_value(self): - return self.get_value(device_value = super().native_value) diff --git a/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_settings.py b/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_settings.py deleted file mode 100644 index 121392b..0000000 --- a/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_settings.py +++ /dev/null @@ -1,13 +0,0 @@ -from gehomesdk import ErdCode -from ...devices import ApplianceApi -from ..common import GeErdButton - -class GeCcmBrewSettingsButton(GeErdButton): - def __init__(self, api: ApplianceApi): - super().__init__(api, erd_code=ErdCode.CCM_BREW_SETTINGS) - - async def async_press(self) -> 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/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_strength.py b/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_strength.py deleted file mode 100644 index b6faa93..0000000 --- a/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_strength.py +++ /dev/null @@ -1,47 +0,0 @@ -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.warn(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/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_temperature.py b/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_temperature.py deleted file mode 100644 index 86cac03..0000000 --- a/custom_components/ge_appliances/entities/ccm/ge_ccm_brew_temperature.py +++ /dev/null @@ -1,19 +0,0 @@ -from gehomesdk import ErdCode -from ...devices import ApplianceApi -from ..common import GeErdNumber -from .ge_ccm_cached_value import GeCcmCachedValue -from homeassistant.const import TEMP_FAHRENHEIT - -class GeCcmBrewTemperatureNumber(GeErdNumber, GeCcmCachedValue): - def __init__(self, api: ApplianceApi): - min_temp, max_temp, _ = api.appliance.get_erd_value(ErdCode.CCM_BREW_TEMPERATURE_RANGE) - 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/custom_components/ge_appliances/entities/ccm/ge_ccm_cached_value.py b/custom_components/ge_appliances/entities/ccm/ge_ccm_cached_value.py deleted file mode 100644 index 95c2b94..0000000 --- a/custom_components/ge_appliances/entities/ccm/ge_ccm_cached_value.py +++ /dev/null @@ -1,20 +0,0 @@ -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/custom_components/ge_appliances/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py b/custom_components/ge_appliances/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py deleted file mode 100644 index 124914a..0000000 --- a/custom_components/ge_appliances/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py +++ /dev/null @@ -1,8 +0,0 @@ -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/custom_components/ge_appliances/entities/common/__init__.py b/custom_components/ge_appliances/entities/common/__init__.py old mode 100644 new mode 100755 index 0b555ca..972aafb --- a/custom_components/ge_appliances/entities/common/__init__.py +++ b/custom_components/ge_appliances/entities/common/__init__.py @@ -5,12 +5,5 @@ 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 \ No newline at end of file diff --git a/custom_components/ge_appliances/entities/common/bool_converter.py b/custom_components/ge_appliances/entities/common/bool_converter.py old mode 100644 new mode 100755 index b8dcd14..ddd1499 --- a/custom_components/ge_appliances/entities/common/bool_converter.py +++ b/custom_components/ge_appliances/entities/common/bool_converter.py @@ -1,6 +1,6 @@ from typing import Any -from gehomesdk import ErdOnOff +from ...api import ErdOnOff class BoolConverter: def boolify(self, value: Any) -> bool: diff --git a/custom_components/ge_appliances/entities/common/ge_climate.py b/custom_components/ge_appliances/entities/common/ge_climate.py deleted file mode 100644 index 7f44edb..0000000 --- a/custom_components/ge_appliances/entities/common/ge_climate.py +++ /dev/null @@ -1,194 +0,0 @@ -import logging -from typing import List, Optional - -from homeassistant.components.climate import ClimateEntity -from homeassistant.const import ( - ATTR_TEMPERATURE, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, -) -from homeassistant.components.climate.const import ( - HVAC_MODE_FAN_ONLY, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, - HVAC_MODE_OFF -) -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 = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE - -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): - #appears to always be Fahrenheit internally, hardcode this - return TEMP_FAHRENHEIT - #measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) - #if measurement_system == ErdMeasurementUnits.METRIC: - # return TEMP_CELSIUS - #return TEMP_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]: - return float(self.appliance.get_erd_value(self.target_temperature_erd_code)) - - @property - def current_temperature(self) -> Optional[float]: - return float(self.appliance.get_erd_value(self.current_temperature_erd_code)) - - @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 HVAC_MODE_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 [HVAC_MODE_OFF] + self._hvac_mode_converter.options - - @property - def fan_mode(self): - if self.hvac_mode == HVAC_MODE_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 == HVAC_MODE_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 == HVAC_MODE_OFF: - await self.appliance.async_set_erd_value(self.power_status_erd_code, ErdOnOff.OFF) - else: - #if it's not on, turn it on - if not self.is_on: - await self.appliance.async_set_erd_value(self.power_status_erd_code, ErdOnOff.ON) - - #then set the mode - 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 == HVAC_MODE_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: - #get the temperature if available - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: - return - - #convert to int (setting can only handle ints) - 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 == TEMP_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/custom_components/ge_appliances/entities/common/ge_entity.py b/custom_components/ge_appliances/entities/common/ge_entity.py old mode 100644 new mode 100755 index 34f4037..bdd2c76 --- a/custom_components/ge_appliances/entities/common/ge_entity.py +++ b/custom_components/ge_appliances/entities/common/ge_entity.py @@ -1,14 +1,20 @@ +from __future__ import annotations + from datetime import timedelta from typing import Optional, Dict, Any -from gehomesdk import GeAppliance +from homeassistant.helpers.entity import DeviceInfo + +from ...api import GeAppliance from ...devices import ApplianceApi + class GeEntity: """Base class for all GE Entities""" - should_poll = False + _attr_should_poll: bool = False + _attr_translation_key: str | None = "all" - def __init__(self, api: ApplianceApi): + def __init__(self, api: ApplianceApi) -> None: self._api = api self.hass = None @@ -21,7 +27,7 @@ def api(self) -> ApplianceApi: return self._api @property - def device_info(self) -> Optional[Dict[str, Any]]: + def device_info(self) -> DeviceInfo: return self.api.device_info @property @@ -56,6 +62,10 @@ def icon(self) -> Optional[str]: def device_class(self) -> Optional[str]: return self._get_device_class() + @property + def entity_category(self) -> Optional[str]: + return self._get_entity_category() + def _stringify(self, value: any, **kwargs) -> Optional[str]: if isinstance(value, timedelta): return str(value)[:-3] if value else "" @@ -71,3 +81,6 @@ def _get_icon(self) -> Optional[str]: def _get_device_class(self) -> Optional[str]: return None + + def _get_entity_category(self) -> Optional[str]: + return None diff --git a/custom_components/ge_appliances/entities/common/ge_erd_binary_sensor.py b/custom_components/ge_appliances/entities/common/ge_erd_binary_sensor.py old mode 100644 new mode 100755 index 55afc01..e0d5510 --- a/custom_components/ge_appliances/entities/common/ge_erd_binary_sensor.py +++ b/custom_components/ge_appliances/entities/common/ge_erd_binary_sensor.py @@ -1,38 +1,36 @@ +from __future__ import annotations + from typing import Optional from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.const import EntityCategory -from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass +from ...api 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""" + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + name: str, + device_class: str = None, + entity_category: str[EntityCategory] | None = EntityCategory.DIAGNOSTIC, + icon: str = None, + ) -> None: + super().__init__( + api=api, + erd_code=erd_code, + device_class=device_class, + entity_category=entity_category, + icon=icon, + name=name, + ) + @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/custom_components/ge_appliances/entities/common/ge_erd_button.py b/custom_components/ge_appliances/entities/common/ge_erd_button.py deleted file mode 100644 index ef28295..0000000 --- a/custom_components/ge_appliances/entities/common/ge_erd_button.py +++ /dev/null @@ -1,17 +0,0 @@ -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/custom_components/ge_appliances/entities/common/ge_erd_entity.py b/custom_components/ge_appliances/entities/common/ge_erd_entity.py old mode 100644 new mode 100755 index 36b7883..979aee9 --- a/custom_components/ge_appliances/entities/common/ge_erd_entity.py +++ b/custom_components/ge_appliances/entities/common/ge_erd_entity.py @@ -1,8 +1,11 @@ +from __future__ import annotations + from datetime import timedelta from typing import Optional -from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits +from homeassistant.const import EntityCategory +from ...api import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits from ...const import DOMAIN from ...devices import ApplianceApi from .ge_entity import GeEntity @@ -15,16 +18,20 @@ def __init__( self, api: ApplianceApi, erd_code: ErdCodeType, - erd_override: str = None, - icon_override: str = None, - device_class_override: str = None, - ): - super().__init__(api) + name: str, + device_class: str = None, + entity_category: str[EntityCategory] | None = None, + icon: str = None, + ) -> None: + super().__init__( + api=api, + ) + self._device_class = device_class + self._entity_category = entity_category 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 + self._icon = icon + self._name = name if not self._erd_code_class: self._erd_code_class = ErdCodeClass.GENERAL @@ -46,18 +53,12 @@ def erd_string(self) -> str: @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}" + return f"{self.device_info['name']} {self._name}" +# return f"{self.serial_or_mac} {self._name}" @property def unique_id(self) -> Optional[str]: - return f"{DOMAIN}_{self.serial_or_mac}_{self.erd_string.lower()}" + return f"{self.serial_or_mac}_{self.erd_string.lower()}" def _stringify(self, value: any, **kwargs) -> Optional[str]: """Stringify a value""" @@ -69,10 +70,12 @@ def _stringify(self, value: any, **kwargs) -> Optional[str]: 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" +# return str(value)[:-3] if value else "Off" + return value if value is None: return None return self.appliance.stringify_erd_value(value, **kwargs) +# return "_".join(self.appliance.stringify_erd_value(value, **kwargs).split()).lower() @property def _measurement_system(self) -> Optional[ErdMeasurementUnits]: @@ -88,62 +91,10 @@ def _measurement_system(self) -> Optional[ErdMeasurementUnits]: def _get_icon(self): """Select an appropriate icon.""" + return self._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" - - return None + def _get_device_class(self) -> Optional[str]: + return self._device_class + + def _get_entity_category(self) -> str[EntityCategory]: + return self._entity_category diff --git a/custom_components/ge_appliances/entities/common/ge_erd_light.py b/custom_components/ge_appliances/entities/common/ge_erd_light.py deleted file mode 100644 index 1d9d714..0000000 --- a/custom_components/ge_appliances/entities/common/ge_erd_light.py +++ /dev/null @@ -1,70 +0,0 @@ -import logging - -from gehomesdk import ErdCodeType -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - COLOR_MODE_BRIGHTNESS, - SUPPORT_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 = COLOR_MODE_BRIGHTNESS): - super().__init__(api, erd_code, erd_override) - self._color_mode = color_mode - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - - @property - def supported_color_modes(self): - """Flag supported color modes.""" - return {COLOR_MODE_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/custom_components/ge_appliances/entities/common/ge_erd_number.py b/custom_components/ge_appliances/entities/common/ge_erd_number.py deleted file mode 100644 index d773ad1..0000000 --- a/custom_components/ge_appliances/entities/common/ge_erd_number.py +++ /dev/null @@ -1,127 +0,0 @@ -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 TEMP_FAHRENHEIT -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 TEMP_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/custom_components/ge_appliances/entities/common/ge_erd_property_binary_sensor.py b/custom_components/ge_appliances/entities/common/ge_erd_property_binary_sensor.py old mode 100644 new mode 100755 index d7504ce..d11bfd8 --- a/custom_components/ge_appliances/entities/common/ge_erd_property_binary_sensor.py +++ b/custom_components/ge_appliances/entities/common/ge_erd_property_binary_sensor.py @@ -1,16 +1,39 @@ -from typing import Optional +from __future__ import annotations +from typing import Optional import magicattr -from gehomesdk import ErdCodeType + +from homeassistant.const import EntityCategory + +from ...api 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) + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_property: str, + name: str, + device_class: str = None, + entity_category: str[EntityCategory] | None = EntityCategory.DIAGNOSTIC, + icon: str = None, + ) -> None: + super().__init__( + api=api, + erd_code=erd_code, + device_class=device_class, + entity_category=entity_category, + icon=icon, + name=name, + ) 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}" @property def is_on(self) -> Optional[bool]: @@ -19,14 +42,7 @@ def is_on(self) -> Optional[bool]: value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) except KeyError: return None + if self._name: + if "Enabled" in self._name: + return bool(self._stringify(value) == "enable") 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/custom_components/ge_appliances/entities/common/ge_erd_property_sensor.py b/custom_components/ge_appliances/entities/common/ge_erd_property_sensor.py old mode 100644 new mode 100755 index 70938d0..771ad1c --- a/custom_components/ge_appliances/entities/common/ge_erd_property_sensor.py +++ b/custom_components/ge_appliances/entities/common/ge_erd_property_sensor.py @@ -1,7 +1,11 @@ -from typing import Optional +from __future__ import annotations import magicattr -from gehomesdk import ErdCode, ErdCodeType, ErdMeasurementUnits, ErdDataType +from typing import Optional + +from homeassistant.const import EntityCategory + +from ...api import ErdCode, ErdCodeType, ErdMeasurementUnits, ErdDataType from ...devices import ApplianceApi from .ge_erd_sensor import GeErdSensor @@ -9,29 +13,34 @@ 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 - ): + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_property: str, + name: str, + device_class: str = None, + entity_category: str[EntityCategory] | None = EntityCategory.DIAGNOSTIC, + icon: str = None, + data_type: ErdDataType = None, + native_unit_of_measurement: str = None, + state_class: str = None, + ) -> 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 + api=api, + erd_code=erd_code, + device_class=device_class, + entity_category=entity_category, + icon=icon, + name=name, + data_type=data_type, + native_unit_of_measurement=native_unit_of_measurement, + state_class=state_class, ) 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}" + return f"{super().unique_id}_{self.erd_property}" @property def native_value(self): @@ -39,7 +48,7 @@ def native_value(self): 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]: + if self.data_type in [ErdDataType.INT, ErdDataType.FLOAT]: return value # otherwise, return a stringified version diff --git a/custom_components/ge_appliances/entities/common/ge_erd_select.py b/custom_components/ge_appliances/entities/common/ge_erd_select.py deleted file mode 100644 index 833dea2..0000000 --- a/custom_components/ge_appliances/entities/common/ge_erd_select.py +++ /dev/null @@ -1,35 +0,0 @@ - -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/custom_components/ge_appliances/entities/common/ge_erd_sensor.py b/custom_components/ge_appliances/entities/common/ge_erd_sensor.py old mode 100644 new mode 100755 index d161fee..68e22a8 --- a/custom_components/ge_appliances/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_appliances/entities/common/ge_erd_sensor.py @@ -1,8 +1,9 @@ +from __future__ import annotations + import logging from typing import Optional -from gehomesdk.erd.erd_data_type import ErdDataType -from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -10,8 +11,11 @@ DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER_FACTOR, TEMP_FAHRENHEIT, + EntityCategory, ) -from gehomesdk import ErdCodeType, ErdCodeClass + +from ...api import ErdCodeType, ErdCodeClass +from ...api.erd.erd_data_type import ErdDataType from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi @@ -23,18 +27,26 @@ class GeErdSensor(GeErdEntity, SensorEntity): 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 + erd_code: ErdCodeType, + name: str, + device_class: str = None, + entity_category: str[EntityCategory] | None = EntityCategory.DIAGNOSTIC, + icon: str = None, + data_type: ErdDataType = None, + native_unit_of_measurement: str = None, + state_class: str = None, + ) -> None: + super().__init__( + api=api, + erd_code=erd_code, + device_class=device_class, + entity_category=entity_category, + icon=icon, + name=name, + ) + self._data_type = data_type + self._native_unit_of_measurement = native_unit_of_measurement + self._state_class = state_class @property def native_value(self): @@ -42,7 +54,7 @@ def native_value(self): 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]: + if self.data_type in [ErdDataType.INT, ErdDataType.FLOAT]: return self._convert_numeric_value_from_device(value) # otherwise, return a stringified version @@ -53,17 +65,9 @@ def native_value(self): 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 + def data_type(self) -> ErdDataType: + if self._data_type is not None: + return self._data_type return self.appliance.get_erd_code_data_type(self.erd_code) @@ -81,83 +85,18 @@ def _temp_units(self) -> Optional[str]: def _convert_numeric_value_from_device(self, value): """Convert to expected data type""" - if self._data_type == ErdDataType.INT: + 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 == DEVICE_CLASS_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 TEMP_FAHRENHEIT - if ( - self.erd_code_class == ErdCodeClass.BATTERY - or self.device_class == DEVICE_CLASS_BATTERY - ): - return "%" - if self.erd_code_class == ErdCodeClass.PERCENTAGE: - return "%" - if self.device_class == DEVICE_CLASS_POWER_FACTOR: - 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 DEVICE_CLASS_TEMPERATURE - if self.erd_code_class == ErdCodeClass.BATTERY: - return DEVICE_CLASS_BATTERY - if self.erd_code_class == ErdCodeClass.POWER: - return DEVICE_CLASS_POWER - if self.erd_code_class == ErdCodeClass.ENERGY: - return DEVICE_CLASS_ENERGY - - return None - - def _get_state_class(self) -> Optional[str]: - if self._state_class_override: - return self._state_class_override - - if self.device_class in [DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_ENERGY]: - return SensorStateClass.MEASUREMENT - if self.erd_code_class in [ErdCodeClass.FLOW_RATE, ErdCodeClass.PERCENTAGE]: - 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() + @property + def native_unit_of_measurement(self) -> Optional[str]: + return self._native_unit_of_measurement + + @property + def state_class(self) -> Optional[str]: + return self._state_class async def set_value(self, value): """Sets the ERD value, assumes that the data type is correct""" diff --git a/custom_components/ge_appliances/entities/common/ge_erd_switch.py b/custom_components/ge_appliances/entities/common/ge_erd_switch.py old mode 100644 new mode 100755 index cf39f9a..2a6890b --- a/custom_components/ge_appliances/entities/common/ge_erd_switch.py +++ b/custom_components/ge_appliances/entities/common/ge_erd_switch.py @@ -1,20 +1,37 @@ +from __future__ import annotations + import logging -from gehomesdk import ErdCodeType from homeassistant.components.switch import SwitchEntity +from homeassistant.const import EntityCategory +from ...api import ErdCodeType from ...devices import ApplianceApi -from .ge_erd_binary_sensor import GeErdBinarySensor from .bool_converter import BoolConverter +from .ge_erd_entity import GeErdEntity _LOGGER = logging.getLogger(__name__) -class GeErdSwitch(GeErdBinarySensor, SwitchEntity): +class GeErdSwitch(GeErdEntity, 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) + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + name: str, + device_class: str = None, + entity_category: str[EntityCategory] | None = EntityCategory.CONFIG, + icon: str = None, + bool_converter: BoolConverter = BoolConverter(), + ) -> None: + super().__init__( + api=api, + erd_code=erd_code, + device_class=device_class, + entity_category=entity_category, + icon=icon, + name=name, + ) self._converter = bool_converter @property diff --git a/custom_components/ge_appliances/entities/common/ge_erd_timer_sensor.py b/custom_components/ge_appliances/entities/common/ge_erd_timer_sensor.py deleted file mode 100644 index 33cdfee..0000000 --- a/custom_components/ge_appliances/entities/common/ge_erd_timer_sensor.py +++ /dev/null @@ -1,30 +0,0 @@ -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.warn("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.warn("Could not clear timer value", exc_info=1) diff --git a/custom_components/ge_appliances/entities/common/ge_water_heater.py b/custom_components/ge_appliances/entities/common/ge_water_heater.py deleted file mode 100644 index 55ae4d9..0000000 --- a/custom_components/ge_appliances/entities/common/ge_water_heater.py +++ /dev/null @@ -1,45 +0,0 @@ -import abc -import logging -from typing import Any, Dict, List, Optional - -from homeassistant.components.water_heater import WaterHeaterEntity -from homeassistant.const import ( - TEMP_FAHRENHEIT, - TEMP_CELSIUS -) -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 TEMP_CELSIUS - return TEMP_FAHRENHEIT - - @property - def supported_features(self): - raise NotImplementedError diff --git a/custom_components/ge_appliances/entities/common/options_converter.py b/custom_components/ge_appliances/entities/common/options_converter.py old mode 100644 new mode 100755 diff --git a/custom_components/ge_appliances/entities/dishwasher/__init__.py b/custom_components/ge_appliances/entities/dishwasher/__init__.py old mode 100644 new mode 100755 diff --git a/custom_components/ge_appliances/entities/dishwasher/ge_dishwasher_control_locked_switch.py b/custom_components/ge_appliances/entities/dishwasher/ge_dishwasher_control_locked_switch.py old mode 100644 new mode 100755 index 55923d8..999bc0c --- a/custom_components/ge_appliances/entities/dishwasher/ge_dishwasher_control_locked_switch.py +++ b/custom_components/ge_appliances/entities/dishwasher/ge_dishwasher_control_locked_switch.py @@ -1,4 +1,4 @@ -from gehomesdk import ErdCode, ErdOperatingMode +from ...api import ErdCode, ErdOperatingMode from ..common import GeErdSwitch diff --git a/custom_components/ge_appliances/entities/fridge/__init__.py b/custom_components/ge_appliances/entities/fridge/__init__.py deleted file mode 100644 index b277fcf..0000000 --- a/custom_components/ge_appliances/entities/fridge/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .ge_fridge import GeFridge -from .ge_freezer import GeFreezer -from .ge_dispenser import GeDispenser -from .convertable_drawer_mode_options import ConvertableDrawerModeOptionsConverter \ No newline at end of file diff --git a/custom_components/ge_appliances/entities/fridge/const.py b/custom_components/ge_appliances/entities/fridge/const.py deleted file mode 100644 index f7ca729..0000000 --- a/custom_components/ge_appliances/entities/fridge/const.py +++ /dev/null @@ -1,19 +0,0 @@ -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) - -ATTR_DOOR_STATUS = "door_status" -GE_FRIDGE_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) - -HEATER_TYPE_FRIDGE = "fridge" -HEATER_TYPE_FREEZER = "freezer" -HEATER_TYPE_DISPENSER = "dispenser" - -# Fridge/Freezer -OP_MODE_OFF = "Off" -OP_MODE_K_CUP = "K-Cup Brewing" -OP_MODE_NORMAL = "Normal" -OP_MODE_SABBATH = "Sabbath Mode" -OP_MODE_TURBO_COOL = "Turbo Cool" -OP_MODE_TURBO_FREEZE = "Turbo Freeze" diff --git a/custom_components/ge_appliances/entities/fridge/convertable_drawer_mode_options.py b/custom_components/ge_appliances/entities/fridge/convertable_drawer_mode_options.py deleted file mode 100644 index b9b933c..0000000 --- a/custom_components/ge_appliances/entities/fridge/convertable_drawer_mode_options.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging -from typing import List, Any, Optional - -from gehomesdk import ErdConvertableDrawerMode -from homeassistant.const import TEMP_FAHRENHEIT -from homeassistant.util.unit_system import UnitSystem -from ..common import OptionsConverter - -_LOGGER = logging.getLogger(__name__) - -_TEMP_MAP = { - ErdConvertableDrawerMode.MEAT: 29, - ErdConvertableDrawerMode.BEVERAGE: 33, - ErdConvertableDrawerMode.SNACK: 37, - ErdConvertableDrawerMode.WINE: 42 -} - -class ConvertableDrawerModeOptionsConverter(OptionsConverter): - def __init__(self, units: UnitSystem): - super().__init__() - self._excluded_options = [ - ErdConvertableDrawerMode.UNKNOWN0, - ErdConvertableDrawerMode.UNKNOWN1, - ErdConvertableDrawerMode.NA - ] - self._units = units - - @property - def options(self) -> 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.warn(f"Could not set hood light level 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.is_metric: - t = self._units.temperature(float(t), TEMP_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/custom_components/ge_appliances/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_appliances/entities/fridge/ge_abstract_fridge.py deleted file mode 100644 index 312a3dd..0000000 --- a/custom_components/ge_appliances/entities/fridge/ge_abstract_fridge.py +++ /dev/null @@ -1,199 +0,0 @@ -"""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, TEMP_FAHRENHEIT -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"], TEMP_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"], TEMP_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/custom_components/ge_appliances/entities/fridge/ge_dispenser.py b/custom_components/ge_appliances/entities/fridge/ge_dispenser.py deleted file mode 100644 index 04bc543..0000000 --- a/custom_components/ge_appliances/entities/fridge/ge_dispenser.py +++ /dev/null @@ -1,126 +0,0 @@ -"""GE Home Sensor Entities - Dispenser""" - -import logging -from typing import List, Optional, Dict, Any - -from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT -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, TEMP_FAHRENHEIT, self.temperature_unit) - - @property - def max_temp(self): - """Return the maximum temperature.""" - return TemperatureConverter.convert(self._max_temp, TEMP_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/custom_components/ge_appliances/entities/fridge/ge_freezer.py b/custom_components/ge_appliances/entities/fridge/ge_freezer.py deleted file mode 100644 index 005dba9..0000000 --- a/custom_components/ge_appliances/entities/fridge/ge_freezer.py +++ /dev/null @@ -1,35 +0,0 @@ -"""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/custom_components/ge_appliances/entities/fridge/ge_fridge.py b/custom_components/ge_appliances/entities/fridge/ge_fridge.py deleted file mode 100644 index e24c3e0..0000000 --- a/custom_components/ge_appliances/entities/fridge/ge_fridge.py +++ /dev/null @@ -1,62 +0,0 @@ -"""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/custom_components/ge_appliances/entities/hood/__init__.py b/custom_components/ge_appliances/entities/hood/__init__.py deleted file mode 100644 index abba26b..0000000 --- a/custom_components/ge_appliances/entities/hood/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .ge_hood_fan_speed import GeHoodFanSpeedSelect -from .ge_hood_light_level import GeHoodLightLevelSelect \ No newline at end of file diff --git a/custom_components/ge_appliances/entities/hood/ge_hood_fan_speed.py b/custom_components/ge_appliances/entities/hood/ge_hood_fan_speed.py deleted file mode 100644 index e38196c..0000000 --- a/custom_components/ge_appliances/entities/hood/ge_hood_fan_speed.py +++ /dev/null @@ -1,46 +0,0 @@ -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.warn(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/custom_components/ge_appliances/entities/hood/ge_hood_light_level.py b/custom_components/ge_appliances/entities/hood/ge_hood_light_level.py deleted file mode 100644 index 52e2516..0000000 --- a/custom_components/ge_appliances/entities/hood/ge_hood_light_level.py +++ /dev/null @@ -1,42 +0,0 @@ -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.warn(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/custom_components/ge_appliances/entities/opal_ice_maker/__init__.py b/custom_components/ge_appliances/entities/opal_ice_maker/__init__.py deleted file mode 100644 index 5ec3f31..0000000 --- a/custom_components/ge_appliances/entities/opal_ice_maker/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .oim_light_level_options import OimLightLevelOptionsConverter \ No newline at end of file diff --git a/custom_components/ge_appliances/entities/opal_ice_maker/oim_light_level_options.py b/custom_components/ge_appliances/entities/opal_ice_maker/oim_light_level_options.py deleted file mode 100644 index 019500d..0000000 --- a/custom_components/ge_appliances/entities/opal_ice_maker/oim_light_level_options.py +++ /dev/null @@ -1,26 +0,0 @@ -import logging -from typing import List, Any, Optional - -from gehomesdk import ErdCodeType, ErdOimLightLevel, ErdCode -from ...devices import ApplianceApi -from ..common import GeErdSelect, OptionsConverter - -_LOGGER = logging.getLogger(__name__) - -class OimLightLevelOptionsConverter(OptionsConverter): - @property - def options(self) -> List[str]: - return [i.stringify() for i in ErdOimLightLevel] - def from_option_string(self, value: str) -> Any: - try: - return ErdOimLightLevel[value.upper()] - except: - _LOGGER.warn(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/custom_components/ge_appliances/entities/oven/__init__.py b/custom_components/ge_appliances/entities/oven/__init__.py deleted file mode 100644 index e4166e8..0000000 --- a/custom_components/ge_appliances/entities/oven/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .ge_oven import GeOven -from .ge_oven_light_level_select import GeOvenLightLevelSelect -from .const import UPPER_OVEN, LOWER_OVEN \ No newline at end of file diff --git a/custom_components/ge_appliances/entities/oven/const.py b/custom_components/ge_appliances/entities/oven/const.py deleted file mode 100644 index af9d602..0000000 --- a/custom_components/ge_appliances/entities/oven/const.py +++ /dev/null @@ -1,50 +0,0 @@ -import bidict - -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) -from gehomesdk import ErdOvenCookMode - -SUPPORT_NONE = 0 -GE_OVEN_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) - -OP_MODE_OFF = "Off" -OP_MODE_BAKE = "Bake" -OP_MODE_CONVMULTIBAKE = "Conv. Multi-Bake" -OP_MODE_CONVBAKE = "Convection Bake" -OP_MODE_CONVROAST = "Convection Roast" -OP_MODE_COOK_UNK = "Unknown" -OP_MODE_PIZZA = "Frozen Pizza" -OP_MODE_FROZEN_SNACKS = "Frozen Snacks" -OP_MODE_BAKED_GOODS = "Baked Goods" -OP_MODE_FROZEN_PIZZA_MULTI = "Frozen Pizza Multi" -OP_MODE_FROZEN_SNACKS_MULTI = "Frozen Snacks Multi" -OP_MODE_BROIL_HIGH = "Broil High" -OP_MODE_BROIL_LOW = "Broil Low" -OP_MODE_PROOF = "Proof" -OP_MODE_WARM = "Warm" - -OP_MODE_AIRFRY = "Air Fry" - -UPPER_OVEN = "UPPER_OVEN" -LOWER_OVEN = "LOWER_OVEN" - -COOK_MODE_OP_MAP = bidict.bidict({ - ErdOvenCookMode.NOMODE: OP_MODE_OFF, - ErdOvenCookMode.CONVMULTIBAKE_NOOPTION: OP_MODE_CONVMULTIBAKE, - ErdOvenCookMode.CONVBAKE_NOOPTION: OP_MODE_CONVBAKE, - ErdOvenCookMode.CONVROAST_NOOPTION: OP_MODE_CONVROAST, - ErdOvenCookMode.BROIL_LOW: OP_MODE_BROIL_LOW, - ErdOvenCookMode.BROIL_HIGH: OP_MODE_BROIL_HIGH, - ErdOvenCookMode.BAKE_NOOPTION: OP_MODE_BAKE, - ErdOvenCookMode.PROOF_NOOPTION: OP_MODE_PROOF, - ErdOvenCookMode.WARM_NOOPTION: OP_MODE_WARM, - ErdOvenCookMode.FROZEN_PIZZA: OP_MODE_PIZZA, - ErdOvenCookMode.FROZEN_SNACKS: OP_MODE_FROZEN_SNACKS, - ErdOvenCookMode.BAKED_GOODS: OP_MODE_BAKED_GOODS, - ErdOvenCookMode.FROZEN_PIZZA_MULTI: OP_MODE_FROZEN_PIZZA_MULTI, - ErdOvenCookMode.FROZEN_SNACKS_MULTI: OP_MODE_FROZEN_SNACKS_MULTI, - ErdOvenCookMode.AIRFRY: OP_MODE_AIRFRY -}) - diff --git a/custom_components/ge_appliances/entities/oven/ge_oven.py b/custom_components/ge_appliances/entities/oven/ge_oven.py deleted file mode 100644 index 297b2c1..0000000 --- a/custom_components/ge_appliances/entities/oven/ge_oven.py +++ /dev/null @@ -1,229 +0,0 @@ -"""GE Home Sensor Entities - Oven""" -import logging -from typing import Any, Dict, List, Optional, Set - -from gehomesdk import ( - ErdCode, - ErdMeasurementUnits, - ErdOvenCookMode, - OVEN_COOK_MODE_MAP, - OvenCookSetting -) - -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from ...const import DOMAIN -from ...devices import ApplianceApi -from ..common import GeAbstractWaterHeater -from .const import * - -_LOGGER = logging.getLogger(__name__) - -class GeOven(GeAbstractWaterHeater): - """GE Appliance Oven""" - - icon = "mdi:stove" - - def __init__(self, api: ApplianceApi, oven_select: str = UPPER_OVEN, two_cavity: bool = False, temperature_erd_code: str = "RAW_TEMPERATURE"): - if oven_select not in (UPPER_OVEN, LOWER_OVEN): - raise ValueError(f"Invalid `oven_select` value ({oven_select})") - - self._oven_select = oven_select - self._two_cavity = two_cavity - self._temperature_erd_code = temperature_erd_code - super().__init__(api) - - @property - def supported_features(self): - if self.remote_enabled: - return GE_OVEN_SUPPORT - else: - return SUPPORT_NONE - - @property - def unique_id(self) -> 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 TEMP_CELSIUS - return TEMP_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 == TEMP_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/custom_components/ge_appliances/entities/oven/ge_oven_light_level_select.py b/custom_components/ge_appliances/entities/oven/ge_oven_light_level_select.py deleted file mode 100644 index 8c63973..0000000 --- a/custom_components/ge_appliances/entities/oven/ge_oven_light_level_select.py +++ /dev/null @@ -1,66 +0,0 @@ -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.warn(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/custom_components/ge_appliances/entities/water_filter/__init__.py b/custom_components/ge_appliances/entities/water_filter/__init__.py deleted file mode 100644 index 1d37958..0000000 --- a/custom_components/ge_appliances/entities/water_filter/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .filter_position import GeErdFilterPositionSelect diff --git a/custom_components/ge_appliances/entities/water_filter/filter_position.py b/custom_components/ge_appliances/entities/water_filter/filter_position.py deleted file mode 100644 index a94bdc8..0000000 --- a/custom_components/ge_appliances/entities/water_filter/filter_position.py +++ /dev/null @@ -1,63 +0,0 @@ -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.warn(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/custom_components/ge_appliances/entities/water_heater/__init__.py b/custom_components/ge_appliances/entities/water_heater/__init__.py deleted file mode 100644 index c0fa79f..0000000 --- a/custom_components/ge_appliances/entities/water_heater/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .heater_modes import WhHeaterModeConverter -from .ge_water_heater import GeWaterHeater \ No newline at end of file diff --git a/custom_components/ge_appliances/entities/water_heater/ge_water_heater.py b/custom_components/ge_appliances/entities/water_heater/ge_water_heater.py deleted file mode 100644 index 7954055..0000000 --- a/custom_components/ge_appliances/entities/water_heater/ge_water_heater.py +++ /dev/null @@ -1,89 +0,0 @@ -"""GE Home Sensor Entities - Oven""" -import logging -from typing import List, Optional - -from gehomesdk import ( - ErdCode, - ErdWaterHeaterMode -) - -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) - -from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT -from ...devices import ApplianceApi -from ..common import GeAbstractWaterHeater -from .heater_modes import WhHeaterModeConverter - -_LOGGER = logging.getLogger(__name__) - -class GeWaterHeater(GeAbstractWaterHeater): - """GE Whole Home Water Heater""" - - icon = "mdi:water-boiler" - - def __init__(self, api: ApplianceApi): - super().__init__(api) - self._modes_converter = WhHeaterModeConverter() - - @property - def heater_type(self) -> str: - return "heater" - - @property - def supported_features(self): - return (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) - - @property - def temperature_unit(self): - return TEMP_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/custom_components/ge_appliances/entities/water_heater/heater_modes.py b/custom_components/ge_appliances/entities/water_heater/heater_modes.py deleted file mode 100644 index cd2e39a..0000000 --- a/custom_components/ge_appliances/entities/water_heater/heater_modes.py +++ /dev/null @@ -1,26 +0,0 @@ -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.warn(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/custom_components/ge_appliances/entities/water_softener/__init__.py b/custom_components/ge_appliances/entities/water_softener/__init__.py deleted file mode 100644 index 7ae738e..0000000 --- a/custom_components/ge_appliances/entities/water_softener/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .shutoff_position import GeErdShutoffPositionSelect \ No newline at end of file diff --git a/custom_components/ge_appliances/entities/water_softener/shutoff_position.py b/custom_components/ge_appliances/entities/water_softener/shutoff_position.py deleted file mode 100644 index 38b0929..0000000 --- a/custom_components/ge_appliances/entities/water_softener/shutoff_position.py +++ /dev/null @@ -1,65 +0,0 @@ -import logging -from typing import List, Any, Optional - -from gehomesdk import ErdCodeType, ErdWaterSoftenerShutoffValveState, ErdCode -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 ErdWaterSoftenerShutoffValveState - if i not in [ErdWaterSoftenerShutoffValveState.UNKNOWN, ErdWaterSoftenerShutoffValveState.TRANSITION]] - def from_option_string(self, value: str) -> Any: - try: - return ErdWaterSoftenerShutoffValveState[value.upper()] - except: - _LOGGER.warn(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/custom_components/ge_appliances/exceptions.py b/custom_components/ge_appliances/exceptions.py old mode 100644 new mode 100755 diff --git a/custom_components/ge_appliances/light.py b/custom_components/ge_appliances/light.py deleted file mode 100644 index b652d02..0000000 --- a/custom_components/ge_appliances/light.py +++ /dev/null @@ -1,40 +0,0 @@ -"""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) - - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_appliances/manifest.json b/custom_components/ge_appliances/manifest.json old mode 100644 new mode 100755 index 49a425b..9ba59bb --- a/custom_components/ge_appliances/manifest.json +++ b/custom_components/ge_appliances/manifest.json @@ -10,7 +10,6 @@ "issue_tracker": "https://github.com/schmittx/home-assistant-ge-appliances/issues", "quality_scale": "gold", "requirements": [ - "gehomesdk==0.5.10", "magicattr==0.1.6", "slixmpp==1.8.3" ], diff --git a/custom_components/ge_appliances/number.py b/custom_components/ge_appliances/number.py deleted file mode 100644 index 2b4213c..0000000 --- a/custom_components/ge_appliances/number.py +++ /dev/null @@ -1,37 +0,0 @@ -"""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} unregisterd numbers') - async_add_entities(entities) - - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_appliances/select.py b/custom_components/ge_appliances/select.py deleted file mode 100644 index 613b03b..0000000 --- a/custom_components/ge_appliances/select.py +++ /dev/null @@ -1,40 +0,0 @@ -"""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) - - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_appliances/sensor.py b/custom_components/ge_appliances/sensor.py old mode 100644 new mode 100755 index 9732dbf..361fe62 --- a/custom_components/ge_appliances/sensor.py +++ b/custom_components/ge_appliances/sensor.py @@ -1,21 +1,13 @@ -"""GE Home Sensor Entities""" +"""GE Appliances 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 homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ( - DOMAIN, - SERVICE_SET_TIMER, - SERVICE_CLEAR_TIMER, - SERVICE_SET_INT_VALUE -) +from .const import DOMAIN from .entities import GeErdSensor from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator @@ -25,15 +17,13 @@ _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') + """GE Appliances sensors.""" + _LOGGER.debug('Adding GE Appliances 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') @@ -48,33 +38,3 @@ def async_devices_discovered(apis: list[ApplianceApi]): async_add_entities(entities) 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/custom_components/ge_appliances/services.yaml b/custom_components/ge_appliances/services.yaml deleted file mode 100644 index 4e8a83f..0000000 --- a/custom_components/ge_appliances/services.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# GE Home Services - -set_timer: - name: Set Timer - description: Sets a timer value (timespan) - target: - entity: - integration: "ge_appliances" - 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_appliances" - 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_appliances" - 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/custom_components/ge_appliances/strings.json b/custom_components/ge_appliances/strings.json old mode 100644 new mode 100755 index 7cfb731..2e087f2 --- a/custom_components/ge_appliances/strings.json +++ b/custom_components/ge_appliances/strings.json @@ -17,5 +17,54 @@ "abort": { "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" } + }, + "entity": { + "sensor": { + "all": { + "state": { + "auto": "Auto", + "autosense": "AutoSense", + "boost": "Boost", + "boost_and_sanitize": "Boost and Sanitize", + "both": "Both", + "cycle_active": "Cycle Active", + "delay_start": "Delay Start", + "delicate": "Delicate", + "disabled": "Disabled", + "drying": "Drying", + "eco": "Eco", + "eoc": "End of Cycle", + "heavy_heavy": "Heavy Heavy", + "heavy_max_full_d": "Heavy Max Full D", + "heavy_super_dry": "Heavy Super Dry", + "heavy_pw_pulsing": "Heavy PW Pulsing", + "heavy_pw2_(vs": "Heavy PW2 (VS", + "heavy_pw3_(non-": "Heavy PW3 (Non-", + "heavy_sani_fr": "Heavy Sani Fr", + "heavy_steam_dry": "Heavy Steam Dry", + "heavy_steam_or_p": "Heavy Steam or P", + "inactive": "Inactive", + "intense": "Intense", + "light_super_dry": "Light Super Dry", + "lower": "Lower", + "low_power": "Low Power", + "main_wash": "Main Wash", + "max_dry": "Max Dry", + "normal": "Normal", + "normal_super_dry": "Normal Super Dry", + "pause": "Pause", + "power_dry": "Power Dry", + "pre_wash": "Pre-Wash", + "sanitize": "Sanitize", + "sanitizing": "Sanitizing", + "sensing": "Sensong", + "rinse": "Rinse", + "rinsing": "Rinsing", + "standby": "Standby", + "thirty_min": "Thirty Minute", + "upper": "Upper" + } + } + } } } diff --git a/custom_components/ge_appliances/switch.py b/custom_components/ge_appliances/switch.py old mode 100644 new mode 100755 index 3aa6f11..b42b311 --- a/custom_components/ge_appliances/switch.py +++ b/custom_components/ge_appliances/switch.py @@ -1,4 +1,4 @@ -"""GE Home Switch Entities""" +"""GE Appliances Switch Entities""" import logging from typing import Callable @@ -15,8 +15,8 @@ _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') + """GE Appliances sensors.""" + _LOGGER.debug('Adding GE Appliances switches') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] registry = er.async_get(hass) diff --git a/custom_components/ge_appliances/translations/en.json b/custom_components/ge_appliances/translations/en.json old mode 100644 new mode 100755 index 50680ea..ac8c875 --- a/custom_components/ge_appliances/translations/en.json +++ b/custom_components/ge_appliances/translations/en.json @@ -1,5 +1,5 @@ { - "title": "GE Home", + "title": "GE Appliances", "config": { "step": { "init": { @@ -25,5 +25,54 @@ "abort": { "already_configured_account": "Account is already configured" } + }, + "entity": { + "sensor": { + "all": { + "state": { + "auto": "Auto", + "autosense": "AutoSense", + "boost": "Boost", + "boost_and_sanitize": "Boost and Sanitize", + "both": "Both", + "cycle_active": "Cycle Active", + "delay_start": "Delay Start", + "delicate": "Delicate", + "disabled": "Disabled", + "drying": "Drying", + "eco": "Eco", + "eoc": "End of Cycle", + "heavy_heavy": "Heavy Heavy", + "heavy_max_full_d": "Heavy Max Full D", + "heavy_super_dry": "Heavy Super Dry", + "heavy_pw_pulsing": "Heavy PW Pulsing", + "heavy_pw2_(vs": "Heavy PW2 (VS", + "heavy_pw3_(non-": "Heavy PW3 (Non-", + "heavy_sani_fr": "Heavy Sani Fr", + "heavy_steam_dry": "Heavy Steam Dry", + "heavy_steam_or_p": "Heavy Steam or P", + "inactive": "Inactive", + "intense": "Intense", + "light_super_dry": "Light Super Dry", + "lower": "Lower", + "low_power": "Low Power", + "main_wash": "Main Wash", + "max_dry": "Max Dry", + "normal": "Normal", + "normal_super_dry": "Normal Super Dry", + "pause": "Pause", + "power_dry": "Power Dry", + "pre_wash": "Pre-Wash", + "sanitize": "Sanitize", + "sanitizing": "Sanitizing", + "sensing": "Sensong", + "rinse": "Rinse", + "rinsing": "Rinsing", + "standby": "Standby", + "thirty_min": "Thirty Minute", + "upper": "Upper" + } + } + } } } diff --git a/custom_components/ge_appliances/update_coordinator.py b/custom_components/ge_appliances/update_coordinator.py old mode 100644 new mode 100755 index 6723ccf..d592e97 --- a/custom_components/ge_appliances/update_coordinator.py +++ b/custom_components/ge_appliances/update_coordinator.py @@ -1,11 +1,11 @@ -"""Data update coordinator for GE Home Appliances""" +"""Data update coordinator for GE Appliances Appliances""" import asyncio import async_timeout import logging from typing import Any, Dict, Iterable, Optional, Tuple -from gehomesdk import ( +from .api import ( EVENT_APPLIANCE_INITIAL_UPDATE, EVENT_APPLIANCE_UPDATE_RECEIVED, EVENT_CONNECTED, @@ -13,9 +13,11 @@ EVENT_GOT_APPLIANCE_LIST, ErdCodeType, GeAppliance, + GeAuthFailedError, + GeGeneralServerError, + GeNotAuthenticatedError, GeWebsocketClient, ) -from gehomesdk import GeAuthFailedError, GeGeneralServerError, GeNotAuthenticatedError from .exceptions import HaAuthError, HaCannotConnect from homeassistant.config_entries import ConfigEntry @@ -35,12 +37,16 @@ ) from .devices import ApplianceApi, get_appliance_api_type -PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater", "select", "climate", "light", "button", "number"] +PLATFORMS = [ + "binary_sensor", + "sensor", + "switch", +] _LOGGER = logging.getLogger(__name__) class GeHomeUpdateCoordinator(DataUpdateCoordinator): - """Define a wrapper class to update GE Home data.""" + """Define a wrapper class to update GE Appliances data.""" def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Set up the GeHomeUpdateCoordinator class.""" diff --git a/custom_components/ge_appliances/water_heater.py b/custom_components/ge_appliances/water_heater.py deleted file mode 100644 index 19e8b4d..0000000 --- a/custom_components/ge_appliances/water_heater.py +++ /dev/null @@ -1,38 +0,0 @@ -"""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) - - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)