From df8309df863152ea55ad45cdf64cb729aabc41f2 Mon Sep 17 00:00:00 2001 From: Jack Simbah Date: Thu, 17 Dec 2020 21:20:43 -0500 Subject: [PATCH 001/338] - refactored appliance entities into subdirectory --- ge_kitchen/appliance_api.py | 27 +- ge_kitchen/appliance_entity_types/__init__.py | 4 + .../abstract_fridge_entity.py | 210 ++++++++ .../appliance_entity_types/freezer_entity.py | 35 ++ .../appliance_entity_types/fridge_entity.py | 66 +++ .../fridge_water_heater_entity.py | 101 ++++ .../appliance_entity_types/oven_entity.py | 220 ++++++++ ge_kitchen/water_heater.py | 509 ------------------ 8 files changed, 659 insertions(+), 513 deletions(-) create mode 100644 ge_kitchen/appliance_entity_types/__init__.py create mode 100644 ge_kitchen/appliance_entity_types/abstract_fridge_entity.py create mode 100644 ge_kitchen/appliance_entity_types/freezer_entity.py create mode 100644 ge_kitchen/appliance_entity_types/fridge_entity.py create mode 100644 ge_kitchen/appliance_entity_types/fridge_water_heater_entity.py create mode 100644 ge_kitchen/appliance_entity_types/oven_entity.py diff --git a/ge_kitchen/appliance_api.py b/ge_kitchen/appliance_api.py index f91b6b3..57d0ec8 100644 --- a/ge_kitchen/appliance_api.py +++ b/ge_kitchen/appliance_api.py @@ -17,7 +17,7 @@ from .entities import GeErdEntity from .sensor import GeErdSensor from .switch import GeErdSwitch -from .water_heater import ( +from .appliance_entity_types import ( GeFreezerEntity, GeFridgeEntity, GeOvenHeaterEntity, @@ -25,18 +25,18 @@ UPPER_OVEN, ) - - - _LOGGER = logging.getLogger(__name__) def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: + _LOGGER.debug(f"Found device type: {appliance_type}") """Get the appropriate appliance type""" if appliance_type == ErdApplianceType.OVEN: return OvenApi if appliance_type == ErdApplianceType.FRIDGE: return FridgeApi + if appliance_type == ErdApplianceType.DISH_WASHER: + return DishwasherApi # Fallback return ApplianceApi @@ -175,3 +175,22 @@ def get_all_entities(self) -> List[Entity]: ] entities = base_entities + fridge_entities return entities + +class DishwasherApi(ApplianceApi): + """API class for oven objects""" + APPLIANCE_TYPE = ErdApplianceType.DISH_WASHER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + dishwasher_entities = [ + GeErdSensor(self, ErdCode.CYCLE_NAME), + GeErdSensor(self, ErdCode.CYCLE_STATE), + GeErdSensor(self, ErdCode.OPERATING_MODE), + GeErdSensor(self, ErdCode.PODS_REMAINING_VALUE), + GeErdSensor(self, ErdCode.RINSE_AGENT), + GeErdSensor(self, ErdCode.SOUND), + GeErdSensor(self, ErdCode.TIME_REMAINING), + ] + entities = base_entities + dishwasher_entities + return entities diff --git a/ge_kitchen/appliance_entity_types/__init__.py b/ge_kitchen/appliance_entity_types/__init__.py new file mode 100644 index 0000000..26dbf14 --- /dev/null +++ b/ge_kitchen/appliance_entity_types/__init__.py @@ -0,0 +1,4 @@ +from .freezer_entity import GeFreezerEntity +from .fridge_entity import GeFridgeEntity +from .fridge_water_heater_entity import GeFridgeWaterHeater +from .oven_entity import GeOvenHeaterEntity, UPPER_OVEN, LOWER_OVEN diff --git a/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py b/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py new file mode 100644 index 0000000..5486f61 --- /dev/null +++ b/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py @@ -0,0 +1,210 @@ +"""GE Kitchen Sensor Entities - Abstract Fridge""" +import sys +import os +import abc +import async_timeout +from datetime import timedelta +import logging +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING + +sys.path.append(os.getcwd() + '/..') + +from bidict import bidict +from gekitchen import ( + ErdCode, + ErdOnOff, + ErdDoorStatus, + ErdFilterStatus, + ErdFullNotFull, + ErdHotWaterStatus, + ErdMeasurementUnits, + ErdPodStatus +) +from gekitchen.erd_types import ( + FridgeDoorStatus, + FridgeSetPointLimits, + FridgeSetPoints, + FridgeIceBucketStatus, + HotWaterStatus, + IceMakerControlStatus +) + +from homeassistant.components.water_heater import ( + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterEntity, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT + +from ..entities import GeEntity, stringify_erd_value +from ..const import DOMAIN + +if TYPE_CHECKING: + from ..appliance_api import ApplianceApi + from ..update_coordinator import GeKitchenUpdateCoordinator + +ATTR_DOOR_STATUS = "door_status" +GE_FRIDGE_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) +HEATER_TYPE_FRIDGE = "fridge" +HEATER_TYPE_FREEZER = "freezer" + +# Fridge/Freezer +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" + +_LOGGER = logging.getLogger(__name__) + +class GeAbstractFridgeEntity(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): + """Mock a fridge or freezer as a water heater.""" + + @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]: + return [OP_MODE_NORMAL, OP_MODE_SABBATH, self.turbo_mode] + + @property + def unique_id(self) -> str: + return f"{self.serial_number}-{self.heater_type}" + + @property + def name(self) -> Optional[str]: + return f"GE {self.heater_type.title()} {self.serial_number}" + + @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 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.""" + 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 + + 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.""" + return getattr(self.setpoint_limits, f"{self.heater_type}_min") + + @property + def max_temp(self): + """Return the maximum temperature.""" + return getattr(self.setpoint_limits, f"{self.heater_type}_max") + + @property + def current_operation(self) -> str: + """Get the current operation mode.""" + if self.appliance.get_erd_value(ErdCode.SABBATH_MODE): + return OP_MODE_SABBATH + if self.appliance.get_erd_value(self.turbo_erd_code): + return self.turbo_mode + 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 = {} + + 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"] = ice_bucket_status.name.replace("_", " ").title() + + 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"] = ice_control_status.name.replace("_", " ").lower() + + 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]: + """State attributes to be optionally overridden in subclasses.""" + return {} + + @property + def device_state_attributes(self) -> Dict[str, Any]: + door_attrs = self.door_state_attrs + ice_maker_attrs = self.ice_maker_state_attrs + other_attrs = self.other_state_attrs + return {**door_attrs, **ice_maker_attrs, **other_attrs} diff --git a/ge_kitchen/appliance_entity_types/freezer_entity.py b/ge_kitchen/appliance_entity_types/freezer_entity.py new file mode 100644 index 0000000..6fe182e --- /dev/null +++ b/ge_kitchen/appliance_entity_types/freezer_entity.py @@ -0,0 +1,35 @@ +"""GE Kitchen Sensor Entities - Freezer""" +import logging +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING + +from gekitchen import ( + ErdCode, + ErdDoorStatus +) + +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from ..entities import GeEntity +from .abstract_fridge_entity import ( + ATTR_DOOR_STATUS, + HEATER_TYPE_FREEZER, + OP_MODE_TURBO_FREEZE, + GeAbstractFridgeEntity +) + +_LOGGER = logging.getLogger(__name__) + +class GeFreezerEntity(GeAbstractFridgeEntity): + """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]]: + door_status = self.door_status.freezer + if door_status and door_status != ErdDoorStatus.NA: + return {ATTR_DOOR_STATUS: door_status.name.title()} + return {} + diff --git a/ge_kitchen/appliance_entity_types/fridge_entity.py b/ge_kitchen/appliance_entity_types/fridge_entity.py new file mode 100644 index 0000000..615b558 --- /dev/null +++ b/ge_kitchen/appliance_entity_types/fridge_entity.py @@ -0,0 +1,66 @@ +"""GE Kitchen Sensor Entities - Fridge""" +import logging +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING + +from gekitchen import ( + ErdCode, + ErdDoorStatus, + ErdFilterStatus +) + +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from ..entities import GeEntity +from .abstract_fridge_entity import ( + ATTR_DOOR_STATUS, + HEATER_TYPE_FRIDGE, + OP_MODE_TURBO_COOL, + GeAbstractFridgeEntity +) + +_LOGGER = logging.getLogger(__name__) + +class GeFridgeEntity(GeAbstractFridgeEntity): + heater_type = HEATER_TYPE_FRIDGE + turbo_erd_code = ErdCode.TURBO_COOL_STATUS + turbo_mode = OP_MODE_TURBO_COOL + icon = "mdi:fridge-bottom" + + @property + def available(self) -> bool: + available = super().available + if not available: + app = self.appliance + _LOGGER.critical(f"{self.name} unavailable. Appliance info: Availaible - {app._available} and Init - {app.initialized}") + return available + + @property + def other_state_attrs(self) -> Dict[str, Any]: + """Water filter state.""" + filter_status: ErdFilterStatus = self.appliance.get_erd_value(ErdCode.WATER_FILTER_STATUS) + if filter_status == ErdFilterStatus.NA: + return {} + return {"water_filter_status": filter_status.name.replace("_", " ").title()} + + @property + def door_state_attrs(self) -> Dict[str, Any]: + """Get state attributes for the doors.""" + 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.fridge_left.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 diff --git a/ge_kitchen/appliance_entity_types/fridge_water_heater_entity.py b/ge_kitchen/appliance_entity_types/fridge_water_heater_entity.py new file mode 100644 index 0000000..68b2bd2 --- /dev/null +++ b/ge_kitchen/appliance_entity_types/fridge_water_heater_entity.py @@ -0,0 +1,101 @@ +"""GE Kitchen Sensor Entities - Fridge Water Heater""" +import sys +import os +import abc +import async_timeout +from datetime import timedelta +import logging +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING + +sys.path.append(os.getcwd() + '/..') + +from bidict import bidict +from gekitchen import ( + ErdCode, + ErdPresent, + ErdMeasurementUnits, + ErdPodStatus +) +from gekitchen.erd_types import ( + HotWaterStatus +) + +from homeassistant.components.water_heater import ( + WaterHeaterEntity, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from ..entities import GeEntity +from .abstract_fridge_entity import ( + OP_MODE_K_CUP, + OP_MODE_NORMAL, + OP_MODE_SABBATH, + GeAbstractFridgeEntity +) + +_LOGGER = logging.getLogger(__name__) + +class GeFridgeWaterHeater(GeEntity, WaterHeaterEntity): + """Entity for in-fridge water heaters""" + + # These values are from FridgeHotWaterFragment.smali in the android app + min_temp = 90 + max_temp = 185 + + @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 unique_id(self) -> str: + """Make a unique id.""" + return f"{self.serial_number}-fridge-hot-water" + + @property + def name(self) -> Optional[str]: + """Name it reasonably.""" + return f"GE Fridge Water Heater {self.serial_number}" + + @property + def temperature_unit(self): + """Select the appropriate temperature unit.""" + measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + if measurement_system == ErdMeasurementUnits.METRIC: + return TEMP_CELSIUS + return TEMP_FAHRENHEIT + + @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] + if self.supports_k_cups: + ops_list.append(OP_MODE_K_CUP) + return ops_list + + async def async_set_temperature(self, **kwargs): + pass + + async def async_set_operation_mode(self, operation_mode): + pass + + @property + def supported_features(self): + pass + + @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 diff --git a/ge_kitchen/appliance_entity_types/oven_entity.py b/ge_kitchen/appliance_entity_types/oven_entity.py new file mode 100644 index 0000000..70f9a64 --- /dev/null +++ b/ge_kitchen/appliance_entity_types/oven_entity.py @@ -0,0 +1,220 @@ +"""GE Kitchen Sensor Entities - Oven""" +import sys +import os +import logging +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING + +sys.path.append(os.getcwd() + '/..') + +from bidict import bidict +from gekitchen import ( + ErdCode, + ErdMeasurementUnits, + ErdOvenCookMode, + OVEN_COOK_MODE_MAP, +) +from gekitchen.erd_types import ( + OvenCookMode, + OvenCookSetting, +) + +from homeassistant.components.water_heater import ( + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterEntity, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from ..entities import GeEntity, stringify_erd_value + +if TYPE_CHECKING: + from ..appliance_api import ApplianceApi + from ..update_coordinator import GeKitchenUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +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" + +UPPER_OVEN = "UPPER_OVEN" +LOWER_OVEN = "LOWER_OVEN" + +COOK_MODE_OP_MAP = 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.BAKE_NOOPTION: OP_MODE_BAKE, +}) + +class GeOvenHeaterEntity(GeEntity, WaterHeaterEntity): + """Water Heater entity for ovens""" + + icon = "mdi:stove" + + def __init__(self, api: "ApplianceApi", oven_select: str = UPPER_OVEN, two_cavity: bool = False): + 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 + super().__init__(api) + + @property + def supported_features(self): + return GE_OVEN_SUPPORT + + @property + def unique_id(self) -> str: + return f"{self.serial_number}-{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"GE {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 current_temperature(self) -> Optional[int]: + current_temp = self.get_erd_value("DISPLAY_TEMPERATURE") + if current_temp: + return current_temp + return self.get_erd_value("RAW_TEMPERATURE") + + @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]: + erd_code = self.get_erd_code("AVAILABLE_COOK_MODES") + cook_modes: Set[ErdOvenCookMode] = self.appliance.get_erd_value(erd_code) + 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 stringify_erd_value(erd_code, erd_value, self.temperature_unit) + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + probe_present = self.get_erd_value("PROBE_PRESENT") + data = { + "display_state": self.display_state, + "probe_present": probe_present, + "raw_temperature": self.get_erd_value("RAW_TEMPERATURE"), + } + if probe_present: + data["probe_temperature"] = self.get_erd_value("PROBE_DISPLAY_TEMP") + elapsed_time = self.get_erd_value("ELAPSED_COOK_TIME") + cook_time_left = self.get_erd_value("COOK_TIME_REMAINING") + kitchen_timer = self.get_erd_value("KITCHEN_TIMER") + delay_time = self.get_erd_value("DELAY_TIME_REMAINING") + if elapsed_time: + data["cook_time_elapsed"] = str(elapsed_time) + if cook_time_left: + data["cook_time_left"] = str(cook_time_left) + if kitchen_timer: + data["cook_time_remaining"] = str(kitchen_timer) + if delay_time: + data["delay_time_remaining"] = str(delay_time) + return data diff --git a/ge_kitchen/water_heater.py b/ge_kitchen/water_heater.py index 2d60c64..c14f584 100644 --- a/ge_kitchen/water_heater.py +++ b/ge_kitchen/water_heater.py @@ -5,38 +5,12 @@ import logging from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING -from bidict import bidict -from gekitchen import ( - ErdCode, - ErdDoorStatus, - ErdFilterStatus, - ErdFullNotFull, - ErdHotWaterStatus, - ErdMeasurementUnits, - ErdOnOff, - ErdOvenCookMode, - ErdPodStatus, - ErdPresent, - OVEN_COOK_MODE_MAP, -) -from gekitchen.erd_types import ( - FridgeDoorStatus, - FridgeSetPointLimits, - FridgeSetPoints, - FridgeIceBucketStatus, - HotWaterStatus, - IceMakerControlStatus, - OvenCookMode, - OvenCookSetting, -) - from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, WaterHeaterEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant from .entities import GeEntity, stringify_erd_value @@ -48,489 +22,6 @@ _LOGGER = logging.getLogger(__name__) -ATTR_DOOR_STATUS = "door_status" -GE_FRIDGE_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) -HEATER_TYPE_FRIDGE = "fridge" -HEATER_TYPE_FREEZER = "freezer" - -# Fridge/Freezer -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" - -# Oven -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" - -UPPER_OVEN = "UPPER_OVEN" -LOWER_OVEN = "LOWER_OVEN" - -COOK_MODE_OP_MAP = 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.BAKE_NOOPTION: OP_MODE_BAKE, -}) - - -class GeAbstractFridgeEntity(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): - """Mock a fridge or freezer as a water heater.""" - - @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]: - return [OP_MODE_NORMAL, OP_MODE_SABBATH, self.turbo_mode] - - @property - def unique_id(self) -> str: - return f"{self.serial_number}-{self.heater_type}" - - @property - def name(self) -> Optional[str]: - return f"GE {self.heater_type.title()} {self.serial_number}" - - @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 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.""" - 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 - - 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.""" - return getattr(self.setpoint_limits, f"{self.heater_type}_min") - - @property - def max_temp(self): - """Return the maximum temperature.""" - return getattr(self.setpoint_limits, f"{self.heater_type}_max") - - @property - def current_operation(self) -> str: - """Get ther current operation mode.""" - if self.appliance.get_erd_value(ErdCode.SABBATH_MODE): - return OP_MODE_SABBATH - if self.appliance.get_erd_value(self.turbo_erd_code): - return self.turbo_mode - 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 = {} - - 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"] = ice_bucket_status.name.replace("_", " ").title() - - 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"] = ice_control_status.name.replace("_", " ").lower() - - 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]: - """State attributes to be optionally overridden in subclasses.""" - return {} - - @property - def device_state_attributes(self) -> Dict[str, Any]: - door_attrs = self.door_state_attrs - ice_maker_attrs = self.ice_maker_state_attrs - other_attrs = self.other_state_attrs - return {**door_attrs, **ice_maker_attrs, **other_attrs} - - -class GeFridgeEntity(GeAbstractFridgeEntity): - heater_type = HEATER_TYPE_FRIDGE - turbo_erd_code = ErdCode.TURBO_COOL_STATUS - turbo_mode = OP_MODE_TURBO_COOL - icon = "mdi:fridge-bottom" - - @property - def available(self) -> bool: - available = super().available - if not available: - app = self.appliance - _LOGGER.critical(f"{self.name} unavailable. Appliance info: Availaible - {app._available} and Init - {app.initialized}") - return available - - @property - def other_state_attrs(self) -> Dict[str, Any]: - """Water filter state.""" - filter_status: ErdFilterStatus = self.appliance.get_erd_value(ErdCode.WATER_FILTER_STATUS) - if filter_status == ErdFilterStatus.NA: - return {} - return {"water_filter_status": filter_status.name.replace("_", " ").title()} - - @property - def door_state_attrs(self) -> Dict[str, Any]: - """Get state attributes for the doors.""" - 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.fridge_left.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 - - -class GeFreezerEntity(GeAbstractFridgeEntity): - """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]]: - door_status = self.door_status.freezer - if door_status and door_status != ErdDoorStatus.NA: - return {ATTR_DOOR_STATUS: door_status.name.title()} - return {} - - -class GeFridgeWaterHeater(GeEntity, WaterHeaterEntity): - """Entity for in-fridge water heaters""" - - # These values are from FridgeHotWaterFragment.smali in the android app - min_temp = 90 - max_temp = 185 - - @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 unique_id(self) -> str: - """Make a unique id.""" - return f"{self.serial_number}-fridge-hot-water" - - @property - def name(self) -> Optional[str]: - """Name it reasonably.""" - return f"GE Fridge Water Heater {self.serial_number}" - - @property - def temperature_unit(self): - """Select the appropriate temperature unit.""" - measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) - if measurement_system == ErdMeasurementUnits.METRIC: - return TEMP_CELSIUS - return TEMP_FAHRENHEIT - - @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] - if self.supports_k_cups: - ops_list.append(OP_MODE_K_CUP) - return ops_list - - async def async_set_temperature(self, **kwargs): - pass - - async def async_set_operation_mode(self, operation_mode): - pass - - @property - def supported_features(self): - pass - - @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 - - -class GeOvenHeaterEntity(GeEntity, WaterHeaterEntity): - """Water Heater entity for ovens""" - - icon = "mdi:stove" - - def __init__(self, api: "ApplianceApi", oven_select: str = UPPER_OVEN, two_cavity: bool = False): - 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 - super().__init__(api) - - @property - def supported_features(self): - return GE_FRIDGE_SUPPORT - - @property - def unique_id(self) -> str: - return f"{self.serial_number}-{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"GE {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 current_temperature(self) -> Optional[int]: - current_temp = self.get_erd_value("DISPLAY_TEMPERATURE") - if current_temp: - return current_temp - return self.get_erd_value("RAW_TEMPERATURE") - - @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]: - erd_code = self.get_erd_code("AVAILABLE_COOK_MODES") - cook_modes: Set[ErdOvenCookMode] = self.appliance.get_erd_value(erd_code) - 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 stringify_erd_value(erd_code, erd_value, self.temperature_unit) - - @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: - probe_present = self.get_erd_value("PROBE_PRESENT") - data = { - "display_state": self.display_state, - "probe_present": probe_present, - "raw_temperature": self.get_erd_value("RAW_TEMPERATURE"), - } - if probe_present: - data["probe_temperature"] = self.get_erd_value("PROBE_DISPLAY_TEMP") - elapsed_time = self.get_erd_value("ELAPSED_COOK_TIME") - cook_time_left = self.get_erd_value("COOK_TIME_REMAINING") - kitchen_timer = self.get_erd_value("KITCHEN_TIMER") - delay_time = self.get_erd_value("DELAY_TIME_REMAINING") - if elapsed_time: - data["cook_time_elapsed"] = str(elapsed_time) - if cook_time_left: - data["cook_time_left"] = str(cook_time_left) - if kitchen_timer: - data["cook_time_remaining"] = str(kitchen_timer) - if delay_time: - data["delay_time_remaining"] = str(delay_time) - return data - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): """GE Kitchen sensors.""" _LOGGER.debug('Adding GE "Water Heaters"') From efa8ccc82a4e184f22ef17376b70cd7b39f62205 Mon Sep 17 00:00:00 2001 From: Jack Simbah Date: Thu, 17 Dec 2020 22:14:56 -0500 Subject: [PATCH 002/338] - fixed issue with unmapped op modes - disabled dishwasher entities for now --- ge_kitchen/appliance_api.py | 14 +++++++------- ge_kitchen/appliance_entity_types/oven_entity.py | 7 ++++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ge_kitchen/appliance_api.py b/ge_kitchen/appliance_api.py index 57d0ec8..ecd3561 100644 --- a/ge_kitchen/appliance_api.py +++ b/ge_kitchen/appliance_api.py @@ -184,13 +184,13 @@ def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() dishwasher_entities = [ - GeErdSensor(self, ErdCode.CYCLE_NAME), - GeErdSensor(self, ErdCode.CYCLE_STATE), - GeErdSensor(self, ErdCode.OPERATING_MODE), - GeErdSensor(self, ErdCode.PODS_REMAINING_VALUE), - GeErdSensor(self, ErdCode.RINSE_AGENT), - GeErdSensor(self, ErdCode.SOUND), - GeErdSensor(self, ErdCode.TIME_REMAINING), + #GeErdSensor(self, ErdCode.CYCLE_NAME), + #GeErdSensor(self, ErdCode.CYCLE_STATE), + #GeErdSensor(self, ErdCode.OPERATING_MODE), + #GeErdSensor(self, ErdCode.PODS_REMAINING_VALUE), + #GeErdSensor(self, ErdCode.RINSE_AGENT), + #GeErdSensor(self, ErdCode.SOUND), + #GeErdSensor(self, ErdCode.TIME_REMAINING), ] entities = base_entities + dishwasher_entities return entities diff --git a/ge_kitchen/appliance_entity_types/oven_entity.py b/ge_kitchen/appliance_entity_types/oven_entity.py index 70f9a64..416d512 100644 --- a/ge_kitchen/appliance_entity_types/oven_entity.py +++ b/ge_kitchen/appliance_entity_types/oven_entity.py @@ -49,7 +49,7 @@ ErdOvenCookMode.CONVMULTIBAKE_NOOPTION: OP_MODE_CONVMULTIBAKE, ErdOvenCookMode.CONVBAKE_NOOPTION: OP_MODE_CONVBAKE, ErdOvenCookMode.CONVROAST_NOOPTION: OP_MODE_CONVROAST, - ErdOvenCookMode.BAKE_NOOPTION: OP_MODE_BAKE, + ErdOvenCookMode.BAKE_NOOPTION: OP_MODE_BAKE }) class GeOvenHeaterEntity(GeEntity, WaterHeaterEntity): @@ -118,8 +118,13 @@ def current_operation(self) -> Optional[str]: @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) + #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"found 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 From 7654fe49771e30dae1cbd7b98d7baef01661ec53 Mon Sep 17 00:00:00 2001 From: Jack Simbah Date: Thu, 17 Dec 2020 23:26:42 -0500 Subject: [PATCH 003/338] - updated the entity names --- ge_kitchen/appliance_entity_types/abstract_fridge_entity.py | 4 ++-- ge_kitchen/appliance_entity_types/oven_entity.py | 5 +++-- ge_kitchen/entities.py | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py b/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py index 5486f61..c4f12cd 100644 --- a/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py +++ b/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py @@ -78,11 +78,11 @@ def operation_list(self) -> List[str]: @property def unique_id(self) -> str: - return f"{self.serial_number}-{self.heater_type}" + return f"{DOMAIN}_{self.serial_number}_{self.heater_type}" @property def name(self) -> Optional[str]: - return f"GE {self.heater_type.title()} {self.serial_number}" + return f"{self.heater_type.title()} {self.serial_number}" @property def temperature_unit(self): diff --git a/ge_kitchen/appliance_entity_types/oven_entity.py b/ge_kitchen/appliance_entity_types/oven_entity.py index 416d512..af0835b 100644 --- a/ge_kitchen/appliance_entity_types/oven_entity.py +++ b/ge_kitchen/appliance_entity_types/oven_entity.py @@ -25,6 +25,7 @@ ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from ..entities import GeEntity, stringify_erd_value +from ..const import DOMAIN if TYPE_CHECKING: from ..appliance_api import ApplianceApi @@ -71,7 +72,7 @@ def supported_features(self): @property def unique_id(self) -> str: - return f"{self.serial_number}-{self.oven_select.lower()}" + return f"{DOMAIN}_{self.serial_number}_{self.oven_select.lower()}" @property def name(self) -> Optional[str]: @@ -80,7 +81,7 @@ def name(self) -> Optional[str]: else: oven_title = "Oven" - return f"GE {oven_title}" + return f"{oven_title} {self.serial_number}" @property def temperature_unit(self): diff --git a/ge_kitchen/entities.py b/ge_kitchen/entities.py index 14a9323..a32d3e7 100644 --- a/ge_kitchen/entities.py +++ b/ge_kitchen/entities.py @@ -218,7 +218,8 @@ def erd_string(self) -> str: @property def name(self) -> Optional[str]: erd_string = self.erd_string - return " ".join(erd_string.split("_")).title() + erd_title = " ".join(erd_string.split("_")).title() + return f"{self.appliance.serial_number} {erd_title}" @property def unique_id(self) -> Optional[str]: From 0475306eb5b15a87f71cc908f5de2133e6df1b6e Mon Sep 17 00:00:00 2001 From: Jack Simbah Date: Thu, 17 Dec 2020 23:33:31 -0500 Subject: [PATCH 004/338] - fixed naming bug --- ge_kitchen/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/entities.py b/ge_kitchen/entities.py index a32d3e7..3f3b0f3 100644 --- a/ge_kitchen/entities.py +++ b/ge_kitchen/entities.py @@ -219,7 +219,7 @@ def erd_string(self) -> str: def name(self) -> Optional[str]: erd_string = self.erd_string erd_title = " ".join(erd_string.split("_")).title() - return f"{self.appliance.serial_number} {erd_title}" + return f"{self.serial_number} {erd_title}" @property def unique_id(self) -> Optional[str]: From 32a93cfea01f2e97ffb05db58c34b67978bf5ea1 Mon Sep 17 00:00:00 2001 From: Jack Simbah Date: Fri, 18 Dec 2020 00:00:48 -0500 Subject: [PATCH 005/338] - more naming changes --- ge_kitchen/appliance_api.py | 23 ++++++++++++------- .../abstract_fridge_entity.py | 2 +- .../appliance_entity_types/oven_entity.py | 2 +- ge_kitchen/entities.py | 10 ++++++-- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/ge_kitchen/appliance_api.py b/ge_kitchen/appliance_api.py index ecd3561..83f427d 100644 --- a/ge_kitchen/appliance_api.py +++ b/ge_kitchen/appliance_api.py @@ -133,16 +133,16 @@ def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() oven_config: OvenConfiguration = self.appliance.get_erd_value(ErdCode.OVEN_CONFIGURATION) _LOGGER.debug(f"Oven Config: {oven_config}") - oven_entities = [ - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE), - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING), - GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER), - GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED), - ] + oven_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), + GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER), + GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED), + GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE), GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING), GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET), @@ -151,7 +151,14 @@ def get_all_entities(self) -> List[Entity]: GeOvenHeaterEntity(self, UPPER_OVEN, True), ]) else: - oven_entities.append(GeOvenHeaterEntity(self, UPPER_OVEN, False)) + oven_entities.extend([ + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, "Oven Cook Mode"), + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, "Oven Cook Time Remaining"), + GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, "Oven Kitchen Timer"), + GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, "Oven User Temp Offset"), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, "Oven Remote Enabled"), + GeOvenHeaterEntity(self, UPPER_OVEN, False) + ]) return base_entities + oven_entities diff --git a/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py b/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py index c4f12cd..84cbe09 100644 --- a/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py +++ b/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py @@ -82,7 +82,7 @@ def unique_id(self) -> str: @property def name(self) -> Optional[str]: - return f"{self.heater_type.title()} {self.serial_number}" + return f"{self.serial_number} {self.heater_type.title()}" @property def temperature_unit(self): diff --git a/ge_kitchen/appliance_entity_types/oven_entity.py b/ge_kitchen/appliance_entity_types/oven_entity.py index af0835b..cb5ca95 100644 --- a/ge_kitchen/appliance_entity_types/oven_entity.py +++ b/ge_kitchen/appliance_entity_types/oven_entity.py @@ -81,7 +81,7 @@ def name(self) -> Optional[str]: else: oven_title = "Oven" - return f"{oven_title} {self.serial_number}" + return f"{self.serial_number} {oven_title}" @property def temperature_unit(self): diff --git a/ge_kitchen/entities.py b/ge_kitchen/entities.py index 3f3b0f3..09ae848 100644 --- a/ge_kitchen/entities.py +++ b/ge_kitchen/entities.py @@ -200,10 +200,11 @@ def name(self) -> Optional[str]: class GeErdEntity(GeEntity): """Parent class for GE entities tied to a specific ERD""" - def __init__(self, api: "ApplianceApi", erd_code: ErdCodeType): + def __init__(self, api: "ApplianceApi", erd_code: ErdCodeType, erd_override: str = None): super().__init__(api) self._erd_code = translate_erd_code(erd_code) - + self._erd_override = erd_override + @property def erd_code(self) -> ErdCodeType: return self._erd_code @@ -218,6 +219,11 @@ 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_number} {erd_title}" From b4f4a172f07c1f41cc0336fb9211abd1d5011ceb Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 28 Dec 2020 17:46:15 -0500 Subject: [PATCH 006/338] - reorganization (part 1) - broke apart the monolithic code files into individual files - reorganized the files into multiple packages - incorporated many of the helper functions into the base gekitchen package - resolved changes from base gekitchen packages --- ge_kitchen/__init__.py | 30 +-- ge_kitchen/appliance_api.py | 203 --------------- ge_kitchen/appliance_entity_types/__init__.py | 4 - ge_kitchen/binary_sensor.py | 64 +---- ge_kitchen/config_flow.py | 27 +- ge_kitchen/devices/__init__.py | 23 ++ ge_kitchen/devices/base.py | 98 ++++++++ ge_kitchen/devices/dishwasher.py | 31 +++ ge_kitchen/devices/fridge.py | 31 +++ ge_kitchen/devices/oven.py | 46 ++++ ge_kitchen/entities.py | 236 ------------------ ge_kitchen/entities/__init__.py | 4 + ge_kitchen/entities/common/__init__.py | 8 + ge_kitchen/entities/common/ge_entity.py | 40 +++ .../entities/common/ge_erd_binary_sensor.py | 24 ++ ge_kitchen/entities/common/ge_erd_entity.py | 67 +++++ .../common/ge_erd_property_binary_sensor.py | 34 +++ .../entities/common/ge_erd_property_sensor.py | 44 ++++ ge_kitchen/entities/common/ge_erd_sensor.py | 44 ++++ ge_kitchen/entities/common/ge_erd_switch.py | 26 ++ ge_kitchen/entities/common/ge_water_heater.py | 57 +++++ .../dishwasher}/__init__.py | 0 ge_kitchen/entities/entities.py | 105 ++++++++ ge_kitchen/entities/fridge/__init__.py | 0 .../fridge/ge_abstract_fridge_entity.py} | 0 .../fridge/ge_freezer_entity.py} | 0 .../fridge/ge_fridge_entity.py} | 0 .../fridge/ge_fridge_water_heater_entity.py} | 0 ge_kitchen/entities/oven/__init__.py | 0 .../oven/ge_oven_heater_entity.py} | 0 ge_kitchen/erd_constants/oven_constants.py | 85 ------- ge_kitchen/erd_string_utils.py | 69 ----- ge_kitchen/exceptions.py | 9 +- ge_kitchen/sensor.py | 97 +------ ge_kitchen/switch.py | 34 +-- ge_kitchen/update_coordinator.py | 9 +- ge_kitchen/water_heater.py | 27 +- 37 files changed, 735 insertions(+), 841 deletions(-) delete mode 100644 ge_kitchen/appliance_api.py delete mode 100644 ge_kitchen/appliance_entity_types/__init__.py create mode 100644 ge_kitchen/devices/__init__.py create mode 100644 ge_kitchen/devices/base.py create mode 100644 ge_kitchen/devices/dishwasher.py create mode 100644 ge_kitchen/devices/fridge.py create mode 100644 ge_kitchen/devices/oven.py delete mode 100644 ge_kitchen/entities.py create mode 100644 ge_kitchen/entities/__init__.py create mode 100644 ge_kitchen/entities/common/__init__.py create mode 100644 ge_kitchen/entities/common/ge_entity.py create mode 100644 ge_kitchen/entities/common/ge_erd_binary_sensor.py create mode 100644 ge_kitchen/entities/common/ge_erd_entity.py create mode 100644 ge_kitchen/entities/common/ge_erd_property_binary_sensor.py create mode 100644 ge_kitchen/entities/common/ge_erd_property_sensor.py create mode 100644 ge_kitchen/entities/common/ge_erd_sensor.py create mode 100644 ge_kitchen/entities/common/ge_erd_switch.py create mode 100644 ge_kitchen/entities/common/ge_water_heater.py rename ge_kitchen/{erd_constants => entities/dishwasher}/__init__.py (100%) create mode 100644 ge_kitchen/entities/entities.py create mode 100644 ge_kitchen/entities/fridge/__init__.py rename ge_kitchen/{appliance_entity_types/abstract_fridge_entity.py => entities/fridge/ge_abstract_fridge_entity.py} (100%) rename ge_kitchen/{appliance_entity_types/freezer_entity.py => entities/fridge/ge_freezer_entity.py} (100%) rename ge_kitchen/{appliance_entity_types/fridge_entity.py => entities/fridge/ge_fridge_entity.py} (100%) rename ge_kitchen/{appliance_entity_types/fridge_water_heater_entity.py => entities/fridge/ge_fridge_water_heater_entity.py} (100%) create mode 100644 ge_kitchen/entities/oven/__init__.py rename ge_kitchen/{appliance_entity_types/oven_entity.py => entities/oven/ge_oven_heater_entity.py} (100%) delete mode 100644 ge_kitchen/erd_constants/oven_constants.py delete mode 100644 ge_kitchen/erd_string_utils.py diff --git a/ge_kitchen/__init__.py b/ge_kitchen/__init__.py index 9b8cff0..631e5e6 100644 --- a/ge_kitchen/__init__.py +++ b/ge_kitchen/__init__.py @@ -5,20 +5,14 @@ import logging import voluptuous as vol -from gekitchen import GeAuthError, GeServerError +from gekitchen import GeAuthFailedError, GeGeneralServerError, GeNotAuthenticatedError -from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT -from homeassistant.const import CONF_USERNAME +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from . import config_flow from .const import ( - AUTH_HANDLER, - COORDINATOR, - DOMAIN, - OAUTH2_AUTH_URL, - OAUTH2_TOKEN_URL, + DOMAIN ) -from .exceptions import AuthError, CannotConnect +from .exceptions import HaAuthError, HaCannotConnect from .update_coordinator import GeKitchenUpdateCoordinator CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -26,14 +20,12 @@ _LOGGER = logging.getLogger(__name__) - async def async_setup(hass: HomeAssistant, config: dict): """Set up the ge_kitchen component.""" hass.data.setdefault(DOMAIN, {}) if DOMAIN not in config: return True - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up ge_kitchen from a config entry.""" coordinator = GeKitchenUpdateCoordinator(hass, entry) @@ -41,18 +33,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: await coordinator.async_start_client() - except GeAuthError: - raise AuthError('Authentication failure') - except GeServerError: - raise CannotConnect('Cannot connect (server error)') + except (GeNotAuthenticatedError, GeAuthFailedError): + raise HaAuthError('Authentication failure') + except GeGeneralServerError: + raise HaCannotConnect('Cannot connect (server error)') except Exception: - raise CannotConnect('Unknown connection failure') + raise HaCannotConnect('Unknown connection failure') try: with async_timeout.timeout(30): await coordinator.initialization_future except TimeoutError: - raise CannotConnect('Initialization timed out') + raise HaCannotConnect('Initialization timed out') for component in PLATFORMS: hass.async_create_task( @@ -61,7 +53,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( @@ -77,7 +68,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok - async def async_update_options(hass, config_entry): """Update options.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/ge_kitchen/appliance_api.py b/ge_kitchen/appliance_api.py deleted file mode 100644 index 83f427d..0000000 --- a/ge_kitchen/appliance_api.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Oven state representation.""" - -import asyncio -import logging -from typing import Dict, List, Optional, Type, TYPE_CHECKING - -from gekitchen import GeAppliance -from gekitchen.erd_constants import * -from gekitchen.erd_types import * - -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .binary_sensor import GeErdBinarySensor -from .const import DOMAIN -from .entities import GeErdEntity -from .sensor import GeErdSensor -from .switch import GeErdSwitch -from .appliance_entity_types import ( - GeFreezerEntity, - GeFridgeEntity, - GeOvenHeaterEntity, - LOWER_OVEN, - UPPER_OVEN, -) - -_LOGGER = logging.getLogger(__name__) - - -def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: - _LOGGER.debug(f"Found device type: {appliance_type}") - """Get the appropriate appliance type""" - if appliance_type == ErdApplianceType.OVEN: - return OvenApi - if appliance_type == ErdApplianceType.FRIDGE: - return FridgeApi - if appliance_type == ErdApplianceType.DISH_WASHER: - return DishwasherApi - # Fallback - return ApplianceApi - - -class ApplianceApi: - """ - API class to represent a single physical device. - - Since a physical device can have many entities, we"ll pool common elements here - """ - APPLIANCE_TYPE = None # type: Optional[ErdApplianceType] - - def __init__(self, coordinator: DataUpdateCoordinator, appliance: GeAppliance): - if not appliance.initialized: - raise RuntimeError("Appliance not ready") - self._appliance = appliance - self._loop = appliance.client.loop - self._hass = coordinator.hass - self.coordinator = coordinator - self.initial_update = False - self._entities = {} # type: Optional[Dict[str, Entity]] - - @property - def hass(self) -> HomeAssistant: - return self._hass - - @property - def loop(self) -> Optional[asyncio.AbstractEventLoop]: - if self._loop is None: - self._loop = self._appliance.client.loop - return self._loop - - @property - def appliance(self) -> GeAppliance: - return self._appliance - - @property - def serial_number(self) -> str: - return self.appliance.get_erd_value(ErdCode.SERIAL_NUMBER) - - @property - def model_number(self) -> str: - return self.appliance.get_erd_value(ErdCode.MODEL_NUMBER) - - @property - def name(self) -> str: - appliance_type = self.appliance.appliance_type - if appliance_type is None or appliance_type == ErdApplianceType.UNKNOWN: - appliance_type = "Appliance" - else: - appliance_type = appliance_type.name.replace("_", " ").title() - return f"GE {appliance_type} {self.serial_number}" - - @property - def device_info(self) -> Dict: - """Device info dictionary.""" - return { - "identifiers": {(DOMAIN, self.serial_number)}, - "name": self.name, - "manufacturer": "GE", - "model": self.model_number, - "sw_version": self.appliance.get_erd_value(ErdCode.WIFI_MODULE_SW_VERSION), - } - - @property - def entities(self) -> List[Entity]: - return list(self._entities.values()) - - def get_all_entities(self) -> List[Entity]: - """Create Entities for this device.""" - entities = [ - GeErdSensor(self, ErdCode.CLOCK_TIME), - GeErdSwitch(self, ErdCode.SABBATH_MODE), - ] - return entities - - def build_entities_list(self) -> None: - """Build the entities list, adding anything new.""" - entities = [ - e for e in self.get_all_entities() - if not isinstance(e, GeErdEntity) or e.erd_code in self.appliance.known_properties - ] - - for entity in entities: - if entity.unique_id not in self._entities: - self._entities[entity.unique_id] = entity - - -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) - _LOGGER.debug(f"Oven Config: {oven_config}") - oven_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), - GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER), - GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED), - - GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE), - GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING), - GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET), - GeErdBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED), - GeOvenHeaterEntity(self, LOWER_OVEN, True), - GeOvenHeaterEntity(self, UPPER_OVEN, True), - ]) - else: - oven_entities.extend([ - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, "Oven Cook Mode"), - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, "Oven Cook Time Remaining"), - GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, "Oven Kitchen Timer"), - GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, "Oven User Temp Offset"), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, "Oven Remote Enabled"), - GeOvenHeaterEntity(self, UPPER_OVEN, False) - ]) - return base_entities + oven_entities - - -class FridgeApi(ApplianceApi): - """API class for oven objects""" - APPLIANCE_TYPE = ErdApplianceType.FRIDGE - - def get_all_entities(self) -> List[Entity]: - base_entities = super().get_all_entities() - - fridge_entities = [ - # GeErdSensor(self, ErdCode.AIR_FILTER_STATUS), - GeErdSensor(self, ErdCode.DOOR_STATUS), - GeErdSensor(self, ErdCode.FRIDGE_MODEL_INFO), - # GeErdSensor(self, ErdCode.HOT_WATER_LOCAL_USE), - # GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), - # GeErdSensor(self, ErdCode.HOT_WATER_STATUS), - GeErdSwitch(self, ErdCode.SABBATH_MODE), - GeFreezerEntity(self), - GeFridgeEntity(self), - ] - entities = base_entities + fridge_entities - return entities - -class DishwasherApi(ApplianceApi): - """API class for oven objects""" - APPLIANCE_TYPE = ErdApplianceType.DISH_WASHER - - def get_all_entities(self) -> List[Entity]: - base_entities = super().get_all_entities() - - dishwasher_entities = [ - #GeErdSensor(self, ErdCode.CYCLE_NAME), - #GeErdSensor(self, ErdCode.CYCLE_STATE), - #GeErdSensor(self, ErdCode.OPERATING_MODE), - #GeErdSensor(self, ErdCode.PODS_REMAINING_VALUE), - #GeErdSensor(self, ErdCode.RINSE_AGENT), - #GeErdSensor(self, ErdCode.SOUND), - #GeErdSensor(self, ErdCode.TIME_REMAINING), - ] - entities = base_entities + dishwasher_entities - return entities diff --git a/ge_kitchen/appliance_entity_types/__init__.py b/ge_kitchen/appliance_entity_types/__init__.py deleted file mode 100644 index 26dbf14..0000000 --- a/ge_kitchen/appliance_entity_types/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .freezer_entity import GeFreezerEntity -from .fridge_entity import GeFridgeEntity -from .fridge_water_heater_entity import GeFridgeWaterHeater -from .oven_entity import GeOvenHeaterEntity, UPPER_OVEN, LOWER_OVEN diff --git a/ge_kitchen/binary_sensor.py b/ge_kitchen/binary_sensor.py index 18f1a9c..6068fa1 100644 --- a/ge_kitchen/binary_sensor.py +++ b/ge_kitchen/binary_sensor.py @@ -1,78 +1,22 @@ """GE Kitchen Sensor Entities""" import async_timeout import logging -from typing import Callable, Optional, TYPE_CHECKING +from typing import Callable -from gekitchen import ErdCodeType - -from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import DOMAIN -from .entities import DOOR_ERD_CODES, GeErdEntity, boolify_erd_value, get_erd_icon - -if TYPE_CHECKING: - from .appliance_api import ApplianceApi - from .update_coordinator import GeKitchenUpdateCoordinator - +from .entities import GeErdBinarySensor +from .update_coordinator import GeKitchenUpdateCoordinator _LOGGER = logging.getLogger(__name__) - -class GeErdBinarySensor(GeErdEntity, BinarySensorEntity): - """GE Entity for binary sensors""" - @property - def is_on(self) -> bool: - """Return True if entity is on.""" - return bool(self.appliance.get_erd_value(self.erd_code)) - - @property - def icon(self) -> Optional[str]: - return get_erd_icon(self.erd_code, self.is_on) - - @property - def device_class(self) -> Optional[str]: - if self.erd_code in DOOR_ERD_CODES: - return "door" - return None - - -class GeErdPropertyBinarySensor(GeErdBinarySensor): - """GE Entity for property binary sensors""" - def __init__(self, api: "ApplianceApi", erd_code: ErdCodeType, erd_property: str): - super().__init__(api, erd_code) - self.erd_property = erd_property - - @property - def is_on(self) -> Optional[bool]: - """Return True if entity is on.""" - try: - value = getattr(self.appliance.get_erd_value(self.erd_code), self.erd_property) - except KeyError: - return None - return boolify_erd_value(self.erd_code, value) - - @property - def icon(self) -> Optional[str]: - return get_erd_icon(self.erd_code, self.is_on) - - @property - def unique_id(self) -> Optional[str]: - return f"{super().unique_id}_{self.erd_property}" - - @property - def name(self) -> Optional[str]: - base_string = super().name - property_name = self.erd_property.replace("_", " ").title() - return f"{base_string} {property_name}" - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): """GE Kitchen sensors.""" - coordinator: "GeKitchenUpdateCoordinator" = hass.data[DOMAIN][config_entry.entry_id] + coordinator: GeKitchenUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] # This should be a NOP, but let's be safe with async_timeout.timeout(20): diff --git a/ge_kitchen/config_flow.py b/ge_kitchen/config_flow.py index 8c996f9..f83d281 100644 --- a/ge_kitchen/config_flow.py +++ b/ge_kitchen/config_flow.py @@ -1,19 +1,20 @@ """Config flow for GE Kitchen integration.""" -import asyncio import logging from typing import Dict, Optional import aiohttp +import asyncio import async_timeout -from gekitchen import GeAuthError, GeServerError, async_get_oauth2_token + +from gekitchen import GeAuthFailedError, GeNotAuthenticatedError, GeGeneralServerError, async_get_oauth2_token import voluptuous as vol from homeassistant import config_entries, core from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DOMAIN # pylint:disable=unused-import -from .exceptions import AuthError, CannotConnect +from .exceptions import HaAuthError, HaCannotConnect _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} ) - async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" @@ -32,19 +32,18 @@ async def validate_input(hass: core.HomeAssistant, data): with async_timeout.timeout(10): _ = await async_get_oauth2_token(session, data[CONF_USERNAME], data[CONF_PASSWORD]) except (asyncio.TimeoutError, aiohttp.ClientError): - raise CannotConnect('Connection failure') - except GeAuthError: - raise AuthError('Authentication failure') - except GeServerError: - raise CannotConnect('Cannot connect (server error)') + raise HaCannotConnect('Connection failure') + except (GeAuthFailedError, GeNotAuthenticatedError): + raise HaAuthError('Authentication failure') + except GeGeneralServerError: + raise HaCannotConnect('Cannot connect (server error)') except Exception as exc: - _LOGGER.exception("Unkown connection failure", exc_info=exc) - raise CannotConnect('Unknown connection failure') + _LOGGER.exception("Unknown connection failure", exc_info=exc) + raise HaCannotConnect('Unknown connection failure') # Return info that you want to store in the config entry. return {"title": f"GE Kitchen ({data[CONF_USERNAME]:s})"} - class GeKitchenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for GE Kitchen.""" @@ -60,9 +59,9 @@ async def _async_validate_input(self, user_input): # noinspection PyBroadException try: info = await validate_input(self.hass, user_input) - except CannotConnect: + except HaCannotConnect: errors["base"] = "cannot_connect" - except AuthError: + except HaAuthError: errors["base"] = "invalid_auth" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") diff --git a/ge_kitchen/devices/__init__.py b/ge_kitchen/devices/__init__.py new file mode 100644 index 0000000..c208ae2 --- /dev/null +++ b/ge_kitchen/devices/__init__.py @@ -0,0 +1,23 @@ +import logging +from typing import Type + +from gekitchen.erd import ErdApplianceType + +from .base import ApplianceApi +from .oven import OvenApi +from .fridge import FridgeApi +from .dishwasher import DishwasherApi + +_LOGGER = logging.getLogger(__name__) + +def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: + _LOGGER.debug(f"Found device type: {appliance_type}") + """Get the appropriate appliance type""" + if appliance_type == ErdApplianceType.OVEN: + return OvenApi + if appliance_type == ErdApplianceType.FRIDGE: + return FridgeApi + if appliance_type == ErdApplianceType.DISH_WASHER: + return DishwasherApi + # Fallback + return ApplianceApi diff --git a/ge_kitchen/devices/base.py b/ge_kitchen/devices/base.py new file mode 100644 index 0000000..207529b --- /dev/null +++ b/ge_kitchen/devices/base.py @@ -0,0 +1,98 @@ +import asyncio +import logging +from typing import Dict, List, Optional + +from gekitchen import GeAppliance +from gekitchen.erd import ErdCode, ErdApplianceType + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from ..entities import GeErdEntity, GeErdSensor, GeErdSwitch +from ..const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +class ApplianceApi: + """ + API class to represent a single physical device. + + Since a physical device can have many entities, we"ll pool common elements here + """ + APPLIANCE_TYPE = None # type: Optional[ErdApplianceType] + + def __init__(self, coordinator: DataUpdateCoordinator, appliance: GeAppliance): + if not appliance.initialized: + raise RuntimeError("Appliance not ready") + self._appliance = appliance + self._loop = appliance.client.loop + self._hass = coordinator.hass + self.coordinator = coordinator + self.initial_update = False + self._entities = {} # type: Optional[Dict[str, Entity]] + + @property + def hass(self) -> HomeAssistant: + return self._hass + + @property + def loop(self) -> Optional[asyncio.AbstractEventLoop]: + if self._loop is None: + self._loop = self._appliance.client.loop + return self._loop + + @property + def appliance(self) -> GeAppliance: + return self._appliance + + @property + def serial_number(self) -> str: + return self.appliance.get_erd_value(ErdCode.SERIAL_NUMBER) + + @property + def model_number(self) -> str: + return self.appliance.get_erd_value(ErdCode.MODEL_NUMBER) + + @property + def name(self) -> str: + appliance_type = self.appliance.appliance_type + if appliance_type is None or appliance_type == ErdApplianceType.UNKNOWN: + appliance_type = "Appliance" + else: + appliance_type = appliance_type.name.replace("_", " ").title() + return f"GE {appliance_type} {self.serial_number}" + + @property + def device_info(self) -> Dict: + """Device info dictionary.""" + return { + "identifiers": {(DOMAIN, self.serial_number)}, + "name": self.name, + "manufacturer": "GE", + "model": self.model_number, + "sw_version": self.appliance.get_erd_value(ErdCode.WIFI_MODULE_SW_VERSION), + } + + @property + def entities(self) -> List[Entity]: + return list(self._entities.values()) + + def get_all_entities(self) -> List[Entity]: + """Create Entities for this device.""" + entities = [ + GeErdSensor(self, ErdCode.CLOCK_TIME), + GeErdSwitch(self, ErdCode.SABBATH_MODE), + ] + return entities + + def build_entities_list(self) -> None: + """Build the entities list, adding anything new.""" + entities = [ + e for e in self.get_all_entities() + if not isinstance(e, GeErdEntity) or e.erd_code in self.appliance.known_properties + ] + + for entity in entities: + if entity.unique_id not in self._entities: + self._entities[entity.unique_id] = entity diff --git a/ge_kitchen/devices/dishwasher.py b/ge_kitchen/devices/dishwasher.py new file mode 100644 index 0000000..ab5509e --- /dev/null +++ b/ge_kitchen/devices/dishwasher.py @@ -0,0 +1,31 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gekitchen.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeErdSensor, GeErdSwitch, GeFridgeEntity, GeFreezerEntity + +_LOGGER = logging.getLogger(__name__) + + +class DishwasherApi(ApplianceApi): + """API class for oven objects""" + APPLIANCE_TYPE = ErdApplianceType.DISH_WASHER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + dishwasher_entities = [ + GeErdSensor(self, ErdCode.CYCLE_NAME), + GeErdSensor(self, ErdCode.CYCLE_STATE), + GeErdSensor(self, ErdCode.OPERATING_MODE), + GeErdSensor(self, ErdCode.PODS_REMAINING_VALUE), + GeErdSensor(self, ErdCode.RINSE_AGENT), + GeErdSensor(self, ErdCode.SOUND), + GeErdSensor(self, ErdCode.TIME_REMAINING), + ] + entities = base_entities + dishwasher_entities + return entities + \ No newline at end of file diff --git a/ge_kitchen/devices/fridge.py b/ge_kitchen/devices/fridge.py new file mode 100644 index 0000000..33dd788 --- /dev/null +++ b/ge_kitchen/devices/fridge.py @@ -0,0 +1,31 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gekitchen.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeErdSensor, GeErdSwitch, GeFridgeEntity, GeFreezerEntity + +_LOGGER = logging.getLogger(__name__) + +class FridgeApi(ApplianceApi): + """API class for oven objects""" + APPLIANCE_TYPE = ErdApplianceType.FRIDGE + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + fridge_entities = [ + GeErdSensor(self, ErdCode.AIR_FILTER_STATUS), + GeErdSensor(self, ErdCode.DOOR_STATUS), + GeErdSensor(self, ErdCode.FRIDGE_MODEL_INFO), + GeErdSensor(self, ErdCode.HOT_WATER_IN_USE), + GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), + GeErdSensor(self, ErdCode.HOT_WATER_STATUS), + GeErdSwitch(self, ErdCode.SABBATH_MODE), + GeFreezerEntity(self), + GeFridgeEntity(self), + ] + entities = base_entities + fridge_entities + return entities diff --git a/ge_kitchen/devices/oven.py b/ge_kitchen/devices/oven.py new file mode 100644 index 0000000..d04db74 --- /dev/null +++ b/ge_kitchen/devices/oven.py @@ -0,0 +1,46 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gekitchen.erd import ErdCode, ErdApplianceType, OvenConfiguration + +from .base import ApplianceApi +from ..entities import GeErdSensor, GeErdBinarySensor, GeOvenHeaterEntity + +_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) + _LOGGER.debug(f"Oven Config: {oven_config}") + oven_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), + GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER), + GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED), + + GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE), + GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING), + GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET), + GeErdBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED), + GeOvenHeaterEntity(self, LOWER_OVEN, True), + GeOvenHeaterEntity(self, UPPER_OVEN, True), + ]) + else: + oven_entities.extend([ + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, "Oven Cook Mode"), + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, "Oven Cook Time Remaining"), + GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, "Oven Kitchen Timer"), + GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, "Oven User Temp Offset"), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, "Oven Remote Enabled"), + GeOvenHeaterEntity(self, UPPER_OVEN, False) + ]) + return base_entities + oven_entities diff --git a/ge_kitchen/entities.py b/ge_kitchen/entities.py deleted file mode 100644 index 09ae848..0000000 --- a/ge_kitchen/entities.py +++ /dev/null @@ -1,236 +0,0 @@ -"""Define all of the entity types""" - -import logging -from typing import Any, Dict, Optional, TYPE_CHECKING - -from gekitchen import ErdCodeType, GeAppliance, translate_erd_code -from gekitchen.erd_types import * -from gekitchen.erd_constants import * -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant - - -from .const import DOMAIN -from .erd_string_utils import * - -if TYPE_CHECKING: - from .appliance_api import ApplianceApi - - -_LOGGER = logging.getLogger(__name__) - -DOOR_ERD_CODES = { - ErdCode.DOOR_STATUS -} -RAW_TEMPERATURE_ERD_CODES = { - ErdCode.LOWER_OVEN_RAW_TEMPERATURE, - ErdCode.LOWER_OVEN_USER_TEMP_OFFSET, - ErdCode.UPPER_OVEN_RAW_TEMPERATURE, - ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, - ErdCode.CURRENT_TEMPERATURE, - ErdCode.TEMPERATURE_SETTING, -} -NONZERO_TEMPERATURE_ERD_CODES = { - ErdCode.HOT_WATER_SET_TEMP, - ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE, - ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP, - ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, - ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, -} -TEMPERATURE_ERD_CODES = RAW_TEMPERATURE_ERD_CODES.union(NONZERO_TEMPERATURE_ERD_CODES) -TIMER_ERD_CODES = { - ErdCode.LOWER_OVEN_ELAPSED_COOK_TIME, - ErdCode.LOWER_OVEN_KITCHEN_TIMER, - ErdCode.LOWER_OVEN_DELAY_TIME_REMAINING, - ErdCode.LOWER_OVEN_COOK_TIME_REMAINING, - ErdCode.LOWER_OVEN_ELAPSED_COOK_TIME, - ErdCode.ELAPSED_ON_TIME, - ErdCode.TIME_REMAINING, - ErdCode.UPPER_OVEN_ELAPSED_COOK_TIME, - ErdCode.UPPER_OVEN_KITCHEN_TIMER, - ErdCode.UPPER_OVEN_DELAY_TIME_REMAINING, - ErdCode.UPPER_OVEN_ELAPSED_COOK_TIME, - ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, -} - - -def boolify_erd_value(erd_code: ErdCodeType, value: Any) -> Optional[bool]: - """ - Convert an erd property value to a bool - - :param erd_code: The ERD code for the property - :param value: The current value in its native format - :return: The value converted to a bool - """ - erd_code = translate_erd_code(erd_code) - if isinstance(value, ErdDoorStatus): - if value == ErdDoorStatus.NA: - return None - return value == ErdDoorStatus.OPEN - if value is None: - return None - return bool(value) - - -def stringify_erd_value(erd_code: ErdCodeType, value: Any, units: Optional[str] = None) -> Optional[str]: - """ - Convert an erd property value to a nice string - - :param erd_code: The ERD code for the property - :param value: The current value in its native format - :param units: Units to apply, if applicable - :return: The value converted to a string - """ - erd_code = translate_erd_code(erd_code) - - if isinstance(value, ErdOvenState): - return oven_display_state_to_str(value) - if isinstance(value, OvenCookSetting): - return oven_cook_setting_to_str(value, units) - if isinstance(value, FridgeDoorStatus): - return value.status - if isinstance(value, FridgeIceBucketStatus): - return bucket_status_to_str(value) - if isinstance(value, ErdFilterStatus): - return value.name.capitalize() - if isinstance(value, HotWaterStatus): - return hot_water_status_str(value) - if isinstance(value, ErdDoorStatus): - return door_status_to_str(value) - - if erd_code == ErdCode.CLOCK_TIME: - return value.strftime("%H:%M:%S") if value else None - if erd_code in RAW_TEMPERATURE_ERD_CODES: - return f"{value}" - if erd_code in NONZERO_TEMPERATURE_ERD_CODES: - return f"{value}" if value else "" - if erd_code in TIMER_ERD_CODES: - return str(value)[:-3] if value else "" - if erd_code == ErdCode.DOOR_STATUS: - return value.status - if value is None: - return None - return str(value) - - -def get_erd_units(erd_code: ErdCodeType, measurement_units: ErdMeasurementUnits): - """Get the units for a sensor.""" - erd_code = translate_erd_code(erd_code) - if not measurement_units: - return None - - if erd_code in TEMPERATURE_ERD_CODES or erd_code in {ErdCode.LOWER_OVEN_COOK_MODE, ErdCode.UPPER_OVEN_COOK_MODE}: - if measurement_units == ErdMeasurementUnits.METRIC: - return TEMP_CELSIUS - return TEMP_FAHRENHEIT - return None - - -def get_erd_icon(erd_code: ErdCodeType, value: Any = None) -> Optional[str]: - """Select an appropriate icon.""" - erd_code = translate_erd_code(erd_code) - if not isinstance(erd_code, ErdCode): - return None - if erd_code in TIMER_ERD_CODES: - return "mdi:timer-outline" - if erd_code in { - ErdCode.LOWER_OVEN_COOK_MODE, - ErdCode.LOWER_OVEN_CURRENT_STATE, - ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE, - ErdCode.UPPER_OVEN_COOK_MODE, - ErdCode.UPPER_OVEN_CURRENT_STATE, - ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, - ErdCode.WARMING_DRAWER_STATE, - }: - return "mdi:stove" - if erd_code in { - ErdCode.TURBO_COOL_STATUS, - ErdCode.TURBO_FREEZE_STATUS, - }: - return "mdi:snowflake" - if erd_code == ErdCode.SABBATH_MODE: - return "mdi:judaism" - - # Let binary sensors assign their own. Might be worth passing - # the actual entity in if we want to do more of this. - if erd_code in DOOR_ERD_CODES and isinstance(value, str): - if "open" in value.lower(): - return "mdi:door-open" - return "mdi:door-closed" - - return None - - -class GeEntity: - """Base class for all GE Entities""" - should_poll = False - - def __init__(self, api: "ApplianceApi"): - self._api = api - self.hass = None # type: Optional[HomeAssistant] - - @property - def unique_id(self) -> str: - raise NotImplementedError - - @property - def api(self) -> "ApplianceApi": - return self._api - - @property - def device_info(self) -> Optional[Dict[str, Any]]: - return self.api.device_info - - @property - def serial_number(self): - return self.api.serial_number - - @property - def available(self) -> bool: - return self.appliance.available - - @property - def appliance(self) -> GeAppliance: - return self.api.appliance - - @property - def name(self) -> Optional[str]: - raise NotImplementedError - - -class GeErdEntity(GeEntity): - """Parent class for GE entities tied to a specific ERD""" - def __init__(self, api: "ApplianceApi", erd_code: ErdCodeType, erd_override: str = None): - super().__init__(api) - self._erd_code = translate_erd_code(erd_code) - self._erd_override = erd_override - - @property - def erd_code(self) -> ErdCodeType: - return self._erd_code - - @property - def erd_string(self) -> str: - erd_code = self.erd_code - if isinstance(self.erd_code, ErdCode): - return erd_code.name - return erd_code - - @property - def name(self) -> Optional[str]: - erd_string = self.erd_string - - #override the name if specified - if self._erd_override != None: - erd_string = self._erd_override - - erd_title = " ".join(erd_string.split("_")).title() - return f"{self.serial_number} {erd_title}" - - @property - def unique_id(self) -> Optional[str]: - return f"{DOMAIN}_{self.serial_number}_{self.erd_string.lower()}" - - @property - def icon(self) -> Optional[str]: - return get_erd_icon(self.erd_code) diff --git a/ge_kitchen/entities/__init__.py b/ge_kitchen/entities/__init__.py new file mode 100644 index 0000000..4fa5c83 --- /dev/null +++ b/ge_kitchen/entities/__init__.py @@ -0,0 +1,4 @@ +from .common import * +from .dishwasher import * +from .fridge import * +from .oven import * diff --git a/ge_kitchen/entities/common/__init__.py b/ge_kitchen/entities/common/__init__.py new file mode 100644 index 0000000..ab3e29d --- /dev/null +++ b/ge_kitchen/entities/common/__init__.py @@ -0,0 +1,8 @@ +from .ge_entity import GeEntity +from .ge_erd_entity import GeErdEntity +from .ge_erd_binary_sensor import GeErdBinarySensor +from .ge_erd_property_binary_sensor import GeErdPropertyBinarySensor +from .ge_erd_sensor import GeErdSensor +from .ge_erd_property_sensor import GeErdPropertySensor +from .ge_erd_switch import GeErdSwitch +from .ge_water_heater import GeWaterHeater \ No newline at end of file diff --git a/ge_kitchen/entities/common/ge_entity.py b/ge_kitchen/entities/common/ge_entity.py new file mode 100644 index 0000000..ba9a55a --- /dev/null +++ b/ge_kitchen/entities/common/ge_entity.py @@ -0,0 +1,40 @@ +from typing import Optional, Dict, Any + +from gekitchen import GeAppliance +from ge_kitchen.devices import ApplianceApi + +class GeEntity: + """Base class for all GE Entities""" + should_poll = False + + def __init__(self, api: ApplianceApi): + self._api = api + self.hass = None + + @property + def unique_id(self) -> str: + raise NotImplementedError + + @property + def api(self) -> ApplianceApi: + return self._api + + @property + def device_info(self) -> Optional[Dict[str, Any]]: + return self.api.device_info + + @property + def serial_number(self): + return self.api.serial_number + + @property + def available(self) -> bool: + return self.appliance.available + + @property + def appliance(self) -> GeAppliance: + return self.api.appliance + + @property + def name(self) -> Optional[str]: + raise NotImplementedError diff --git a/ge_kitchen/entities/common/ge_erd_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_binary_sensor.py new file mode 100644 index 0000000..51ad8dd --- /dev/null +++ b/ge_kitchen/entities/common/ge_erd_binary_sensor.py @@ -0,0 +1,24 @@ +from typing import Optional + +from homeassistant.components.binary_sensor import BinarySensorEntity + +from .ge_erd_entity import GeErdEntity + + +class GeErdBinarySensor(GeErdEntity, BinarySensorEntity): + """GE Entity for binary sensors""" + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return bool(self.appliance.get_erd_value(self.erd_code)) + + @property + def icon(self) -> Optional[str]: + return get_erd_icon(self.erd_code, self.is_on) + + @property + def device_class(self) -> Optional[str]: + if self.erd_code in DOOR_ERD_CODES: + return "door" + return None + diff --git a/ge_kitchen/entities/common/ge_erd_entity.py b/ge_kitchen/entities/common/ge_erd_entity.py new file mode 100644 index 0000000..2c98eda --- /dev/null +++ b/ge_kitchen/entities/common/ge_erd_entity.py @@ -0,0 +1,67 @@ +from typing import Optional + +from gekitchen import ErdCode, ErdCodeType, ErdCodeClass + +from ge_kitchen.const import DOMAIN +from ge_kitchen.devices import ApplianceApi +from .ge_entity import GeEntity + + +class GeErdEntity(GeEntity): + """Parent class for GE entities tied to a specific ERD""" + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): + super().__init__(api) + self._erd_code = api.appliance.translate_erd_code(erd_code) + self._erd_code_class = api.appliance.get_erd_code_class(self._erd_code) + self._erd_override = erd_override + + @property + def erd_code(self) -> ErdCodeType: + return self._erd_code + + @property + def erd_code_class(self) -> ErdCodeClass: + return self._erd_code_class + + @property + def erd_string(self) -> str: + erd_code = self.erd_code + if isinstance(self.erd_code, ErdCode): + return erd_code.name + return erd_code + + @property + def name(self) -> Optional[str]: + erd_string = self.erd_string + + #override the name if specified + if self._erd_override != None: + erd_string = self._erd_override + + erd_title = " ".join(erd_string.split("_")).title() + return f"{self.serial_number} {erd_title}" + + @property + def unique_id(self) -> Optional[str]: + return f"{DOMAIN}_{self.serial_number}_{self.erd_string.lower()}" + + @property + def icon(self) -> Optional[str]: + return get_erd_icon(self.erd_code) + + def _stringify_erd_value(self, value: any, **kwargs) -> Optional[str]: + # perform special processing before passing over to the default method + if self.erd_code == ErdCode.CLOCK_TIME: + return value.strftime("%H:%M:%S") if value else None + if self.erd_code_class == ErdCodeClass.RAW_TEMPERATURE: + return f"{value}" + if self.erd_code_class == ErdCodeClass.NON_ZERO_TEMPERATURE: + return f"{value}" if value else "" + if self.erd_code_class == ErdCodeClass.TIMER: + return str(value)[:-3] if value else "" + if value is None: + return None + return self.appliance.stringify_erd_value(self.erd_code, value, kwargs) + + def _boolify_erd_value(self, value: any) -> Optional[bool]: + return self.appliance.boolify_erd_value(self.erd_code, value) diff --git a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py new file mode 100644 index 0000000..5ab9d47 --- /dev/null +++ b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py @@ -0,0 +1,34 @@ +from typing import Optional + +from gekitchen import ErdCodeType +from ge_kitchen.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): + super().__init__(api, erd_code) + self.erd_property = erd_property + + @property + def is_on(self) -> Optional[bool]: + """Return True if entity is on.""" + try: + value = getattr(self.appliance.get_erd_value(self.erd_code), self.erd_property) + except KeyError: + return None + return boolify_erd_value(self.erd_code, value) + + @property + def icon(self) -> Optional[str]: + return get_erd_icon(self.erd_code, self.is_on) + + @property + def unique_id(self) -> Optional[str]: + return f"{super().unique_id}_{self.erd_property}" + + @property + def name(self) -> Optional[str]: + base_string = super().name + property_name = self.erd_property.replace("_", " ").title() + return f"{base_string} {property_name}" diff --git a/ge_kitchen/entities/common/ge_erd_property_sensor.py b/ge_kitchen/entities/common/ge_erd_property_sensor.py new file mode 100644 index 0000000..1c1d1e3 --- /dev/null +++ b/ge_kitchen/entities/common/ge_erd_property_sensor.py @@ -0,0 +1,44 @@ +from typing import Optional + +from gekitchen import ErdCode, ErdCodeType, ErdMeasurementUnits +from ge_kitchen.devices import ApplianceApi +from .ge_erd_sensor import GeErdSensor + + +class GeErdPropertySensor(GeErdSensor): + """GE Entity for sensors""" + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str): + super().__init__(api, erd_code) + self.erd_property = erd_property + + @property + def unique_id(self) -> Optional[str]: + return f"{super().unique_id}_{self.erd_property}" + + @property + def name(self) -> Optional[str]: + base_string = super().name + property_name = self.erd_property.replace("_", " ").title() + return f"{base_string} {property_name}" + + @property + def state(self) -> Optional[str]: + try: + value = getattr(self.appliance.get_erd_value(self.erd_code), self.erd_property) + except KeyError: + return None + return stringify_erd_value(self.erd_code, value, self.units) + + @property + def measurement_system(self) -> Optional[ErdMeasurementUnits]: + return self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + + @property + def units(self) -> Optional[str]: + return get_erd_units(self.erd_code, self.measurement_system) + + @property + def device_class(self) -> Optional[str]: + if self.erd_code in TEMPERATURE_ERD_CODES: + return "temperature" + return None diff --git a/ge_kitchen/entities/common/ge_erd_sensor.py b/ge_kitchen/entities/common/ge_erd_sensor.py new file mode 100644 index 0000000..76887d2 --- /dev/null +++ b/ge_kitchen/entities/common/ge_erd_sensor.py @@ -0,0 +1,44 @@ +from typing import Optional + +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.entity import Entity +from gekitchen import ErdCode, ErdCodeClass, ErdMeasurementUnits + +from .ge_erd_entity import GeErdEntity + +class GeErdSensor(GeErdEntity, Entity): + """GE Entity for sensors""" + @property + def state(self) -> Optional[str]: + try: + value = self.appliance.get_erd_value(self.erd_code) + except KeyError: + return None + return self._stringify_erd_value(self.erd_code, value, units=self.units) + + @property + def measurement_system(self) -> Optional[ErdMeasurementUnits]: + try: + value = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + except KeyError: + return None + return value + + @property + def units(self) -> Optional[str]: + return get_erd_units(self.erd_code, self.measurement_system) + + @property + def device_class(self) -> Optional[str]: + if self.erd_code_class in [ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE]: + return DEVICE_CLASS_TEMPERATURE + return None + + @property + def icon(self) -> Optional[str]: + return get_erd_icon(self.erd_code, self.state) + + @property + def unit_of_measurement(self) -> Optional[str]: + if self.device_class == DEVICE_CLASS_TEMPERATURE: + return self.units diff --git a/ge_kitchen/entities/common/ge_erd_switch.py b/ge_kitchen/entities/common/ge_erd_switch.py new file mode 100644 index 0000000..1afc311 --- /dev/null +++ b/ge_kitchen/entities/common/ge_erd_switch.py @@ -0,0 +1,26 @@ +import logging + +from homeassistant.components.switch import SwitchEntity +from .ge_erd_binary_sensor import GeErdBinarySensor + +_LOGGER = logging.getLogger(__name__) + +class GeErdSwitch(GeErdBinarySensor, SwitchEntity): + """Switches for boolean ERD codes.""" + device_class = "switch" + + @property + def is_on(self) -> bool: + """Return True if switch is on.""" + return bool(self.appliance.get_erd_value(self.erd_code)) + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + _LOGGER.debug(f"Turning on {self.unique_id}") + await self.appliance.async_set_erd_value(self.erd_code, True) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + _LOGGER.debug(f"Turning on {self.unique_id}") + await self.appliance.async_set_erd_value(self.erd_code, False) + diff --git a/ge_kitchen/entities/common/ge_water_heater.py b/ge_kitchen/entities/common/ge_water_heater.py new file mode 100644 index 0000000..18e70ed --- /dev/null +++ b/ge_kitchen/entities/common/ge_water_heater.py @@ -0,0 +1,57 @@ +import abc +from typing import Any, Dict, List, Optional + +from homeassistant.components.water_heater import WaterHeaterEntity +from homeassistant.const import ( + TEMP_FAHRENHEIT, + TEMP_CELSIUS +) +from gekitchen import ErdCode, ErdMeasurementUnits +from ge_kitchen.const import DOMAIN +from .ge_erd_entity import GeEntity + +class GeWaterHeater(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_number}_{self.heater_type}" + + @property + def name(self) -> Optional[str]: + return f"{self.serial_number} {self.heater_type.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 supported_features(self): + raise NotImplementedError + + @property + def other_state_attrs(self) -> Dict[str, Any]: + """State attributes to be optionally overridden in subclasses.""" + return {} + + @property + def device_state_attributes(self) -> Dict[str, Any]: + other_attrs = self.other_state_attrs + return {**other_attrs} + + 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) diff --git a/ge_kitchen/erd_constants/__init__.py b/ge_kitchen/entities/dishwasher/__init__.py similarity index 100% rename from ge_kitchen/erd_constants/__init__.py rename to ge_kitchen/entities/dishwasher/__init__.py diff --git a/ge_kitchen/entities/entities.py b/ge_kitchen/entities/entities.py new file mode 100644 index 0000000..0c03e2a --- /dev/null +++ b/ge_kitchen/entities/entities.py @@ -0,0 +1,105 @@ +"""Define all of the entity types""" + +import logging +from typing import Any, Dict, Optional, TYPE_CHECKING + +from gekitchen import ErdCodeType, GeAppliance, translate_erd_code +from gekitchen.erd_types import * +from gekitchen.erd_constants import * +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant + + +from .const import DOMAIN +from .erd_string_utils import * + +if TYPE_CHECKING: + from .appliance_api import ApplianceApi + + +_LOGGER = logging.getLogger(__name__) + +DOOR_ERD_CODES = { + ErdCode.DOOR_STATUS +} +RAW_TEMPERATURE_ERD_CODES = { + ErdCode.LOWER_OVEN_RAW_TEMPERATURE, + ErdCode.LOWER_OVEN_USER_TEMP_OFFSET, + ErdCode.UPPER_OVEN_RAW_TEMPERATURE, + ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, + ErdCode.CURRENT_TEMPERATURE, + ErdCode.TEMPERATURE_SETTING, +} +NONZERO_TEMPERATURE_ERD_CODES = { + ErdCode.HOT_WATER_SET_TEMP, + ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE, + ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP, + ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, + ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, +} +TEMPERATURE_ERD_CODES = RAW_TEMPERATURE_ERD_CODES.union(NONZERO_TEMPERATURE_ERD_CODES) +TIMER_ERD_CODES = { + ErdCode.LOWER_OVEN_ELAPSED_COOK_TIME, + ErdCode.LOWER_OVEN_KITCHEN_TIMER, + ErdCode.LOWER_OVEN_DELAY_TIME_REMAINING, + ErdCode.LOWER_OVEN_COOK_TIME_REMAINING, + ErdCode.LOWER_OVEN_ELAPSED_COOK_TIME, + ErdCode.ELAPSED_ON_TIME, + ErdCode.TIME_REMAINING, + ErdCode.UPPER_OVEN_ELAPSED_COOK_TIME, + ErdCode.UPPER_OVEN_KITCHEN_TIMER, + ErdCode.UPPER_OVEN_DELAY_TIME_REMAINING, + ErdCode.UPPER_OVEN_ELAPSED_COOK_TIME, + ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, +} + + +def get_erd_units(erd_code: ErdCodeType, measurement_units: ErdMeasurementUnits): + """Get the units for a sensor.""" + erd_code = translate_erd_code(erd_code) + if not measurement_units: + return None + + if erd_code in TEMPERATURE_ERD_CODES or erd_code in {ErdCode.LOWER_OVEN_COOK_MODE, ErdCode.UPPER_OVEN_COOK_MODE}: + if measurement_units == ErdMeasurementUnits.METRIC: + return TEMP_CELSIUS + return TEMP_FAHRENHEIT + return None + + +def get_erd_icon(erd_code: ErdCodeType, value: Any = None) -> Optional[str]: + """Select an appropriate icon.""" + erd_code = translate_erd_code(erd_code) + if not isinstance(erd_code, ErdCode): + return None + if erd_code in TIMER_ERD_CODES: + return "mdi:timer-outline" + if erd_code in { + ErdCode.LOWER_OVEN_COOK_MODE, + ErdCode.LOWER_OVEN_CURRENT_STATE, + ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE, + ErdCode.UPPER_OVEN_COOK_MODE, + ErdCode.UPPER_OVEN_CURRENT_STATE, + ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, + ErdCode.WARMING_DRAWER_STATE, + }: + return "mdi:stove" + if erd_code in { + ErdCode.TURBO_COOL_STATUS, + ErdCode.TURBO_FREEZE_STATUS, + }: + return "mdi:snowflake" + if erd_code == ErdCode.SABBATH_MODE: + return "mdi:judaism" + + # Let binary sensors assign their own. Might be worth passing + # the actual entity in if we want to do more of this. + if erd_code in DOOR_ERD_CODES and isinstance(value, str): + if "open" in value.lower(): + return "mdi:door-open" + return "mdi:door-closed" + + return None + + + diff --git a/ge_kitchen/entities/fridge/__init__.py b/ge_kitchen/entities/fridge/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ge_kitchen/appliance_entity_types/abstract_fridge_entity.py b/ge_kitchen/entities/fridge/ge_abstract_fridge_entity.py similarity index 100% rename from ge_kitchen/appliance_entity_types/abstract_fridge_entity.py rename to ge_kitchen/entities/fridge/ge_abstract_fridge_entity.py diff --git a/ge_kitchen/appliance_entity_types/freezer_entity.py b/ge_kitchen/entities/fridge/ge_freezer_entity.py similarity index 100% rename from ge_kitchen/appliance_entity_types/freezer_entity.py rename to ge_kitchen/entities/fridge/ge_freezer_entity.py diff --git a/ge_kitchen/appliance_entity_types/fridge_entity.py b/ge_kitchen/entities/fridge/ge_fridge_entity.py similarity index 100% rename from ge_kitchen/appliance_entity_types/fridge_entity.py rename to ge_kitchen/entities/fridge/ge_fridge_entity.py diff --git a/ge_kitchen/appliance_entity_types/fridge_water_heater_entity.py b/ge_kitchen/entities/fridge/ge_fridge_water_heater_entity.py similarity index 100% rename from ge_kitchen/appliance_entity_types/fridge_water_heater_entity.py rename to ge_kitchen/entities/fridge/ge_fridge_water_heater_entity.py diff --git a/ge_kitchen/entities/oven/__init__.py b/ge_kitchen/entities/oven/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ge_kitchen/appliance_entity_types/oven_entity.py b/ge_kitchen/entities/oven/ge_oven_heater_entity.py similarity index 100% rename from ge_kitchen/appliance_entity_types/oven_entity.py rename to ge_kitchen/entities/oven/ge_oven_heater_entity.py diff --git a/ge_kitchen/erd_constants/oven_constants.py b/ge_kitchen/erd_constants/oven_constants.py deleted file mode 100644 index ae1335e..0000000 --- a/ge_kitchen/erd_constants/oven_constants.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Constants for GE Oven states""" - -from gekitchen.erd_constants import ErdOvenState - -STATE_OVEN_BAKE = "Bake" -STATE_OVEN_BAKE_TWO_TEMP = "Bake (Two Temp.)" -STATE_OVEN_BAKED_GOODS = "Baked Goods" -STATE_OVEN_BROIL_HIGH = "Broil (High)" -STATE_OVEN_BROIL_LOW = "Broil (Low)" -STATE_OVEN_CONV_BAKE = "Convection Bake" -STATE_OVEN_CONV_BAKE_TWO_TEMP = "Convection Bake (Two Temp.)" -STATE_OVEN_CONV_BROIL_CRISP = "Convection Broil (Crisp)" -STATE_OVEN_CONV_BROIL_HIGH = "Convection Broil (High)" -STATE_OVEN_CONV_BROIL_LOW = "Convection Broil (Low)" -STATE_OVEN_CONV_BAKE_MULTI = "Convection Multi Bake" -STATE_OVEN_CONV_ROAST = "Convection Roast" -STATE_OVEN_CONV_ROAST_TWO_TEMP = "Convection Roast (Two Temp.)" -STATE_OVEN_DUAL_BROIL_HIGH = "Dual Broil (High)" -STATE_OVEN_DUAL_BROIL_LOW = "Dual Broil (Low)" -STATE_OVEN_DELAY = "Delayed Start" -STATE_OVEN_FROZEN_PIZZA = "Frozen Pizza" -STATE_OVEN_FROZEN_SNACKS = "Frozen Snacks" -STATE_OVEN_MULTI_BAKE = "Multi Bake" -STATE_OVEN_PREHEAT = "Preheat" -STATE_OVEN_PROBE = "Probe" -STATE_OVEN_PROOF = "Proof" -STATE_OVEN_OFF = "Off" -STATE_OVEN_SABBATH = "Sabbath Mode" -STATE_OVEN_SELF_CLEAN = "Self Clean" -STATE_OVEN_SPECIAL = "Special" -STATE_OVEN_STEAM_CLEAN = "Steam Clean" -STATE_OVEN_TIMED = "Timed" -STATE_OVEN_UNKNOWN = "Unknown" -STATE_OVEN_WARM = "Keep Warm" - -OVEN_DISPLAY_STATE_MAP = { - ErdOvenState.BAKE: STATE_OVEN_BAKE, - ErdOvenState.BAKE_PREHEAT: STATE_OVEN_PREHEAT, - ErdOvenState.BAKE_TWO_TEMP: STATE_OVEN_BAKE_TWO_TEMP, - ErdOvenState.BROIL_HIGH: STATE_OVEN_BROIL_HIGH, - ErdOvenState.BROIL_LOW: STATE_OVEN_BROIL_LOW, - ErdOvenState.CLEAN_COOL_DOWN: STATE_OVEN_SELF_CLEAN, - ErdOvenState.CLEAN_STAGE1: STATE_OVEN_SELF_CLEAN, - ErdOvenState.CLEAN_STAGE2: STATE_OVEN_SELF_CLEAN, - ErdOvenState.CONV_BAKE: STATE_OVEN_CONV_BAKE, - ErdOvenState.CONV_BAKE_PREHEAT: STATE_OVEN_PREHEAT, - ErdOvenState.CONV_BAKE_TWO_TEMP: STATE_OVEN_CONV_BAKE_TWO_TEMP, - ErdOvenState.CONV_BROIL_CRISP: STATE_OVEN_CONV_BROIL_CRISP, - ErdOvenState.CONV_BROIL_HIGH: STATE_OVEN_CONV_BROIL_HIGH, - ErdOvenState.CONV_BROIL_LOW: STATE_OVEN_CONV_BROIL_LOW, - ErdOvenState.CONV_MULTI_BAKE_PREHEAT: STATE_OVEN_PREHEAT, - ErdOvenState.CONV_MULTI_TWO_BAKE: STATE_OVEN_MULTI_BAKE, - ErdOvenState.CONV_MUTLI_BAKE: STATE_OVEN_MULTI_BAKE, - ErdOvenState.CONV_ROAST: STATE_OVEN_CONV_ROAST, - ErdOvenState.CONV_ROAST2: STATE_OVEN_CONV_ROAST_TWO_TEMP, - ErdOvenState.CONV_ROAST_BAKE_PREHEAT: STATE_OVEN_PREHEAT, - ErdOvenState.CUSTOM_CLEAN_STAGE2: STATE_OVEN_SELF_CLEAN, - ErdOvenState.DELAY: STATE_OVEN_DELAY, - ErdOvenState.NO_MODE: STATE_OVEN_OFF, - ErdOvenState.PROOF: STATE_OVEN_PROOF, - ErdOvenState.SABBATH: STATE_OVEN_SABBATH, - ErdOvenState.STEAM_CLEAN_STAGE2: STATE_OVEN_STEAM_CLEAN, - ErdOvenState.STEAM_COOL_DOWN: STATE_OVEN_STEAM_CLEAN, - ErdOvenState.WARM: STATE_OVEN_WARM, - ErdOvenState.OVEN_STATE_BAKE: STATE_OVEN_BAKE, - ErdOvenState.OVEN_STATE_BAKED_GOODS: STATE_OVEN_BAKED_GOODS, - ErdOvenState.OVEN_STATE_BROIL: STATE_OVEN_BROIL_HIGH, - ErdOvenState.OVEN_STATE_CONV_BAKE: STATE_OVEN_CONV_BAKE, - ErdOvenState.OVEN_STATE_CONV_BAKE_MULTI: STATE_OVEN_CONV_BAKE_MULTI, - ErdOvenState.OVEN_STATE_CONV_BROIL: STATE_OVEN_CONV_BROIL_HIGH, - ErdOvenState.OVEN_STATE_CONV_ROAST: STATE_OVEN_CONV_ROAST, - ErdOvenState.OVEN_STATE_DUAL_BROIL_HIGH: STATE_OVEN_DUAL_BROIL_HIGH, - ErdOvenState.OVEN_STATE_DUAL_BROIL_LOW: STATE_OVEN_DUAL_BROIL_LOW, - ErdOvenState.OVEN_STATE_DELAY_START: STATE_OVEN_DELAY, - ErdOvenState.OVEN_STATE_FROZEN_PIZZA: STATE_OVEN_FROZEN_PIZZA, - ErdOvenState.OVEN_STATE_FROZEN_PIZZA_MULTI: STATE_OVEN_FROZEN_PIZZA, - ErdOvenState.OVEN_STATE_FROZEN_SNACKS: STATE_OVEN_FROZEN_SNACKS, - ErdOvenState.OVEN_STATE_FROZEN_SNACKS_MULTI: STATE_OVEN_FROZEN_SNACKS, - ErdOvenState.OVEN_STATE_PROOF: STATE_OVEN_PROOF, - ErdOvenState.OVEN_STATE_SELF_CLEAN: STATE_OVEN_SELF_CLEAN, - ErdOvenState.OVEN_STATE_SPECIAL_X: STATE_OVEN_SPECIAL, - ErdOvenState.OVEN_STATE_STEAM_START: STATE_OVEN_STEAM_CLEAN, - ErdOvenState.OVEN_STATE_WARM: STATE_OVEN_WARM, - ErdOvenState.STATUS_DASH: STATE_OVEN_OFF, -} diff --git a/ge_kitchen/erd_string_utils.py b/ge_kitchen/erd_string_utils.py deleted file mode 100644 index b6d5dbf..0000000 --- a/ge_kitchen/erd_string_utils.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Utilities to make nice strings from ERD values.""" - -__all__ = ( - "hot_water_status_str", - "oven_display_state_to_str", - "oven_cook_setting_to_str", - "bucket_status_to_str", - "door_status_to_str", -) - -from typing import Optional - -from gekitchen.erd_types import OvenCookSetting, FridgeIceBucketStatus, HotWaterStatus -from gekitchen.erd_constants import ErdOvenState, ErdFullNotFull, ErdDoorStatus -from .erd_constants.oven_constants import ( - OVEN_DISPLAY_STATE_MAP, - STATE_OVEN_DELAY, - STATE_OVEN_PROBE, - STATE_OVEN_SABBATH, - STATE_OVEN_TIMED, - STATE_OVEN_UNKNOWN, -) - - -def oven_display_state_to_str(oven_state: ErdOvenState) -> str: - """Translate ErdOvenState values to a nice constant.""" - return OVEN_DISPLAY_STATE_MAP.get(oven_state, STATE_OVEN_UNKNOWN) - - -def oven_cook_setting_to_str(cook_setting: OvenCookSetting, units: str) -> str: - """Format OvenCookSetting values nicely.""" - cook_mode = cook_setting.cook_mode - cook_state = cook_mode.oven_state - temperature = cook_setting.temperature - - modifiers = [] - if cook_mode.timed: - modifiers.append(STATE_OVEN_TIMED) - if cook_mode.delayed: - modifiers.append(STATE_OVEN_DELAY) - if cook_mode.probe: - modifiers.append(STATE_OVEN_PROBE) - if cook_mode.sabbath: - modifiers.append(STATE_OVEN_SABBATH) - - temp_str = f" ({temperature}{units})" if temperature > 0 else "" - modifier_str = f" ({', '.join(modifiers)})" if modifiers else "" - display_state = oven_display_state_to_str(cook_state) - return f"{display_state}{temp_str}{modifier_str}" - - -def bucket_status_to_str(bucket_status: FridgeIceBucketStatus) -> str: - status = bucket_status.total_status - if status == ErdFullNotFull.FULL: - return "Full" - if status == ErdFullNotFull.NOT_FULL: - return "Not Full" - if status == ErdFullNotFull.NA: - return "NA" - - -def hot_water_status_str(water_status: HotWaterStatus) -> str: - raise NotImplementedError - - -def door_status_to_str(door_status: ErdDoorStatus) -> Optional[str]: - if door_status == ErdDoorStatus.NA: - return None - return door_status.name.title() diff --git a/ge_kitchen/exceptions.py b/ge_kitchen/exceptions.py index 1c837f5..b262a6a 100644 --- a/ge_kitchen/exceptions.py +++ b/ge_kitchen/exceptions.py @@ -1,11 +1,8 @@ -"""Exceptions go here.""" +""" Home Assistant derived exceptions""" from homeassistant import exceptions as ha_exc - -class CannotConnect(ha_exc.HomeAssistantError): +class HaCannotConnect(ha_exc.HomeAssistantError): """Error to indicate we cannot connect.""" - - -class AuthError(ha_exc.HomeAssistantError): +class HaAuthError(ha_exc.HomeAssistantError): """Error to indicate authentication failure.""" diff --git a/ge_kitchen/sensor.py b/ge_kitchen/sensor.py index d020200..d01cdba 100644 --- a/ge_kitchen/sensor.py +++ b/ge_kitchen/sensor.py @@ -1,110 +1,21 @@ """GE Kitchen Sensor Entities""" import async_timeout import logging -from typing import Optional, Callable, TYPE_CHECKING - -from gekitchen import ErdCodeType -from gekitchen.erd_constants import * +from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity from .const import DOMAIN -from .entities import ( - TEMPERATURE_ERD_CODES, - GeErdEntity, - get_erd_icon, - get_erd_units, - stringify_erd_value, -) - -if TYPE_CHECKING: - from .update_coordinator import GeKitchenUpdateCoordinator - from .appliance_api import ApplianceApi - +from .entities import GeErdSensor +from .update_coordinator import GeKitchenUpdateCoordinator _LOGGER = logging.getLogger(__name__) - -class GeErdSensor(GeErdEntity, Entity): - """GE Entity for sensors""" - @property - def state(self) -> Optional[str]: - try: - value = self.appliance.get_erd_value(self.erd_code) - except KeyError: - return None - return stringify_erd_value(self.erd_code, value, self.units) - - @property - def measurement_system(self) -> Optional[ErdMeasurementUnits]: - return self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) - - @property - def units(self) -> Optional[str]: - return get_erd_units(self.erd_code, self.measurement_system) - - @property - def device_class(self) -> Optional[str]: - if self.erd_code in TEMPERATURE_ERD_CODES: - return DEVICE_CLASS_TEMPERATURE - return None - - @property - def icon(self) -> Optional[str]: - return get_erd_icon(self.erd_code, self.state) - - @property - def unit_of_measurement(self) -> Optional[str]: - if self.device_class == DEVICE_CLASS_TEMPERATURE: - return self.units - - -class GeErdPropertySensor(GeErdSensor): - """GE Entity for sensors""" - def __init__(self, api: "ApplianceApi", erd_code: ErdCodeType, erd_property: str): - super().__init__(api, erd_code) - self.erd_property = erd_property - - @property - def unique_id(self) -> Optional[str]: - return f"{super().unique_id}_{self.erd_property}" - - @property - def name(self) -> Optional[str]: - base_string = super().name - property_name = self.erd_property.replace("_", " ").title() - return f"{base_string} {property_name}" - - @property - def state(self) -> Optional[str]: - try: - value = getattr(self.appliance.get_erd_value(self.erd_code), self.erd_property) - except KeyError: - return None - return stringify_erd_value(self.erd_code, value, self.units) - - @property - def measurement_system(self) -> Optional[ErdMeasurementUnits]: - return self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) - - @property - def units(self) -> Optional[str]: - return get_erd_units(self.erd_code, self.measurement_system) - - @property - def device_class(self) -> Optional[str]: - if self.erd_code in TEMPERATURE_ERD_CODES: - return "temperature" - return None - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): """GE Kitchen sensors.""" _LOGGER.debug('Adding GE Kitchen sensors') - coordinator: "GeKitchenUpdateCoordinator" = hass.data[DOMAIN][config_entry.entry_id] + coordinator: GeKitchenUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] # This should be a NOP, but let's be safe with async_timeout.timeout(20): diff --git a/ge_kitchen/switch.py b/ge_kitchen/switch.py index be5017d..d2cf2cf 100644 --- a/ge_kitchen/switch.py +++ b/ge_kitchen/switch.py @@ -1,45 +1,21 @@ -"""GE Kitchen Sensor Entities""" +"""GE Kitchen Switch Entities""" import async_timeout import logging -from typing import Callable, TYPE_CHECKING +from typing import Callable -from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .binary_sensor import GeErdBinarySensor +from .entities import GeErdSwitch from .const import DOMAIN - -if TYPE_CHECKING: - from .update_coordinator import GeKitchenUpdateCoordinator +from .update_coordinator import GeKitchenUpdateCoordinator _LOGGER = logging.getLogger(__name__) - -class GeErdSwitch(GeErdBinarySensor, SwitchEntity): - """Switches for boolean ERD codes.""" - device_class = "switch" - - @property - def is_on(self) -> bool: - """Return True if switch is on.""" - return bool(self.appliance.get_erd_value(self.erd_code)) - - async def async_turn_on(self, **kwargs): - """Turn the switch on.""" - _LOGGER.debug(f"Turning on {self.unique_id}") - await self.appliance.async_set_erd_value(self.erd_code, True) - - async def async_turn_off(self, **kwargs): - """Turn the switch off.""" - _LOGGER.debug(f"Turning on {self.unique_id}") - await self.appliance.async_set_erd_value(self.erd_code, False) - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): """GE Kitchen sensors.""" _LOGGER.debug('Adding GE Kitchen switches') - coordinator: "GeKitchenUpdateCoordinator" = hass.data[DOMAIN][config_entry.entry_id] + coordinator: GeKitchenUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] # This should be a NOP, but let's be safe with async_timeout.timeout(20): diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index 9d921e7..4ab5892 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -1,4 +1,4 @@ -"""Data update coordinator for shark iq vacuums.""" +"""Data update coordinator for GE Kitchen Appliances""" import asyncio import logging @@ -21,16 +21,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, EVENT_ALL_APPLIANCES_READY, UPDATE_INTERVAL -from .appliance_api import ApplianceApi, get_appliance_api_type +from .devices import ApplianceApi, get_appliance_api_type _LOGGER = logging.getLogger(__name__) - class GeKitchenUpdateCoordinator(DataUpdateCoordinator): - """Define a wrapper class to update Shark IQ data.""" + """Define a wrapper class to update GE Kitchen data.""" def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: - """Set up the SharkIqUpdateCoordinator class.""" + """Set up the GeKitchenUpdateCoordinator class.""" self._hass = hass self._config_entry = config_entry self._username = config_entry.data[CONF_USERNAME] diff --git a/ge_kitchen/water_heater.py b/ge_kitchen/water_heater.py index c14f584..4fb4593 100644 --- a/ge_kitchen/water_heater.py +++ b/ge_kitchen/water_heater.py @@ -1,31 +1,22 @@ """GE Kitchen Sensor Entities""" -import abc import async_timeout -from datetime import timedelta import logging -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING +from typing import Callable -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, - WaterHeaterEntity, -) +from homeassistant.components.water_heater import WaterHeaterEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .entities import GeEntity, stringify_erd_value +from .entities import GeWaterHeater from .const import DOMAIN - -if TYPE_CHECKING: - from .appliance_api import ApplianceApi - from .update_coordinator import GeKitchenUpdateCoordinator +from .update_coordinator import GeKitchenUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Kitchen sensors.""" + """GE Kitchen Water Heaters.""" _LOGGER.debug('Adding GE "Water Heaters"') - coordinator: "GeKitchenUpdateCoordinator" = hass.data[DOMAIN][config_entry.entry_id] + coordinator: GeKitchenUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] # This should be a NOP, but let's be safe with async_timeout.timeout(20): @@ -35,8 +26,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn apis = list(coordinator.appliance_apis.values()) _LOGGER.debug(f'Found {len(apis):d} appliance APIs') entities = [ - entity for api in apis for entity in api.entities - if isinstance(entity, WaterHeaterEntity) + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeWaterHeater) ] _LOGGER.debug(f'Found {len(entities):d} "water heaters"') async_add_entities(entities) From 6f0bacbdbfbabdd77dbb3f234c70098f070c63d5 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 28 Dec 2020 18:36:38 -0500 Subject: [PATCH 007/338] - cleaned up the fridge water heater entities --- ge_kitchen/entities/common/ge_water_heater.py | 17 ++++-- ge_kitchen/entities/fridge/__init__.py | 3 + ge_kitchen/entities/fridge/const.py | 18 ++++++ ...fridge_entity.py => ge_abstract_fridge.py} | 58 ++----------------- ...water_heater_entity.py => ge_dispenser.py} | 57 +++++------------- .../{ge_freezer_entity.py => ge_freezer.py} | 11 ++-- .../{ge_fridge_entity.py => ge_fridge.py} | 19 ++---- 7 files changed, 60 insertions(+), 123 deletions(-) create mode 100644 ge_kitchen/entities/fridge/const.py rename ge_kitchen/entities/fridge/{ge_abstract_fridge_entity.py => ge_abstract_fridge.py} (78%) rename ge_kitchen/entities/fridge/{ge_fridge_water_heater_entity.py => ge_dispenser.py} (53%) rename ge_kitchen/entities/fridge/{ge_freezer_entity.py => ge_freezer.py} (69%) rename ge_kitchen/entities/fridge/{ge_fridge_entity.py => ge_fridge.py} (73%) diff --git a/ge_kitchen/entities/common/ge_water_heater.py b/ge_kitchen/entities/common/ge_water_heater.py index 18e70ed..5b9aafd 100644 --- a/ge_kitchen/entities/common/ge_water_heater.py +++ b/ge_kitchen/entities/common/ge_water_heater.py @@ -1,4 +1,5 @@ import abc +import logging from typing import Any, Dict, List, Optional from homeassistant.components.water_heater import WaterHeaterEntity @@ -10,9 +11,19 @@ from ge_kitchen.const import DOMAIN from .ge_erd_entity import GeEntity +_LOGGER = logging.getLogger(__name__) + class GeWaterHeater(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): """Mock temperature/operation mode supporting device as a water heater""" + @property + def available(self) -> bool: + available = super().available + if not available: + app = self.appliance + _LOGGER.critical(f"{self.name} unavailable. Appliance info: Availaible - {app._available} and Init - {app.initialized}") + return available + @property def heater_type(self) -> str: raise NotImplementedError @@ -49,9 +60,3 @@ def other_state_attrs(self) -> Dict[str, Any]: def device_state_attributes(self) -> Dict[str, Any]: other_attrs = self.other_state_attrs return {**other_attrs} - - 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) diff --git a/ge_kitchen/entities/fridge/__init__.py b/ge_kitchen/entities/fridge/__init__.py index e69de29..5dde001 100644 --- a/ge_kitchen/entities/fridge/__init__.py +++ b/ge_kitchen/entities/fridge/__init__.py @@ -0,0 +1,3 @@ +from .ge_fridge import GeFridge +from .ge_freezer import GeFreezer +from .ge_dispenser import GeDispenser \ No newline at end of file diff --git a/ge_kitchen/entities/fridge/const.py b/ge_kitchen/entities/fridge/const.py new file mode 100644 index 0000000..a3eb352 --- /dev/null +++ b/ge_kitchen/entities/fridge/const.py @@ -0,0 +1,18 @@ +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_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/ge_kitchen/entities/fridge/ge_abstract_fridge_entity.py b/ge_kitchen/entities/fridge/ge_abstract_fridge.py similarity index 78% rename from ge_kitchen/entities/fridge/ge_abstract_fridge_entity.py rename to ge_kitchen/entities/fridge/ge_abstract_fridge.py index 84cbe09..cd35b3a 100644 --- a/ge_kitchen/entities/fridge/ge_abstract_fridge_entity.py +++ b/ge_kitchen/entities/fridge/ge_abstract_fridge.py @@ -2,62 +2,28 @@ import sys import os import abc -import async_timeout -from datetime import timedelta import logging -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING +from typing import Any, Dict, List, Optional -sys.path.append(os.getcwd() + '/..') +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from bidict import bidict from gekitchen import ( ErdCode, ErdOnOff, - ErdDoorStatus, - ErdFilterStatus, ErdFullNotFull, - ErdHotWaterStatus, - ErdMeasurementUnits, - ErdPodStatus -) -from gekitchen.erd_types import ( FridgeDoorStatus, FridgeSetPointLimits, FridgeSetPoints, FridgeIceBucketStatus, - HotWaterStatus, IceMakerControlStatus ) - -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, - WaterHeaterEntity, -) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT - -from ..entities import GeEntity, stringify_erd_value -from ..const import DOMAIN - -if TYPE_CHECKING: - from ..appliance_api import ApplianceApi - from ..update_coordinator import GeKitchenUpdateCoordinator - -ATTR_DOOR_STATUS = "door_status" -GE_FRIDGE_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) -HEATER_TYPE_FRIDGE = "fridge" -HEATER_TYPE_FREEZER = "freezer" - -# Fridge/Freezer -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" +from ge_kitchen.const import DOMAIN +from ..common import GeWaterHeater +from .const import * _LOGGER = logging.getLogger(__name__) -class GeAbstractFridgeEntity(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): +class GeAbstractFridge(GeWaterHeater): """Mock a fridge or freezer as a water heater.""" @property @@ -84,13 +50,6 @@ def unique_id(self) -> str: def name(self) -> Optional[str]: return f"{self.serial_number} {self.heater_type.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 target_temps(self) -> FridgeSetPoints: """Get the current temperature settings tuple.""" @@ -197,11 +156,6 @@ def door_state_attrs(self) -> Dict[str, Any]: """Get state attributes for the doors.""" return {} - @property - def other_state_attrs(self) -> Dict[str, Any]: - """State attributes to be optionally overridden in subclasses.""" - return {} - @property def device_state_attributes(self) -> Dict[str, Any]: door_attrs = self.door_state_attrs diff --git a/ge_kitchen/entities/fridge/ge_fridge_water_heater_entity.py b/ge_kitchen/entities/fridge/ge_dispenser.py similarity index 53% rename from ge_kitchen/entities/fridge/ge_fridge_water_heater_entity.py rename to ge_kitchen/entities/fridge/ge_dispenser.py index 68b2bd2..f972e37 100644 --- a/ge_kitchen/entities/fridge/ge_fridge_water_heater_entity.py +++ b/ge_kitchen/entities/fridge/ge_dispenser.py @@ -1,69 +1,38 @@ -"""GE Kitchen Sensor Entities - Fridge Water Heater""" -import sys -import os -import abc -import async_timeout -from datetime import timedelta -import logging -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING +"""GE Kitchen Sensor Entities - Dispenser""" -sys.path.append(os.getcwd() + '/..') +import logging +from typing import List, Optional -from bidict import bidict from gekitchen import ( ErdCode, ErdPresent, - ErdMeasurementUnits, - ErdPodStatus -) -from gekitchen.erd_types import ( + ErdPodStatus, HotWaterStatus ) -from homeassistant.components.water_heater import ( - WaterHeaterEntity, -) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from ..entities import GeEntity -from .abstract_fridge_entity import ( - OP_MODE_K_CUP, +from ..common import GeWaterHeater +from .const import ( + HEATER_TYPE_DISPENSER, OP_MODE_K_CUP, OP_MODE_NORMAL, - OP_MODE_SABBATH, - GeAbstractFridgeEntity + OP_MODE_SABBATH ) _LOGGER = logging.getLogger(__name__) -class GeFridgeWaterHeater(GeEntity, WaterHeaterEntity): - """Entity for in-fridge water heaters""" - +class GeDispenser(GeWaterHeater): + """Entity for in-fridge dispensers""" + # These values are from FridgeHotWaterFragment.smali in the android app min_temp = 90 max_temp = 185 + 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 unique_id(self) -> str: - """Make a unique id.""" - return f"{self.serial_number}-fridge-hot-water" - - @property - def name(self) -> Optional[str]: - """Name it reasonably.""" - return f"GE Fridge Water Heater {self.serial_number}" - - @property - def temperature_unit(self): - """Select the appropriate temperature unit.""" - measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) - if measurement_system == ErdMeasurementUnits.METRIC: - return TEMP_CELSIUS - return TEMP_FAHRENHEIT - @property def supports_k_cups(self) -> bool: """Return True if the device supports k-cup brewing.""" diff --git a/ge_kitchen/entities/fridge/ge_freezer_entity.py b/ge_kitchen/entities/fridge/ge_freezer.py similarity index 69% rename from ge_kitchen/entities/fridge/ge_freezer_entity.py rename to ge_kitchen/entities/fridge/ge_freezer.py index 6fe182e..1580d9e 100644 --- a/ge_kitchen/entities/fridge/ge_freezer_entity.py +++ b/ge_kitchen/entities/fridge/ge_freezer.py @@ -1,24 +1,22 @@ """GE Kitchen Sensor Entities - Freezer""" import logging -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING +from typing import Any, Dict, Optional from gekitchen import ( ErdCode, ErdDoorStatus ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from ..entities import GeEntity -from .abstract_fridge_entity import ( +from .ge_abstract_fridge import ( ATTR_DOOR_STATUS, HEATER_TYPE_FREEZER, OP_MODE_TURBO_FREEZE, - GeAbstractFridgeEntity + GeAbstractFridge ) _LOGGER = logging.getLogger(__name__) -class GeFreezerEntity(GeAbstractFridgeEntity): +class GeFreezer(GeAbstractFridge): """A freezer is basically a fridge.""" heater_type = HEATER_TYPE_FREEZER @@ -32,4 +30,3 @@ def door_state_attrs(self) -> Optional[Dict[str, Any]]: if door_status and door_status != ErdDoorStatus.NA: return {ATTR_DOOR_STATUS: door_status.name.title()} return {} - diff --git a/ge_kitchen/entities/fridge/ge_fridge_entity.py b/ge_kitchen/entities/fridge/ge_fridge.py similarity index 73% rename from ge_kitchen/entities/fridge/ge_fridge_entity.py rename to ge_kitchen/entities/fridge/ge_fridge.py index 615b558..c744dfc 100644 --- a/ge_kitchen/entities/fridge/ge_fridge_entity.py +++ b/ge_kitchen/entities/fridge/ge_fridge.py @@ -1,6 +1,6 @@ """GE Kitchen Sensor Entities - Fridge""" import logging -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING +from typing import Any, Dict from gekitchen import ( ErdCode, @@ -8,31 +8,22 @@ ErdFilterStatus ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from ..entities import GeEntity -from .abstract_fridge_entity import ( +from .const import * +from .ge_abstract_fridge import ( ATTR_DOOR_STATUS, HEATER_TYPE_FRIDGE, OP_MODE_TURBO_COOL, - GeAbstractFridgeEntity + GeAbstractFridge ) _LOGGER = logging.getLogger(__name__) -class GeFridgeEntity(GeAbstractFridgeEntity): +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 available(self) -> bool: - available = super().available - if not available: - app = self.appliance - _LOGGER.critical(f"{self.name} unavailable. Appliance info: Availaible - {app._available} and Init - {app.initialized}") - return available - @property def other_state_attrs(self) -> Dict[str, Any]: """Water filter state.""" From 71a459f5f4c621c0ee8a42dd08bac71c995cf911 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 28 Dec 2020 22:13:46 -0500 Subject: [PATCH 008/338] - set capabilities for the ge water heater/dispenser --- ge_kitchen/entities/common/ge_water_heater.py | 2 +- ge_kitchen/entities/fridge/const.py | 2 + ge_kitchen/entities/fridge/ge_dispenser.py | 85 +++++++++++++++---- 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/ge_kitchen/entities/common/ge_water_heater.py b/ge_kitchen/entities/common/ge_water_heater.py index 5b9aafd..099acfe 100644 --- a/ge_kitchen/entities/common/ge_water_heater.py +++ b/ge_kitchen/entities/common/ge_water_heater.py @@ -21,7 +21,7 @@ def available(self) -> bool: available = super().available if not available: app = self.appliance - _LOGGER.critical(f"{self.name} unavailable. Appliance info: Availaible - {app._available} and Init - {app.initialized}") + _LOGGER.critical(f"{self.name} unavailable. Appliance info: Available - {app._available} and Init - {app.initialized}") return available @property diff --git a/ge_kitchen/entities/fridge/const.py b/ge_kitchen/entities/fridge/const.py index a3eb352..0e72b5e 100644 --- a/ge_kitchen/entities/fridge/const.py +++ b/ge_kitchen/entities/fridge/const.py @@ -11,6 +11,8 @@ HEATER_TYPE_DISPENSER = "dispenser" # Fridge/Freezer +OP_MODE_OFF = "Off" +OP_MODE_HEAT = "Heat" OP_MODE_K_CUP = "K-Cup Brewing" OP_MODE_NORMAL = "Normal" OP_MODE_SABBATH = "Sabbath Mode" diff --git a/ge_kitchen/entities/fridge/ge_dispenser.py b/ge_kitchen/entities/fridge/ge_dispenser.py index f972e37..1571822 100644 --- a/ge_kitchen/entities/fridge/ge_dispenser.py +++ b/ge_kitchen/entities/fridge/ge_dispenser.py @@ -1,20 +1,27 @@ """GE Kitchen Sensor Entities - Dispenser""" import logging -from typing import List, Optional +from typing import List, Optional, Dict, Any + +from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.util.temperature import convert as convert_temperature from gekitchen import ( ErdCode, + ErdHotWaterStatus, ErdPresent, ErdPodStatus, + ErdFullNotFull, HotWaterStatus ) from ..common import GeWaterHeater from .const import ( - HEATER_TYPE_DISPENSER, OP_MODE_K_CUP, - OP_MODE_NORMAL, - OP_MODE_SABBATH + HEATER_TYPE_DISPENSER, + OP_MODE_OFF, + OP_MODE_HEAT, + OP_MODE_SABBATH, + GE_FRIDGE_SUPPORT ) _LOGGER = logging.getLogger(__name__) @@ -22,10 +29,13 @@ class GeDispenser(GeWaterHeater): """Entity for in-fridge dispensers""" - # These values are from FridgeHotWaterFragment.smali in the android app - min_temp = 90 - max_temp = 185 - + # 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 @@ -42,29 +52,74 @@ def supports_k_cups(self) -> bool: @property def operation_list(self) -> List[str]: """Supported Operations List""" - ops_list = [OP_MODE_NORMAL, OP_MODE_SABBATH] - if self.supports_k_cups: - ops_list.append(OP_MODE_K_CUP) + ops_list = [OP_MODE_OFF, OP_MODE_HEAT, OP_MODE_SABBATH] return ops_list async def async_set_temperature(self, **kwargs): - pass + 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): - pass + """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: + if operation_mode == OP_MODE_HEAT: + await self.async_set_temperature(temperature=self.max_temp) + else: + await self.async_set_temperature(temperature=self.min_temp) @property def supported_features(self): - pass + 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 + if self.hot_water_status.status in (ErdHotWaterStatus.HEATING, ErdHotWaterStatus.READY): + return OP_MODE_HEAT + return OP_MODE_OFF @property def current_temperature(self) -> Optional[int]: """Return the current temperature.""" return self.hot_water_status.current_temp + + @property + def min_temp(self): + """Return the minimum temperature.""" + return convert_temperature(self._min_temp, TEMP_FAHRENHEIT, self.temperature_unit) + + @property + def max_temp(self): + """Return the maximum temperature.""" + return convert_temperature(self._max_temp, TEMP_FAHRENHEIT, self.temperature_unit) + + @property + def other_state_attrs(self) -> Dict[str, Any]: + data = {} + if self.hot_water_status.status in [ErdHotWaterStatus.FAULT_LOCKED_OUT, ErdHotWaterStatus.FAULT_NEED_CLEARED]: + data["fault_status"] = self.hot_water_status.status.name.replace("_", " ").title() + if self.supports_k_cups: + data["pod_status"] = self.hot_water_status.pod_status.name.replace("_", "").title() + if self.hot_water_status.time_until_ready: + data["time_until_ready"] = str(self.hot_water_status.time_until_ready)[:-3] + if self.hot_water_status.tank_full != ErdFullNotFull.NA: + data["tank_status"] = self.hot_water_status.tank_full.name.replace("_", " ").title() From c22622f67b9da2207c8755854cb43e7b2e8fde41 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 00:29:00 -0500 Subject: [PATCH 009/338] - added magicattr to allow following of a dot path - modified the property sensors to use magicattr - added new cooktop burner sensor - modified oven with additional entities - modified oven with cooktop --- ge_kitchen/devices/oven.py | 60 +++++++++++++++---- .../common/ge_erd_property_binary_sensor.py | 10 ++-- .../entities/common/ge_erd_property_sensor.py | 10 ++-- ge_kitchen/entities/oven/__init__.py | 3 + ge_kitchen/entities/oven/const.py | 28 +++++++++ .../oven/ge_erd_cooktop_burner_sensor.py | 13 ++++ .../{ge_oven_heater_entity.py => ge_oven.py} | 55 ++++------------- ge_kitchen/manifest.json | 2 +- 8 files changed, 116 insertions(+), 65 deletions(-) create mode 100644 ge_kitchen/entities/oven/const.py create mode 100644 ge_kitchen/entities/oven/ge_erd_cooktop_burner_sensor.py rename ge_kitchen/entities/oven/{ge_oven_heater_entity.py => ge_oven.py} (81%) diff --git a/ge_kitchen/devices/oven.py b/ge_kitchen/devices/oven.py index d04db74..4a8d8f7 100644 --- a/ge_kitchen/devices/oven.py +++ b/ge_kitchen/devices/oven.py @@ -2,10 +2,24 @@ from typing import List from homeassistant.helpers.entity import Entity -from gekitchen.erd import ErdCode, ErdApplianceType, OvenConfiguration +from gekitchen import ( + ErdCode, + ErdApplianceType, + OvenConfiguration, + ErdCooktopConfig, + CooktopStatus +) from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdBinarySensor, GeOvenHeaterEntity +from ..entities import ( + GeErdSensor, + GeErdBinarySensor, + GeErdPropertyBinarySensor, + GeOven, + GeErdCooktopBurnerSensor, + UPPER_OVEN, + LOWER_OVEN +) _LOGGER = logging.getLogger(__name__) @@ -16,8 +30,12 @@ class OvenApi(ApplianceApi): 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 = self.appliance.get_erd_value(ErdCode.COOKTOP_CONFIG) + _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([ @@ -25,22 +43,42 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING), GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER), GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET), + GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE), GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED), GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE), GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING), + GeErdSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER), GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET), + GeErdSensor(self, ErdCode.LOWER_OVEN_RAW_TEMPERATURE), GeErdBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED), - GeOvenHeaterEntity(self, LOWER_OVEN, True), - GeOvenHeaterEntity(self, UPPER_OVEN, True), + + GeOven(self, LOWER_OVEN, True), + GeOven(self, UPPER_OVEN, True), ]) else: oven_entities.extend([ - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, "Oven Cook Mode"), - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, "Oven Cook Time Remaining"), - GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, "Oven Kitchen Timer"), - GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, "Oven User Temp Offset"), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, "Oven Remote Enabled"), - GeOvenHeaterEntity(self, UPPER_OVEN, False) + 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)), + GeErdSensor(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.LOWER_OVEN_USER_TEMP_OFFSET)), + GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE, self.single_name(ErdCode.UPPER_OVEN_RAW_TEMPERATURE)), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self.single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED)), + GeOven(self, UPPER_OVEN, False) ]) - return base_entities + oven_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: + cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, k+".on")) + cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, k+".synchronized")) + if not v.on_off_only: + cooktop_entities.append(GeErdCooktopBurnerSensor(self, ErdCode.COOKTOP_STATUS, k+".power_pct")) + + return base_entities + oven_entities + cooktop_entities + + def single_name(erd_code: ErdCode): + return erd_code.name.replace(UPPER_OVEN+"_","").replace("_", " ").title() diff --git a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py index 5ab9d47..44bc0fe 100644 --- a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py @@ -1,5 +1,6 @@ from typing import Optional +import magicattr from gekitchen import ErdCodeType from ge_kitchen.devices import ApplianceApi from .ge_erd_binary_sensor import GeErdBinarySensor @@ -9,15 +10,16 @@ class GeErdPropertyBinarySensor(GeErdBinarySensor): def __init__(self, api: "ApplianceApi", erd_code: ErdCodeType, erd_property: str): super().__init__(api, erd_code) self.erd_property = erd_property + self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") @property def is_on(self) -> Optional[bool]: """Return True if entity is on.""" try: - value = getattr(self.appliance.get_erd_value(self.erd_code), self.erd_property) + value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) except KeyError: return None - return boolify_erd_value(self.erd_code, value) + return self.appliance.boolify_erd_value(self.erd_code, value) @property def icon(self) -> Optional[str]: @@ -25,10 +27,10 @@ def icon(self) -> Optional[str]: @property def unique_id(self) -> Optional[str]: - return f"{super().unique_id}_{self.erd_property}" + return f"{super().unique_id}_{self._erd_property_cleansed}" @property def name(self) -> Optional[str]: base_string = super().name - property_name = self.erd_property.replace("_", " ").title() + property_name = self._erd_property_cleansed.replace("_", " ").title() return f"{base_string} {property_name}" diff --git a/ge_kitchen/entities/common/ge_erd_property_sensor.py b/ge_kitchen/entities/common/ge_erd_property_sensor.py index 1c1d1e3..2e308b2 100644 --- a/ge_kitchen/entities/common/ge_erd_property_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_sensor.py @@ -1,5 +1,6 @@ from typing import Optional +import magicattr from gekitchen import ErdCode, ErdCodeType, ErdMeasurementUnits from ge_kitchen.devices import ApplianceApi from .ge_erd_sensor import GeErdSensor @@ -10,24 +11,25 @@ class GeErdPropertySensor(GeErdSensor): def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str): super().__init__(api, erd_code) 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}" + return f"{super().unique_id}_{self._erd_property_cleansed}" @property def name(self) -> Optional[str]: base_string = super().name - property_name = self.erd_property.replace("_", " ").title() + property_name = self._erd_property_cleansed.replace("_", " ").title() return f"{base_string} {property_name}" @property def state(self) -> Optional[str]: try: - value = getattr(self.appliance.get_erd_value(self.erd_code), self.erd_property) + value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) except KeyError: return None - return stringify_erd_value(self.erd_code, value, self.units) + return self.appliance.stringify_erd_value(self.erd_code, value, units=self.units) @property def measurement_system(self) -> Optional[ErdMeasurementUnits]: diff --git a/ge_kitchen/entities/oven/__init__.py b/ge_kitchen/entities/oven/__init__.py index e69de29..bd936f2 100644 --- a/ge_kitchen/entities/oven/__init__.py +++ b/ge_kitchen/entities/oven/__init__.py @@ -0,0 +1,3 @@ +from .ge_oven import GeOven +from .ge_erd_cooktop_burner_sensor import GeErdCooktopBurnerSensor +from .const import UPPER_OVEN, LOWER_OVEN \ No newline at end of file diff --git a/ge_kitchen/entities/oven/const.py b/ge_kitchen/entities/oven/const.py new file mode 100644 index 0000000..89250a4 --- /dev/null +++ b/ge_kitchen/entities/oven/const.py @@ -0,0 +1,28 @@ +import bidict + +from homeassistant.components.water_heater import ( + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE +) +from gekitchen import ErdOvenCookMode + +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" + +UPPER_OVEN = "UPPER_OVEN" +LOWER_OVEN = "LOWER_OVEN" + +COOK_MODE_OP_MAP = 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.BAKE_NOOPTION: OP_MODE_BAKE +}) + diff --git a/ge_kitchen/entities/oven/ge_erd_cooktop_burner_sensor.py b/ge_kitchen/entities/oven/ge_erd_cooktop_burner_sensor.py new file mode 100644 index 0000000..840a836 --- /dev/null +++ b/ge_kitchen/entities/oven/ge_erd_cooktop_burner_sensor.py @@ -0,0 +1,13 @@ +from typing import Optional +from ..common import GeErdPropertySensor + +class GeErdCooktopBurnerSensor(GeErdPropertySensor): + icon = "mdi:fire" + + @property + def units(self) -> Optional[str]: + return "%" + + @property + def device_class(self) -> Optional[str]: + return "power_factor" diff --git a/ge_kitchen/entities/oven/ge_oven_heater_entity.py b/ge_kitchen/entities/oven/ge_oven.py similarity index 81% rename from ge_kitchen/entities/oven/ge_oven_heater_entity.py rename to ge_kitchen/entities/oven/ge_oven.py index cb5ca95..5aebf0b 100644 --- a/ge_kitchen/entities/oven/ge_oven_heater_entity.py +++ b/ge_kitchen/entities/oven/ge_oven.py @@ -1,64 +1,29 @@ """GE Kitchen Sensor Entities - Oven""" -import sys -import os import logging -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING +from typing import Any, Dict, List, Optional, Set -sys.path.append(os.getcwd() + '/..') - -from bidict import bidict from gekitchen import ( ErdCode, ErdMeasurementUnits, ErdOvenCookMode, OVEN_COOK_MODE_MAP, -) -from gekitchen.erd_types import ( - OvenCookMode, - OvenCookSetting, + OvenCookSetting ) -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, - WaterHeaterEntity, -) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from ..entities import GeEntity, stringify_erd_value -from ..const import DOMAIN - -if TYPE_CHECKING: - from ..appliance_api import ApplianceApi - from ..update_coordinator import GeKitchenUpdateCoordinator +from ge_kitchen.const import DOMAIN +from ge_kitchen.devices import ApplianceApi +from ..common import GeWaterHeater +from .const import * _LOGGER = logging.getLogger(__name__) -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" - -UPPER_OVEN = "UPPER_OVEN" -LOWER_OVEN = "LOWER_OVEN" - -COOK_MODE_OP_MAP = 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.BAKE_NOOPTION: OP_MODE_BAKE -}) - -class GeOvenHeaterEntity(GeEntity, WaterHeaterEntity): - """Water Heater entity for ovens""" +class GeOven(GeWaterHeater): + """GE Appliance Oven""" icon = "mdi:stove" - def __init__(self, api: "ApplianceApi", oven_select: str = UPPER_OVEN, two_cavity: bool = False): + def __init__(self, api: ApplianceApi, oven_select: str = UPPER_OVEN, two_cavity: bool = False): if oven_select not in (UPPER_OVEN, LOWER_OVEN): raise ValueError(f"Invalid `oven_select` value ({oven_select})") @@ -199,7 +164,7 @@ def get_erd_value(self, suffix: str) -> Any: def display_state(self) -> Optional[str]: erd_code = self.get_erd_code("CURRENT_STATE") erd_value = self.appliance.get_erd_value(erd_code) - return stringify_erd_value(erd_code, erd_value, self.temperature_unit) + return self.appliance.stringify_erd_value(erd_code, erd_value, units=self.temperature_unit) @property def device_state_attributes(self) -> Optional[Dict[str, Any]]: diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index 6cf23c4..284814c 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ge_kitchen", - "requirements": ["gekitchen==0.2.19"], + "requirements": ["gekitchen==0.2.19","magicattr==0.1.15"], "codeowners": ["@ajmarks"] } From a688a75f04370b14b55a0b2270cab13fccef3645 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 00:32:06 -0500 Subject: [PATCH 010/338] - removed the sound sensor from dishwasher --- ge_kitchen/devices/dishwasher.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ge_kitchen/devices/dishwasher.py b/ge_kitchen/devices/dishwasher.py index ab5509e..9cb8c22 100644 --- a/ge_kitchen/devices/dishwasher.py +++ b/ge_kitchen/devices/dishwasher.py @@ -23,7 +23,6 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.OPERATING_MODE), GeErdSensor(self, ErdCode.PODS_REMAINING_VALUE), GeErdSensor(self, ErdCode.RINSE_AGENT), - GeErdSensor(self, ErdCode.SOUND), GeErdSensor(self, ErdCode.TIME_REMAINING), ] entities = base_entities + dishwasher_entities From a73c9f81f3f31934242f65b602c55c3aa714ac7a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 09:38:41 -0500 Subject: [PATCH 011/338] - more dispenser updates - changed location of stringify and boolify methods --- ge_kitchen/entities/common/ge_entity.py | 11 ++++++++ ge_kitchen/entities/common/ge_erd_entity.py | 7 ++--- ge_kitchen/entities/fridge/const.py | 1 - ge_kitchen/entities/fridge/ge_dispenser.py | 29 ++++++++++----------- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/ge_kitchen/entities/common/ge_entity.py b/ge_kitchen/entities/common/ge_entity.py index ba9a55a..141ca3e 100644 --- a/ge_kitchen/entities/common/ge_entity.py +++ b/ge_kitchen/entities/common/ge_entity.py @@ -1,3 +1,4 @@ +from datetime import timedelta from typing import Optional, Dict, Any from gekitchen import GeAppliance @@ -38,3 +39,13 @@ def appliance(self) -> GeAppliance: @property def name(self) -> Optional[str]: raise NotImplementedError + + def _stringify_erd_value(self, value: any, **kwargs) -> Optional[str]: + if isinstance(timedelta): + return str(value)[:-3] if value else "" + if value is None: + return None + return self.appliance.stringify_erd_value(value, kwargs) + + def _boolify_erd_value(self, value: any) -> Optional[bool]: + return self.appliance.boolify_erd_value(value) diff --git a/ge_kitchen/entities/common/ge_erd_entity.py b/ge_kitchen/entities/common/ge_erd_entity.py index 2c98eda..5495625 100644 --- a/ge_kitchen/entities/common/ge_erd_entity.py +++ b/ge_kitchen/entities/common/ge_erd_entity.py @@ -1,3 +1,4 @@ +from datetime import timedelta from typing import Optional from gekitchen import ErdCode, ErdCodeType, ErdCodeClass @@ -57,11 +58,11 @@ def _stringify_erd_value(self, value: any, **kwargs) -> Optional[str]: return f"{value}" if self.erd_code_class == ErdCodeClass.NON_ZERO_TEMPERATURE: return f"{value}" if value else "" - if self.erd_code_class == ErdCodeClass.TIMER: + if self.erd_code_class == ErdCodeClass.TIMER or isinstance(value, timedelta): return str(value)[:-3] if value else "" if value is None: return None - return self.appliance.stringify_erd_value(self.erd_code, value, kwargs) + return self.appliance.stringify_erd_value(value, kwargs) def _boolify_erd_value(self, value: any) -> Optional[bool]: - return self.appliance.boolify_erd_value(self.erd_code, value) + return self.appliance.boolify_erd_value(value) diff --git a/ge_kitchen/entities/fridge/const.py b/ge_kitchen/entities/fridge/const.py index 0e72b5e..f7ca729 100644 --- a/ge_kitchen/entities/fridge/const.py +++ b/ge_kitchen/entities/fridge/const.py @@ -12,7 +12,6 @@ # Fridge/Freezer OP_MODE_OFF = "Off" -OP_MODE_HEAT = "Heat" OP_MODE_K_CUP = "K-Cup Brewing" OP_MODE_NORMAL = "Normal" OP_MODE_SABBATH = "Sabbath Mode" diff --git a/ge_kitchen/entities/fridge/ge_dispenser.py b/ge_kitchen/entities/fridge/ge_dispenser.py index 1571822..570b1d8 100644 --- a/ge_kitchen/entities/fridge/ge_dispenser.py +++ b/ge_kitchen/entities/fridge/ge_dispenser.py @@ -18,8 +18,7 @@ from ..common import GeWaterHeater from .const import ( HEATER_TYPE_DISPENSER, - OP_MODE_OFF, - OP_MODE_HEAT, + OP_MODE_NORMAL, OP_MODE_SABBATH, GE_FRIDGE_SUPPORT ) @@ -52,7 +51,7 @@ def supports_k_cups(self) -> bool: @property def operation_list(self) -> List[str]: """Supported Operations List""" - ops_list = [OP_MODE_OFF, OP_MODE_HEAT, OP_MODE_SABBATH] + ops_list = [OP_MODE_NORMAL, OP_MODE_SABBATH] return ops_list async def async_set_temperature(self, **kwargs): @@ -78,11 +77,6 @@ async def async_set_operation_mode(self, operation_mode): return sabbath_mode = operation_mode == OP_MODE_SABBATH await self.async_set_sabbath_mode(sabbath_mode) - if not sabbath_mode: - if operation_mode == OP_MODE_HEAT: - await self.async_set_temperature(temperature=self.max_temp) - else: - await self.async_set_temperature(temperature=self.min_temp) @property def supported_features(self): @@ -93,15 +87,18 @@ def current_operation(self) -> str: """Get the current operation mode.""" if self.appliance.get_erd_value(ErdCode.SABBATH_MODE): return OP_MODE_SABBATH - if self.hot_water_status.status in (ErdHotWaterStatus.HEATING, ErdHotWaterStatus.READY): - return OP_MODE_HEAT - return OP_MODE_OFF + 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.""" @@ -115,11 +112,13 @@ def max_temp(self): @property def other_state_attrs(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.hot_water_status.status.name.replace("_", " ").title() + data["fault_status"] = self._stringify_erd_value(self.hot_water_status.status) if self.supports_k_cups: - data["pod_status"] = self.hot_water_status.pod_status.name.replace("_", "").title() + data["pod_status"] = self._stringify_erd_value(self.hot_water_status.pod_status) if self.hot_water_status.time_until_ready: - data["time_until_ready"] = str(self.hot_water_status.time_until_ready)[:-3] + data["time_until_ready"] = self._stringify_erd_value(self.hot_water_status.time_until_ready) if self.hot_water_status.tank_full != ErdFullNotFull.NA: - data["tank_status"] = self.hot_water_status.tank_full.name.replace("_", " ").title() + data["tank_status"] = self._stringify_erd_value(self.hot_water_status.tank_full) From 0ee76fc4dbc6561ad72c1ede5aefe81ca26cf642 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 10:03:37 -0500 Subject: [PATCH 012/338] - more stringify related changes --- ge_kitchen/entities/common/ge_entity.py | 4 ++-- ge_kitchen/entities/common/ge_erd_entity.py | 9 ++++----- .../entities/common/ge_erd_property_binary_sensor.py | 2 +- ge_kitchen/entities/common/ge_erd_property_sensor.py | 2 +- ge_kitchen/entities/common/ge_erd_sensor.py | 4 ++-- ge_kitchen/entities/fridge/ge_abstract_fridge.py | 4 ++-- ge_kitchen/entities/fridge/ge_dispenser.py | 8 ++++---- ge_kitchen/entities/fridge/ge_freezer.py | 2 +- ge_kitchen/entities/fridge/ge_fridge.py | 2 +- ge_kitchen/entities/oven/ge_oven.py | 10 +++++----- 10 files changed, 23 insertions(+), 24 deletions(-) diff --git a/ge_kitchen/entities/common/ge_entity.py b/ge_kitchen/entities/common/ge_entity.py index 141ca3e..2d3d820 100644 --- a/ge_kitchen/entities/common/ge_entity.py +++ b/ge_kitchen/entities/common/ge_entity.py @@ -40,12 +40,12 @@ def appliance(self) -> GeAppliance: def name(self) -> Optional[str]: raise NotImplementedError - def _stringify_erd_value(self, value: any, **kwargs) -> Optional[str]: + def _stringify(self, value: any, **kwargs) -> Optional[str]: if isinstance(timedelta): return str(value)[:-3] if value else "" if value is None: return None return self.appliance.stringify_erd_value(value, kwargs) - def _boolify_erd_value(self, value: any) -> Optional[bool]: + def _boolify(self, value: any) -> Optional[bool]: return self.appliance.boolify_erd_value(value) diff --git a/ge_kitchen/entities/common/ge_erd_entity.py b/ge_kitchen/entities/common/ge_erd_entity.py index 5495625..fbe4bcb 100644 --- a/ge_kitchen/entities/common/ge_erd_entity.py +++ b/ge_kitchen/entities/common/ge_erd_entity.py @@ -10,11 +10,13 @@ class GeErdEntity(GeEntity): """Parent class for GE entities tied to a specific ERD""" - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None, icon_override: str = None, device_class_override: str = None): super().__init__(api) self._erd_code = api.appliance.translate_erd_code(erd_code) self._erd_code_class = api.appliance.get_erd_code_class(self._erd_code) self._erd_override = erd_override + self._icon_override = icon_override + self._device_class_override = device_class_override @property def erd_code(self) -> ErdCodeType: @@ -50,7 +52,7 @@ def unique_id(self) -> Optional[str]: def icon(self) -> Optional[str]: return get_erd_icon(self.erd_code) - def _stringify_erd_value(self, value: any, **kwargs) -> Optional[str]: + def _stringify(self, value: any, **kwargs) -> Optional[str]: # perform special processing before passing over to the default method if self.erd_code == ErdCode.CLOCK_TIME: return value.strftime("%H:%M:%S") if value else None @@ -63,6 +65,3 @@ def _stringify_erd_value(self, value: any, **kwargs) -> Optional[str]: if value is None: return None return self.appliance.stringify_erd_value(value, kwargs) - - def _boolify_erd_value(self, value: any) -> Optional[bool]: - return self.appliance.boolify_erd_value(value) diff --git a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py index 44bc0fe..f2df631 100644 --- a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py @@ -19,7 +19,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 - return self.appliance.boolify_erd_value(self.erd_code, value) + return self._boolify(self.erd_code, value) @property def icon(self) -> Optional[str]: diff --git a/ge_kitchen/entities/common/ge_erd_property_sensor.py b/ge_kitchen/entities/common/ge_erd_property_sensor.py index 2e308b2..689cf87 100644 --- a/ge_kitchen/entities/common/ge_erd_property_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_sensor.py @@ -29,7 +29,7 @@ def state(self) -> Optional[str]: value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) except KeyError: return None - return self.appliance.stringify_erd_value(self.erd_code, value, units=self.units) + return self._stringify(value, units=self.units) @property def measurement_system(self) -> Optional[ErdMeasurementUnits]: diff --git a/ge_kitchen/entities/common/ge_erd_sensor.py b/ge_kitchen/entities/common/ge_erd_sensor.py index 76887d2..678d6ba 100644 --- a/ge_kitchen/entities/common/ge_erd_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_sensor.py @@ -6,7 +6,7 @@ from .ge_erd_entity import GeErdEntity -class GeErdSensor(GeErdEntity, Entity): +class GeErdSensor(GeErdEntity, Entity): """GE Entity for sensors""" @property def state(self) -> Optional[str]: @@ -14,7 +14,7 @@ def state(self) -> Optional[str]: value = self.appliance.get_erd_value(self.erd_code) except KeyError: return None - return self._stringify_erd_value(self.erd_code, value, units=self.units) + return self._stringify(value, units=self.units) @property def measurement_system(self) -> Optional[ErdMeasurementUnits]: diff --git a/ge_kitchen/entities/fridge/ge_abstract_fridge.py b/ge_kitchen/entities/fridge/ge_abstract_fridge.py index cd35b3a..a019e38 100644 --- a/ge_kitchen/entities/fridge/ge_abstract_fridge.py +++ b/ge_kitchen/entities/fridge/ge_abstract_fridge.py @@ -142,12 +142,12 @@ def ice_maker_state_attrs(self) -> Dict[str, Any]: 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"] = ice_bucket_status.name.replace("_", " ").title() + data["ice_bucket"] = self._stringify(ice_bucket_status) 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"] = ice_control_status.name.replace("_", " ").lower() + data["ice_maker"] = self._stringify(ice_control_status) return data diff --git a/ge_kitchen/entities/fridge/ge_dispenser.py b/ge_kitchen/entities/fridge/ge_dispenser.py index 570b1d8..98f116b 100644 --- a/ge_kitchen/entities/fridge/ge_dispenser.py +++ b/ge_kitchen/entities/fridge/ge_dispenser.py @@ -115,10 +115,10 @@ def other_state_attrs(self) -> Dict[str, Any]: 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_erd_value(self.hot_water_status.status) + data["fault_status"] = self._stringify(self.hot_water_status.status) if self.supports_k_cups: - data["pod_status"] = self._stringify_erd_value(self.hot_water_status.pod_status) + 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_erd_value(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_erd_value(self.hot_water_status.tank_full) + data["tank_status"] = self._stringify(self.hot_water_status.tank_full) diff --git a/ge_kitchen/entities/fridge/ge_freezer.py b/ge_kitchen/entities/fridge/ge_freezer.py index 1580d9e..5b85e82 100644 --- a/ge_kitchen/entities/fridge/ge_freezer.py +++ b/ge_kitchen/entities/fridge/ge_freezer.py @@ -28,5 +28,5 @@ class GeFreezer(GeAbstractFridge): def door_state_attrs(self) -> Optional[Dict[str, Any]]: door_status = self.door_status.freezer if door_status and door_status != ErdDoorStatus.NA: - return {ATTR_DOOR_STATUS: door_status.name.title()} + return {ATTR_DOOR_STATUS: self._stringify(door_status)} return {} diff --git a/ge_kitchen/entities/fridge/ge_fridge.py b/ge_kitchen/entities/fridge/ge_fridge.py index c744dfc..9b34892 100644 --- a/ge_kitchen/entities/fridge/ge_fridge.py +++ b/ge_kitchen/entities/fridge/ge_fridge.py @@ -30,7 +30,7 @@ def other_state_attrs(self) -> Dict[str, Any]: filter_status: ErdFilterStatus = self.appliance.get_erd_value(ErdCode.WATER_FILTER_STATUS) if filter_status == ErdFilterStatus.NA: return {} - return {"water_filter_status": filter_status.name.replace("_", " ").title()} + return {"water_filter_status": self._stringify(filter_status)} @property def door_state_attrs(self) -> Dict[str, Any]: diff --git a/ge_kitchen/entities/oven/ge_oven.py b/ge_kitchen/entities/oven/ge_oven.py index 5aebf0b..5c4578f 100644 --- a/ge_kitchen/entities/oven/ge_oven.py +++ b/ge_kitchen/entities/oven/ge_oven.py @@ -164,7 +164,7 @@ def get_erd_value(self, suffix: str) -> Any: 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.appliance.stringify_erd_value(erd_code, erd_value, units=self.temperature_unit) + return self._stringify(erd_value, units=self.temperature_unit) @property def device_state_attributes(self) -> Optional[Dict[str, Any]]: @@ -181,11 +181,11 @@ def device_state_attributes(self) -> Optional[Dict[str, Any]]: kitchen_timer = self.get_erd_value("KITCHEN_TIMER") delay_time = self.get_erd_value("DELAY_TIME_REMAINING") if elapsed_time: - data["cook_time_elapsed"] = str(elapsed_time) + data["cook_time_elapsed"] = self._stringify(elapsed_time) if cook_time_left: - data["cook_time_left"] = str(cook_time_left) + data["cook_time_left"] = self._stringify(cook_time_left) if kitchen_timer: - data["cook_time_remaining"] = str(kitchen_timer) + data["cook_time_remaining"] = self._stringify(kitchen_timer) if delay_time: - data["delay_time_remaining"] = str(delay_time) + data["delay_time_remaining"] = self._stringify(delay_time) return data From ebd50d10de2edee7fa5ebbb6ba614005d94a7d12 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 11:13:41 -0500 Subject: [PATCH 013/338] - updates for device class, icon, and uom --- ge_kitchen/entities/common/ge_entity.py | 14 +++ .../entities/common/ge_erd_binary_sensor.py | 30 +++-- ge_kitchen/entities/common/ge_erd_entity.py | 48 +++++++- .../common/ge_erd_property_binary_sensor.py | 8 +- .../entities/common/ge_erd_property_sensor.py | 18 +-- ge_kitchen/entities/common/ge_erd_sensor.py | 49 ++++---- ge_kitchen/entities/entities.py | 105 ------------------ 7 files changed, 110 insertions(+), 162 deletions(-) delete mode 100644 ge_kitchen/entities/entities.py diff --git a/ge_kitchen/entities/common/ge_entity.py b/ge_kitchen/entities/common/ge_entity.py index 2d3d820..ceaf893 100644 --- a/ge_kitchen/entities/common/ge_entity.py +++ b/ge_kitchen/entities/common/ge_entity.py @@ -40,6 +40,14 @@ def appliance(self) -> GeAppliance: def name(self) -> Optional[str]: raise NotImplementedError + @property + def icon(self) -> Optional[str]: + return self._get_icon() + + @property + def device_class(self) -> Optional[str]: + return self._get_device_class() + def _stringify(self, value: any, **kwargs) -> Optional[str]: if isinstance(timedelta): return str(value)[:-3] if value else "" @@ -49,3 +57,9 @@ def _stringify(self, value: any, **kwargs) -> Optional[str]: def _boolify(self, value: any) -> Optional[bool]: return self.appliance.boolify_erd_value(value) + + def _get_icon(self) -> Optional[str]: + return None + + def _get_device_class(self) -> Optional[str]: + return None \ No newline at end of file diff --git a/ge_kitchen/entities/common/ge_erd_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_binary_sensor.py index 51ad8dd..0260c0e 100644 --- a/ge_kitchen/entities/common/ge_erd_binary_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_binary_sensor.py @@ -2,23 +2,37 @@ from homeassistant.components.binary_sensor import BinarySensorEntity +from gekitchen import ErdCode, ErdCodeType, ErdCodeClass +from ge_kitchen.devices import ApplianceApi from .ge_erd_entity import GeErdEntity class GeErdBinarySensor(GeErdEntity, BinarySensorEntity): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str, icon_on_override: str, icon_off_override: str, device_class_override: str): + super().__init__(api, erd_code, erd_override=erd_override, icon_override=icon_on_override, device_class_override=device_class_override) + self._icon_on_override = icon_on_override + self._icon_off_override = icon_off_override + """GE Entity for binary sensors""" @property def is_on(self) -> bool: """Return True if entity is on.""" - return bool(self.appliance.get_erd_value(self.erd_code)) + return self._boolify(self.appliance.get_erd_value(self.erd_code)) - @property - def icon(self) -> Optional[str]: - return get_erd_icon(self.erd_code, self.is_on) + def _get_erd_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" - @property - def device_class(self) -> Optional[str]: - if self.erd_code in DOOR_ERD_CODES: - return "door" return None + 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/ge_kitchen/entities/common/ge_erd_entity.py b/ge_kitchen/entities/common/ge_erd_entity.py index fbe4bcb..9b0d99d 100644 --- a/ge_kitchen/entities/common/ge_erd_entity.py +++ b/ge_kitchen/entities/common/ge_erd_entity.py @@ -1,7 +1,8 @@ from datetime import timedelta from typing import Optional -from gekitchen import ErdCode, ErdCodeType, ErdCodeClass +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from gekitchen import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits from ge_kitchen.const import DOMAIN from ge_kitchen.devices import ApplianceApi @@ -48,11 +49,8 @@ def name(self) -> Optional[str]: def unique_id(self) -> Optional[str]: return f"{DOMAIN}_{self.serial_number}_{self.erd_string.lower()}" - @property - def icon(self) -> Optional[str]: - return get_erd_icon(self.erd_code) - def _stringify(self, value: any, **kwargs) -> Optional[str]: + """ Stringify a value """ # perform special processing before passing over to the default method if self.erd_code == ErdCode.CLOCK_TIME: return value.strftime("%H:%M:%S") if value else None @@ -65,3 +63,43 @@ def _stringify(self, value: any, **kwargs) -> Optional[str]: if value is None: return None return self.appliance.stringify_erd_value(value, kwargs) + + @property + def _temp_measurement_system(self) -> Optional[ErdMeasurementUnits]: + try: + value = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + except KeyError: + return None + return value + + def _get_icon(self): + """Select an appropriate icon.""" + + if self._icon_override: + return self._icon_override + if not isinstance(self.erd_code, ErdCode): + return None + if self.erd_code_class == ErdCodeClass.CLOCK: + return "mdi:clock" + if self.erd_code_class == ErdCodeClass.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:judaism" + 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.DISWASHER_SENSOR: + return "mdi:dishwasher" + + return None diff --git a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py index f2df631..b6d8983 100644 --- a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py @@ -7,8 +7,8 @@ class GeErdPropertyBinarySensor(GeErdBinarySensor): """GE Entity for property binary sensors""" - def __init__(self, api: "ApplianceApi", erd_code: ErdCodeType, erd_property: str): - super().__init__(api, erd_code) + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str, erd_override: str, icon_on_override: str, icon_off_override: str, device_class_override: str): + super().__init__(api, erd_code, erd_override, icon_on_override, icon_off_override, device_class_override) self.erd_property = erd_property self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") @@ -21,10 +21,6 @@ def is_on(self) -> Optional[bool]: return None return self._boolify(self.erd_code, value) - @property - def icon(self) -> Optional[str]: - return get_erd_icon(self.erd_code, self.is_on) - @property def unique_id(self) -> Optional[str]: return f"{super().unique_id}_{self._erd_property_cleansed}" diff --git a/ge_kitchen/entities/common/ge_erd_property_sensor.py b/ge_kitchen/entities/common/ge_erd_property_sensor.py index 689cf87..3b80398 100644 --- a/ge_kitchen/entities/common/ge_erd_property_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_sensor.py @@ -8,8 +8,8 @@ class GeErdPropertySensor(GeErdSensor): """GE Entity for sensors""" - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str): - super().__init__(api, erd_code) + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str, erd_override: str, icon_override: str, device_class_override: str): + super().__init__(api, erd_code, erd_override=erd_override, icon_override=icon_override, device_class_override=device_class_override) self.erd_property = erd_property self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") @@ -30,17 +30,3 @@ def state(self) -> Optional[str]: except KeyError: return None return self._stringify(value, units=self.units) - - @property - def measurement_system(self) -> Optional[ErdMeasurementUnits]: - return self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) - - @property - def units(self) -> Optional[str]: - return get_erd_units(self.erd_code, self.measurement_system) - - @property - def device_class(self) -> Optional[str]: - if self.erd_code in TEMPERATURE_ERD_CODES: - return "temperature" - return None diff --git a/ge_kitchen/entities/common/ge_erd_sensor.py b/ge_kitchen/entities/common/ge_erd_sensor.py index 678d6ba..7152631 100644 --- a/ge_kitchen/entities/common/ge_erd_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_sensor.py @@ -1,6 +1,12 @@ from typing import Optional -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_POWER_FACTOR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT +) from homeassistant.helpers.entity import Entity from gekitchen import ErdCode, ErdCodeClass, ErdMeasurementUnits @@ -17,28 +23,27 @@ def state(self) -> Optional[str]: return self._stringify(value, units=self.units) @property - def measurement_system(self) -> Optional[ErdMeasurementUnits]: - try: - value = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) - except KeyError: - return None - return value - - @property - def units(self) -> Optional[str]: - return get_erd_units(self.erd_code, self.measurement_system) + def unit_of_measurement(self) -> Optional[str]: + return self._get_uom() + + def _get_uom(self): + """ Select appropriate units """ + if self.erd_code_class == ErdCodeClass.TEMPERATURE or self.device_class == DEVICE_CLASS_TEMPERATURE: + if self._temp_measurement_system == ErdMeasurementUnits.METRIC: + return TEMP_CELSIUS + return TEMP_FAHRENHEIT + if self.erd_code_class == ErdCodeClass.BATTERY or self.device_class == DEVICE_CLASS_BATTERY: + return "%" + if self.device_class == DEVICE_CLASS_POWER_FACTOR: + return "%" + return None - @property - def device_class(self) -> Optional[str]: + 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 - return None - - @property - def icon(self) -> Optional[str]: - return get_erd_icon(self.erd_code, self.state) + if self.erd_code_class == ErdCodeClass.BATTERY: + return DEVICE_CLASS_BATTERY - @property - def unit_of_measurement(self) -> Optional[str]: - if self.device_class == DEVICE_CLASS_TEMPERATURE: - return self.units + return None diff --git a/ge_kitchen/entities/entities.py b/ge_kitchen/entities/entities.py deleted file mode 100644 index 0c03e2a..0000000 --- a/ge_kitchen/entities/entities.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Define all of the entity types""" - -import logging -from typing import Any, Dict, Optional, TYPE_CHECKING - -from gekitchen import ErdCodeType, GeAppliance, translate_erd_code -from gekitchen.erd_types import * -from gekitchen.erd_constants import * -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant - - -from .const import DOMAIN -from .erd_string_utils import * - -if TYPE_CHECKING: - from .appliance_api import ApplianceApi - - -_LOGGER = logging.getLogger(__name__) - -DOOR_ERD_CODES = { - ErdCode.DOOR_STATUS -} -RAW_TEMPERATURE_ERD_CODES = { - ErdCode.LOWER_OVEN_RAW_TEMPERATURE, - ErdCode.LOWER_OVEN_USER_TEMP_OFFSET, - ErdCode.UPPER_OVEN_RAW_TEMPERATURE, - ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, - ErdCode.CURRENT_TEMPERATURE, - ErdCode.TEMPERATURE_SETTING, -} -NONZERO_TEMPERATURE_ERD_CODES = { - ErdCode.HOT_WATER_SET_TEMP, - ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE, - ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP, - ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, - ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, -} -TEMPERATURE_ERD_CODES = RAW_TEMPERATURE_ERD_CODES.union(NONZERO_TEMPERATURE_ERD_CODES) -TIMER_ERD_CODES = { - ErdCode.LOWER_OVEN_ELAPSED_COOK_TIME, - ErdCode.LOWER_OVEN_KITCHEN_TIMER, - ErdCode.LOWER_OVEN_DELAY_TIME_REMAINING, - ErdCode.LOWER_OVEN_COOK_TIME_REMAINING, - ErdCode.LOWER_OVEN_ELAPSED_COOK_TIME, - ErdCode.ELAPSED_ON_TIME, - ErdCode.TIME_REMAINING, - ErdCode.UPPER_OVEN_ELAPSED_COOK_TIME, - ErdCode.UPPER_OVEN_KITCHEN_TIMER, - ErdCode.UPPER_OVEN_DELAY_TIME_REMAINING, - ErdCode.UPPER_OVEN_ELAPSED_COOK_TIME, - ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, -} - - -def get_erd_units(erd_code: ErdCodeType, measurement_units: ErdMeasurementUnits): - """Get the units for a sensor.""" - erd_code = translate_erd_code(erd_code) - if not measurement_units: - return None - - if erd_code in TEMPERATURE_ERD_CODES or erd_code in {ErdCode.LOWER_OVEN_COOK_MODE, ErdCode.UPPER_OVEN_COOK_MODE}: - if measurement_units == ErdMeasurementUnits.METRIC: - return TEMP_CELSIUS - return TEMP_FAHRENHEIT - return None - - -def get_erd_icon(erd_code: ErdCodeType, value: Any = None) -> Optional[str]: - """Select an appropriate icon.""" - erd_code = translate_erd_code(erd_code) - if not isinstance(erd_code, ErdCode): - return None - if erd_code in TIMER_ERD_CODES: - return "mdi:timer-outline" - if erd_code in { - ErdCode.LOWER_OVEN_COOK_MODE, - ErdCode.LOWER_OVEN_CURRENT_STATE, - ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE, - ErdCode.UPPER_OVEN_COOK_MODE, - ErdCode.UPPER_OVEN_CURRENT_STATE, - ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, - ErdCode.WARMING_DRAWER_STATE, - }: - return "mdi:stove" - if erd_code in { - ErdCode.TURBO_COOL_STATUS, - ErdCode.TURBO_FREEZE_STATUS, - }: - return "mdi:snowflake" - if erd_code == ErdCode.SABBATH_MODE: - return "mdi:judaism" - - # Let binary sensors assign their own. Might be worth passing - # the actual entity in if we want to do more of this. - if erd_code in DOOR_ERD_CODES and isinstance(value, str): - if "open" in value.lower(): - return "mdi:door-open" - return "mdi:door-closed" - - return None - - - From 1374b3629e4283a7fe8659d59920c2f03997d8f4 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 12:02:56 -0500 Subject: [PATCH 014/338] - sensor configuration updates --- ge_kitchen/devices/dishwasher.py | 2 +- ge_kitchen/devices/fridge.py | 51 +++++++++++++++---- ge_kitchen/devices/oven.py | 4 +- ge_kitchen/entities/common/ge_erd_entity.py | 2 + ge_kitchen/entities/common/ge_erd_sensor.py | 8 +++ ge_kitchen/entities/oven/__init__.py | 1 - .../oven/ge_erd_cooktop_burner_sensor.py | 13 ----- 7 files changed, 54 insertions(+), 27 deletions(-) delete mode 100644 ge_kitchen/entities/oven/ge_erd_cooktop_burner_sensor.py diff --git a/ge_kitchen/devices/dishwasher.py b/ge_kitchen/devices/dishwasher.py index 9cb8c22..99bc43b 100644 --- a/ge_kitchen/devices/dishwasher.py +++ b/ge_kitchen/devices/dishwasher.py @@ -22,7 +22,7 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.CYCLE_STATE), GeErdSensor(self, ErdCode.OPERATING_MODE), GeErdSensor(self, ErdCode.PODS_REMAINING_VALUE), - GeErdSensor(self, ErdCode.RINSE_AGENT), + GeErdSensor(self, ErdCode.RINSE_AGENT, icon_override="mdi:sparkle"), GeErdSensor(self, ErdCode.TIME_REMAINING), ] entities = base_entities + dishwasher_entities diff --git a/ge_kitchen/devices/fridge.py b/ge_kitchen/devices/fridge.py index 33dd788..024b9ac 100644 --- a/ge_kitchen/devices/fridge.py +++ b/ge_kitchen/devices/fridge.py @@ -1,3 +1,5 @@ +from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM +from homeassistant.const import DEVICE_CLASS_TEMPERATURE import logging from typing import List @@ -5,7 +7,15 @@ from gekitchen.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdSwitch, GeFridgeEntity, GeFreezerEntity +from ..entities import ( + GeErdSensor, + GeErdSwitch, + GeFridge, + GeFreezer, + GeDispenser, + GeErdPropertySensor, + GeErdPropertyBinarySensor +) _LOGGER = logging.getLogger(__name__) @@ -15,17 +25,38 @@ class FridgeApi(ApplianceApi): def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() + + fridge_entities = [] + freezer_entities = [] + dispenser_entities = [] - fridge_entities = [ - GeErdSensor(self, ErdCode.AIR_FILTER_STATUS), - GeErdSensor(self, ErdCode.DOOR_STATUS), + common_entities = [ GeErdSensor(self, ErdCode.FRIDGE_MODEL_INFO), - GeErdSensor(self, ErdCode.HOT_WATER_IN_USE), - GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), - GeErdSensor(self, ErdCode.HOT_WATER_STATUS), GeErdSwitch(self, ErdCode.SABBATH_MODE), - GeFreezerEntity(self), - GeFridgeEntity(self), + GeErdSensor(self, ErdCode.DOOR_STATUS), + GeErdPropertyBinarySensor(self, ErdCode.DOOR_STATUS, "any_open") ] - entities = base_entities + fridge_entities + + fridge_entities.extend([ + GeErdSensor(self, ErdCode.WATER_FILTER_STATUS), + GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "fridge"), + GeFridge(self), + ]) + + freezer_entities.extend([ + GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "freezer"), + GeFreezer(self), + ]) + + dispenser_entities.extend([ + GeErdSensor(self, ErdCode.HOT_WATER_IN_USE), + GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "status"), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_remaining", icon_override="mdi:timer-outline"), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_remaining", device_class_override=DEVICE_CLASS_TEMPERATURE), + 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/ge_kitchen/devices/oven.py b/ge_kitchen/devices/oven.py index 4a8d8f7..eec0e9b 100644 --- a/ge_kitchen/devices/oven.py +++ b/ge_kitchen/devices/oven.py @@ -1,6 +1,7 @@ import logging from typing import List +from homeassistant.const import DEVICE_CLASS_POWER_FACTOR from homeassistant.helpers.entity import Entity from gekitchen import ( ErdCode, @@ -16,7 +17,6 @@ GeErdBinarySensor, GeErdPropertyBinarySensor, GeOven, - GeErdCooktopBurnerSensor, UPPER_OVEN, LOWER_OVEN ) @@ -76,7 +76,7 @@ def get_all_entities(self) -> List[Entity]: cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, k+".on")) cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, k+".synchronized")) if not v.on_off_only: - cooktop_entities.append(GeErdCooktopBurnerSensor(self, ErdCode.COOKTOP_STATUS, k+".power_pct")) + cooktop_entities.append(GeErdSensor(self, ErdCode.COOKTOP_STATUS, k+".power_pct", icon_override="mdi:fire", device_class_override=DEVICE_CLASS_POWER_FACTOR)) return base_entities + oven_entities + cooktop_entities diff --git a/ge_kitchen/entities/common/ge_erd_entity.py b/ge_kitchen/entities/common/ge_erd_entity.py index 9b0d99d..01c9c90 100644 --- a/ge_kitchen/entities/common/ge_erd_entity.py +++ b/ge_kitchen/entities/common/ge_erd_entity.py @@ -81,6 +81,8 @@ def _get_icon(self): 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: diff --git a/ge_kitchen/entities/common/ge_erd_sensor.py b/ge_kitchen/entities/common/ge_erd_sensor.py index 7152631..308b022 100644 --- a/ge_kitchen/entities/common/ge_erd_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_sensor.py @@ -47,3 +47,11 @@ def _get_device_class(self) -> Optional[str]: return DEVICE_CLASS_BATTERY 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() \ No newline at end of file diff --git a/ge_kitchen/entities/oven/__init__.py b/ge_kitchen/entities/oven/__init__.py index bd936f2..a5e7f85 100644 --- a/ge_kitchen/entities/oven/__init__.py +++ b/ge_kitchen/entities/oven/__init__.py @@ -1,3 +1,2 @@ from .ge_oven import GeOven -from .ge_erd_cooktop_burner_sensor import GeErdCooktopBurnerSensor from .const import UPPER_OVEN, LOWER_OVEN \ No newline at end of file diff --git a/ge_kitchen/entities/oven/ge_erd_cooktop_burner_sensor.py b/ge_kitchen/entities/oven/ge_erd_cooktop_burner_sensor.py deleted file mode 100644 index 840a836..0000000 --- a/ge_kitchen/entities/oven/ge_erd_cooktop_burner_sensor.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Optional -from ..common import GeErdPropertySensor - -class GeErdCooktopBurnerSensor(GeErdPropertySensor): - icon = "mdi:fire" - - @property - def units(self) -> Optional[str]: - return "%" - - @property - def device_class(self) -> Optional[str]: - return "power_factor" From d432b77f60e65a31cc6cd928759806c9e7a946ac Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 12:06:10 -0500 Subject: [PATCH 015/338] - fixed code typo --- ge_kitchen/entities/common/ge_erd_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/entities/common/ge_erd_entity.py b/ge_kitchen/entities/common/ge_erd_entity.py index 01c9c90..6e5140a 100644 --- a/ge_kitchen/entities/common/ge_erd_entity.py +++ b/ge_kitchen/entities/common/ge_erd_entity.py @@ -101,7 +101,7 @@ def _get_icon(self): return "mdi:fridge-top" if self.erd_code_class == ErdCodeClass.DISPENSER_SENSOR: return "mdi:cup-water" - if self.erd_code_class == ErdCodeClass.DISWASHER_SENSOR: + if self.erd_code_class == ErdCodeClass.DISHWASHER_SENSOR: return "mdi:dishwasher" return None From 28c145e7eef463f8d0da1522a8cc31a7ee70820d Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 12:40:29 -0500 Subject: [PATCH 016/338] - fridge device sensor updates --- ge_kitchen/devices/fridge.py | 56 +++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/ge_kitchen/devices/fridge.py b/ge_kitchen/devices/fridge.py index 024b9ac..d3c0a95 100644 --- a/ge_kitchen/devices/fridge.py +++ b/ge_kitchen/devices/fridge.py @@ -4,7 +4,16 @@ from typing import List from homeassistant.helpers.entity import Entity -from gekitchen.erd import ErdCode, ErdApplianceType +from gekitchen import ( + ErdCode, + ErdApplianceType, + ErdOnOff, + ErdHotWaterStatus, + FridgeIceBucketStatus, + IceMakerControlStatus, + ErdFilterStatus, + HotWaterStatus +) from .base import ApplianceApi from ..entities import ( @@ -30,33 +39,58 @@ def get_all_entities(self) -> List[Entity]: freezer_entities = [] dispenser_entities = [] + # Get the statuses used to determine presence + ice_maker_control: IceMakerControlStatus = self.appliance.get_erd_value(ErdCode.ICE_MAKER_CONTROL) + ice_bucket_status: FridgeIceBucketStatus = self.appliance.get_erd_value(ErdCode.ICE_MAKER_BUCKET_STATUS) + water_filter: ErdFilterStatus = self.appliance.get_erd_value(ErdCode.WATER_FILTER_STATUS) + air_filter: ErdFilterStatus = self.appliance.get_erd_value(ErdCode.AIR_FILTER_STATUS) + hot_water_status: HotWaterStatus = self.appliance.get_erd_value(ErdCode.HOT_WATER_STATUS) + + # 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 fridge_entities.extend([ - GeErdSensor(self, ErdCode.WATER_FILTER_STATUS), 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): + GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_fridge") + # Freezer entities freezer_entities.extend([ GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "freezer"), GeFreezer(self), ]) + if(ice_maker_control and ice_maker_control.status_freezer != ErdOnOff.NA): + GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_freezer") + if(ice_bucket_status and ice_bucket_status.is_present_freezer): + GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_freezer") - dispenser_entities.extend([ - GeErdSensor(self, ErdCode.HOT_WATER_IN_USE), - GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), - GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "status"), - GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_remaining", icon_override="mdi:timer-outline"), - GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_remaining", device_class_override=DEVICE_CLASS_TEMPERATURE), - GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=DEVICE_CLASS_PROBLEM), - GeDispenser(self) - ]) + if(hot_water_status and hot_water_status.status != ErdHotWaterStatus.NA): + # Dispenser entities + dispenser_entities.extend([ + GeErdSensor(self, ErdCode.HOT_WATER_IN_USE), + GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "status"), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_remaining", icon_override="mdi:timer-outline"), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "current_temp", device_class_override=DEVICE_CLASS_TEMPERATURE), + 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 From 5ed7e8b3e0bdac02299ced37f8114843ab2ba9b0 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 13:18:05 -0500 Subject: [PATCH 017/338] - incorporated fridge model info --- ge_kitchen/devices/fridge.py | 48 +++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/ge_kitchen/devices/fridge.py b/ge_kitchen/devices/fridge.py index d3c0a95..bf701f8 100644 --- a/ge_kitchen/devices/fridge.py +++ b/ge_kitchen/devices/fridge.py @@ -12,7 +12,8 @@ FridgeIceBucketStatus, IceMakerControlStatus, ErdFilterStatus, - HotWaterStatus + HotWaterStatus, + FridgeModelInfo ) from .base import ApplianceApi @@ -45,6 +46,7 @@ def get_all_entities(self) -> List[Entity]: water_filter: ErdFilterStatus = self.appliance.get_erd_value(ErdCode.WATER_FILTER_STATUS) air_filter: ErdFilterStatus = self.appliance.get_erd_value(ErdCode.AIR_FILTER_STATUS) hot_water_status: HotWaterStatus = self.appliance.get_erd_value(ErdCode.HOT_WATER_STATUS) + fridge_model_info: FridgeModelInfo = self.appliance.get_erd_value(ErdCode.FRIDGE_MODEL_INFO) # Common entities common_entities = [ @@ -57,31 +59,33 @@ def get_all_entities(self) -> List[Entity]: common_entities.append(GeErdSensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS)) # Fridge entities - 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): - GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_fridge") + if 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): + GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_fridge") # Freezer entities - freezer_entities.extend([ - GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "freezer"), - GeFreezer(self), - ]) - if(ice_maker_control and ice_maker_control.status_freezer != ErdOnOff.NA): - GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_freezer") - if(ice_bucket_status and ice_bucket_status.is_present_freezer): - GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_freezer") + if 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): + GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_freezer") + if(ice_bucket_status and ice_bucket_status.is_present_freezer): + 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 dispenser_entities.extend([ GeErdSensor(self, ErdCode.HOT_WATER_IN_USE), GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), From 5e591a2f93c579e4b9b7303644876d55f8a84e2d Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 14:40:34 -0500 Subject: [PATCH 018/338] - added dishwasher locked controls switch --- ge_kitchen/devices/dishwasher.py | 3 ++- ge_kitchen/entities/dishwasher/__init__.py | 1 + .../dishwasher/ge_dishwasher_control_locked_switch.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py diff --git a/ge_kitchen/devices/dishwasher.py b/ge_kitchen/devices/dishwasher.py index 99bc43b..bb55575 100644 --- a/ge_kitchen/devices/dishwasher.py +++ b/ge_kitchen/devices/dishwasher.py @@ -5,7 +5,7 @@ from gekitchen.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdSwitch, GeFridgeEntity, GeFreezerEntity +from ..entities import GeErdSensor, GeDishwasherControlLockedSwitch _LOGGER = logging.getLogger(__name__) @@ -18,6 +18,7 @@ def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() dishwasher_entities = [ + GeDishwasherControlLockedSwitch(self, ErdCode.USER_INTERFACE_LOCKED), GeErdSensor(self, ErdCode.CYCLE_NAME), GeErdSensor(self, ErdCode.CYCLE_STATE), GeErdSensor(self, ErdCode.OPERATING_MODE), diff --git a/ge_kitchen/entities/dishwasher/__init__.py b/ge_kitchen/entities/dishwasher/__init__.py index e69de29..bef929d 100644 --- a/ge_kitchen/entities/dishwasher/__init__.py +++ b/ge_kitchen/entities/dishwasher/__init__.py @@ -0,0 +1 @@ +from .ge_dishwasher_control_locked_switch import GeDishwasherControlLockedSwitch \ No newline at end of file diff --git a/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py b/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py new file mode 100644 index 0000000..17d94a5 --- /dev/null +++ b/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py @@ -0,0 +1,10 @@ +from gekitchen import ErdCode, ErdOperatingMode + +from ..common import GeErdSwitch + +class GeDishwasherControlLockedSwitch(GeErdSwitch): + @property + def is_on(self) -> bool: + mode: ErdOperatingMode = self.appliance.get_erd_value(ErdCode.OPERATING_MODE) + return mode == ErdOperatingMode.CONTROL_LOCKED + \ No newline at end of file From ca33a90db4350a112b8ace207f781203056658da Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 17:01:29 -0500 Subject: [PATCH 019/338] - updated references from gekitchen to gekitchensdk --- ge_kitchen/__init__.py | 2 +- ge_kitchen/config_flow.py | 2 +- ge_kitchen/const.py | 2 +- ge_kitchen/devices/__init__.py | 2 +- ge_kitchen/devices/base.py | 4 ++-- ge_kitchen/devices/dishwasher.py | 2 +- ge_kitchen/devices/fridge.py | 2 +- ge_kitchen/devices/oven.py | 2 +- ge_kitchen/entities/common/ge_entity.py | 2 +- ge_kitchen/entities/common/ge_erd_binary_sensor.py | 2 +- ge_kitchen/entities/common/ge_erd_entity.py | 2 +- ge_kitchen/entities/common/ge_erd_property_binary_sensor.py | 2 +- ge_kitchen/entities/common/ge_erd_property_sensor.py | 2 +- ge_kitchen/entities/common/ge_erd_sensor.py | 2 +- ge_kitchen/entities/common/ge_water_heater.py | 2 +- .../dishwasher/ge_dishwasher_control_locked_switch.py | 2 +- ge_kitchen/entities/fridge/ge_abstract_fridge.py | 2 +- ge_kitchen/entities/fridge/ge_dispenser.py | 2 +- ge_kitchen/entities/fridge/ge_freezer.py | 2 +- ge_kitchen/entities/fridge/ge_fridge.py | 2 +- ge_kitchen/entities/oven/const.py | 2 +- ge_kitchen/entities/oven/ge_oven.py | 2 +- ge_kitchen/manifest.json | 6 +++--- ge_kitchen/update_coordinator.py | 2 +- 24 files changed, 27 insertions(+), 27 deletions(-) diff --git a/ge_kitchen/__init__.py b/ge_kitchen/__init__.py index 631e5e6..c82468d 100644 --- a/ge_kitchen/__init__.py +++ b/ge_kitchen/__init__.py @@ -5,7 +5,7 @@ import logging import voluptuous as vol -from gekitchen import GeAuthFailedError, GeGeneralServerError, GeNotAuthenticatedError +from gekitchensdk import GeAuthFailedError, GeGeneralServerError, GeNotAuthenticatedError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/ge_kitchen/config_flow.py b/ge_kitchen/config_flow.py index f83d281..3d93d22 100644 --- a/ge_kitchen/config_flow.py +++ b/ge_kitchen/config_flow.py @@ -7,7 +7,7 @@ import asyncio import async_timeout -from gekitchen import GeAuthFailedError, GeNotAuthenticatedError, GeGeneralServerError, async_get_oauth2_token +from gekitchensdk import GeAuthFailedError, GeNotAuthenticatedError, GeGeneralServerError, async_get_oauth2_token import voluptuous as vol from homeassistant import config_entries, core diff --git a/ge_kitchen/const.py b/ge_kitchen/const.py index da77a82..6cfbadb 100644 --- a/ge_kitchen/const.py +++ b/ge_kitchen/const.py @@ -1,5 +1,5 @@ """Constants for the ge_kitchen integration.""" -from gekitchen.const import LOGIN_URL +from gekitchensdk.const import LOGIN_URL DOMAIN = "ge_kitchen" diff --git a/ge_kitchen/devices/__init__.py b/ge_kitchen/devices/__init__.py index c208ae2..66a9b89 100644 --- a/ge_kitchen/devices/__init__.py +++ b/ge_kitchen/devices/__init__.py @@ -1,7 +1,7 @@ import logging from typing import Type -from gekitchen.erd import ErdApplianceType +from gekitchensdk.erd import ErdApplianceType from .base import ApplianceApi from .oven import OvenApi diff --git a/ge_kitchen/devices/base.py b/ge_kitchen/devices/base.py index 207529b..f550efc 100644 --- a/ge_kitchen/devices/base.py +++ b/ge_kitchen/devices/base.py @@ -2,8 +2,8 @@ import logging from typing import Dict, List, Optional -from gekitchen import GeAppliance -from gekitchen.erd import ErdCode, ErdApplianceType +from gekitchensdk import GeAppliance +from gekitchensdk.erd import ErdCode, ErdApplianceType from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity diff --git a/ge_kitchen/devices/dishwasher.py b/ge_kitchen/devices/dishwasher.py index bb55575..a77c6b2 100644 --- a/ge_kitchen/devices/dishwasher.py +++ b/ge_kitchen/devices/dishwasher.py @@ -2,7 +2,7 @@ from typing import List from homeassistant.helpers.entity import Entity -from gekitchen.erd import ErdCode, ErdApplianceType +from gekitchensdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import GeErdSensor, GeDishwasherControlLockedSwitch diff --git a/ge_kitchen/devices/fridge.py b/ge_kitchen/devices/fridge.py index bf701f8..495b5cf 100644 --- a/ge_kitchen/devices/fridge.py +++ b/ge_kitchen/devices/fridge.py @@ -4,7 +4,7 @@ from typing import List from homeassistant.helpers.entity import Entity -from gekitchen import ( +from gekitchensdk import ( ErdCode, ErdApplianceType, ErdOnOff, diff --git a/ge_kitchen/devices/oven.py b/ge_kitchen/devices/oven.py index eec0e9b..1e6f062 100644 --- a/ge_kitchen/devices/oven.py +++ b/ge_kitchen/devices/oven.py @@ -3,7 +3,7 @@ from homeassistant.const import DEVICE_CLASS_POWER_FACTOR from homeassistant.helpers.entity import Entity -from gekitchen import ( +from gekitchensdk import ( ErdCode, ErdApplianceType, OvenConfiguration, diff --git a/ge_kitchen/entities/common/ge_entity.py b/ge_kitchen/entities/common/ge_entity.py index ceaf893..4df5f58 100644 --- a/ge_kitchen/entities/common/ge_entity.py +++ b/ge_kitchen/entities/common/ge_entity.py @@ -1,7 +1,7 @@ from datetime import timedelta from typing import Optional, Dict, Any -from gekitchen import GeAppliance +from gekitchensdk import GeAppliance from ge_kitchen.devices import ApplianceApi class GeEntity: diff --git a/ge_kitchen/entities/common/ge_erd_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_binary_sensor.py index 0260c0e..ac2f0dc 100644 --- a/ge_kitchen/entities/common/ge_erd_binary_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_binary_sensor.py @@ -2,7 +2,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity -from gekitchen import ErdCode, ErdCodeType, ErdCodeClass +from gekitchensdk import ErdCode, ErdCodeType, ErdCodeClass from ge_kitchen.devices import ApplianceApi from .ge_erd_entity import GeErdEntity diff --git a/ge_kitchen/entities/common/ge_erd_entity.py b/ge_kitchen/entities/common/ge_erd_entity.py index 6e5140a..72e9463 100644 --- a/ge_kitchen/entities/common/ge_erd_entity.py +++ b/ge_kitchen/entities/common/ge_erd_entity.py @@ -2,7 +2,7 @@ from typing import Optional from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from gekitchen import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits +from gekitchensdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits from ge_kitchen.const import DOMAIN from ge_kitchen.devices import ApplianceApi diff --git a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py index b6d8983..b4377cf 100644 --- a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py @@ -1,7 +1,7 @@ from typing import Optional import magicattr -from gekitchen import ErdCodeType +from gekitchensdk import ErdCodeType from ge_kitchen.devices import ApplianceApi from .ge_erd_binary_sensor import GeErdBinarySensor diff --git a/ge_kitchen/entities/common/ge_erd_property_sensor.py b/ge_kitchen/entities/common/ge_erd_property_sensor.py index 3b80398..9767854 100644 --- a/ge_kitchen/entities/common/ge_erd_property_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_sensor.py @@ -1,7 +1,7 @@ from typing import Optional import magicattr -from gekitchen import ErdCode, ErdCodeType, ErdMeasurementUnits +from gekitchensdk import ErdCode, ErdCodeType, ErdMeasurementUnits from ge_kitchen.devices import ApplianceApi from .ge_erd_sensor import GeErdSensor diff --git a/ge_kitchen/entities/common/ge_erd_sensor.py b/ge_kitchen/entities/common/ge_erd_sensor.py index 308b022..eec1612 100644 --- a/ge_kitchen/entities/common/ge_erd_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_sensor.py @@ -8,7 +8,7 @@ TEMP_FAHRENHEIT ) from homeassistant.helpers.entity import Entity -from gekitchen import ErdCode, ErdCodeClass, ErdMeasurementUnits +from gekitchensdk import ErdCode, ErdCodeClass, ErdMeasurementUnits from .ge_erd_entity import GeErdEntity diff --git a/ge_kitchen/entities/common/ge_water_heater.py b/ge_kitchen/entities/common/ge_water_heater.py index 099acfe..6303a71 100644 --- a/ge_kitchen/entities/common/ge_water_heater.py +++ b/ge_kitchen/entities/common/ge_water_heater.py @@ -7,7 +7,7 @@ TEMP_FAHRENHEIT, TEMP_CELSIUS ) -from gekitchen import ErdCode, ErdMeasurementUnits +from gekitchensdk import ErdCode, ErdMeasurementUnits from ge_kitchen.const import DOMAIN from .ge_erd_entity import GeEntity diff --git a/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py b/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py index 17d94a5..6b72bfa 100644 --- a/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py +++ b/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py @@ -1,4 +1,4 @@ -from gekitchen import ErdCode, ErdOperatingMode +from gekitchensdk import ErdCode, ErdOperatingMode from ..common import GeErdSwitch diff --git a/ge_kitchen/entities/fridge/ge_abstract_fridge.py b/ge_kitchen/entities/fridge/ge_abstract_fridge.py index a019e38..2e59e99 100644 --- a/ge_kitchen/entities/fridge/ge_abstract_fridge.py +++ b/ge_kitchen/entities/fridge/ge_abstract_fridge.py @@ -7,7 +7,7 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from gekitchen import ( +from gekitchensdk import ( ErdCode, ErdOnOff, ErdFullNotFull, diff --git a/ge_kitchen/entities/fridge/ge_dispenser.py b/ge_kitchen/entities/fridge/ge_dispenser.py index 98f116b..0783109 100644 --- a/ge_kitchen/entities/fridge/ge_dispenser.py +++ b/ge_kitchen/entities/fridge/ge_dispenser.py @@ -6,7 +6,7 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT from homeassistant.util.temperature import convert as convert_temperature -from gekitchen import ( +from gekitchensdk import ( ErdCode, ErdHotWaterStatus, ErdPresent, diff --git a/ge_kitchen/entities/fridge/ge_freezer.py b/ge_kitchen/entities/fridge/ge_freezer.py index 5b85e82..440f31d 100644 --- a/ge_kitchen/entities/fridge/ge_freezer.py +++ b/ge_kitchen/entities/fridge/ge_freezer.py @@ -2,7 +2,7 @@ import logging from typing import Any, Dict, Optional -from gekitchen import ( +from gekitchensdk import ( ErdCode, ErdDoorStatus ) diff --git a/ge_kitchen/entities/fridge/ge_fridge.py b/ge_kitchen/entities/fridge/ge_fridge.py index 9b34892..457b304 100644 --- a/ge_kitchen/entities/fridge/ge_fridge.py +++ b/ge_kitchen/entities/fridge/ge_fridge.py @@ -2,7 +2,7 @@ import logging from typing import Any, Dict -from gekitchen import ( +from gekitchensdk import ( ErdCode, ErdDoorStatus, ErdFilterStatus diff --git a/ge_kitchen/entities/oven/const.py b/ge_kitchen/entities/oven/const.py index 89250a4..83fe063 100644 --- a/ge_kitchen/entities/oven/const.py +++ b/ge_kitchen/entities/oven/const.py @@ -4,7 +4,7 @@ SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE ) -from gekitchen import ErdOvenCookMode +from gekitchensdk import ErdOvenCookMode GE_OVEN_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) diff --git a/ge_kitchen/entities/oven/ge_oven.py b/ge_kitchen/entities/oven/ge_oven.py index 5c4578f..78813e9 100644 --- a/ge_kitchen/entities/oven/ge_oven.py +++ b/ge_kitchen/entities/oven/ge_oven.py @@ -2,7 +2,7 @@ import logging from typing import Any, Dict, List, Optional, Set -from gekitchen import ( +from gekitchensdk import ( ErdCode, ErdMeasurementUnits, ErdOvenCookMode, diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index 284814c..8168117 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -2,7 +2,7 @@ "domain": "ge_kitchen", "name": "GE Kitchen", "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/ge_kitchen", - "requirements": ["gekitchen==0.2.19","magicattr==0.1.15"], - "codeowners": ["@ajmarks"] + "documentation": "https://github.com/simbaja/ha_components", + "requirements": ["gekitchensdk==0.3.0","magicattr==0.1.15"], + "codeowners": ["@simbaja"] } diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index 4ab5892..9335de8 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -4,7 +4,7 @@ import logging from typing import Any, Dict, Iterable, Optional, Tuple -from gekitchen import ( +from gekitchensdk import ( EVENT_APPLIANCE_INITIAL_UPDATE, EVENT_APPLIANCE_UPDATE_RECEIVED, EVENT_CONNECTED, From 08fa7fda7ba14392bd2993595a22f8d9b0993cbe Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 17:03:30 -0500 Subject: [PATCH 020/338] - fixed manifest error --- ge_kitchen/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index 8168117..ad38f2f 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.0","magicattr==0.1.15"], + "requirements": ["gekitchensdk==0.3.0","magicattr==0.1.5"], "codeowners": ["@simbaja"] } From c11f27d66b7bc63282931e22ac2fb832cd73acf8 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 17:25:14 -0500 Subject: [PATCH 021/338] - changed absolute package references to relative --- ge_kitchen/entities/common/ge_entity.py | 2 +- ge_kitchen/entities/common/ge_erd_binary_sensor.py | 2 +- ge_kitchen/entities/common/ge_erd_entity.py | 4 ++-- ge_kitchen/entities/common/ge_erd_property_binary_sensor.py | 2 +- ge_kitchen/entities/common/ge_erd_property_sensor.py | 2 +- ge_kitchen/entities/common/ge_water_heater.py | 2 +- ge_kitchen/entities/fridge/ge_abstract_fridge.py | 2 +- ge_kitchen/entities/oven/ge_oven.py | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ge_kitchen/entities/common/ge_entity.py b/ge_kitchen/entities/common/ge_entity.py index 4df5f58..e4f6d86 100644 --- a/ge_kitchen/entities/common/ge_entity.py +++ b/ge_kitchen/entities/common/ge_entity.py @@ -2,7 +2,7 @@ from typing import Optional, Dict, Any from gekitchensdk import GeAppliance -from ge_kitchen.devices import ApplianceApi +from ...devices import ApplianceApi class GeEntity: """Base class for all GE Entities""" diff --git a/ge_kitchen/entities/common/ge_erd_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_binary_sensor.py index ac2f0dc..c6149db 100644 --- a/ge_kitchen/entities/common/ge_erd_binary_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_binary_sensor.py @@ -3,7 +3,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from gekitchensdk import ErdCode, ErdCodeType, ErdCodeClass -from ge_kitchen.devices import ApplianceApi +from ...devices import ApplianceApi from .ge_erd_entity import GeErdEntity diff --git a/ge_kitchen/entities/common/ge_erd_entity.py b/ge_kitchen/entities/common/ge_erd_entity.py index 72e9463..e263a96 100644 --- a/ge_kitchen/entities/common/ge_erd_entity.py +++ b/ge_kitchen/entities/common/ge_erd_entity.py @@ -4,8 +4,8 @@ from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from gekitchensdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits -from ge_kitchen.const import DOMAIN -from ge_kitchen.devices import ApplianceApi +from ...const import DOMAIN +from ...devices import ApplianceApi from .ge_entity import GeEntity diff --git a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py index b4377cf..900d3a2 100644 --- a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py @@ -2,7 +2,7 @@ import magicattr from gekitchensdk import ErdCodeType -from ge_kitchen.devices import ApplianceApi +from ...devices import ApplianceApi from .ge_erd_binary_sensor import GeErdBinarySensor class GeErdPropertyBinarySensor(GeErdBinarySensor): diff --git a/ge_kitchen/entities/common/ge_erd_property_sensor.py b/ge_kitchen/entities/common/ge_erd_property_sensor.py index 9767854..bc9b107 100644 --- a/ge_kitchen/entities/common/ge_erd_property_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_sensor.py @@ -2,7 +2,7 @@ import magicattr from gekitchensdk import ErdCode, ErdCodeType, ErdMeasurementUnits -from ge_kitchen.devices import ApplianceApi +from ...devices import ApplianceApi from .ge_erd_sensor import GeErdSensor diff --git a/ge_kitchen/entities/common/ge_water_heater.py b/ge_kitchen/entities/common/ge_water_heater.py index 6303a71..b243774 100644 --- a/ge_kitchen/entities/common/ge_water_heater.py +++ b/ge_kitchen/entities/common/ge_water_heater.py @@ -8,7 +8,7 @@ TEMP_CELSIUS ) from gekitchensdk import ErdCode, ErdMeasurementUnits -from ge_kitchen.const import DOMAIN +from ...const import DOMAIN from .ge_erd_entity import GeEntity _LOGGER = logging.getLogger(__name__) diff --git a/ge_kitchen/entities/fridge/ge_abstract_fridge.py b/ge_kitchen/entities/fridge/ge_abstract_fridge.py index 2e59e99..f869398 100644 --- a/ge_kitchen/entities/fridge/ge_abstract_fridge.py +++ b/ge_kitchen/entities/fridge/ge_abstract_fridge.py @@ -17,7 +17,7 @@ FridgeIceBucketStatus, IceMakerControlStatus ) -from ge_kitchen.const import DOMAIN +from ...const import DOMAIN from ..common import GeWaterHeater from .const import * diff --git a/ge_kitchen/entities/oven/ge_oven.py b/ge_kitchen/entities/oven/ge_oven.py index 78813e9..984757d 100644 --- a/ge_kitchen/entities/oven/ge_oven.py +++ b/ge_kitchen/entities/oven/ge_oven.py @@ -11,8 +11,8 @@ ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from ge_kitchen.const import DOMAIN -from ge_kitchen.devices import ApplianceApi +from ...const import DOMAIN +from ...devices import ApplianceApi from ..common import GeWaterHeater from .const import * From a900c150025ddfd2671ed29d4f218155d7b22667 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 18:29:43 -0500 Subject: [PATCH 022/338] - bug fixes --- ge_kitchen/devices/base.py | 3 ++- ge_kitchen/devices/oven.py | 25 +++++++++++-------- ge_kitchen/entities/common/ge_entity.py | 4 +-- .../entities/common/ge_erd_binary_sensor.py | 2 +- ge_kitchen/entities/common/ge_erd_entity.py | 5 +++- .../common/ge_erd_property_binary_sensor.py | 4 +-- .../entities/common/ge_erd_property_sensor.py | 4 +-- ge_kitchen/entities/common/ge_erd_sensor.py | 9 ++++--- ge_kitchen/entities/oven/const.py | 2 +- 9 files changed, 35 insertions(+), 23 deletions(-) diff --git a/ge_kitchen/devices/base.py b/ge_kitchen/devices/base.py index f550efc..fa7e59b 100644 --- a/ge_kitchen/devices/base.py +++ b/ge_kitchen/devices/base.py @@ -9,7 +9,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from ..entities import GeErdEntity, GeErdSensor, GeErdSwitch from ..const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -80,6 +79,7 @@ def entities(self) -> List[Entity]: def get_all_entities(self) -> List[Entity]: """Create Entities for this device.""" + from ..entities import GeErdSensor, GeErdSwitch entities = [ GeErdSensor(self, ErdCode.CLOCK_TIME), GeErdSwitch(self, ErdCode.SABBATH_MODE), @@ -88,6 +88,7 @@ def get_all_entities(self) -> List[Entity]: def build_entities_list(self) -> None: """Build the entities list, adding anything new.""" + from ..entities import GeErdEntity entities = [ e for e in self.get_all_entities() if not isinstance(e, GeErdEntity) or e.erd_code in self.appliance.known_properties diff --git a/ge_kitchen/devices/oven.py b/ge_kitchen/devices/oven.py index 1e6f062..afe0981 100644 --- a/ge_kitchen/devices/oven.py +++ b/ge_kitchen/devices/oven.py @@ -15,6 +15,7 @@ from ..entities import ( GeErdSensor, GeErdBinarySensor, + GeErdPropertySensor, GeErdPropertyBinarySensor, GeOven, UPPER_OVEN, @@ -58,12 +59,12 @@ def get_all_entities(self) -> List[Entity]: ]) 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)), - GeErdSensor(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.LOWER_OVEN_USER_TEMP_OFFSET)), - GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE, self.single_name(ErdCode.UPPER_OVEN_RAW_TEMPERATURE)), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self.single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED)), + 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)), + GeErdSensor(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_RAW_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_RAW_TEMPERATURE)), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED)), GeOven(self, UPPER_OVEN, False) ]) @@ -73,12 +74,16 @@ def get_all_entities(self) -> List[Entity]: for (k, v) in cooktop_status.burners.items(): if v.exists: - cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, k+".on")) - cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, k+".synchronized")) + 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(GeErdSensor(self, ErdCode.COOKTOP_STATUS, k+".power_pct", icon_override="mdi:fire", device_class_override=DEVICE_CLASS_POWER_FACTOR)) + cooktop_entities.append(GeErdPropertySensor(self, ErdCode.COOKTOP_STATUS, prop+".power_pct", icon_override="mdi:fire", device_class_override=DEVICE_CLASS_POWER_FACTOR)) return base_entities + oven_entities + cooktop_entities - def single_name(erd_code: ErdCode): + 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('_') diff --git a/ge_kitchen/entities/common/ge_entity.py b/ge_kitchen/entities/common/ge_entity.py index e4f6d86..5945261 100644 --- a/ge_kitchen/entities/common/ge_entity.py +++ b/ge_kitchen/entities/common/ge_entity.py @@ -49,11 +49,11 @@ def device_class(self) -> Optional[str]: return self._get_device_class() def _stringify(self, value: any, **kwargs) -> Optional[str]: - if isinstance(timedelta): + if isinstance(value, timedelta): return str(value)[:-3] if value else "" if value is None: return None - return self.appliance.stringify_erd_value(value, kwargs) + return self.appliance.stringify_erd_value(value, **kwargs) def _boolify(self, value: any) -> Optional[bool]: return self.appliance.boolify_erd_value(value) diff --git a/ge_kitchen/entities/common/ge_erd_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_binary_sensor.py index c6149db..e02c298 100644 --- a/ge_kitchen/entities/common/ge_erd_binary_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_binary_sensor.py @@ -8,7 +8,7 @@ class GeErdBinarySensor(GeErdEntity, BinarySensorEntity): - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str, icon_on_override: str, icon_off_override: str, device_class_override: str): + 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 diff --git a/ge_kitchen/entities/common/ge_erd_entity.py b/ge_kitchen/entities/common/ge_erd_entity.py index e263a96..a32d66e 100644 --- a/ge_kitchen/entities/common/ge_erd_entity.py +++ b/ge_kitchen/entities/common/ge_erd_entity.py @@ -18,6 +18,9 @@ def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = self._erd_override = erd_override self._icon_override = icon_override self._device_class_override = device_class_override + + if not self._erd_code_class: + self._erd_code_class = ErdCodeClass.GENERAL @property def erd_code(self) -> ErdCodeType: @@ -62,7 +65,7 @@ def _stringify(self, value: any, **kwargs) -> Optional[str]: return str(value)[:-3] if value else "" if value is None: return None - return self.appliance.stringify_erd_value(value, kwargs) + return self.appliance.stringify_erd_value(value, **kwargs) @property def _temp_measurement_system(self) -> Optional[ErdMeasurementUnits]: diff --git a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py index 900d3a2..8f6a425 100644 --- a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py @@ -7,7 +7,7 @@ class GeErdPropertyBinarySensor(GeErdBinarySensor): """GE Entity for property binary sensors""" - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str, erd_override: str, icon_on_override: str, icon_off_override: str, device_class_override: str): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str, erd_override: str = None, icon_on_override: str = None, icon_off_override: str = None, device_class_override: str = None): super().__init__(api, erd_code, erd_override, icon_on_override, icon_off_override, device_class_override) self.erd_property = erd_property self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") @@ -19,7 +19,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 - return self._boolify(self.erd_code, value) + return self._boolify(value) @property def unique_id(self) -> Optional[str]: diff --git a/ge_kitchen/entities/common/ge_erd_property_sensor.py b/ge_kitchen/entities/common/ge_erd_property_sensor.py index bc9b107..a4d6f81 100644 --- a/ge_kitchen/entities/common/ge_erd_property_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_sensor.py @@ -8,7 +8,7 @@ class GeErdPropertySensor(GeErdSensor): """GE Entity for sensors""" - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str, erd_override: str, icon_override: str, device_class_override: str): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str, 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.erd_property = erd_property self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") @@ -29,4 +29,4 @@ def state(self) -> Optional[str]: value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) except KeyError: return None - return self._stringify(value, units=self.units) + return self._stringify(value, units=self.unit_of_measurement) diff --git a/ge_kitchen/entities/common/ge_erd_sensor.py b/ge_kitchen/entities/common/ge_erd_sensor.py index eec1612..6e3306b 100644 --- a/ge_kitchen/entities/common/ge_erd_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_sensor.py @@ -20,7 +20,7 @@ def state(self) -> Optional[str]: value = self.appliance.get_erd_value(self.erd_code) except KeyError: return None - return self._stringify(value, units=self.units) + return self._stringify(value, units=self.unit_of_measurement) @property def unit_of_measurement(self) -> Optional[str]: @@ -28,7 +28,10 @@ def unit_of_measurement(self) -> Optional[str]: def _get_uom(self): """ Select appropriate units """ - if self.erd_code_class == ErdCodeClass.TEMPERATURE or self.device_class == DEVICE_CLASS_TEMPERATURE: + if ( + self.erd_code_class in [ErdCodeClass.RAW_TEMPERATURE,ErdCodeClass.NON_ZERO_TEMPERATURE] or + self.device_class == DEVICE_CLASS_TEMPERATURE + ): if self._temp_measurement_system == ErdMeasurementUnits.METRIC: return TEMP_CELSIUS return TEMP_FAHRENHEIT @@ -54,4 +57,4 @@ def _get_icon(self): return "mdi:door-open" if self.state.lower().endswith("closed"): return "mdi:door-closed" - return super()._get_icon() \ No newline at end of file + return super()._get_icon() diff --git a/ge_kitchen/entities/oven/const.py b/ge_kitchen/entities/oven/const.py index 83fe063..a42059f 100644 --- a/ge_kitchen/entities/oven/const.py +++ b/ge_kitchen/entities/oven/const.py @@ -18,7 +18,7 @@ UPPER_OVEN = "UPPER_OVEN" LOWER_OVEN = "LOWER_OVEN" -COOK_MODE_OP_MAP = bidict({ +COOK_MODE_OP_MAP = bidict.bidict({ ErdOvenCookMode.NOMODE: OP_MODE_OFF, ErdOvenCookMode.CONVMULTIBAKE_NOOPTION: OP_MODE_CONVMULTIBAKE, ErdOvenCookMode.CONVBAKE_NOOPTION: OP_MODE_CONVBAKE, From 8dfb9c23ed30a07c3469bb7c51e3d17b9a1e5b12 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 19:32:48 -0500 Subject: [PATCH 023/338] - added logic to prevent oven mode changes if not remote enabled - added additional cook modes to oven op list --- ge_kitchen/entities/oven/const.py | 13 +++++- ge_kitchen/entities/oven/ge_oven.py | 66 +++++++++++++++++------------ 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/ge_kitchen/entities/oven/const.py b/ge_kitchen/entities/oven/const.py index a42059f..0373d9c 100644 --- a/ge_kitchen/entities/oven/const.py +++ b/ge_kitchen/entities/oven/const.py @@ -6,6 +6,7 @@ ) from gekitchensdk import ErdOvenCookMode +SUPPORT_NONE = 0 GE_OVEN_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) OP_MODE_OFF = "Off" @@ -14,6 +15,11 @@ 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" UPPER_OVEN = "UPPER_OVEN" LOWER_OVEN = "LOWER_OVEN" @@ -23,6 +29,11 @@ ErdOvenCookMode.CONVMULTIBAKE_NOOPTION: OP_MODE_CONVMULTIBAKE, ErdOvenCookMode.CONVBAKE_NOOPTION: OP_MODE_CONVBAKE, ErdOvenCookMode.CONVROAST_NOOPTION: OP_MODE_CONVROAST, - ErdOvenCookMode.BAKE_NOOPTION: OP_MODE_BAKE + ErdOvenCookMode.BAKE_NOOPTION: OP_MODE_BAKE, + 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 }) diff --git a/ge_kitchen/entities/oven/ge_oven.py b/ge_kitchen/entities/oven/ge_oven.py index 984757d..b395ccd 100644 --- a/ge_kitchen/entities/oven/ge_oven.py +++ b/ge_kitchen/entities/oven/ge_oven.py @@ -33,7 +33,10 @@ def __init__(self, api: ApplianceApi, oven_select: str = UPPER_OVEN, two_cavity: @property def supported_features(self): - return GE_OVEN_SUPPORT + if self.remote_enabled: + return GE_OVEN_SUPPORT + else: + return SUPPORT_NONE @property def unique_id(self) -> str: @@ -63,6 +66,12 @@ 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]: current_temp = self.get_erd_value("DISPLAY_TEMPERATURE") @@ -124,37 +133,40 @@ def max_temp(self) -> int: 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) + if self.remote_enabled: + 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 + if self.remote_enabled: + target_temp = kwargs.get(ATTR_TEMPERATURE) + if target_temp is None: + return - 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) + 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) From efcaee0f95f68d2d95c015042233af371960a87e Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 19:57:10 -0500 Subject: [PATCH 024/338] - fixed issue with cook mode display --- ge_kitchen/entities/common/ge_erd_sensor.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ge_kitchen/entities/common/ge_erd_sensor.py b/ge_kitchen/entities/common/ge_erd_sensor.py index 6e3306b..3a5e750 100644 --- a/ge_kitchen/entities/common/ge_erd_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_sensor.py @@ -20,12 +20,21 @@ def state(self) -> Optional[str]: value = self.appliance.get_erd_value(self.erd_code) except KeyError: return None - return self._stringify(value, units=self.unit_of_measurement) + # TODO: change "units" to "temp_units" (so both are available)" + # TODO: perhaps enhance so that there's a list of variables available + # for the stringify function to consume... + return self._stringify(value, units=self._temp_units) @property def unit_of_measurement(self) -> Optional[str]: return self._get_uom() + @property + def _temp_units(self) -> Optional[str]: + if self._temp_measurement_system == ErdMeasurementUnits.METRIC: + return TEMP_CELSIUS + return TEMP_FAHRENHEIT + def _get_uom(self): """ Select appropriate units """ if ( From b5f959a098e868c792fd45419c87950c152561f0 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 20:19:01 -0500 Subject: [PATCH 025/338] - updated stringify variable --- ge_kitchen/entities/common/ge_erd_property_sensor.py | 2 +- ge_kitchen/entities/common/ge_erd_sensor.py | 3 +-- ge_kitchen/entities/oven/ge_oven.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ge_kitchen/entities/common/ge_erd_property_sensor.py b/ge_kitchen/entities/common/ge_erd_property_sensor.py index a4d6f81..5e9b0ee 100644 --- a/ge_kitchen/entities/common/ge_erd_property_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_property_sensor.py @@ -29,4 +29,4 @@ def state(self) -> Optional[str]: value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) except KeyError: return None - return self._stringify(value, units=self.unit_of_measurement) + return self._stringify(value, temp_units=self._temp_units) diff --git a/ge_kitchen/entities/common/ge_erd_sensor.py b/ge_kitchen/entities/common/ge_erd_sensor.py index 3a5e750..513d4ae 100644 --- a/ge_kitchen/entities/common/ge_erd_sensor.py +++ b/ge_kitchen/entities/common/ge_erd_sensor.py @@ -20,10 +20,9 @@ def state(self) -> Optional[str]: value = self.appliance.get_erd_value(self.erd_code) except KeyError: return None - # TODO: change "units" to "temp_units" (so both are available)" # TODO: perhaps enhance so that there's a list of variables available # for the stringify function to consume... - return self._stringify(value, units=self._temp_units) + return self._stringify(value, temp_units=self._temp_units) @property def unit_of_measurement(self) -> Optional[str]: diff --git a/ge_kitchen/entities/oven/ge_oven.py b/ge_kitchen/entities/oven/ge_oven.py index b395ccd..b5af4b4 100644 --- a/ge_kitchen/entities/oven/ge_oven.py +++ b/ge_kitchen/entities/oven/ge_oven.py @@ -176,7 +176,7 @@ def get_erd_value(self, suffix: str) -> Any: 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, units=self.temperature_unit) + return self._stringify(erd_value, temp_units=self.temperature_unit) @property def device_state_attributes(self) -> Optional[Dict[str, Any]]: From cf8165aae804e48246818ca7ca2391a75d3904ac Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 20:19:28 -0500 Subject: [PATCH 026/338] - bumped requirements --- ge_kitchen/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index ad38f2f..d08c0af 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.0","magicattr==0.1.5"], + "requirements": ["gekitchensdk==0.3.1","magicattr==0.1.5"], "codeowners": ["@simbaja"] } From aedbb04af17af32101dbdc20a8093bab9c5e65bd Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 20:38:51 -0500 Subject: [PATCH 027/338] - requirements bump --- ge_kitchen/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index d08c0af..1712452 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.1","magicattr==0.1.5"], + "requirements": ["gekitchensdk==0.3.2","magicattr==0.1.5"], "codeowners": ["@simbaja"] } From 112a2b19481a8535eaff1ef8e4a295b7e15506c9 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 20:45:07 -0500 Subject: [PATCH 028/338] - fixed missing rinse agent icon --- ge_kitchen/devices/dishwasher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/devices/dishwasher.py b/ge_kitchen/devices/dishwasher.py index a77c6b2..4eac3f7 100644 --- a/ge_kitchen/devices/dishwasher.py +++ b/ge_kitchen/devices/dishwasher.py @@ -23,7 +23,7 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.CYCLE_STATE), GeErdSensor(self, ErdCode.OPERATING_MODE), GeErdSensor(self, ErdCode.PODS_REMAINING_VALUE), - GeErdSensor(self, ErdCode.RINSE_AGENT, icon_override="mdi:sparkle"), + GeErdSensor(self, ErdCode.RINSE_AGENT, icon_override="mdi:sparkles"), GeErdSensor(self, ErdCode.TIME_REMAINING), ] entities = base_entities + dishwasher_entities From e5044846c5a36b98d9c4d43a469a86913b249245 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 22:21:28 -0500 Subject: [PATCH 029/338] - removed control locked from dishwasher for now --- ge_kitchen/devices/dishwasher.py | 2 +- .../entities/dishwasher/ge_dishwasher_control_locked_switch.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ge_kitchen/devices/dishwasher.py b/ge_kitchen/devices/dishwasher.py index 4eac3f7..b89c374 100644 --- a/ge_kitchen/devices/dishwasher.py +++ b/ge_kitchen/devices/dishwasher.py @@ -18,7 +18,7 @@ def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() dishwasher_entities = [ - GeDishwasherControlLockedSwitch(self, ErdCode.USER_INTERFACE_LOCKED), + #GeDishwasherControlLockedSwitch(self, ErdCode.USER_INTERFACE_LOCKED), GeErdSensor(self, ErdCode.CYCLE_NAME), GeErdSensor(self, ErdCode.CYCLE_STATE), GeErdSensor(self, ErdCode.OPERATING_MODE), diff --git a/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py b/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py index 6b72bfa..29bae93 100644 --- a/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py +++ b/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py @@ -2,6 +2,8 @@ from ..common import GeErdSwitch +# TODO: This is actually controlled through the 0x3007 ERD value (SOUND). +# The conversions are a pain in the butt, so this will be left for later. class GeDishwasherControlLockedSwitch(GeErdSwitch): @property def is_on(self) -> bool: From 7bbeeb679a84ab3f656959fe661187b76eb20799 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 29 Dec 2020 22:49:12 -0500 Subject: [PATCH 030/338] - removed guards on the ove set temp/set op (supported modes seems sufficient) --- ge_kitchen/entities/oven/ge_oven.py | 58 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/ge_kitchen/entities/oven/ge_oven.py b/ge_kitchen/entities/oven/ge_oven.py index b5af4b4..91ec9eb 100644 --- a/ge_kitchen/entities/oven/ge_oven.py +++ b/ge_kitchen/entities/oven/ge_oven.py @@ -133,40 +133,38 @@ def max_temp(self) -> int: async def async_set_operation_mode(self, operation_mode: str): """Set the operation mode.""" - if self.remote_enabled: - 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) + 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""" - if self.remote_enabled: - 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) + 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) From 56bb7479f5a5f7552e924796ba89546656cdebe1 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 30 Dec 2020 16:31:58 -0500 Subject: [PATCH 031/338] - rewrote setup to be more like the unifi component - enabled reconnection if the socket errors - added additional exception handling/logging --- ge_kitchen/__init__.py | 53 +++---------- ge_kitchen/const.py | 1 + ge_kitchen/update_coordinator.py | 132 ++++++++++++++++++++++++------- 3 files changed, 116 insertions(+), 70 deletions(-) diff --git a/ge_kitchen/__init__.py b/ge_kitchen/__init__.py index c82468d..9fce3a0 100644 --- a/ge_kitchen/__init__.py +++ b/ge_kitchen/__init__.py @@ -1,72 +1,43 @@ """The ge_kitchen integration.""" -import asyncio -import async_timeout -import logging +from homeassistant.const import EVENT_HOMEASSISTANT_STOP import voluptuous as vol -from gekitchensdk import GeAuthFailedError, GeGeneralServerError, GeNotAuthenticatedError - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import ( DOMAIN ) -from .exceptions import HaAuthError, HaCannotConnect from .update_coordinator import GeKitchenUpdateCoordinator CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) -PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater"] - -_LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: dict): + return True + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up the ge_kitchen component.""" hass.data.setdefault(DOMAIN, {}) - if DOMAIN not in config: - return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up ge_kitchen from a config entry.""" coordinator = GeKitchenUpdateCoordinator(hass, entry) hass.data[DOMAIN][entry.entry_id] = coordinator - try: - await coordinator.async_start_client() - except (GeNotAuthenticatedError, GeAuthFailedError): - raise HaAuthError('Authentication failure') - except GeGeneralServerError: - raise HaCannotConnect('Cannot connect (server error)') - except Exception: - raise HaCannotConnect('Unknown connection failure') - - try: - with async_timeout.timeout(30): - await coordinator.initialization_future - except TimeoutError: - raise HaCannotConnect('Initialization timed out') + if not await coordinator.async_setup(): + return False - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) - if unload_ok: + coordinator: GeKitchenUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + ok = await coordinator.async_reset() + if ok: hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + + return ok async def async_update_options(hass, config_entry): """Update options.""" diff --git a/ge_kitchen/const.py b/ge_kitchen/const.py index 6cfbadb..5676492 100644 --- a/ge_kitchen/const.py +++ b/ge_kitchen/const.py @@ -16,3 +16,4 @@ XMPP_CREDENTIALS = "xmpp_credentials" UPDATE_INTERVAL = 30 +RETRY_TIMER = 15 diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index 9335de8..7392e3b 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -1,6 +1,7 @@ """Data update coordinator for GE Kitchen Appliances""" import asyncio +import async_timeout import logging from typing import Any, Dict, Iterable, Optional, Tuple @@ -14,15 +15,18 @@ GeAppliance, GeWebsocketClient, ) +from gekitchensdk import GeAuthFailedError, GeGeneralServerError, GeNotAuthenticatedError +from .exceptions import HaAuthError, HaCannotConnect from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, EVENT_ALL_APPLIANCES_READY, UPDATE_INTERVAL +from .const import DOMAIN, EVENT_ALL_APPLIANCES_READY, UPDATE_INTERVAL, RETRY_TIMER from .devices import ApplianceApi, get_appliance_api_type +PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater"] _LOGGER = logging.getLogger(__name__) class GeKitchenUpdateCoordinator(DataUpdateCoordinator): @@ -87,30 +91,114 @@ def maybe_add_appliance_api(self, appliance: GeAppliance): async def get_client(self) -> GeWebsocketClient: """Get a new GE Websocket client.""" - if self.client is not None: - await self.client.disconnect() - + if self.client: + try: + #for now, just clear the one we care about using internals... + #new version of sdk needed to clear handles + self.client._event_handlers[EVENT_DISCONNECTED].clear() + await self.client.disconnect() + except Exception as err: + _LOGGER.warn(f'exception while disconnecting client {err}') + finally: + self.client = None + loop = self._hass.loop self.client = self.create_ge_client(event_loop=loop) return self.client + async def async_setup(self): + """Setup a new coordinator""" + _LOGGER.debug("Setting up coordinator") + + for component in PLATFORMS: + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup(self._config_entry, component) + ) + + try: + await self.async_start_client() + except (GeNotAuthenticatedError, GeAuthFailedError): + raise HaAuthError('Authentication failure') + except GeGeneralServerError: + raise HaCannotConnect('Cannot connect (server error)') + except Exception: + raise HaCannotConnect('Unknown connection failure') + + try: + with async_timeout.timeout(30): + await self.initialization_future + except TimeoutError: + raise HaCannotConnect('Initialization timed out') + async def async_start_client(self): """Start a new GeClient in the HASS event loop.""" - _LOGGER.debug('Running client') - client = await self.get_client() - + try: + _LOGGER.debug('Creating and starting client') + await self.get_client() + await self.async_begin_session() + except: + _LOGGER.debug('could not start the client') + self.client = None + raise + + async def async_begin_session(self): + """Begins the ge_kitchen session.""" + _LOGGER.debug("Beginning session") session = self._hass.helpers.aiohttp_client.async_get_clientsession() - await client.async_get_credentials(session) - fut = asyncio.ensure_future(client.async_run_client(), loop=self._hass.loop) + await self.client.async_get_credentials(session) + fut = asyncio.ensure_future(self.client.async_run_client(), loop=self._hass.loop) _LOGGER.debug('Client running') return fut + async def async_reset(self): + """Resets the coordinator.""" + _LOGGER.debug("resetting the coordinator") + entry = self._config_entry + unload_ok = all( + await asyncio.gather( + *[ + self.hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + return unload_ok + async def _kill_client(self): """Kill the client. Leaving this in for testing purposes.""" await asyncio.sleep(30) _LOGGER.critical('Killing the connection. Popcorn time.') await self.client.websocket.close() + @callback + def reconnect(self, log=False) -> None: + """Prepare to reconnect ge_kitchen session.""" + if log: + _LOGGER.info("Will try to reconnect to ge_kitchen service") + self.hass.loop.create_task(self.async_reconnect()) + + async def async_reconnect(self) -> None: + """Try to reconnect ge_kitchen session.""" + _LOGGER.info("attempting to reconnect to ge_kitchen service") + try: + with async_timeout.timeout(RETRY_TIMER): + #it was easier to just get a new client here + #TODO: rewrite to potentially re-establish the connection instead + # of tossing it completely + await self.async_start_client() + except Exception as err: + _LOGGER.warn(f"could not reconnect: {err}, will retry in {RETRY_TIMER} seconds") + self.hass.loop.call_later(RETRY_TIMER, self.reconnect) + + @callback + def shutdown(self, event) -> None: + """Close the connection on shutdown. + Used as an argument to EventBus.async_listen_once. + """ + _LOGGER.info("ge_kitchen shutting down") + if self.client: + self.client.disconnect() + async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]): """Let HA know there's new state.""" self.last_update_success = True @@ -144,9 +232,11 @@ async def on_device_initial_update(self, appliance: GeAppliance): self.maybe_add_appliance_api(appliance) await self.async_maybe_trigger_all_ready() _LOGGER.debug(f'Requesting updates for {appliance.mac_addr}') - while not self.client.websocket.closed and appliance.available: + while self.client and not self.client.websocket.closed and appliance.available: await asyncio.sleep(UPDATE_INTERVAL) - await appliance.async_request_update() + if self.client and not self.client.websocket.closed: + await appliance.async_request_update() + _LOGGER.debug(f'No longer requesting updates for {appliance.mac_addr}') async def on_disconnect(self, _): @@ -154,23 +244,7 @@ async def on_disconnect(self, _): _LOGGER.debug("Disconnected. Attempting to reconnect.") self.last_update_success = False - flow_context = { - "source": "reauth", - "unique_id": self._config_entry.unique_id, - } - - matching_flows = [ - flow - for flow in self.hass.config_entries.flow.async_progress() - if flow["context"] == flow_context - ] - - if not matching_flows: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - DOMAIN, context=flow_context, data=self._config_entry.data, - ) - ) + self.hass.loop.call_later(RETRY_TIMER, self.reconnect, True) async def on_connect(self, _): """Set state upon connection.""" From 12ea3d1728de49f4dec54ca8cbb7d5df0529c912 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 30 Dec 2020 17:28:42 -0500 Subject: [PATCH 032/338] - requirements version bump - updated get client to use new sdk method --- ge_kitchen/manifest.json | 2 +- ge_kitchen/update_coordinator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index 1712452..312bf08 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.2","magicattr==0.1.5"], + "requirements": ["gekitchensdk==0.3.3","magicattr==0.1.5"], "codeowners": ["@simbaja"] } diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index 7392e3b..fc3b4ad 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -95,7 +95,7 @@ async def get_client(self) -> GeWebsocketClient: try: #for now, just clear the one we care about using internals... #new version of sdk needed to clear handles - self.client._event_handlers[EVENT_DISCONNECTED].clear() + self.client.clear_event_handlers() await self.client.disconnect() except Exception as err: _LOGGER.warn(f'exception while disconnecting client {err}') From 79670a0aa62fe1cf12066019ddee7c14d2749e2b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 30 Dec 2020 18:03:50 -0500 Subject: [PATCH 033/338] - implemented exponential backoff for reconnects --- ge_kitchen/const.py | 5 ++++- ge_kitchen/update_coordinator.py | 31 +++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/ge_kitchen/const.py b/ge_kitchen/const.py index 5676492..3fcafed 100644 --- a/ge_kitchen/const.py +++ b/ge_kitchen/const.py @@ -16,4 +16,7 @@ XMPP_CREDENTIALS = "xmpp_credentials" UPDATE_INTERVAL = 30 -RETRY_TIMER = 15 +ASYNC_TIMEOUT = 30 +MIN_RETRY_DELAY = 15 +MAX_RETRY_DELAY = 1800 + diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index fc3b4ad..bab1cce 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -23,7 +23,14 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, EVENT_ALL_APPLIANCES_READY, UPDATE_INTERVAL, RETRY_TIMER +from .const import ( + DOMAIN, + EVENT_ALL_APPLIANCES_READY, + UPDATE_INTERVAL, + MIN_RETRY_DELAY, + MAX_RETRY_DELAY, + ASYNC_TIMEOUT +) from .devices import ApplianceApi, get_appliance_api_type PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater"] @@ -44,6 +51,7 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: # Some record keeping to let us know when we can start generating entities self._got_roster = False self._init_done = False + self._retry_count = 0 self.initialization_future = asyncio.Future() super().__init__(hass, _LOGGER, name=DOMAIN) @@ -179,16 +187,18 @@ def reconnect(self, log=False) -> None: async def async_reconnect(self) -> None: """Try to reconnect ge_kitchen session.""" - _LOGGER.info("attempting to reconnect to ge_kitchen service") + self._retry_count += 1 + _LOGGER.info(f"attempting to reconnect to ge_kitchen service (attempt {self._retry_count})") + try: - with async_timeout.timeout(RETRY_TIMER): + with async_timeout.timeout(ASYNC_TIMEOUT): #it was easier to just get a new client here #TODO: rewrite to potentially re-establish the connection instead # of tossing it completely await self.async_start_client() except Exception as err: - _LOGGER.warn(f"could not reconnect: {err}, will retry in {RETRY_TIMER} seconds") - self.hass.loop.call_later(RETRY_TIMER, self.reconnect) + _LOGGER.warn(f"could not reconnect: {err}, will retry in {self._get_retry_delay()} seconds") + self.hass.loop.call_later(self._get_retry_delay(), self.reconnect) @callback def shutdown(self, event) -> None: @@ -197,6 +207,7 @@ def shutdown(self, event) -> None: """ _LOGGER.info("ge_kitchen shutting down") if self.client: + self.client.clear_event_handlers() self.client.disconnect() async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]): @@ -241,14 +252,14 @@ async def on_device_initial_update(self, appliance: GeAppliance): async def on_disconnect(self, _): """Handle disconnection.""" - _LOGGER.debug("Disconnected. Attempting to reconnect.") + _LOGGER.debug(f"Disconnected. Attempting to reconnect in {MIN_RETRY_DELAY} seconds") self.last_update_success = False - - self.hass.loop.call_later(RETRY_TIMER, self.reconnect, True) + self.hass.loop.call_later(MIN_RETRY_DELAY, self.reconnect, True) async def on_connect(self, _): """Set state upon connection.""" self.last_update_success = True + self._retry_count = 0 async def async_maybe_trigger_all_ready(self): """See if we're all ready to go, and if so, let the games begin.""" @@ -262,3 +273,7 @@ async def async_maybe_trigger_all_ready(self): await asyncio.sleep(2) self.initialization_future.set_result(True) await self.client.async_event(EVENT_ALL_APPLIANCES_READY, None) + + def _get_retry_delay(self) -> int: + delay = MIN_RETRY_DELAY * 2 ** (self._retry_count - 1) + return min(delay, MAX_RETRY_DELAY) From 47de3772eda3534df91fed0ed6aea8bc418a6f2f Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 30 Dec 2020 19:55:04 -0500 Subject: [PATCH 034/338] - improved available/unavailable detection --- ge_kitchen/const.py | 1 + ge_kitchen/devices/base.py | 7 +++++ ge_kitchen/entities/common/ge_entity.py | 2 +- ge_kitchen/entities/common/ge_water_heater.py | 8 ------ ge_kitchen/update_coordinator.py | 26 ++++++++++++++++++- 5 files changed, 34 insertions(+), 10 deletions(-) diff --git a/ge_kitchen/const.py b/ge_kitchen/const.py index 3fcafed..69ca8e5 100644 --- a/ge_kitchen/const.py +++ b/ge_kitchen/const.py @@ -19,4 +19,5 @@ ASYNC_TIMEOUT = 30 MIN_RETRY_DELAY = 15 MAX_RETRY_DELAY = 1800 +RETRY_OFFLINE_COUNT = 5 diff --git a/ge_kitchen/devices/base.py b/ge_kitchen/devices/base.py index fa7e59b..a0bf107 100644 --- a/ge_kitchen/devices/base.py +++ b/ge_kitchen/devices/base.py @@ -45,6 +45,13 @@ def loop(self) -> Optional[asyncio.AbstractEventLoop]: def appliance(self) -> GeAppliance: return self._appliance + @property + def available(self) -> bool: + #Note - online will be there since we're using the GE coordinator + #Didn't want to deal with the circular references to get the type hints + #working. + return self.appliance.available and self.coordinator.online + @property def serial_number(self) -> str: return self.appliance.get_erd_value(ErdCode.SERIAL_NUMBER) diff --git a/ge_kitchen/entities/common/ge_entity.py b/ge_kitchen/entities/common/ge_entity.py index 5945261..e8c04db 100644 --- a/ge_kitchen/entities/common/ge_entity.py +++ b/ge_kitchen/entities/common/ge_entity.py @@ -30,7 +30,7 @@ def serial_number(self): @property def available(self) -> bool: - return self.appliance.available + return self.api.available @property def appliance(self) -> GeAppliance: diff --git a/ge_kitchen/entities/common/ge_water_heater.py b/ge_kitchen/entities/common/ge_water_heater.py index b243774..180a134 100644 --- a/ge_kitchen/entities/common/ge_water_heater.py +++ b/ge_kitchen/entities/common/ge_water_heater.py @@ -16,14 +16,6 @@ class GeWaterHeater(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): """Mock temperature/operation mode supporting device as a water heater""" - @property - def available(self) -> bool: - available = super().available - if not available: - app = self.appliance - _LOGGER.critical(f"{self.name} unavailable. Appliance info: Available - {app._available} and Init - {app.initialized}") - return available - @property def heater_type(self) -> str: raise NotImplementedError diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index bab1cce..da22c18 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -25,10 +25,11 @@ from .const import ( DOMAIN, - EVENT_ALL_APPLIANCES_READY, + EVENT_ALL_APPLIANCES_READY, UPDATE_INTERVAL, MIN_RETRY_DELAY, MAX_RETRY_DELAY, + RETRY_OFFLINE_COUNT, ASYNC_TIMEOUT ) from .devices import ApplianceApi, get_appliance_api_type @@ -78,6 +79,14 @@ def appliances(self) -> Iterable[GeAppliance]: @property def appliance_apis(self) -> Dict[str, ApplianceApi]: return self._appliance_apis + + @property + def online(self) -> bool: + """ + Indicates whether the services is online. If it's retried several times, it's assumed + that it's offline for some reason + """ + return self.client is not None or self._retry_count <= RETRY_OFFLINE_COUNT def _get_appliance_api(self, appliance: GeAppliance) -> ApplianceApi: api_type = get_appliance_api_type(appliance.appliance_type) @@ -199,6 +208,8 @@ async def async_reconnect(self) -> None: except Exception as err: _LOGGER.warn(f"could not reconnect: {err}, will retry in {self._get_retry_delay()} seconds") self.hass.loop.call_later(self._get_retry_delay(), self.reconnect) + _LOGGER.debug("forcing a state refresh while disconnected") + await self._refresh_ha_state() @callback def shutdown(self, event) -> None: @@ -222,6 +233,19 @@ async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any] _LOGGER.debug(f'Updating {entity} ({entity.unique_id}, {entity.entity_id})') entity.async_write_ha_state() + async def _refresh_ha_state(self): + entities = [ + entity + for api in self.appliance_apis.values() + for entity in api.entities + ] + for entity in entities: + try: + _LOGGER.debug(f'Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}') + entity.async_write_ha_state() + except: + _LOGGER.debug(f'Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}') + @property def all_appliances_updated(self) -> bool: """True if all appliances have had an initial update.""" From 3409ae9e03ee488fa355fe298af1f224cfa9d8ee Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 30 Dec 2020 19:59:43 -0500 Subject: [PATCH 035/338] - fixed display issue with oven temperature --- ge_kitchen/entities/oven/ge_oven.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ge_kitchen/entities/oven/ge_oven.py b/ge_kitchen/entities/oven/ge_oven.py index 91ec9eb..59e103c 100644 --- a/ge_kitchen/entities/oven/ge_oven.py +++ b/ge_kitchen/entities/oven/ge_oven.py @@ -74,9 +74,12 @@ def remote_enabled(self) -> bool: @property def current_temperature(self) -> Optional[int]: - current_temp = self.get_erd_value("DISPLAY_TEMPERATURE") - if current_temp: - return current_temp + #DISPLAY_TEMPERATURE appears to be out of line with what's + #actually going on in the oven, RAW_TEMPERATURE seems to be + #accurate. + #current_temp = self.get_erd_value("DISPLAY_TEMPERATURE") + #if current_temp: + # return current_temp return self.get_erd_value("RAW_TEMPERATURE") @property From 3e845764ec813266f70c1b6265ba269dd8d75a61 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 3 Jan 2021 16:44:54 -0500 Subject: [PATCH 036/338] - more reconnection/online/offline handling --- ge_kitchen/const.py | 1 + ge_kitchen/manifest.json | 2 +- ge_kitchen/update_coordinator.py | 45 +++++++++++++++++++++++++------- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/ge_kitchen/const.py b/ge_kitchen/const.py index 69ca8e5..4816c15 100644 --- a/ge_kitchen/const.py +++ b/ge_kitchen/const.py @@ -16,6 +16,7 @@ XMPP_CREDENTIALS = "xmpp_credentials" UPDATE_INTERVAL = 30 +APPLIANCE_LIST_UPDATE_INTERVAL = 300 ASYNC_TIMEOUT = 30 MIN_RETRY_DELAY = 15 MAX_RETRY_DELAY = 1800 diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index 312bf08..c73b991 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.3","magicattr==0.1.5"], + "requirements": ["gekitchensdk==0.3.4","magicattr==0.1.5"], "codeowners": ["@simbaja"] } diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index da22c18..8c11cf5 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -27,6 +27,7 @@ DOMAIN, EVENT_ALL_APPLIANCES_READY, UPDATE_INTERVAL, + APPLIANCE_LIST_UPDATE_INTERVAL, MIN_RETRY_DELAY, MAX_RETRY_DELAY, RETRY_OFFLINE_COUNT, @@ -46,6 +47,11 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self._config_entry = config_entry self._username = config_entry.data[CONF_USERNAME] self._password = config_entry.data[CONF_PASSWORD] + self._reset_initialization() + + super().__init__(hass, _LOGGER, name=DOMAIN) + + def _reset_initialization(self): self.client = None # type: Optional[GeWebsocketClient] self._appliance_apis = {} # type: Dict[str, ApplianceApi] @@ -55,8 +61,6 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self._retry_count = 0 self.initialization_future = asyncio.Future() - super().__init__(hass, _LOGGER, name=DOMAIN) - def create_ge_client(self, event_loop: Optional[asyncio.AbstractEventLoop]) -> GeWebsocketClient: """ Create a new GeClient object with some helpful callbacks. @@ -88,6 +92,14 @@ def online(self) -> bool: """ return self.client is not None or self._retry_count <= RETRY_OFFLINE_COUNT + @property + def connected(self) -> bool: + """ + Indicates whether the coordinator is connected + TODO: Make this a part of the client properties as websocket is not applicable for all + """ + return self.client and self.client.websocket and not self.client.websocket.closed + def _get_appliance_api(self, appliance: GeAppliance) -> ApplianceApi: api_type = get_appliance_api_type(appliance.appliance_type) return api_type(self, appliance) @@ -105,20 +117,18 @@ def maybe_add_appliance_api(self, appliance: GeAppliance): api = self._get_appliance_api(appliance) api.build_entities_list() self.appliance_apis[mac_addr] = api - + async def get_client(self) -> GeWebsocketClient: """Get a new GE Websocket client.""" if self.client: try: - #for now, just clear the one we care about using internals... - #new version of sdk needed to clear handles self.client.clear_event_handlers() await self.client.disconnect() except Exception as err: _LOGGER.warn(f'exception while disconnecting client {err}') finally: - self.client = None - + self._reset_initialization() + loop = self._hass.loop self.client = self.create_ge_client(event_loop=loop) return self.client @@ -211,6 +221,19 @@ async def async_reconnect(self) -> None: _LOGGER.debug("forcing a state refresh while disconnected") await self._refresh_ha_state() + @callback + def appliance_list_refresh(self) -> None: + self.hass.loop.create_task(self.async_appliance_list_refresh()) + + async def async_appliance_list_refresh(self) -> None: + """Try to refresh the appliance list, including online/offline state""" + _LOGGER.debug("refreshing appliance list/states") + try: + if(self.connected): + await self.client.get_appliance_list() + except: + _LOGGER.debug("could not refresh appliance list") + @callback def shutdown(self, event) -> None: """Close the connection on shutdown. @@ -257,8 +280,12 @@ async def on_appliance_list(self, _): self.last_update_success = True if not self._got_roster: self._got_roster = True + #TODO: Probably should have a better way of confirming we're good to go... await asyncio.sleep(5) # After the initial roster update, wait a bit and hit go await self.async_maybe_trigger_all_ready() + + #initialize the next refresh + self.hass.loop.call_later(APPLIANCE_LIST_UPDATE_INTERVAL, self.appliance_list_refresh) async def on_device_initial_update(self, appliance: GeAppliance): """When an appliance first becomes ready, let the system know and schedule periodic updates.""" @@ -267,9 +294,9 @@ async def on_device_initial_update(self, appliance: GeAppliance): self.maybe_add_appliance_api(appliance) await self.async_maybe_trigger_all_ready() _LOGGER.debug(f'Requesting updates for {appliance.mac_addr}') - while self.client and not self.client.websocket.closed and appliance.available: + while self.connected and appliance.available: await asyncio.sleep(UPDATE_INTERVAL) - if self.client and not self.client.websocket.closed: + if self.connected: await appliance.async_request_update() _LOGGER.debug(f'No longer requesting updates for {appliance.mac_addr}') From e3c626938dc365ee88767360411c8320b6b8120a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 3 Jan 2021 17:37:14 -0500 Subject: [PATCH 037/338] - another update loop related to online/offline state --- ge_kitchen/update_coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index 8c11cf5..d067184 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -294,7 +294,7 @@ async def on_device_initial_update(self, appliance: GeAppliance): self.maybe_add_appliance_api(appliance) await self.async_maybe_trigger_all_ready() _LOGGER.debug(f'Requesting updates for {appliance.mac_addr}') - while self.connected and appliance.available: + while self.connected: await asyncio.sleep(UPDATE_INTERVAL) if self.connected: await appliance.async_request_update() From 11de81bce1bc9b1d475d7f8bc22f9a2244b1c85d Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 3 Jan 2021 21:26:58 -0500 Subject: [PATCH 038/338] - online detection update --- ge_kitchen/update_coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index d067184..3c60c34 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -90,7 +90,7 @@ def online(self) -> bool: Indicates whether the services is online. If it's retried several times, it's assumed that it's offline for some reason """ - return self.client is not None or self._retry_count <= RETRY_OFFLINE_COUNT + return self.connected or self._retry_count <= RETRY_OFFLINE_COUNT @property def connected(self) -> bool: From 7d95de1532dd047322f59b9cfaf0eed3f03af09d Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 3 Jan 2021 21:27:42 -0500 Subject: [PATCH 039/338] - requirements bump --- ge_kitchen/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index c73b991..5d0378c 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.4","magicattr==0.1.5"], + "requirements": ["gekitchensdk==0.3.5","magicattr==0.1.5"], "codeowners": ["@simbaja"] } From 5c8d2eac6d337c1d4c57fd3149d975dd59797707 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 4 Jan 2021 13:42:17 -0500 Subject: [PATCH 040/338] - changed to not reset appliance apis --- ge_kitchen/devices/base.py | 4 ++++ ge_kitchen/update_coordinator.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/ge_kitchen/devices/base.py b/ge_kitchen/devices/base.py index a0bf107..eea90ac 100644 --- a/ge_kitchen/devices/base.py +++ b/ge_kitchen/devices/base.py @@ -45,6 +45,10 @@ def loop(self) -> Optional[asyncio.AbstractEventLoop]: def appliance(self) -> GeAppliance: return self._appliance + @appliance.setter + def appliance(self, value: GeAppliance): + self._appliance = value + @property def available(self) -> bool: #Note - online will be there since we're using the GE coordinator diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index 3c60c34..3bece65 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -47,13 +47,18 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self._config_entry = config_entry self._username = config_entry.data[CONF_USERNAME] self._password = config_entry.data[CONF_PASSWORD] + self._appliance_apis = {} # type: Dict[str, ApplianceApi] + self._reset_initialization() super().__init__(hass, _LOGGER, name=DOMAIN) def _reset_initialization(self): self.client = None # type: Optional[GeWebsocketClient] - self._appliance_apis = {} # type: Dict[str, ApplianceApi] + + # Mark all appliances as not initialized yet + for a in self.appliance_apis.values(): + a.appliance.initialized = False # Some record keeping to let us know when we can start generating entities self._got_roster = False @@ -117,6 +122,10 @@ def maybe_add_appliance_api(self, appliance: GeAppliance): api = self._get_appliance_api(appliance) api.build_entities_list() self.appliance_apis[mac_addr] = api + else: + #if we already have the API, switch out its appliance reference for this one + api = self.appliance_apis[mac_addr] + api.appliance = appliance async def get_client(self) -> GeWebsocketClient: """Get a new GE Websocket client.""" From 82a8a92627ac04aa1df6db514cf5378cee553f5a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 10 Jan 2021 21:16:31 -0500 Subject: [PATCH 041/338] - requirements bump --- ge_kitchen/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index 5d0378c..363fe18 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.5","magicattr==0.1.5"], + "requirements": ["gekitchensdk==0.3.6","magicattr==0.1.5"], "codeowners": ["@simbaja"] } From 365de0bc06d825afd074caa1a3517203735e2257 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 10 Jan 2021 21:39:43 -0500 Subject: [PATCH 042/338] - updates based on new sdk version - miscellaneous cleanup --- ge_kitchen/const.py | 11 ----------- ge_kitchen/update_coordinator.py | 32 +++++++------------------------- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/ge_kitchen/const.py b/ge_kitchen/const.py index 4816c15..2d86b56 100644 --- a/ge_kitchen/const.py +++ b/ge_kitchen/const.py @@ -3,20 +3,9 @@ DOMAIN = "ge_kitchen" -# TODO Update with your own urls -# OAUTH2_AUTHORIZE = f"{LOGIN_URL}/oauth2/auth" -OAUTH2_AUTH_URL = f"{LOGIN_URL}/oauth2/auth" -OAUTH2_TOKEN_URL = f"{LOGIN_URL}/oauth2/token" - -AUTH_HANDLER = "auth_handler" EVENT_ALL_APPLIANCES_READY = 'all_appliances_ready' -COORDINATOR = "coordinator" -GE_TOKEN = "ge_token" -MOBILE_DEVICE_TOKEN = "mdt" -XMPP_CREDENTIALS = "xmpp_credentials" UPDATE_INTERVAL = 30 -APPLIANCE_LIST_UPDATE_INTERVAL = 300 ASYNC_TIMEOUT = 30 MIN_RETRY_DELAY = 15 MAX_RETRY_DELAY = 1800 diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index 3bece65..096d616 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -27,7 +27,6 @@ DOMAIN, EVENT_ALL_APPLIANCES_READY, UPDATE_INTERVAL, - APPLIANCE_LIST_UPDATE_INTERVAL, MIN_RETRY_DELAY, MAX_RETRY_DELAY, RETRY_OFFLINE_COUNT, @@ -73,7 +72,7 @@ def create_ge_client(self, event_loop: Optional[asyncio.AbstractEventLoop]) -> G :param event_loop: Event loop :return: GeWebsocketClient """ - client = GeWebsocketClient(event_loop=event_loop, username=self._username, password=self._password) + client = GeWebsocketClient(self._username, self._password, event_loop=event_loop, ) client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update) client.add_event_handler(EVENT_APPLIANCE_UPDATE_RECEIVED, self.on_device_update) client.add_event_handler(EVENT_GOT_APPLIANCE_LIST, self.on_appliance_list) @@ -101,9 +100,8 @@ def online(self) -> bool: def connected(self) -> bool: """ Indicates whether the coordinator is connected - TODO: Make this a part of the client properties as websocket is not applicable for all """ - return self.client and self.client.websocket and not self.client.websocket.closed + return self.client and self.client.connected def _get_appliance_api(self, appliance: GeAppliance) -> ApplianceApi: api_type = get_appliance_api_type(appliance.appliance_type) @@ -220,28 +218,15 @@ async def async_reconnect(self) -> None: try: with async_timeout.timeout(ASYNC_TIMEOUT): - #it was easier to just get a new client here - #TODO: rewrite to potentially re-establish the connection instead - # of tossing it completely await self.async_start_client() except Exception as err: _LOGGER.warn(f"could not reconnect: {err}, will retry in {self._get_retry_delay()} seconds") self.hass.loop.call_later(self._get_retry_delay(), self.reconnect) _LOGGER.debug("forcing a state refresh while disconnected") - await self._refresh_ha_state() - - @callback - def appliance_list_refresh(self) -> None: - self.hass.loop.create_task(self.async_appliance_list_refresh()) - - async def async_appliance_list_refresh(self) -> None: - """Try to refresh the appliance list, including online/offline state""" - _LOGGER.debug("refreshing appliance list/states") - try: - if(self.connected): - await self.client.get_appliance_list() - except: - _LOGGER.debug("could not refresh appliance list") + try: + await self._refresh_ha_state() + except Exception as err: + _LOGGER.debug(f"error refreshing state: {err}") @callback def shutdown(self, event) -> None: @@ -292,9 +277,6 @@ async def on_appliance_list(self, _): #TODO: Probably should have a better way of confirming we're good to go... await asyncio.sleep(5) # After the initial roster update, wait a bit and hit go await self.async_maybe_trigger_all_ready() - - #initialize the next refresh - self.hass.loop.call_later(APPLIANCE_LIST_UPDATE_INTERVAL, self.appliance_list_refresh) async def on_device_initial_update(self, appliance: GeAppliance): """When an appliance first becomes ready, let the system know and schedule periodic updates.""" @@ -305,7 +287,7 @@ async def on_device_initial_update(self, appliance: GeAppliance): _LOGGER.debug(f'Requesting updates for {appliance.mac_addr}') while self.connected: await asyncio.sleep(UPDATE_INTERVAL) - if self.connected: + if self.connected and self.client.available: await appliance.async_request_update() _LOGGER.debug(f'No longer requesting updates for {appliance.mac_addr}') From 30b30ae41149b0a8269b18563a73b0b847a1986a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 10 Jan 2021 22:10:23 -0500 Subject: [PATCH 043/338] - replaced timeout with constant --- ge_kitchen/update_coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index 096d616..ec657d5 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -159,7 +159,7 @@ async def async_setup(self): raise HaCannotConnect('Unknown connection failure') try: - with async_timeout.timeout(30): + with async_timeout.timeout(ASYNC_TIMEOUT): await self.initialization_future except TimeoutError: raise HaCannotConnect('Initialization timed out') @@ -202,7 +202,7 @@ async def _kill_client(self): """Kill the client. Leaving this in for testing purposes.""" await asyncio.sleep(30) _LOGGER.critical('Killing the connection. Popcorn time.') - await self.client.websocket.close() + await self.client.disconnect() @callback def reconnect(self, log=False) -> None: From 8242039eba38a6640658f939cfcf497a430b4f0f Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 10 Jan 2021 22:56:04 -0500 Subject: [PATCH 044/338] - fixed dishwasher erd codes in appliance - updated requirements --- ge_kitchen/devices/dishwasher.py | 12 ++++++------ ge_kitchen/manifest.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ge_kitchen/devices/dishwasher.py b/ge_kitchen/devices/dishwasher.py index b89c374..918e3be 100644 --- a/ge_kitchen/devices/dishwasher.py +++ b/ge_kitchen/devices/dishwasher.py @@ -19,12 +19,12 @@ def get_all_entities(self) -> List[Entity]: dishwasher_entities = [ #GeDishwasherControlLockedSwitch(self, ErdCode.USER_INTERFACE_LOCKED), - GeErdSensor(self, ErdCode.CYCLE_NAME), - GeErdSensor(self, ErdCode.CYCLE_STATE), - GeErdSensor(self, ErdCode.OPERATING_MODE), - GeErdSensor(self, ErdCode.PODS_REMAINING_VALUE), - GeErdSensor(self, ErdCode.RINSE_AGENT, icon_override="mdi:sparkles"), - GeErdSensor(self, ErdCode.TIME_REMAINING), + GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_NAME), + GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_STATE), + GeErdSensor(self, ErdCode.DISHWASHER_OPERATING_MODE), + GeErdSensor(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE), + GeErdSensor(self, ErdCode.DISHWASHER_RINSE_AGENT, icon_override="mdi:sparkles"), + GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING), ] entities = base_entities + dishwasher_entities return entities diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index 363fe18..15e4a85 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.6","magicattr==0.1.5"], + "requirements": ["gekitchensdk==0.3.8","magicattr==0.1.5"], "codeowners": ["@simbaja"] } From 431895b0f431a81bd0d93b38f95dd0dc2167057a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 11 Jan 2021 20:39:36 -0500 Subject: [PATCH 045/338] - requirements bump --- ge_kitchen/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index 15e4a85..d11699b 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.8","magicattr==0.1.5"], + "requirements": ["gekitchensdk==0.3.9","magicattr==0.1.5"], "codeowners": ["@simbaja"] } From 27b810ac75ca1f8767789318a7a4e28f10c156bd Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 14 Jan 2021 22:18:27 -0500 Subject: [PATCH 046/338] - updated timeout exceptions --- ge_kitchen/update_coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ge_kitchen/update_coordinator.py b/ge_kitchen/update_coordinator.py index ec657d5..0c4ae2d 100644 --- a/ge_kitchen/update_coordinator.py +++ b/ge_kitchen/update_coordinator.py @@ -161,7 +161,7 @@ async def async_setup(self): try: with async_timeout.timeout(ASYNC_TIMEOUT): await self.initialization_future - except TimeoutError: + except (asyncio.CancelledError, asyncio.TimeoutError): raise HaCannotConnect('Initialization timed out') async def async_start_client(self): From 2ed51f39a6a59a6ce981162fc49f174d8e00cc91 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 14 Jan 2021 22:38:23 -0500 Subject: [PATCH 047/338] - fixed fridge issues - requirements bump --- ge_kitchen/devices/base.py | 9 ++++++++- ge_kitchen/devices/fridge.py | 23 ++++++++++++----------- ge_kitchen/manifest.json | 2 +- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/ge_kitchen/devices/base.py b/ge_kitchen/devices/base.py index eea90ac..350b57d 100644 --- a/ge_kitchen/devices/base.py +++ b/ge_kitchen/devices/base.py @@ -3,7 +3,7 @@ from typing import Dict, List, Optional from gekitchensdk import GeAppliance -from gekitchensdk.erd import ErdCode, ErdApplianceType +from gekitchensdk.erd import ErdCode, ErdCodeType, ErdApplianceType from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity @@ -108,3 +108,10 @@ def build_entities_list(self) -> None: for entity in entities: if entity.unique_id not in self._entities: self._entities[entity.unique_id] = entity + + def try_get_erd_value(self, code: ErdCodeType): + try: + return self.appliance.get_erd_value(code) + except: + return None + \ No newline at end of file diff --git a/ge_kitchen/devices/fridge.py b/ge_kitchen/devices/fridge.py index 495b5cf..65c1374 100644 --- a/ge_kitchen/devices/fridge.py +++ b/ge_kitchen/devices/fridge.py @@ -41,12 +41,13 @@ def get_all_entities(self) -> List[Entity]: dispenser_entities = [] # Get the statuses used to determine presence - ice_maker_control: IceMakerControlStatus = self.appliance.get_erd_value(ErdCode.ICE_MAKER_CONTROL) - ice_bucket_status: FridgeIceBucketStatus = self.appliance.get_erd_value(ErdCode.ICE_MAKER_BUCKET_STATUS) - water_filter: ErdFilterStatus = self.appliance.get_erd_value(ErdCode.WATER_FILTER_STATUS) - air_filter: ErdFilterStatus = self.appliance.get_erd_value(ErdCode.AIR_FILTER_STATUS) - hot_water_status: HotWaterStatus = self.appliance.get_erd_value(ErdCode.HOT_WATER_STATUS) - fridge_model_info: FridgeModelInfo = self.appliance.get_erd_value(ErdCode.FRIDGE_MODEL_INFO) + + 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) # Common entities common_entities = [ @@ -59,7 +60,7 @@ def get_all_entities(self) -> List[Entity]: common_entities.append(GeErdSensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS)) # Fridge entities - if fridge_model_info.has_fridge: + if fridge_model_info is None or fridge_model_info.has_fridge: fridge_entities.extend([ GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "fridge"), GeFridge(self), @@ -71,18 +72,18 @@ def get_all_entities(self) -> List[Entity]: 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): - GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_fridge") + fridge_entities.append(GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_fridge")) # Freezer entities - if fridge_model_info.has_freezer: + 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): - GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_freezer") + freezer_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_freezer")) if(ice_bucket_status and ice_bucket_status.is_present_freezer): - GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_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): diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index d11699b..a2b9478 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.9","magicattr==0.1.5"], + "requirements": ["gekitchensdk==0.3.10","magicattr==0.1.5"], "codeowners": ["@simbaja"] } From 6c0b46ed6cf8139563b521765991ec2aed4b93b4 Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Mon, 1 Feb 2021 16:32:23 -0800 Subject: [PATCH 048/338] renamed: ge_kitchen/__init__.py -> custom_components/ge_kitchen/__init__.py renamed: ge_kitchen/binary_sensor.py -> custom_components/ge_kitchen/binary_sensor.py renamed: ge_kitchen/config_flow.py -> custom_components/ge_kitchen/config_flow.py renamed: ge_kitchen/const.py -> custom_components/ge_kitchen/const.py renamed: ge_kitchen/devices/__init__.py -> custom_components/ge_kitchen/devices/__init__.py renamed: ge_kitchen/devices/base.py -> custom_components/ge_kitchen/devices/base.py renamed: ge_kitchen/devices/dishwasher.py -> custom_components/ge_kitchen/devices/dishwasher.py renamed: ge_kitchen/devices/fridge.py -> custom_components/ge_kitchen/devices/fridge.py renamed: ge_kitchen/devices/oven.py -> custom_components/ge_kitchen/devices/oven.py renamed: ge_kitchen/entities/__init__.py -> custom_components/ge_kitchen/entities/__init__.py renamed: ge_kitchen/entities/common/__init__.py -> custom_components/ge_kitchen/entities/common/__init__.py renamed: ge_kitchen/entities/common/ge_entity.py -> custom_components/ge_kitchen/entities/common/ge_entity.py renamed: ge_kitchen/entities/common/ge_erd_binary_sensor.py -> custom_components/ge_kitchen/entities/common/ge_erd_binary_sensor.py renamed: ge_kitchen/entities/common/ge_erd_entity.py -> custom_components/ge_kitchen/entities/common/ge_erd_entity.py renamed: ge_kitchen/entities/common/ge_erd_property_binary_sensor.py -> custom_components/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py renamed: ge_kitchen/entities/common/ge_erd_property_sensor.py -> custom_components/ge_kitchen/entities/common/ge_erd_property_sensor.py renamed: ge_kitchen/entities/common/ge_erd_sensor.py -> custom_components/ge_kitchen/entities/common/ge_erd_sensor.py renamed: ge_kitchen/entities/common/ge_erd_switch.py -> custom_components/ge_kitchen/entities/common/ge_erd_switch.py renamed: ge_kitchen/entities/common/ge_water_heater.py -> custom_components/ge_kitchen/entities/common/ge_water_heater.py renamed: ge_kitchen/entities/dishwasher/__init__.py -> custom_components/ge_kitchen/entities/dishwasher/__init__.py renamed: ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py -> custom_components/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py renamed: ge_kitchen/entities/fridge/__init__.py -> custom_components/ge_kitchen/entities/fridge/__init__.py renamed: ge_kitchen/entities/fridge/const.py -> custom_components/ge_kitchen/entities/fridge/const.py renamed: ge_kitchen/entities/fridge/ge_abstract_fridge.py -> custom_components/ge_kitchen/entities/fridge/ge_abstract_fridge.py renamed: ge_kitchen/entities/fridge/ge_dispenser.py -> custom_components/ge_kitchen/entities/fridge/ge_dispenser.py renamed: ge_kitchen/entities/fridge/ge_freezer.py -> custom_components/ge_kitchen/entities/fridge/ge_freezer.py renamed: ge_kitchen/entities/fridge/ge_fridge.py -> custom_components/ge_kitchen/entities/fridge/ge_fridge.py renamed: ge_kitchen/entities/oven/__init__.py -> custom_components/ge_kitchen/entities/oven/__init__.py renamed: ge_kitchen/entities/oven/const.py -> custom_components/ge_kitchen/entities/oven/const.py renamed: ge_kitchen/entities/oven/ge_oven.py -> custom_components/ge_kitchen/entities/oven/ge_oven.py renamed: ge_kitchen/exceptions.py -> custom_components/ge_kitchen/exceptions.py renamed: ge_kitchen/manifest.json -> custom_components/ge_kitchen/manifest.json renamed: ge_kitchen/sensor.py -> custom_components/ge_kitchen/sensor.py renamed: ge_kitchen/strings.json -> custom_components/ge_kitchen/strings.json renamed: ge_kitchen/switch.py -> custom_components/ge_kitchen/switch.py renamed: ge_kitchen/translations/en.json -> custom_components/ge_kitchen/translations/en.json renamed: ge_kitchen/update_coordinator.py -> custom_components/ge_kitchen/update_coordinator.py renamed: ge_kitchen/water_heater.py -> custom_components/ge_kitchen/water_heater.py --- {ge_kitchen => custom_components/ge_kitchen}/__init__.py | 0 {ge_kitchen => custom_components/ge_kitchen}/binary_sensor.py | 0 {ge_kitchen => custom_components/ge_kitchen}/config_flow.py | 0 {ge_kitchen => custom_components/ge_kitchen}/const.py | 0 {ge_kitchen => custom_components/ge_kitchen}/devices/__init__.py | 0 {ge_kitchen => custom_components/ge_kitchen}/devices/base.py | 0 .../ge_kitchen}/devices/dishwasher.py | 0 {ge_kitchen => custom_components/ge_kitchen}/devices/fridge.py | 0 {ge_kitchen => custom_components/ge_kitchen}/devices/oven.py | 0 {ge_kitchen => custom_components/ge_kitchen}/entities/__init__.py | 0 .../ge_kitchen}/entities/common/__init__.py | 0 .../ge_kitchen}/entities/common/ge_entity.py | 0 .../ge_kitchen}/entities/common/ge_erd_binary_sensor.py | 0 .../ge_kitchen}/entities/common/ge_erd_entity.py | 0 .../ge_kitchen}/entities/common/ge_erd_property_binary_sensor.py | 0 .../ge_kitchen}/entities/common/ge_erd_property_sensor.py | 0 .../ge_kitchen}/entities/common/ge_erd_sensor.py | 0 .../ge_kitchen}/entities/common/ge_erd_switch.py | 0 .../ge_kitchen}/entities/common/ge_water_heater.py | 0 .../ge_kitchen}/entities/dishwasher/__init__.py | 0 .../entities/dishwasher/ge_dishwasher_control_locked_switch.py | 0 .../ge_kitchen}/entities/fridge/__init__.py | 0 .../ge_kitchen}/entities/fridge/const.py | 0 .../ge_kitchen}/entities/fridge/ge_abstract_fridge.py | 0 .../ge_kitchen}/entities/fridge/ge_dispenser.py | 0 .../ge_kitchen}/entities/fridge/ge_freezer.py | 0 .../ge_kitchen}/entities/fridge/ge_fridge.py | 0 .../ge_kitchen}/entities/oven/__init__.py | 0 .../ge_kitchen}/entities/oven/const.py | 0 .../ge_kitchen}/entities/oven/ge_oven.py | 0 {ge_kitchen => custom_components/ge_kitchen}/exceptions.py | 0 {ge_kitchen => custom_components/ge_kitchen}/manifest.json | 0 {ge_kitchen => custom_components/ge_kitchen}/sensor.py | 0 {ge_kitchen => custom_components/ge_kitchen}/strings.json | 0 {ge_kitchen => custom_components/ge_kitchen}/switch.py | 0 {ge_kitchen => custom_components/ge_kitchen}/translations/en.json | 0 .../ge_kitchen}/update_coordinator.py | 0 {ge_kitchen => custom_components/ge_kitchen}/water_heater.py | 0 38 files changed, 0 insertions(+), 0 deletions(-) rename {ge_kitchen => custom_components/ge_kitchen}/__init__.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/binary_sensor.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/config_flow.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/const.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/devices/__init__.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/devices/base.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/devices/dishwasher.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/devices/fridge.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/devices/oven.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/__init__.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/common/__init__.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/common/ge_entity.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/common/ge_erd_binary_sensor.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/common/ge_erd_entity.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/common/ge_erd_property_binary_sensor.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/common/ge_erd_property_sensor.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/common/ge_erd_sensor.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/common/ge_erd_switch.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/common/ge_water_heater.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/dishwasher/__init__.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/dishwasher/ge_dishwasher_control_locked_switch.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/fridge/__init__.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/fridge/const.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/fridge/ge_abstract_fridge.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/fridge/ge_dispenser.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/fridge/ge_freezer.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/fridge/ge_fridge.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/oven/__init__.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/oven/const.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/entities/oven/ge_oven.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/exceptions.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/manifest.json (100%) rename {ge_kitchen => custom_components/ge_kitchen}/sensor.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/strings.json (100%) rename {ge_kitchen => custom_components/ge_kitchen}/switch.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/translations/en.json (100%) rename {ge_kitchen => custom_components/ge_kitchen}/update_coordinator.py (100%) rename {ge_kitchen => custom_components/ge_kitchen}/water_heater.py (100%) diff --git a/ge_kitchen/__init__.py b/custom_components/ge_kitchen/__init__.py similarity index 100% rename from ge_kitchen/__init__.py rename to custom_components/ge_kitchen/__init__.py diff --git a/ge_kitchen/binary_sensor.py b/custom_components/ge_kitchen/binary_sensor.py similarity index 100% rename from ge_kitchen/binary_sensor.py rename to custom_components/ge_kitchen/binary_sensor.py diff --git a/ge_kitchen/config_flow.py b/custom_components/ge_kitchen/config_flow.py similarity index 100% rename from ge_kitchen/config_flow.py rename to custom_components/ge_kitchen/config_flow.py diff --git a/ge_kitchen/const.py b/custom_components/ge_kitchen/const.py similarity index 100% rename from ge_kitchen/const.py rename to custom_components/ge_kitchen/const.py diff --git a/ge_kitchen/devices/__init__.py b/custom_components/ge_kitchen/devices/__init__.py similarity index 100% rename from ge_kitchen/devices/__init__.py rename to custom_components/ge_kitchen/devices/__init__.py diff --git a/ge_kitchen/devices/base.py b/custom_components/ge_kitchen/devices/base.py similarity index 100% rename from ge_kitchen/devices/base.py rename to custom_components/ge_kitchen/devices/base.py diff --git a/ge_kitchen/devices/dishwasher.py b/custom_components/ge_kitchen/devices/dishwasher.py similarity index 100% rename from ge_kitchen/devices/dishwasher.py rename to custom_components/ge_kitchen/devices/dishwasher.py diff --git a/ge_kitchen/devices/fridge.py b/custom_components/ge_kitchen/devices/fridge.py similarity index 100% rename from ge_kitchen/devices/fridge.py rename to custom_components/ge_kitchen/devices/fridge.py diff --git a/ge_kitchen/devices/oven.py b/custom_components/ge_kitchen/devices/oven.py similarity index 100% rename from ge_kitchen/devices/oven.py rename to custom_components/ge_kitchen/devices/oven.py diff --git a/ge_kitchen/entities/__init__.py b/custom_components/ge_kitchen/entities/__init__.py similarity index 100% rename from ge_kitchen/entities/__init__.py rename to custom_components/ge_kitchen/entities/__init__.py diff --git a/ge_kitchen/entities/common/__init__.py b/custom_components/ge_kitchen/entities/common/__init__.py similarity index 100% rename from ge_kitchen/entities/common/__init__.py rename to custom_components/ge_kitchen/entities/common/__init__.py diff --git a/ge_kitchen/entities/common/ge_entity.py b/custom_components/ge_kitchen/entities/common/ge_entity.py similarity index 100% rename from ge_kitchen/entities/common/ge_entity.py rename to custom_components/ge_kitchen/entities/common/ge_entity.py diff --git a/ge_kitchen/entities/common/ge_erd_binary_sensor.py b/custom_components/ge_kitchen/entities/common/ge_erd_binary_sensor.py similarity index 100% rename from ge_kitchen/entities/common/ge_erd_binary_sensor.py rename to custom_components/ge_kitchen/entities/common/ge_erd_binary_sensor.py diff --git a/ge_kitchen/entities/common/ge_erd_entity.py b/custom_components/ge_kitchen/entities/common/ge_erd_entity.py similarity index 100% rename from ge_kitchen/entities/common/ge_erd_entity.py rename to custom_components/ge_kitchen/entities/common/ge_erd_entity.py diff --git a/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py b/custom_components/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py similarity index 100% rename from ge_kitchen/entities/common/ge_erd_property_binary_sensor.py rename to custom_components/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py diff --git a/ge_kitchen/entities/common/ge_erd_property_sensor.py b/custom_components/ge_kitchen/entities/common/ge_erd_property_sensor.py similarity index 100% rename from ge_kitchen/entities/common/ge_erd_property_sensor.py rename to custom_components/ge_kitchen/entities/common/ge_erd_property_sensor.py diff --git a/ge_kitchen/entities/common/ge_erd_sensor.py b/custom_components/ge_kitchen/entities/common/ge_erd_sensor.py similarity index 100% rename from ge_kitchen/entities/common/ge_erd_sensor.py rename to custom_components/ge_kitchen/entities/common/ge_erd_sensor.py diff --git a/ge_kitchen/entities/common/ge_erd_switch.py b/custom_components/ge_kitchen/entities/common/ge_erd_switch.py similarity index 100% rename from ge_kitchen/entities/common/ge_erd_switch.py rename to custom_components/ge_kitchen/entities/common/ge_erd_switch.py diff --git a/ge_kitchen/entities/common/ge_water_heater.py b/custom_components/ge_kitchen/entities/common/ge_water_heater.py similarity index 100% rename from ge_kitchen/entities/common/ge_water_heater.py rename to custom_components/ge_kitchen/entities/common/ge_water_heater.py diff --git a/ge_kitchen/entities/dishwasher/__init__.py b/custom_components/ge_kitchen/entities/dishwasher/__init__.py similarity index 100% rename from ge_kitchen/entities/dishwasher/__init__.py rename to custom_components/ge_kitchen/entities/dishwasher/__init__.py diff --git a/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py b/custom_components/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py similarity index 100% rename from ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py rename to custom_components/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py diff --git a/ge_kitchen/entities/fridge/__init__.py b/custom_components/ge_kitchen/entities/fridge/__init__.py similarity index 100% rename from ge_kitchen/entities/fridge/__init__.py rename to custom_components/ge_kitchen/entities/fridge/__init__.py diff --git a/ge_kitchen/entities/fridge/const.py b/custom_components/ge_kitchen/entities/fridge/const.py similarity index 100% rename from ge_kitchen/entities/fridge/const.py rename to custom_components/ge_kitchen/entities/fridge/const.py diff --git a/ge_kitchen/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_kitchen/entities/fridge/ge_abstract_fridge.py similarity index 100% rename from ge_kitchen/entities/fridge/ge_abstract_fridge.py rename to custom_components/ge_kitchen/entities/fridge/ge_abstract_fridge.py diff --git a/ge_kitchen/entities/fridge/ge_dispenser.py b/custom_components/ge_kitchen/entities/fridge/ge_dispenser.py similarity index 100% rename from ge_kitchen/entities/fridge/ge_dispenser.py rename to custom_components/ge_kitchen/entities/fridge/ge_dispenser.py diff --git a/ge_kitchen/entities/fridge/ge_freezer.py b/custom_components/ge_kitchen/entities/fridge/ge_freezer.py similarity index 100% rename from ge_kitchen/entities/fridge/ge_freezer.py rename to custom_components/ge_kitchen/entities/fridge/ge_freezer.py diff --git a/ge_kitchen/entities/fridge/ge_fridge.py b/custom_components/ge_kitchen/entities/fridge/ge_fridge.py similarity index 100% rename from ge_kitchen/entities/fridge/ge_fridge.py rename to custom_components/ge_kitchen/entities/fridge/ge_fridge.py diff --git a/ge_kitchen/entities/oven/__init__.py b/custom_components/ge_kitchen/entities/oven/__init__.py similarity index 100% rename from ge_kitchen/entities/oven/__init__.py rename to custom_components/ge_kitchen/entities/oven/__init__.py diff --git a/ge_kitchen/entities/oven/const.py b/custom_components/ge_kitchen/entities/oven/const.py similarity index 100% rename from ge_kitchen/entities/oven/const.py rename to custom_components/ge_kitchen/entities/oven/const.py diff --git a/ge_kitchen/entities/oven/ge_oven.py b/custom_components/ge_kitchen/entities/oven/ge_oven.py similarity index 100% rename from ge_kitchen/entities/oven/ge_oven.py rename to custom_components/ge_kitchen/entities/oven/ge_oven.py diff --git a/ge_kitchen/exceptions.py b/custom_components/ge_kitchen/exceptions.py similarity index 100% rename from ge_kitchen/exceptions.py rename to custom_components/ge_kitchen/exceptions.py diff --git a/ge_kitchen/manifest.json b/custom_components/ge_kitchen/manifest.json similarity index 100% rename from ge_kitchen/manifest.json rename to custom_components/ge_kitchen/manifest.json diff --git a/ge_kitchen/sensor.py b/custom_components/ge_kitchen/sensor.py similarity index 100% rename from ge_kitchen/sensor.py rename to custom_components/ge_kitchen/sensor.py diff --git a/ge_kitchen/strings.json b/custom_components/ge_kitchen/strings.json similarity index 100% rename from ge_kitchen/strings.json rename to custom_components/ge_kitchen/strings.json diff --git a/ge_kitchen/switch.py b/custom_components/ge_kitchen/switch.py similarity index 100% rename from ge_kitchen/switch.py rename to custom_components/ge_kitchen/switch.py diff --git a/ge_kitchen/translations/en.json b/custom_components/ge_kitchen/translations/en.json similarity index 100% rename from ge_kitchen/translations/en.json rename to custom_components/ge_kitchen/translations/en.json diff --git a/ge_kitchen/update_coordinator.py b/custom_components/ge_kitchen/update_coordinator.py similarity index 100% rename from ge_kitchen/update_coordinator.py rename to custom_components/ge_kitchen/update_coordinator.py diff --git a/ge_kitchen/water_heater.py b/custom_components/ge_kitchen/water_heater.py similarity index 100% rename from ge_kitchen/water_heater.py rename to custom_components/ge_kitchen/water_heater.py From 95215e4421940a49b57c43cbeb968b62554bd1b6 Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Mon, 1 Feb 2021 16:45:49 -0800 Subject: [PATCH 049/338] new file: hacs.json --- hacs.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 hacs.json diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..f8f165a --- /dev/null +++ b/hacs.json @@ -0,0 +1,12 @@ +{ + "name": "ge-kitchen", + "render_readme": true, + "homeassistant": + "domains": [ + "binary_sensor", + "sensor", + "switch", + "water_heater" + ], + "iot_class": "Cloud Polling" +} From bf8775fc0f2d4b158c33129ba8cb58a7437bed96 Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Tue, 2 Feb 2021 17:25:34 -0800 Subject: [PATCH 050/338] Update hacs.json --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index f8f165a..c16f731 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { - "name": "ge-kitchen", + "name": "ge_kitchen", "render_readme": true, "homeassistant": "domains": [ From 2b75af138d6034e3c1f353910cd77b4b21b9a9af Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Thu, 4 Feb 2021 08:40:02 -0800 Subject: [PATCH 051/338] Update hacs.json --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index c16f731..062c918 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { - "name": "ge_kitchen", + "name": "GE Kitchen Appliances (SmartHQ)", "render_readme": true, "homeassistant": "domains": [ From 9d3e375c3ffc8f5999aa1c9b6bdaef9da666d5d8 Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Thu, 4 Feb 2021 08:41:12 -0800 Subject: [PATCH 052/338] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 79ceb95..fe6f0b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Custom Components for Home Assistant +# GE Kitchen Appliances (SmartHQ) ## `ge_kitchen` Integration for GE WiFi-enabled kitchen appliances. So far, I've only done fridges and ovens (because that's what I @@ -20,4 +20,4 @@ Oven Controls: ## What happened to `shark_iq`? -It's part of Home Assistant as of [0.115](https://www.home-assistant.io/blog/2020/09/17/release-115/)! \ No newline at end of file +It's part of Home Assistant as of [0.115](https://www.home-assistant.io/blog/2020/09/17/release-115/)! From 2e16f0079b540660b90aaf428c745e9116851d20 Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Thu, 4 Feb 2021 08:41:31 -0800 Subject: [PATCH 053/338] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index fe6f0b9..60b77f6 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,3 @@ Oven Controls: ![Fridge controls](https://raw.githubusercontent.com/ajmarks/ha_components/master/img/oven_controls.png) -## What happened to `shark_iq`? - -It's part of Home Assistant as of [0.115](https://www.home-assistant.io/blog/2020/09/17/release-115/)! From 991c33befd50a0b34a34688002247eaf43571323 Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Thu, 4 Feb 2021 22:12:22 -0800 Subject: [PATCH 054/338] Update hacs.json --- hacs.json | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/hacs.json b/hacs.json index 062c918..17a7278 100644 --- a/hacs.json +++ b/hacs.json @@ -1,12 +1,7 @@ { "name": "GE Kitchen Appliances (SmartHQ)", "render_readme": true, - "homeassistant": - "domains": [ - "binary_sensor", - "sensor", - "switch", - "water_heater" - ], + "homeassistant": "2021.1.5", + "domains": ["binary_sensor", "sensor", "switch", "water_heater"], "iot_class": "Cloud Polling" } From 24e2f879b5c3c2fb7fe477a33aaf3afdd0382d64 Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Thu, 4 Feb 2021 22:13:09 -0800 Subject: [PATCH 055/338] Create info.md --- info.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 info.md diff --git a/info.md b/info.md new file mode 100644 index 0000000..f8aee06 --- /dev/null +++ b/info.md @@ -0,0 +1,20 @@ +# GE Kitchen Appliances (SmartHQ) + +## `ge_kitchen` +Integration for GE WiFi-enabled kitchen appliances. So far, I've only done fridges and ovens (because that's what I +have), but I hope to to dishwashers next. Because HA doesn't have Fridge or Oven platforms, both fridges and ovens are +primarily represented as water heater entities, which works surprisingly well. If anybody who has other GE appliances +sees this and wants to pitch in, please shoot me a message or make a PR. + +Entities card: + +![Entities](https://raw.githubusercontent.com/ajmarks/ha_components/master/img/appliance_entities.png) + +Fridge Controls: + +![Fridge controls](https://raw.githubusercontent.com/ajmarks/ha_components/master/img/fridge_control.png) + +Oven Controls: + +![Fridge controls](https://raw.githubusercontent.com/ajmarks/ha_components/master/img/oven_controls.png) + From a4acb6ab08fdd502969de0573ef21c40871a2b28 Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Thu, 4 Feb 2021 22:13:29 -0800 Subject: [PATCH 056/338] Update hacs.json --- hacs.json | 1 - 1 file changed, 1 deletion(-) diff --git a/hacs.json b/hacs.json index 17a7278..a9df915 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,5 @@ { "name": "GE Kitchen Appliances (SmartHQ)", - "render_readme": true, "homeassistant": "2021.1.5", "domains": ["binary_sensor", "sensor", "switch", "water_heater"], "iot_class": "Cloud Polling" From 8b96a25893d2f03588b18bdb80596fd04785bfa9 Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Sat, 6 Feb 2021 17:41:34 -0800 Subject: [PATCH 057/338] Change named attribute for hot water timer --- custom_components/ge_kitchen/devices/fridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_kitchen/devices/fridge.py b/custom_components/ge_kitchen/devices/fridge.py index 65c1374..cc7880d 100644 --- a/custom_components/ge_kitchen/devices/fridge.py +++ b/custom_components/ge_kitchen/devices/fridge.py @@ -91,7 +91,7 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.HOT_WATER_IN_USE), GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "status"), - GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_remaining", icon_override="mdi:timer-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), GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=DEVICE_CLASS_PROBLEM), GeDispenser(self) From e52c8dc5670a9843b9cab8be105ddb8ef2c6d709 Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Sun, 7 Feb 2021 18:25:05 -0800 Subject: [PATCH 058/338] Update ge_dispenser.py --- custom_components/ge_kitchen/entities/fridge/ge_dispenser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/ge_kitchen/entities/fridge/ge_dispenser.py b/custom_components/ge_kitchen/entities/fridge/ge_dispenser.py index 0783109..69bf3c0 100644 --- a/custom_components/ge_kitchen/entities/fridge/ge_dispenser.py +++ b/custom_components/ge_kitchen/entities/fridge/ge_dispenser.py @@ -122,3 +122,5 @@ def other_state_attrs(self) -> Dict[str, Any]: 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 From cb54ddb9be41a7c84751882dd6743b63d0c3a32a Mon Sep 17 00:00:00 2001 From: Joel Moses Date: Thu, 11 Feb 2021 12:07:13 -0800 Subject: [PATCH 059/338] Update fridge.py --- custom_components/ge_kitchen/devices/fridge.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_kitchen/devices/fridge.py b/custom_components/ge_kitchen/devices/fridge.py index cc7880d..ea5f92a 100644 --- a/custom_components/ge_kitchen/devices/fridge.py +++ b/custom_components/ge_kitchen/devices/fridge.py @@ -18,7 +18,8 @@ from .base import ApplianceApi from ..entities import ( - GeErdSensor, + GeErdSensor, + GeErdBinarySensor, GeErdSwitch, GeFridge, GeFreezer, @@ -88,7 +89,7 @@ def get_all_entities(self) -> List[Entity]: # Dispenser entities if(hot_water_status and hot_water_status.status != ErdHotWaterStatus.NA): dispenser_entities.extend([ - GeErdSensor(self, ErdCode.HOT_WATER_IN_USE), + GeErdBinarySensor(self, ErdCode.HOT_WATER_IN_USE), GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "status"), GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_until_ready", icon_override="mdi:timer-outline"), From 2c9e3ca1b0722b685bc6b77ea83bb9fc3ebd1244 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 16 May 2021 12:07:48 -0400 Subject: [PATCH 060/338] - added initial version of laundry support --- custom_components/ge_kitchen/const.py | 2 +- .../ge_kitchen/devices/__init__.py | 9 ++++ .../ge_kitchen/devices/dishwasher.py | 4 +- custom_components/ge_kitchen/devices/dryer.py | 25 +++++++++++ .../ge_kitchen/devices/fridge.py | 2 +- .../ge_kitchen/devices/washer.py | 38 +++++++++++++++++ .../ge_kitchen/devices/washer_dryer.py | 41 +++++++++++++++++++ custom_components/ge_kitchen/manifest.json | 2 +- 8 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 custom_components/ge_kitchen/devices/dryer.py create mode 100644 custom_components/ge_kitchen/devices/washer.py create mode 100644 custom_components/ge_kitchen/devices/washer_dryer.py diff --git a/custom_components/ge_kitchen/const.py b/custom_components/ge_kitchen/const.py index 2d86b56..0076de8 100644 --- a/custom_components/ge_kitchen/const.py +++ b/custom_components/ge_kitchen/const.py @@ -1,5 +1,5 @@ """Constants for the ge_kitchen integration.""" -from gekitchensdk.const import LOGIN_URL +from gekitchensdk.clients.const import LOGIN_URL DOMAIN = "ge_kitchen" diff --git a/custom_components/ge_kitchen/devices/__init__.py b/custom_components/ge_kitchen/devices/__init__.py index 66a9b89..a3083a1 100644 --- a/custom_components/ge_kitchen/devices/__init__.py +++ b/custom_components/ge_kitchen/devices/__init__.py @@ -7,6 +7,9 @@ from .oven import OvenApi from .fridge import FridgeApi from .dishwasher import DishwasherApi +from .washer import WasherApi +from .dryer import DryerApi +from .washer_dryer import WasherDryerApi _LOGGER = logging.getLogger(__name__) @@ -19,5 +22,11 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return FridgeApi if appliance_type == ErdApplianceType.DISH_WASHER: return DishwasherApi + if appliance_type == ErdApplianceType.WASHER: + return WasherApi + if appliance_type == ErdApplianceType.DRYER: + return DryerApi + if appliance_type == ErdApplianceType.COMBINATION_WASHER_DRYER: + return WasherDryerApi # Fallback return ApplianceApi diff --git a/custom_components/ge_kitchen/devices/dishwasher.py b/custom_components/ge_kitchen/devices/dishwasher.py index 918e3be..1221432 100644 --- a/custom_components/ge_kitchen/devices/dishwasher.py +++ b/custom_components/ge_kitchen/devices/dishwasher.py @@ -11,8 +11,8 @@ class DishwasherApi(ApplianceApi): - """API class for oven objects""" - APPLIANCE_TYPE = ErdApplianceType.DISH_WASHER + """API class for dishwasher objects""" + APPLIANCE_TYPE = ErdApplianceType.WASHER def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() diff --git a/custom_components/ge_kitchen/devices/dryer.py b/custom_components/ge_kitchen/devices/dryer.py new file mode 100644 index 0000000..b406077 --- /dev/null +++ b/custom_components/ge_kitchen/devices/dryer.py @@ -0,0 +1,25 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gekitchensdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeErdSensor, GeDishwasherControlLockedSwitch + +_LOGGER = logging.getLogger(__name__) + + +class DryerApi(ApplianceApi): + """API class for dryer objects""" + APPLIANCE_TYPE = ErdApplianceType.WASHER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + dryer_entities = [ + + ] + entities = base_entities + dryer_entities + return entities + \ No newline at end of file diff --git a/custom_components/ge_kitchen/devices/fridge.py b/custom_components/ge_kitchen/devices/fridge.py index ea5f92a..3c888f1 100644 --- a/custom_components/ge_kitchen/devices/fridge.py +++ b/custom_components/ge_kitchen/devices/fridge.py @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) class FridgeApi(ApplianceApi): - """API class for oven objects""" + """API class for fridge objects""" APPLIANCE_TYPE = ErdApplianceType.FRIDGE def get_all_entities(self) -> List[Entity]: diff --git a/custom_components/ge_kitchen/devices/washer.py b/custom_components/ge_kitchen/devices/washer.py new file mode 100644 index 0000000..11f325a --- /dev/null +++ b/custom_components/ge_kitchen/devices/washer.py @@ -0,0 +1,38 @@ +from custom_components.ge_kitchen.entities.common.ge_erd_binary_sensor import GeErdBinarySensor +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gekitchensdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeErdSensor, GeErdBinarySensor + +_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() + + washer_entities = [ + GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_STATE), + GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_SUBCYCLE), + GeErdBinarySensor(self, ErdCode.LAUNDRY_END_OF_CYCLE), + GeErdSensor(self, ErdCode.LAUNDRY_TIME_REMAINING), + GeErdSensor(self, ErdCode.LAUNDRY_CYCLE), + GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), + GeErdSensor(self, ErdCode.LAUNDRY_DOOR), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR_LOCK), + GeErdSensor(self, ErdCode.LAUNDRY_SOIL_LEVEL), + GeErdSensor(self, ErdCode.LAUNDRY_WASHTEMP_LEVEL), + GeErdSensor(self, ErdCode.LAUNDRY_SPINTIME_LEVEL), + GeErdSensor(self, ErdCode.LAUNDRY_RINSE_OPTION), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS) + ] + entities = base_entities + washer_entities + return entities + \ No newline at end of file diff --git a/custom_components/ge_kitchen/devices/washer_dryer.py b/custom_components/ge_kitchen/devices/washer_dryer.py new file mode 100644 index 0000000..86789df --- /dev/null +++ b/custom_components/ge_kitchen/devices/washer_dryer.py @@ -0,0 +1,41 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gekitchensdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeErdSensor, GeErdBinarySensor + +_LOGGER = logging.getLogger(__name__) + + +class WasherDryerApi(ApplianceApi): + """API class for washer/dryer objects""" + APPLIANCE_TYPE = ErdApplianceType.COMBINATION_WASHER_DRYER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + washer_entities = [ + GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_STATE), + GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_SUBCYCLE), + GeErdBinarySensor(self, ErdCode.LAUNDRY_END_OF_CYCLE), + GeErdSensor(self, ErdCode.LAUNDRY_TIME_REMAINING), + GeErdSensor(self, ErdCode.LAUNDRY_CYCLE), + GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), + GeErdSensor(self, ErdCode.LAUNDRY_DOOR), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR_LOCK), + GeErdSensor(self, ErdCode.LAUNDRY_SOIL_LEVEL), + GeErdSensor(self, ErdCode.LAUNDRY_WASHTEMP_LEVEL), + GeErdSensor(self, ErdCode.LAUNDRY_SPINTIME_LEVEL), + GeErdSensor(self, ErdCode.LAUNDRY_RINSE_OPTION), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS) + ] + + dryer_entities = [ + + ] + entities = base_entities + washer_entities + dryer_entities + return entities + \ No newline at end of file diff --git a/custom_components/ge_kitchen/manifest.json b/custom_components/ge_kitchen/manifest.json index a2b9478..f2c38c7 100644 --- a/custom_components/ge_kitchen/manifest.json +++ b/custom_components/ge_kitchen/manifest.json @@ -3,6 +3,6 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.10","magicattr==0.1.5"], + "requirements": ["gekitchensdk==0.3.11","magicattr==0.1.5"], "codeowners": ["@simbaja"] } From b01799ff78f37acf4b7af5d9dbea68444f170d69 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 16 May 2021 20:47:55 -0400 Subject: [PATCH 061/338] - added version to manifest --- custom_components/ge_kitchen/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/ge_kitchen/manifest.json b/custom_components/ge_kitchen/manifest.json index f2c38c7..5eafb9c 100644 --- a/custom_components/ge_kitchen/manifest.json +++ b/custom_components/ge_kitchen/manifest.json @@ -5,4 +5,5 @@ "documentation": "https://github.com/simbaja/ha_components", "requirements": ["gekitchensdk==0.3.11","magicattr==0.1.5"], "codeowners": ["@simbaja"] + "version": "0.3.11" } From e9a2441df9a04cb81323095e3fa4e8774f583641 Mon Sep 17 00:00:00 2001 From: Warren Rees Date: Tue, 18 May 2021 14:13:29 -0400 Subject: [PATCH 062/338] - add preliminary dryer support, update manifest for version, change entity version to display applicance SW version and not the wifi SW version --- custom_components/ge_kitchen/devices/base.py | 4 ++-- custom_components/ge_kitchen/devices/dryer.py | 14 ++++++++++++-- custom_components/ge_kitchen/manifest.json | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/custom_components/ge_kitchen/devices/base.py b/custom_components/ge_kitchen/devices/base.py index 350b57d..3bcb57c 100644 --- a/custom_components/ge_kitchen/devices/base.py +++ b/custom_components/ge_kitchen/devices/base.py @@ -81,7 +81,7 @@ def device_info(self) -> Dict: "name": self.name, "manufacturer": "GE", "model": self.model_number, - "sw_version": self.appliance.get_erd_value(ErdCode.WIFI_MODULE_SW_VERSION), + "sw_version": self.appliance.get_erd_value(ErdCode.APPLIANCE_SW_VERSION), } @property @@ -114,4 +114,4 @@ def try_get_erd_value(self, code: ErdCodeType): return self.appliance.get_erd_value(code) except: return None - \ No newline at end of file + diff --git a/custom_components/ge_kitchen/devices/dryer.py b/custom_components/ge_kitchen/devices/dryer.py index b406077..8a231a8 100644 --- a/custom_components/ge_kitchen/devices/dryer.py +++ b/custom_components/ge_kitchen/devices/dryer.py @@ -12,14 +12,24 @@ class DryerApi(ApplianceApi): """API class for dryer objects""" - APPLIANCE_TYPE = ErdApplianceType.WASHER + APPLIANCE_TYPE = ErdApplianceType.DRYER def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() dryer_entities = [ + GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_STATE), + GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_SUBCYCLE), + GeErdBinarySensor(self, ErdCode.LAUNDRY_END_OF_CYCLE), + GeErdSensor(self, ErdCode.LAUNDRY_TIME_REMAINING), + GeErdSensor(self, ErdCode.LAUNDRY_CYCLE), + GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), + GeErdSensor(self, ErdCode.LAUNDRY_DOOR), + GeErdSensor(self, ErdCode.LAUNDRY_DRYNESSNEW_LEVEL), + GeErdSensor(self, ErdCode.LAUNDRY_TEMPERATURENEW_OPTION), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS) ] entities = base_entities + dryer_entities return entities - \ No newline at end of file + diff --git a/custom_components/ge_kitchen/manifest.json b/custom_components/ge_kitchen/manifest.json index 5eafb9c..dc82b75 100644 --- a/custom_components/ge_kitchen/manifest.json +++ b/custom_components/ge_kitchen/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", "requirements": ["gekitchensdk==0.3.11","magicattr==0.1.5"], - "codeowners": ["@simbaja"] + "codeowners": ["@simbaja"], "version": "0.3.11" } From e399ff1652a00da11bfc4451f6b93bad326d5037 Mon Sep 17 00:00:00 2001 From: Warren Rees Date: Thu, 20 May 2021 11:15:49 -0400 Subject: [PATCH 063/338] - Update to GE Home and use the gehomesdk --- LICENSE | 2 +- README.md | 39 +++++++++---------- .../{ge_kitchen => ge_home}/__init__.py | 12 +++--- .../{ge_kitchen => ge_home}/binary_sensor.py | 8 ++-- .../{ge_kitchen => ge_home}/config_flow.py | 16 ++++---- .../{ge_kitchen => ge_home}/const.py | 6 +-- .../devices/__init__.py | 2 +- .../{ge_kitchen => ge_home}/devices/base.py | 4 +- .../devices/dishwasher.py | 4 +- .../{ge_kitchen => ge_home}/devices/dryer.py | 2 +- .../{ge_kitchen => ge_home}/devices/fridge.py | 2 +- .../{ge_kitchen => ge_home}/devices/oven.py | 2 +- .../{ge_kitchen => ge_home}/devices/washer.py | 6 +-- .../devices/washer_dryer.py | 4 +- .../entities/__init__.py | 0 .../entities/common/__init__.py | 0 .../entities/common/ge_entity.py | 4 +- .../entities/common/ge_erd_binary_sensor.py | 2 +- .../entities/common/ge_erd_entity.py | 2 +- .../common/ge_erd_property_binary_sensor.py | 2 +- .../entities/common/ge_erd_property_sensor.py | 2 +- .../entities/common/ge_erd_sensor.py | 2 +- .../entities/common/ge_erd_switch.py | 0 .../entities/common/ge_water_heater.py | 2 +- .../entities/dishwasher/__init__.py | 0 .../ge_dishwasher_control_locked_switch.py | 4 +- .../entities/fridge/__init__.py | 0 .../entities/fridge/const.py | 0 .../entities/fridge/ge_abstract_fridge.py | 4 +- .../entities/fridge/ge_dispenser.py | 4 +- .../entities/fridge/ge_freezer.py | 4 +- .../entities/fridge/ge_fridge.py | 4 +- .../entities/oven/__init__.py | 0 .../entities/oven/const.py | 2 +- .../entities/oven/ge_oven.py | 4 +- .../{ge_kitchen => ge_home}/exceptions.py | 0 custom_components/ge_home/manifest.json | 9 +++++ .../{ge_kitchen => ge_home}/sensor.py | 10 ++--- .../{ge_kitchen => ge_home}/strings.json | 0 .../{ge_kitchen => ge_home}/switch.py | 10 ++--- .../translations/en.json | 4 +- .../update_coordinator.py | 24 ++++++------ .../{ge_kitchen => ge_home}/water_heater.py | 8 ++-- custom_components/ge_kitchen/manifest.json | 9 ----- hacs.json | 2 +- info.md | 17 ++++---- 46 files changed, 121 insertions(+), 123 deletions(-) rename custom_components/{ge_kitchen => ge_home}/__init__.py (76%) rename custom_components/{ge_kitchen => ge_home}/binary_sensor.py (82%) rename custom_components/{ge_kitchen => ge_home}/config_flow.py (86%) rename custom_components/{ge_kitchen => ge_home}/const.py (57%) rename custom_components/{ge_kitchen => ge_home}/devices/__init__.py (95%) rename custom_components/{ge_kitchen => ge_home}/devices/base.py (97%) rename custom_components/{ge_kitchen => ge_home}/devices/dishwasher.py (94%) rename custom_components/{ge_kitchen => ge_home}/devices/dryer.py (95%) rename custom_components/{ge_kitchen => ge_home}/devices/fridge.py (99%) rename custom_components/{ge_kitchen => ge_home}/devices/oven.py (99%) rename custom_components/{ge_kitchen => ge_home}/devices/washer.py (89%) rename custom_components/{ge_kitchen => ge_home}/devices/washer_dryer.py (95%) rename custom_components/{ge_kitchen => ge_home}/entities/__init__.py (100%) rename custom_components/{ge_kitchen => ge_home}/entities/common/__init__.py (100%) rename custom_components/{ge_kitchen => ge_home}/entities/common/ge_entity.py (96%) rename custom_components/{ge_kitchen => ge_home}/entities/common/ge_erd_binary_sensor.py (96%) rename custom_components/{ge_kitchen => ge_home}/entities/common/ge_erd_entity.py (98%) rename custom_components/{ge_kitchen => ge_home}/entities/common/ge_erd_property_binary_sensor.py (97%) rename custom_components/{ge_kitchen => ge_home}/entities/common/ge_erd_property_sensor.py (94%) rename custom_components/{ge_kitchen => ge_home}/entities/common/ge_erd_sensor.py (97%) rename custom_components/{ge_kitchen => ge_home}/entities/common/ge_erd_switch.py (100%) rename custom_components/{ge_kitchen => ge_home}/entities/common/ge_water_heater.py (96%) rename custom_components/{ge_kitchen => ge_home}/entities/dishwasher/__init__.py (100%) rename custom_components/{ge_kitchen => ge_home}/entities/dishwasher/ge_dishwasher_control_locked_switch.py (88%) rename custom_components/{ge_kitchen => ge_home}/entities/fridge/__init__.py (100%) rename custom_components/{ge_kitchen => ge_home}/entities/fridge/const.py (100%) rename custom_components/{ge_kitchen => ge_home}/entities/fridge/ge_abstract_fridge.py (98%) rename custom_components/{ge_kitchen => ge_home}/entities/fridge/ge_dispenser.py (98%) rename custom_components/{ge_kitchen => ge_home}/entities/fridge/ge_freezer.py (91%) rename custom_components/{ge_kitchen => ge_home}/entities/fridge/ge_fridge.py (96%) rename custom_components/{ge_kitchen => ge_home}/entities/oven/__init__.py (100%) rename custom_components/{ge_kitchen => ge_home}/entities/oven/const.py (96%) rename custom_components/{ge_kitchen => ge_home}/entities/oven/ge_oven.py (99%) rename custom_components/{ge_kitchen => ge_home}/exceptions.py (100%) create mode 100644 custom_components/ge_home/manifest.json rename custom_components/{ge_kitchen => ge_home}/sensor.py (78%) rename custom_components/{ge_kitchen => ge_home}/strings.json (100%) rename custom_components/{ge_kitchen => ge_home}/switch.py (78%) rename custom_components/{ge_kitchen => ge_home}/translations/en.json (95%) rename custom_components/{ge_kitchen => ge_home}/update_coordinator.py (94%) rename custom_components/{ge_kitchen => ge_home}/water_heater.py (82%) delete mode 100644 custom_components/ge_kitchen/manifest.json diff --git a/LICENSE b/LICENSE index 2f1db63..1637db2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Andrew Marks +Copyright (c) 2021 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 60b77f6..e8cb73f 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,19 @@ -# GE Kitchen Appliances (SmartHQ) - -## `ge_kitchen` -Integration for GE WiFi-enabled kitchen appliances. So far, I've only done fridges and ovens (because that's what I -have), but I hope to to dishwashers next. Because HA doesn't have Fridge or Oven platforms, both fridges and ovens are -primarily represented as water heater entities, which works surprisingly well. If anybody who has other GE appliances -sees this and wants to pitch in, please shoot me a message or make a PR. - -Entities card: - -![Entities](https://raw.githubusercontent.com/ajmarks/ha_components/master/img/appliance_entities.png) - -Fridge Controls: - -![Fridge controls](https://raw.githubusercontent.com/ajmarks/ha_components/master/img/fridge_control.png) - -Oven Controls: - -![Fridge controls](https://raw.githubusercontent.com/ajmarks/ha_components/master/img/oven_controls.png) - +# GE Home Appliances (SmartHQ) + +## `ge_home` +Integration for GE WiFi-enabled appliances into Home Assistant. This integration currently contains fridge, oven, dishwasher, laundry washer, laundry dryer support. + +**Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** + +Entities card: + +![Entities](https://raw.githubusercontent.com/simbaja/ha_components/master/img/appliance_entities.png) + +Fridge Controls: + +![Fridge controls](https://raw.githubusercontent.com/simbaja/ha_components/master/img/fridge_control.png) + +Oven Controls: + +![Fridge controls](https://raw.githubusercontent.com/simbaja/ha_components/master/img/oven_controls.png) + diff --git a/custom_components/ge_kitchen/__init__.py b/custom_components/ge_home/__init__.py similarity index 76% rename from custom_components/ge_kitchen/__init__.py rename to custom_components/ge_home/__init__.py index 9fce3a0..b9e6949 100644 --- a/custom_components/ge_kitchen/__init__.py +++ b/custom_components/ge_home/__init__.py @@ -1,4 +1,4 @@ -"""The ge_kitchen integration.""" +"""The ge_home integration.""" from homeassistant.const import EVENT_HOMEASSISTANT_STOP import voluptuous as vol @@ -8,7 +8,7 @@ from .const import ( DOMAIN ) -from .update_coordinator import GeKitchenUpdateCoordinator +from .update_coordinator import GeHomeUpdateCoordinator CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -16,11 +16,11 @@ async def async_setup(hass: HomeAssistant, config: dict): return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up the ge_kitchen component.""" + """Set up the ge_home component.""" hass.data.setdefault(DOMAIN, {}) - """Set up ge_kitchen from a config entry.""" - coordinator = GeKitchenUpdateCoordinator(hass, entry) + """Set up ge_home from a config entry.""" + coordinator = GeHomeUpdateCoordinator(hass, entry) hass.data[DOMAIN][entry.entry_id] = coordinator if not await coordinator.async_setup(): @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - coordinator: GeKitchenUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] ok = await coordinator.async_reset() if ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/custom_components/ge_kitchen/binary_sensor.py b/custom_components/ge_home/binary_sensor.py similarity index 82% rename from custom_components/ge_kitchen/binary_sensor.py rename to custom_components/ge_home/binary_sensor.py index 6068fa1..addce09 100644 --- a/custom_components/ge_kitchen/binary_sensor.py +++ b/custom_components/ge_home/binary_sensor.py @@ -1,4 +1,4 @@ -"""GE Kitchen Sensor Entities""" +"""GE Home Sensor Entities""" import async_timeout import logging from typing import Callable @@ -9,14 +9,14 @@ from .const import DOMAIN from .entities import GeErdBinarySensor -from .update_coordinator import GeKitchenUpdateCoordinator +from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Kitchen sensors.""" + """GE Home sensors.""" - coordinator: GeKitchenUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] # This should be a NOP, but let's be safe with async_timeout.timeout(20): diff --git a/custom_components/ge_kitchen/config_flow.py b/custom_components/ge_home/config_flow.py similarity index 86% rename from custom_components/ge_kitchen/config_flow.py rename to custom_components/ge_home/config_flow.py index 3d93d22..b272f07 100644 --- a/custom_components/ge_kitchen/config_flow.py +++ b/custom_components/ge_home/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for GE Kitchen integration.""" +"""Config flow for GE Home integration.""" import logging from typing import Dict, Optional @@ -7,7 +7,7 @@ import asyncio import async_timeout -from gekitchensdk import GeAuthFailedError, GeNotAuthenticatedError, GeGeneralServerError, async_get_oauth2_token +from gehomesdk import GeAuthFailedError, GeNotAuthenticatedError, GeGeneralServerError, async_get_oauth2_token import voluptuous as vol from homeassistant import config_entries, core @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) -GEKITCHEN_SCHEMA = vol.Schema( +GEHOME_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} ) @@ -42,10 +42,10 @@ async def validate_input(hass: core.HomeAssistant, data): raise HaCannotConnect('Unknown connection failure') # Return info that you want to store in the config entry. - return {"title": f"GE Kitchen ({data[CONF_USERNAME]:s})"} + return {"title": f"GE Home ({data[CONF_USERNAME]:s})"} -class GeKitchenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for GE Kitchen.""" +class GeHomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for GE Home.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH @@ -77,7 +77,7 @@ async def async_step_user(self, user_input: Optional[Dict] = None): return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( - step_id="user", data_schema=GEKITCHEN_SCHEMA, errors=errors + step_id="user", data_schema=GEHOME_SCHEMA, errors=errors ) async def async_step_reauth(self, user_input: Optional[dict] = None): @@ -100,5 +100,5 @@ async def async_step_reauth(self, user_input: Optional[dict] = None): return self.async_abort(reason=errors["base"]) return self.async_show_form( - step_id="reauth", data_schema=GEKITCHEN_SCHEMA, errors=errors, + step_id="reauth", data_schema=GEHOME_SCHEMA, errors=errors, ) diff --git a/custom_components/ge_kitchen/const.py b/custom_components/ge_home/const.py similarity index 57% rename from custom_components/ge_kitchen/const.py rename to custom_components/ge_home/const.py index 0076de8..87fe381 100644 --- a/custom_components/ge_kitchen/const.py +++ b/custom_components/ge_home/const.py @@ -1,7 +1,7 @@ -"""Constants for the ge_kitchen integration.""" -from gekitchensdk.clients.const import LOGIN_URL +"""Constants for the gehome integration.""" +from gehomesdk.clients.const import LOGIN_URL -DOMAIN = "ge_kitchen" +DOMAIN = "ge_home" EVENT_ALL_APPLIANCES_READY = 'all_appliances_ready' diff --git a/custom_components/ge_kitchen/devices/__init__.py b/custom_components/ge_home/devices/__init__.py similarity index 95% rename from custom_components/ge_kitchen/devices/__init__.py rename to custom_components/ge_home/devices/__init__.py index a3083a1..3d300a3 100644 --- a/custom_components/ge_kitchen/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -1,7 +1,7 @@ import logging from typing import Type -from gekitchensdk.erd import ErdApplianceType +from gehomesdk.erd import ErdApplianceType from .base import ApplianceApi from .oven import OvenApi diff --git a/custom_components/ge_kitchen/devices/base.py b/custom_components/ge_home/devices/base.py similarity index 97% rename from custom_components/ge_kitchen/devices/base.py rename to custom_components/ge_home/devices/base.py index 3bcb57c..d343340 100644 --- a/custom_components/ge_kitchen/devices/base.py +++ b/custom_components/ge_home/devices/base.py @@ -2,8 +2,8 @@ import logging from typing import Dict, List, Optional -from gekitchensdk import GeAppliance -from gekitchensdk.erd import ErdCode, ErdCodeType, ErdApplianceType +from gehomesdk import GeAppliance +from gehomesdk.erd import ErdCode, ErdCodeType, ErdApplianceType from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity diff --git a/custom_components/ge_kitchen/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py similarity index 94% rename from custom_components/ge_kitchen/devices/dishwasher.py rename to custom_components/ge_home/devices/dishwasher.py index 1221432..e9a8632 100644 --- a/custom_components/ge_kitchen/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -2,7 +2,7 @@ from typing import List from homeassistant.helpers.entity import Entity -from gekitchensdk.erd import ErdCode, ErdApplianceType +from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import GeErdSensor, GeDishwasherControlLockedSwitch @@ -28,4 +28,4 @@ def get_all_entities(self) -> List[Entity]: ] entities = base_entities + dishwasher_entities return entities - \ No newline at end of file + diff --git a/custom_components/ge_kitchen/devices/dryer.py b/custom_components/ge_home/devices/dryer.py similarity index 95% rename from custom_components/ge_kitchen/devices/dryer.py rename to custom_components/ge_home/devices/dryer.py index 8a231a8..2876464 100644 --- a/custom_components/ge_kitchen/devices/dryer.py +++ b/custom_components/ge_home/devices/dryer.py @@ -2,7 +2,7 @@ from typing import List from homeassistant.helpers.entity import Entity -from gekitchensdk.erd import ErdCode, ErdApplianceType +from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import GeErdSensor, GeDishwasherControlLockedSwitch diff --git a/custom_components/ge_kitchen/devices/fridge.py b/custom_components/ge_home/devices/fridge.py similarity index 99% rename from custom_components/ge_kitchen/devices/fridge.py rename to custom_components/ge_home/devices/fridge.py index 3c888f1..10f17b4 100644 --- a/custom_components/ge_kitchen/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -4,7 +4,7 @@ from typing import List from homeassistant.helpers.entity import Entity -from gekitchensdk import ( +from gehomesdk import ( ErdCode, ErdApplianceType, ErdOnOff, diff --git a/custom_components/ge_kitchen/devices/oven.py b/custom_components/ge_home/devices/oven.py similarity index 99% rename from custom_components/ge_kitchen/devices/oven.py rename to custom_components/ge_home/devices/oven.py index afe0981..1e0c84d 100644 --- a/custom_components/ge_kitchen/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -3,7 +3,7 @@ from homeassistant.const import DEVICE_CLASS_POWER_FACTOR from homeassistant.helpers.entity import Entity -from gekitchensdk import ( +from gehomesdk import ( ErdCode, ErdApplianceType, OvenConfiguration, diff --git a/custom_components/ge_kitchen/devices/washer.py b/custom_components/ge_home/devices/washer.py similarity index 89% rename from custom_components/ge_kitchen/devices/washer.py rename to custom_components/ge_home/devices/washer.py index 11f325a..53b1d74 100644 --- a/custom_components/ge_kitchen/devices/washer.py +++ b/custom_components/ge_home/devices/washer.py @@ -1,9 +1,9 @@ -from custom_components.ge_kitchen.entities.common.ge_erd_binary_sensor import GeErdBinarySensor +from custom_components.ge_home.entities.common.ge_erd_binary_sensor import GeErdBinarySensor import logging from typing import List from homeassistant.helpers.entity import Entity -from gekitchensdk.erd import ErdCode, ErdApplianceType +from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import GeErdSensor, GeErdBinarySensor @@ -35,4 +35,4 @@ def get_all_entities(self) -> List[Entity]: ] entities = base_entities + washer_entities return entities - \ No newline at end of file + diff --git a/custom_components/ge_kitchen/devices/washer_dryer.py b/custom_components/ge_home/devices/washer_dryer.py similarity index 95% rename from custom_components/ge_kitchen/devices/washer_dryer.py rename to custom_components/ge_home/devices/washer_dryer.py index 86789df..5b3d720 100644 --- a/custom_components/ge_kitchen/devices/washer_dryer.py +++ b/custom_components/ge_home/devices/washer_dryer.py @@ -2,7 +2,7 @@ from typing import List from homeassistant.helpers.entity import Entity -from gekitchensdk.erd import ErdCode, ErdApplianceType +from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import GeErdSensor, GeErdBinarySensor @@ -38,4 +38,4 @@ def get_all_entities(self) -> List[Entity]: ] entities = base_entities + washer_entities + dryer_entities return entities - \ No newline at end of file + diff --git a/custom_components/ge_kitchen/entities/__init__.py b/custom_components/ge_home/entities/__init__.py similarity index 100% rename from custom_components/ge_kitchen/entities/__init__.py rename to custom_components/ge_home/entities/__init__.py diff --git a/custom_components/ge_kitchen/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py similarity index 100% rename from custom_components/ge_kitchen/entities/common/__init__.py rename to custom_components/ge_home/entities/common/__init__.py diff --git a/custom_components/ge_kitchen/entities/common/ge_entity.py b/custom_components/ge_home/entities/common/ge_entity.py similarity index 96% rename from custom_components/ge_kitchen/entities/common/ge_entity.py rename to custom_components/ge_home/entities/common/ge_entity.py index e8c04db..ed9e090 100644 --- a/custom_components/ge_kitchen/entities/common/ge_entity.py +++ b/custom_components/ge_home/entities/common/ge_entity.py @@ -1,7 +1,7 @@ from datetime import timedelta from typing import Optional, Dict, Any -from gekitchensdk import GeAppliance +from gehomesdk import GeAppliance from ...devices import ApplianceApi class GeEntity: @@ -62,4 +62,4 @@ def _get_icon(self) -> Optional[str]: return None def _get_device_class(self) -> Optional[str]: - return None \ No newline at end of file + return None diff --git a/custom_components/ge_kitchen/entities/common/ge_erd_binary_sensor.py b/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py similarity index 96% rename from custom_components/ge_kitchen/entities/common/ge_erd_binary_sensor.py rename to custom_components/ge_home/entities/common/ge_erd_binary_sensor.py index e02c298..8c6f613 100644 --- a/custom_components/ge_kitchen/entities/common/ge_erd_binary_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py @@ -2,7 +2,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity -from gekitchensdk import ErdCode, ErdCodeType, ErdCodeClass +from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass from ...devices import ApplianceApi from .ge_erd_entity import GeErdEntity diff --git a/custom_components/ge_kitchen/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py similarity index 98% rename from custom_components/ge_kitchen/entities/common/ge_erd_entity.py rename to custom_components/ge_home/entities/common/ge_erd_entity.py index a32d66e..eae8995 100644 --- a/custom_components/ge_kitchen/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -2,7 +2,7 @@ from typing import Optional from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from gekitchensdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits +from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits from ...const import DOMAIN from ...devices import ApplianceApi diff --git a/custom_components/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py b/custom_components/ge_home/entities/common/ge_erd_property_binary_sensor.py similarity index 97% rename from custom_components/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py rename to custom_components/ge_home/entities/common/ge_erd_property_binary_sensor.py index 8f6a425..d7504ce 100644 --- a/custom_components/ge_kitchen/entities/common/ge_erd_property_binary_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_property_binary_sensor.py @@ -1,7 +1,7 @@ from typing import Optional import magicattr -from gekitchensdk import ErdCodeType +from gehomesdk import ErdCodeType from ...devices import ApplianceApi from .ge_erd_binary_sensor import GeErdBinarySensor diff --git a/custom_components/ge_kitchen/entities/common/ge_erd_property_sensor.py b/custom_components/ge_home/entities/common/ge_erd_property_sensor.py similarity index 94% rename from custom_components/ge_kitchen/entities/common/ge_erd_property_sensor.py rename to custom_components/ge_home/entities/common/ge_erd_property_sensor.py index 5e9b0ee..0092d42 100644 --- a/custom_components/ge_kitchen/entities/common/ge_erd_property_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_property_sensor.py @@ -1,7 +1,7 @@ from typing import Optional import magicattr -from gekitchensdk import ErdCode, ErdCodeType, ErdMeasurementUnits +from gehomesdk import ErdCode, ErdCodeType, ErdMeasurementUnits from ...devices import ApplianceApi from .ge_erd_sensor import GeErdSensor diff --git a/custom_components/ge_kitchen/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py similarity index 97% rename from custom_components/ge_kitchen/entities/common/ge_erd_sensor.py rename to custom_components/ge_home/entities/common/ge_erd_sensor.py index 513d4ae..cdeee95 100644 --- a/custom_components/ge_kitchen/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -8,7 +8,7 @@ TEMP_FAHRENHEIT ) from homeassistant.helpers.entity import Entity -from gekitchensdk import ErdCode, ErdCodeClass, ErdMeasurementUnits +from gehomesdk import ErdCode, ErdCodeClass, ErdMeasurementUnits from .ge_erd_entity import GeErdEntity diff --git a/custom_components/ge_kitchen/entities/common/ge_erd_switch.py b/custom_components/ge_home/entities/common/ge_erd_switch.py similarity index 100% rename from custom_components/ge_kitchen/entities/common/ge_erd_switch.py rename to custom_components/ge_home/entities/common/ge_erd_switch.py diff --git a/custom_components/ge_kitchen/entities/common/ge_water_heater.py b/custom_components/ge_home/entities/common/ge_water_heater.py similarity index 96% rename from custom_components/ge_kitchen/entities/common/ge_water_heater.py rename to custom_components/ge_home/entities/common/ge_water_heater.py index 180a134..a99b6c6 100644 --- a/custom_components/ge_kitchen/entities/common/ge_water_heater.py +++ b/custom_components/ge_home/entities/common/ge_water_heater.py @@ -7,7 +7,7 @@ TEMP_FAHRENHEIT, TEMP_CELSIUS ) -from gekitchensdk import ErdCode, ErdMeasurementUnits +from gehomesdk import ErdCode, ErdMeasurementUnits from ...const import DOMAIN from .ge_erd_entity import GeEntity diff --git a/custom_components/ge_kitchen/entities/dishwasher/__init__.py b/custom_components/ge_home/entities/dishwasher/__init__.py similarity index 100% rename from custom_components/ge_kitchen/entities/dishwasher/__init__.py rename to custom_components/ge_home/entities/dishwasher/__init__.py diff --git a/custom_components/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py b/custom_components/ge_home/entities/dishwasher/ge_dishwasher_control_locked_switch.py similarity index 88% rename from custom_components/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py rename to custom_components/ge_home/entities/dishwasher/ge_dishwasher_control_locked_switch.py index 29bae93..55923d8 100644 --- a/custom_components/ge_kitchen/entities/dishwasher/ge_dishwasher_control_locked_switch.py +++ b/custom_components/ge_home/entities/dishwasher/ge_dishwasher_control_locked_switch.py @@ -1,4 +1,4 @@ -from gekitchensdk import ErdCode, ErdOperatingMode +from gehomesdk import ErdCode, ErdOperatingMode from ..common import GeErdSwitch @@ -9,4 +9,4 @@ class GeDishwasherControlLockedSwitch(GeErdSwitch): def is_on(self) -> bool: mode: ErdOperatingMode = self.appliance.get_erd_value(ErdCode.OPERATING_MODE) return mode == ErdOperatingMode.CONTROL_LOCKED - \ No newline at end of file + diff --git a/custom_components/ge_kitchen/entities/fridge/__init__.py b/custom_components/ge_home/entities/fridge/__init__.py similarity index 100% rename from custom_components/ge_kitchen/entities/fridge/__init__.py rename to custom_components/ge_home/entities/fridge/__init__.py diff --git a/custom_components/ge_kitchen/entities/fridge/const.py b/custom_components/ge_home/entities/fridge/const.py similarity index 100% rename from custom_components/ge_kitchen/entities/fridge/const.py rename to custom_components/ge_home/entities/fridge/const.py diff --git a/custom_components/ge_kitchen/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py similarity index 98% rename from custom_components/ge_kitchen/entities/fridge/ge_abstract_fridge.py rename to custom_components/ge_home/entities/fridge/ge_abstract_fridge.py index f869398..06f6948 100644 --- a/custom_components/ge_kitchen/entities/fridge/ge_abstract_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py @@ -1,4 +1,4 @@ -"""GE Kitchen Sensor Entities - Abstract Fridge""" +"""GE Home Sensor Entities - Abstract Fridge""" import sys import os import abc @@ -7,7 +7,7 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from gekitchensdk import ( +from gehomesdk import ( ErdCode, ErdOnOff, ErdFullNotFull, diff --git a/custom_components/ge_kitchen/entities/fridge/ge_dispenser.py b/custom_components/ge_home/entities/fridge/ge_dispenser.py similarity index 98% rename from custom_components/ge_kitchen/entities/fridge/ge_dispenser.py rename to custom_components/ge_home/entities/fridge/ge_dispenser.py index 69bf3c0..eef0e9d 100644 --- a/custom_components/ge_kitchen/entities/fridge/ge_dispenser.py +++ b/custom_components/ge_home/entities/fridge/ge_dispenser.py @@ -1,4 +1,4 @@ -"""GE Kitchen Sensor Entities - Dispenser""" +"""GE Home Sensor Entities - Dispenser""" import logging from typing import List, Optional, Dict, Any @@ -6,7 +6,7 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT from homeassistant.util.temperature import convert as convert_temperature -from gekitchensdk import ( +from gehomesdk import ( ErdCode, ErdHotWaterStatus, ErdPresent, diff --git a/custom_components/ge_kitchen/entities/fridge/ge_freezer.py b/custom_components/ge_home/entities/fridge/ge_freezer.py similarity index 91% rename from custom_components/ge_kitchen/entities/fridge/ge_freezer.py rename to custom_components/ge_home/entities/fridge/ge_freezer.py index 440f31d..4b178fc 100644 --- a/custom_components/ge_kitchen/entities/fridge/ge_freezer.py +++ b/custom_components/ge_home/entities/fridge/ge_freezer.py @@ -1,8 +1,8 @@ -"""GE Kitchen Sensor Entities - Freezer""" +"""GE Home Sensor Entities - Freezer""" import logging from typing import Any, Dict, Optional -from gekitchensdk import ( +from gehomesdk import ( ErdCode, ErdDoorStatus ) diff --git a/custom_components/ge_kitchen/entities/fridge/ge_fridge.py b/custom_components/ge_home/entities/fridge/ge_fridge.py similarity index 96% rename from custom_components/ge_kitchen/entities/fridge/ge_fridge.py rename to custom_components/ge_home/entities/fridge/ge_fridge.py index 457b304..42cc80e 100644 --- a/custom_components/ge_kitchen/entities/fridge/ge_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_fridge.py @@ -1,8 +1,8 @@ -"""GE Kitchen Sensor Entities - Fridge""" +"""GE Home Sensor Entities - Fridge""" import logging from typing import Any, Dict -from gekitchensdk import ( +from gehomesdk import ( ErdCode, ErdDoorStatus, ErdFilterStatus diff --git a/custom_components/ge_kitchen/entities/oven/__init__.py b/custom_components/ge_home/entities/oven/__init__.py similarity index 100% rename from custom_components/ge_kitchen/entities/oven/__init__.py rename to custom_components/ge_home/entities/oven/__init__.py diff --git a/custom_components/ge_kitchen/entities/oven/const.py b/custom_components/ge_home/entities/oven/const.py similarity index 96% rename from custom_components/ge_kitchen/entities/oven/const.py rename to custom_components/ge_home/entities/oven/const.py index 0373d9c..11bd991 100644 --- a/custom_components/ge_kitchen/entities/oven/const.py +++ b/custom_components/ge_home/entities/oven/const.py @@ -4,7 +4,7 @@ SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE ) -from gekitchensdk import ErdOvenCookMode +from gehomesdk import ErdOvenCookMode SUPPORT_NONE = 0 GE_OVEN_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) diff --git a/custom_components/ge_kitchen/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py similarity index 99% rename from custom_components/ge_kitchen/entities/oven/ge_oven.py rename to custom_components/ge_home/entities/oven/ge_oven.py index 59e103c..aefb42c 100644 --- a/custom_components/ge_kitchen/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -1,8 +1,8 @@ -"""GE Kitchen Sensor Entities - Oven""" +"""GE Home Sensor Entities - Oven""" import logging from typing import Any, Dict, List, Optional, Set -from gekitchensdk import ( +from gehomesdk import ( ErdCode, ErdMeasurementUnits, ErdOvenCookMode, diff --git a/custom_components/ge_kitchen/exceptions.py b/custom_components/ge_home/exceptions.py similarity index 100% rename from custom_components/ge_kitchen/exceptions.py rename to custom_components/ge_home/exceptions.py diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json new file mode 100644 index 0000000..5b668b4 --- /dev/null +++ b/custom_components/ge_home/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "ge_home", + "name": "GE Home", + "config_flow": true, + "documentation": "https://github.com/simbaja/ha_components", + "requirements": ["gehomesdk==0.3.12","magicattr==0.1.5"], + "codeowners": ["@simbaja"], + "version": "0.3.12" +} diff --git a/custom_components/ge_kitchen/sensor.py b/custom_components/ge_home/sensor.py similarity index 78% rename from custom_components/ge_kitchen/sensor.py rename to custom_components/ge_home/sensor.py index d01cdba..17a037d 100644 --- a/custom_components/ge_kitchen/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -1,4 +1,4 @@ -"""GE Kitchen Sensor Entities""" +"""GE Home Sensor Entities""" import async_timeout import logging from typing import Callable @@ -8,14 +8,14 @@ from .const import DOMAIN from .entities import GeErdSensor -from .update_coordinator import GeKitchenUpdateCoordinator +from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Kitchen sensors.""" - _LOGGER.debug('Adding GE Kitchen sensors') - coordinator: GeKitchenUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + """GE Home sensors.""" + _LOGGER.debug('Adding GE Home sensors') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] # This should be a NOP, but let's be safe with async_timeout.timeout(20): diff --git a/custom_components/ge_kitchen/strings.json b/custom_components/ge_home/strings.json similarity index 100% rename from custom_components/ge_kitchen/strings.json rename to custom_components/ge_home/strings.json diff --git a/custom_components/ge_kitchen/switch.py b/custom_components/ge_home/switch.py similarity index 78% rename from custom_components/ge_kitchen/switch.py rename to custom_components/ge_home/switch.py index d2cf2cf..78cf896 100644 --- a/custom_components/ge_kitchen/switch.py +++ b/custom_components/ge_home/switch.py @@ -1,4 +1,4 @@ -"""GE Kitchen Switch Entities""" +"""GE Home Switch Entities""" import async_timeout import logging from typing import Callable @@ -8,14 +8,14 @@ from .entities import GeErdSwitch from .const import DOMAIN -from .update_coordinator import GeKitchenUpdateCoordinator +from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Kitchen sensors.""" - _LOGGER.debug('Adding GE Kitchen switches') - coordinator: GeKitchenUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + """GE Home sensors.""" + _LOGGER.debug('Adding GE Home switches') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] # This should be a NOP, but let's be safe with async_timeout.timeout(20): diff --git a/custom_components/ge_kitchen/translations/en.json b/custom_components/ge_home/translations/en.json similarity index 95% rename from custom_components/ge_kitchen/translations/en.json rename to custom_components/ge_home/translations/en.json index 142a25e..ce1c350 100644 --- a/custom_components/ge_kitchen/translations/en.json +++ b/custom_components/ge_home/translations/en.json @@ -1,5 +1,5 @@ { - "title": "GE Kitchen", + "title": "GE Home", "config": { "step": { "init": { @@ -24,4 +24,4 @@ "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" } } -} \ No newline at end of file +} diff --git a/custom_components/ge_kitchen/update_coordinator.py b/custom_components/ge_home/update_coordinator.py similarity index 94% rename from custom_components/ge_kitchen/update_coordinator.py rename to custom_components/ge_home/update_coordinator.py index 0c4ae2d..4f1aef5 100644 --- a/custom_components/ge_kitchen/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -1,11 +1,11 @@ -"""Data update coordinator for GE Kitchen Appliances""" +"""Data update coordinator for GE Home Appliances""" import asyncio import async_timeout import logging from typing import Any, Dict, Iterable, Optional, Tuple -from gekitchensdk import ( +from gehomesdk import ( EVENT_APPLIANCE_INITIAL_UPDATE, EVENT_APPLIANCE_UPDATE_RECEIVED, EVENT_CONNECTED, @@ -15,7 +15,7 @@ GeAppliance, GeWebsocketClient, ) -from gekitchensdk import GeAuthFailedError, GeGeneralServerError, GeNotAuthenticatedError +from gehomesdk import GeAuthFailedError, GeGeneralServerError, GeNotAuthenticatedError from .exceptions import HaAuthError, HaCannotConnect from homeassistant.config_entries import ConfigEntry @@ -37,11 +37,11 @@ PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater"] _LOGGER = logging.getLogger(__name__) -class GeKitchenUpdateCoordinator(DataUpdateCoordinator): - """Define a wrapper class to update GE Kitchen data.""" +class GeHomeUpdateCoordinator(DataUpdateCoordinator): + """Define a wrapper class to update GE Home data.""" def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: - """Set up the GeKitchenUpdateCoordinator class.""" + """Set up the GeHomeUpdateCoordinator class.""" self._hass = hass self._config_entry = config_entry self._username = config_entry.data[CONF_USERNAME] @@ -176,7 +176,7 @@ async def async_start_client(self): raise async def async_begin_session(self): - """Begins the ge_kitchen session.""" + """Begins the ge_home session.""" _LOGGER.debug("Beginning session") session = self._hass.helpers.aiohttp_client.async_get_clientsession() await self.client.async_get_credentials(session) @@ -206,15 +206,15 @@ async def _kill_client(self): @callback def reconnect(self, log=False) -> None: - """Prepare to reconnect ge_kitchen session.""" + """Prepare to reconnect ge_home session.""" if log: - _LOGGER.info("Will try to reconnect to ge_kitchen service") + _LOGGER.info("Will try to reconnect to ge_home service") self.hass.loop.create_task(self.async_reconnect()) async def async_reconnect(self) -> None: - """Try to reconnect ge_kitchen session.""" + """Try to reconnect ge_home session.""" self._retry_count += 1 - _LOGGER.info(f"attempting to reconnect to ge_kitchen service (attempt {self._retry_count})") + _LOGGER.info(f"attempting to reconnect to ge_home service (attempt {self._retry_count})") try: with async_timeout.timeout(ASYNC_TIMEOUT): @@ -233,7 +233,7 @@ def shutdown(self, event) -> None: """Close the connection on shutdown. Used as an argument to EventBus.async_listen_once. """ - _LOGGER.info("ge_kitchen shutting down") + _LOGGER.info("ge_home shutting down") if self.client: self.client.clear_event_handlers() self.client.disconnect() diff --git a/custom_components/ge_kitchen/water_heater.py b/custom_components/ge_home/water_heater.py similarity index 82% rename from custom_components/ge_kitchen/water_heater.py rename to custom_components/ge_home/water_heater.py index 4fb4593..ac0aa85 100644 --- a/custom_components/ge_kitchen/water_heater.py +++ b/custom_components/ge_home/water_heater.py @@ -1,4 +1,4 @@ -"""GE Kitchen Sensor Entities""" +"""GE Home Sensor Entities""" import async_timeout import logging from typing import Callable @@ -9,14 +9,14 @@ from .entities import GeWaterHeater from .const import DOMAIN -from .update_coordinator import GeKitchenUpdateCoordinator +from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Kitchen Water Heaters.""" + """GE Home Water Heaters.""" _LOGGER.debug('Adding GE "Water Heaters"') - coordinator: GeKitchenUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] # This should be a NOP, but let's be safe with async_timeout.timeout(20): diff --git a/custom_components/ge_kitchen/manifest.json b/custom_components/ge_kitchen/manifest.json deleted file mode 100644 index dc82b75..0000000 --- a/custom_components/ge_kitchen/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "ge_kitchen", - "name": "GE Kitchen", - "config_flow": true, - "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gekitchensdk==0.3.11","magicattr==0.1.5"], - "codeowners": ["@simbaja"], - "version": "0.3.11" -} diff --git a/hacs.json b/hacs.json index a9df915..2790e10 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { - "name": "GE Kitchen Appliances (SmartHQ)", + "name": "GE Appliances (SmartHQ)", "homeassistant": "2021.1.5", "domains": ["binary_sensor", "sensor", "switch", "water_heater"], "iot_class": "Cloud Polling" diff --git a/info.md b/info.md index f8aee06..e8cb73f 100644 --- a/info.md +++ b/info.md @@ -1,20 +1,19 @@ -# GE Kitchen Appliances (SmartHQ) +# GE Home Appliances (SmartHQ) -## `ge_kitchen` -Integration for GE WiFi-enabled kitchen appliances. So far, I've only done fridges and ovens (because that's what I -have), but I hope to to dishwashers next. Because HA doesn't have Fridge or Oven platforms, both fridges and ovens are -primarily represented as water heater entities, which works surprisingly well. If anybody who has other GE appliances -sees this and wants to pitch in, please shoot me a message or make a PR. +## `ge_home` +Integration for GE WiFi-enabled appliances into Home Assistant. This integration currently contains fridge, oven, dishwasher, laundry washer, laundry dryer support. + +**Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** Entities card: -![Entities](https://raw.githubusercontent.com/ajmarks/ha_components/master/img/appliance_entities.png) +![Entities](https://raw.githubusercontent.com/simbaja/ha_components/master/img/appliance_entities.png) Fridge Controls: -![Fridge controls](https://raw.githubusercontent.com/ajmarks/ha_components/master/img/fridge_control.png) +![Fridge controls](https://raw.githubusercontent.com/simbaja/ha_components/master/img/fridge_control.png) Oven Controls: -![Fridge controls](https://raw.githubusercontent.com/ajmarks/ha_components/master/img/oven_controls.png) +![Fridge controls](https://raw.githubusercontent.com/simbaja/ha_components/master/img/oven_controls.png) From f415c391c39b7545e5c9d2959c3e29b52b491d9a Mon Sep 17 00:00:00 2001 From: SSinSD <47265616+SSinSD@users.noreply.github.com> Date: Wed, 9 Jun 2021 10:49:27 -0700 Subject: [PATCH 064/338] Fixed error in dryer.py --- custom_components/ge_home/devices/dryer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/dryer.py b/custom_components/ge_home/devices/dryer.py index 2876464..33edcf5 100644 --- a/custom_components/ge_home/devices/dryer.py +++ b/custom_components/ge_home/devices/dryer.py @@ -5,7 +5,7 @@ from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeDishwasherControlLockedSwitch +from ..entities import GeErdSensor, GeErdBinarySensor _LOGGER = logging.getLogger(__name__) From ec9eef64cb1ccaf8d7795eb4e8aa25eb5778a650 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 9 Jun 2021 16:29:48 -0400 Subject: [PATCH 065/338] - added error handling around appliance version --- custom_components/ge_home/devices/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/base.py b/custom_components/ge_home/devices/base.py index d343340..5a5ddc6 100644 --- a/custom_components/ge_home/devices/base.py +++ b/custom_components/ge_home/devices/base.py @@ -64,6 +64,13 @@ def serial_number(self) -> str: def model_number(self) -> str: return self.appliance.get_erd_value(ErdCode.MODEL_NUMBER) + @property + def sw_version(self) -> str: + appVer = self.try_get_erd_value(ErdCode.APPLIANCE_SW_VERSION) + wifiVer = self.try_get_erd_value(ErdCode.WIFI_MODULE_SW_VERSION) + + return 'Appliance=' + str(appVer or 'Unknown') + '/Wifi=' + str(wifiVer or 'Unknown') + @property def name(self) -> str: appliance_type = self.appliance.appliance_type @@ -76,12 +83,13 @@ def name(self) -> str: @property def device_info(self) -> Dict: """Device info dictionary.""" + return { "identifiers": {(DOMAIN, self.serial_number)}, "name": self.name, "manufacturer": "GE", "model": self.model_number, - "sw_version": self.appliance.get_erd_value(ErdCode.APPLIANCE_SW_VERSION), + "sw_version": self.sw_version } @property From f742e0d2969a7cb346ca4fd4bcdb2176864135d4 Mon Sep 17 00:00:00 2001 From: bendavis Date: Mon, 28 Jun 2021 16:08:41 +0000 Subject: [PATCH 066/338] Add support for water filter sensors Initial support is read only for now --- custom_components/ge_home/devices/__init__.py | 5 + custom_components/ge_home/devices/washer.py | 3 +- .../ge_home/devices/waterfilter.py | 35 +++++ .../ge_home/entities/__init__.py | 1 + .../ge_home/entities/common/__init__.py | 2 +- .../ge_home/entities/common/ge_erd_entity.py | 24 +++- .../ge_home/entities/waterfilter/__init__.py | 2 + .../waterfilter/filter_life_remaining.py | 19 +++ .../entities/waterfilter/flow_rate_sensor.py | 21 +++ custom_components/ge_home/manifest.json | 4 +- .../ge_home/update_coordinator.py | 125 +++++++++++------- 11 files changed, 181 insertions(+), 60 deletions(-) create mode 100644 custom_components/ge_home/devices/waterfilter.py create mode 100644 custom_components/ge_home/entities/waterfilter/__init__.py create mode 100644 custom_components/ge_home/entities/waterfilter/filter_life_remaining.py create mode 100644 custom_components/ge_home/entities/waterfilter/flow_rate_sensor.py diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 3d300a3..6d1affd 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -10,9 +10,11 @@ from .washer import WasherApi from .dryer import DryerApi from .washer_dryer import WasherDryerApi +from .waterfilter import WaterFilterApi _LOGGER = logging.getLogger(__name__) + def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: _LOGGER.debug(f"Found device type: {appliance_type}") """Get the appropriate appliance type""" @@ -28,5 +30,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return DryerApi if appliance_type == ErdApplianceType.COMBINATION_WASHER_DRYER: return WasherDryerApi + if appliance_type == ErdApplianceType.POE_WATER_FILTER: + return WaterFilterApi + # Fallback return ApplianceApi diff --git a/custom_components/ge_home/devices/washer.py b/custom_components/ge_home/devices/washer.py index 53b1d74..b6c0073 100644 --- a/custom_components/ge_home/devices/washer.py +++ b/custom_components/ge_home/devices/washer.py @@ -1,4 +1,3 @@ -from custom_components.ge_home.entities.common.ge_erd_binary_sensor import GeErdBinarySensor import logging from typing import List @@ -35,4 +34,4 @@ def get_all_entities(self) -> List[Entity]: ] entities = base_entities + washer_entities return entities - + diff --git a/custom_components/ge_home/devices/waterfilter.py b/custom_components/ge_home/devices/waterfilter.py new file mode 100644 index 0000000..8977472 --- /dev/null +++ b/custom_components/ge_home/devices/waterfilter.py @@ -0,0 +1,35 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import ( + GeErdSensor, + GeErdBinarySensor, + ErdFlowRateSensor, + ErdFilterLifeRemainingSensor, +) + +_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), + GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE), + ErdFlowRateSensor(self, ErdCode.WH_FILTER_FLOW_RATE), + GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE), + ErdFilterLifeRemainingSensor(self, ErdCode.WH_FILTER_LIFE_REMAINING), + GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT), + ] + entities = base_entities + wf_entities + return entities diff --git a/custom_components/ge_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index 4fa5c83..3befece 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -2,3 +2,4 @@ from .dishwasher import * from .fridge import * from .oven import * +from .waterfilter import * diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index ab3e29d..d7c4c76 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -5,4 +5,4 @@ from .ge_erd_sensor import GeErdSensor from .ge_erd_property_sensor import GeErdPropertySensor from .ge_erd_switch import GeErdSwitch -from .ge_water_heater import GeWaterHeater \ No newline at end of file +from .ge_water_heater import GeWaterHeater diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index eae8995..799e924 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -11,7 +11,15 @@ class GeErdEntity(GeEntity): """Parent class for GE entities tied to a specific ERD""" - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None, icon_override: str = None, device_class_override: str = None): + + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_override: str = None, + icon_override: str = None, + device_class_override: str = None, + ): super().__init__(api) self._erd_code = api.appliance.translate_erd_code(erd_code) self._erd_code_class = api.appliance.get_erd_code_class(self._erd_code) @@ -21,11 +29,11 @@ def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = if not self._erd_code_class: self._erd_code_class = ErdCodeClass.GENERAL - + @property def erd_code(self) -> ErdCodeType: return self._erd_code - + @property def erd_code_class(self) -> ErdCodeClass: return self._erd_code_class @@ -40,8 +48,8 @@ def erd_string(self) -> str: @property def name(self) -> Optional[str]: erd_string = self.erd_string - - #override the name if specified + + # override the name if specified if self._erd_override != None: erd_string = self._erd_override @@ -53,10 +61,10 @@ def unique_id(self) -> Optional[str]: return f"{DOMAIN}_{self.serial_number}_{self.erd_string.lower()}" def _stringify(self, value: any, **kwargs) -> Optional[str]: - """ Stringify a value """ + """Stringify a value""" # perform special processing before passing over to the default method if self.erd_code == ErdCode.CLOCK_TIME: - return value.strftime("%H:%M:%S") if value else None + return value.strftime("%H:%M:%S") if value else None if self.erd_code_class == ErdCodeClass.RAW_TEMPERATURE: return f"{value}" if self.erd_code_class == ErdCodeClass.NON_ZERO_TEMPERATURE: @@ -106,5 +114,7 @@ def _get_icon(self): 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" return None diff --git a/custom_components/ge_home/entities/waterfilter/__init__.py b/custom_components/ge_home/entities/waterfilter/__init__.py new file mode 100644 index 0000000..be45dce --- /dev/null +++ b/custom_components/ge_home/entities/waterfilter/__init__.py @@ -0,0 +1,2 @@ +from .flow_rate_sensor import ErdFlowRateSensor +from .filter_life_remaining import ErdFilterLifeRemainingSensor diff --git a/custom_components/ge_home/entities/waterfilter/filter_life_remaining.py b/custom_components/ge_home/entities/waterfilter/filter_life_remaining.py new file mode 100644 index 0000000..8fd5697 --- /dev/null +++ b/custom_components/ge_home/entities/waterfilter/filter_life_remaining.py @@ -0,0 +1,19 @@ +from gehomesdk import ErdCode, ErdOperatingMode +from gehomesdk.erd.values.common.erd_measurement_units import ErdMeasurementUnits +from typing import Optional + +from ..common import GeErdSensor + + +class ErdFilterLifeRemainingSensor(GeErdSensor): + @property + def state(self) -> Optional[int]: + try: + value = self.appliance.get_erd_value(self.erd_code) + except KeyError: + return None + return value.life_remaining + + @property + def unit_of_measurement(self) -> Optional[str]: + return "%" diff --git a/custom_components/ge_home/entities/waterfilter/flow_rate_sensor.py b/custom_components/ge_home/entities/waterfilter/flow_rate_sensor.py new file mode 100644 index 0000000..065270b --- /dev/null +++ b/custom_components/ge_home/entities/waterfilter/flow_rate_sensor.py @@ -0,0 +1,21 @@ +from gehomesdk import ErdCode, ErdOperatingMode +from gehomesdk.erd.values.common.erd_measurement_units import ErdMeasurementUnits +from typing import Optional + +from ..common import GeErdSensor + + +class ErdFlowRateSensor(GeErdSensor): + @property + def state(self) -> Optional[float]: + try: + value = self.appliance.get_erd_value(self.erd_code) + except KeyError: + return None + return value.flow_rate + + @property + def unit_of_measurement(self) -> Optional[str]: + if self._temp_measurement_system == ErdMeasurementUnits.METRIC: + return "lpm" + return "gpm" diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 5b668b4..60d1cc8 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gehomesdk==0.3.12","magicattr==0.1.5"], - "codeowners": ["@simbaja"], + "requirements": ["gehomesdk==0.3.13", "magicattr==0.1.5"], + "codeowners": ["@simbaja"], "version": "0.3.12" } diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 4f1aef5..12d41a6 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -24,19 +24,20 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( - DOMAIN, - EVENT_ALL_APPLIANCES_READY, - UPDATE_INTERVAL, - MIN_RETRY_DELAY, - MAX_RETRY_DELAY, + DOMAIN, + EVENT_ALL_APPLIANCES_READY, + UPDATE_INTERVAL, + MIN_RETRY_DELAY, + MAX_RETRY_DELAY, RETRY_OFFLINE_COUNT, - ASYNC_TIMEOUT + ASYNC_TIMEOUT, ) from .devices import ApplianceApi, get_appliance_api_type PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater"] _LOGGER = logging.getLogger(__name__) + class GeHomeUpdateCoordinator(DataUpdateCoordinator): """Define a wrapper class to update GE Home data.""" @@ -65,15 +66,23 @@ def _reset_initialization(self): self._retry_count = 0 self.initialization_future = asyncio.Future() - def create_ge_client(self, event_loop: Optional[asyncio.AbstractEventLoop]) -> GeWebsocketClient: + def create_ge_client( + self, event_loop: Optional[asyncio.AbstractEventLoop] + ) -> GeWebsocketClient: """ Create a new GeClient object with some helpful callbacks. :param event_loop: Event loop :return: GeWebsocketClient """ - client = GeWebsocketClient(self._username, self._password, event_loop=event_loop, ) - client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update) + client = GeWebsocketClient( + self._username, + self._password, + event_loop=event_loop, + ) + client.add_event_handler( + EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update + ) client.add_event_handler(EVENT_APPLIANCE_UPDATE_RECEIVED, self.on_device_update) client.add_event_handler(EVENT_GOT_APPLIANCE_LIST, self.on_appliance_list) client.add_event_handler(EVENT_DISCONNECTED, self.on_disconnect) @@ -87,10 +96,10 @@ def appliances(self) -> Iterable[GeAppliance]: @property def appliance_apis(self) -> Dict[str, ApplianceApi]: return self._appliance_apis - + @property def online(self) -> bool: - """ + """ Indicates whether the services is online. If it's retried several times, it's assumed that it's offline for some reason """ @@ -116,15 +125,17 @@ def regenerate_appliance_apis(self): def maybe_add_appliance_api(self, appliance: GeAppliance): mac_addr = appliance.mac_addr if mac_addr not in self.appliance_apis: - _LOGGER.debug(f"Adding appliance api for appliance {mac_addr} ({appliance.appliance_type})") + _LOGGER.debug( + f"Adding appliance api for appliance {mac_addr} ({appliance.appliance_type})" + ) api = self._get_appliance_api(appliance) api.build_entities_list() self.appliance_apis[mac_addr] = api else: - #if we already have the API, switch out its appliance reference for this one + # if we already have the API, switch out its appliance reference for this one api = self.appliance_apis[mac_addr] - api.appliance = appliance - + api.appliance = appliance + async def get_client(self) -> GeWebsocketClient: """Get a new GE Websocket client.""" if self.client: @@ -132,10 +143,10 @@ async def get_client(self) -> GeWebsocketClient: self.client.clear_event_handlers() await self.client.disconnect() except Exception as err: - _LOGGER.warn(f'exception while disconnecting client {err}') + _LOGGER.warn(f"exception while disconnecting client {err}") finally: self._reset_initialization() - + loop = self._hass.loop self.client = self.create_ge_client(event_loop=loop) return self.client @@ -146,42 +157,46 @@ async def async_setup(self): for component in PLATFORMS: self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup(self._config_entry, component) + self.hass.config_entries.async_forward_entry_setup( + self._config_entry, component + ) ) try: await self.async_start_client() except (GeNotAuthenticatedError, GeAuthFailedError): - raise HaAuthError('Authentication failure') + raise HaAuthError("Authentication failure") except GeGeneralServerError: - raise HaCannotConnect('Cannot connect (server error)') + raise HaCannotConnect("Cannot connect (server error)") except Exception: - raise HaCannotConnect('Unknown connection failure') + raise HaCannotConnect("Unknown connection failure") try: with async_timeout.timeout(ASYNC_TIMEOUT): await self.initialization_future except (asyncio.CancelledError, asyncio.TimeoutError): - raise HaCannotConnect('Initialization timed out') + raise HaCannotConnect("Initialization timed out") async def async_start_client(self): """Start a new GeClient in the HASS event loop.""" try: - _LOGGER.debug('Creating and starting client') + _LOGGER.debug("Creating and starting client") await self.get_client() await self.async_begin_session() except: - _LOGGER.debug('could not start the client') + _LOGGER.debug("could not start the client") self.client = None raise - + async def async_begin_session(self): """Begins the ge_home session.""" _LOGGER.debug("Beginning session") session = self._hass.helpers.aiohttp_client.async_get_clientsession() await self.client.async_get_credentials(session) - fut = asyncio.ensure_future(self.client.async_run_client(), loop=self._hass.loop) - _LOGGER.debug('Client running') + fut = asyncio.ensure_future( + self.client.async_run_client(), loop=self._hass.loop + ) + _LOGGER.debug("Client running") return fut async def async_reset(self): @@ -191,17 +206,19 @@ async def async_reset(self): unload_ok = all( await asyncio.gather( *[ - self.hass.config_entries.async_forward_entry_unload(entry, component) + self.hass.config_entries.async_forward_entry_unload( + entry, component + ) for component in PLATFORMS ] ) ) - return unload_ok + return unload_ok async def _kill_client(self): """Kill the client. Leaving this in for testing purposes.""" await asyncio.sleep(30) - _LOGGER.critical('Killing the connection. Popcorn time.') + _LOGGER.critical("Killing the connection. Popcorn time.") await self.client.disconnect() @callback @@ -214,13 +231,17 @@ def reconnect(self, log=False) -> None: async def async_reconnect(self) -> None: """Try to reconnect ge_home session.""" self._retry_count += 1 - _LOGGER.info(f"attempting to reconnect to ge_home service (attempt {self._retry_count})") - + _LOGGER.info( + f"attempting to reconnect to ge_home service (attempt {self._retry_count})" + ) + try: with async_timeout.timeout(ASYNC_TIMEOUT): await self.async_start_client() except Exception as err: - _LOGGER.warn(f"could not reconnect: {err}, will retry in {self._get_retry_delay()} seconds") + _LOGGER.warn( + f"could not reconnect: {err}, will retry in {self._get_retry_delay()} seconds" + ) self.hass.loop.call_later(self._get_retry_delay(), self.reconnect) _LOGGER.debug("forcing a state refresh while disconnected") try: @@ -247,21 +268,23 @@ async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any] except KeyError: return for entity in api.entities: - _LOGGER.debug(f'Updating {entity} ({entity.unique_id}, {entity.entity_id})') + _LOGGER.debug(f"Updating {entity} ({entity.unique_id}, {entity.entity_id})") entity.async_write_ha_state() async def _refresh_ha_state(self): entities = [ - entity - for api in self.appliance_apis.values() - for entity in api.entities + entity for api in self.appliance_apis.values() for entity in api.entities ] for entity in entities: try: - _LOGGER.debug(f'Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}') + _LOGGER.debug( + f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}" + ) entity.async_write_ha_state() except: - _LOGGER.debug(f'Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}') + _LOGGER.debug( + f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}" + ) @property def all_appliances_updated(self) -> bool: @@ -270,31 +293,35 @@ def all_appliances_updated(self) -> bool: async def on_appliance_list(self, _): """When we get an appliance list, mark it and maybe trigger all ready.""" - _LOGGER.debug('Got roster update') + _LOGGER.debug("Got roster update") self.last_update_success = True if not self._got_roster: self._got_roster = True - #TODO: Probably should have a better way of confirming we're good to go... - await asyncio.sleep(5) # After the initial roster update, wait a bit and hit go + # TODO: Probably should have a better way of confirming we're good to go... + await asyncio.sleep( + 5 + ) # After the initial roster update, wait a bit and hit go await self.async_maybe_trigger_all_ready() async def on_device_initial_update(self, appliance: GeAppliance): """When an appliance first becomes ready, let the system know and schedule periodic updates.""" - _LOGGER.debug(f'Got initial update for {appliance.mac_addr}') + _LOGGER.debug(f"Got initial update for {appliance.mac_addr}") self.last_update_success = True self.maybe_add_appliance_api(appliance) await self.async_maybe_trigger_all_ready() - _LOGGER.debug(f'Requesting updates for {appliance.mac_addr}') + _LOGGER.debug(f"Requesting updates for {appliance.mac_addr}") while self.connected: await asyncio.sleep(UPDATE_INTERVAL) if self.connected and self.client.available: await appliance.async_request_update() - _LOGGER.debug(f'No longer requesting updates for {appliance.mac_addr}') + _LOGGER.debug(f"No longer requesting updates for {appliance.mac_addr}") async def on_disconnect(self, _): """Handle disconnection.""" - _LOGGER.debug(f"Disconnected. Attempting to reconnect in {MIN_RETRY_DELAY} seconds") + _LOGGER.debug( + f"Disconnected. Attempting to reconnect in {MIN_RETRY_DELAY} seconds" + ) self.last_update_success = False self.hass.loop.call_later(MIN_RETRY_DELAY, self.reconnect, True) @@ -309,7 +336,9 @@ async def async_maybe_trigger_all_ready(self): # Been here, done this return if self._got_roster and self.all_appliances_updated: - _LOGGER.debug('Ready to go. Waiting 2 seconds and setting init future result.') + _LOGGER.debug( + "Ready to go. Waiting 2 seconds and setting init future result." + ) # The the flag and wait to prevent two different fun race conditions self._init_done = True await asyncio.sleep(2) @@ -318,4 +347,4 @@ async def async_maybe_trigger_all_ready(self): def _get_retry_delay(self) -> int: delay = MIN_RETRY_DELAY * 2 ** (self._retry_count - 1) - return min(delay, MAX_RETRY_DELAY) + return min(delay, MAX_RETRY_DELAY) From 4255e970272274ca1b1ed851b587f339a0282522 Mon Sep 17 00:00:00 2001 From: bendavis Date: Tue, 29 Jun 2021 19:54:30 +0000 Subject: [PATCH 067/338] Add write support for filter position For some reason the select type doesn't render the enum in the UI, but the select service is working correctly. --- custom_components/ge_home/__init__.py | 14 ++--- .../ge_home/devices/waterfilter.py | 4 ++ .../ge_home/entities/common/__init__.py | 1 + .../ge_home/entities/common/ge_erd_select.py | 11 ++++ .../ge_home/entities/common/ge_erd_sensor.py | 27 ++++++---- .../ge_home/entities/waterfilter/__init__.py | 1 + .../entities/waterfilter/filter_position.py | 51 +++++++++++++++++++ custom_components/ge_home/select.py | 38 ++++++++++++++ .../ge_home/update_coordinator.py | 2 +- 9 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 custom_components/ge_home/entities/common/ge_erd_select.py create mode 100644 custom_components/ge_home/entities/waterfilter/filter_position.py create mode 100644 custom_components/ge_home/select.py diff --git a/custom_components/ge_home/__init__.py b/custom_components/ge_home/__init__.py index b9e6949..36c2c0b 100644 --- a/custom_components/ge_home/__init__.py +++ b/custom_components/ge_home/__init__.py @@ -5,16 +5,16 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import ( - DOMAIN -) +from .const import DOMAIN from .update_coordinator import GeHomeUpdateCoordinator CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + async def async_setup(hass: HomeAssistant, config: dict): return True - + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up the ge_home component.""" hass.data.setdefault(DOMAIN, {}) @@ -30,15 +30,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] ok = await coordinator.async_reset() if ok: hass.data[DOMAIN].pop(entry.entry_id) - + return ok + async def async_update_options(hass, config_entry): """Update options.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/custom_components/ge_home/devices/waterfilter.py b/custom_components/ge_home/devices/waterfilter.py index 8977472..dcce390 100644 --- a/custom_components/ge_home/devices/waterfilter.py +++ b/custom_components/ge_home/devices/waterfilter.py @@ -1,3 +1,4 @@ +from homeassistant.components.select import SelectEntity import logging from typing import List @@ -10,6 +11,7 @@ GeErdBinarySensor, ErdFlowRateSensor, ErdFilterLifeRemainingSensor, + ErdFilterPositionSelect, ) _LOGGER = logging.getLogger(__name__) @@ -25,6 +27,8 @@ def get_all_entities(self) -> List[Entity]: wf_entities = [ GeErdSensor(self, ErdCode.WH_FILTER_MODE), + GeErdSensor(self, ErdCode.WH_FILTER_VALVE_STATE), + ErdFilterPositionSelect(self, ErdCode.WH_FILTER_POSITION), GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE), ErdFlowRateSensor(self, ErdCode.WH_FILTER_FLOW_RATE), GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE), diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index d7c4c76..eaff1ec 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -6,3 +6,4 @@ from .ge_erd_property_sensor import GeErdPropertySensor from .ge_erd_switch import GeErdSwitch from .ge_water_heater import GeWaterHeater +from .ge_erd_select import GeErdSelect diff --git a/custom_components/ge_home/entities/common/ge_erd_select.py b/custom_components/ge_home/entities/common/ge_erd_select.py new file mode 100644 index 0000000..25e3699 --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_erd_select.py @@ -0,0 +1,11 @@ +import logging + +from homeassistant.components.select import SelectEntity + +_LOGGER = logging.getLogger(__name__) + + +class GeErdSelect(SelectEntity): + """Switches for boolean ERD codes.""" + + device_class = "select" diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index cdeee95..62f41b4 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -1,19 +1,21 @@ from typing import Optional from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER_FACTOR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.helpers.entity import Entity from gehomesdk import ErdCode, ErdCodeClass, ErdMeasurementUnits from .ge_erd_entity import GeErdEntity -class GeErdSensor(GeErdEntity, Entity): + +class GeErdSensor(GeErdEntity, Entity): """GE Entity for sensors""" + @property def state(self) -> Optional[str]: try: @@ -35,15 +37,19 @@ def _temp_units(self) -> Optional[str]: return TEMP_FAHRENHEIT def _get_uom(self): - """ Select appropriate units """ + """Select appropriate units""" if ( - self.erd_code_class in [ErdCodeClass.RAW_TEMPERATURE,ErdCodeClass.NON_ZERO_TEMPERATURE] or - self.device_class == DEVICE_CLASS_TEMPERATURE + self.erd_code_class + in [ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE] + or self.device_class == DEVICE_CLASS_TEMPERATURE ): if self._temp_measurement_system == ErdMeasurementUnits.METRIC: return TEMP_CELSIUS return TEMP_FAHRENHEIT - if self.erd_code_class == ErdCodeClass.BATTERY or self.device_class == DEVICE_CLASS_BATTERY: + if ( + self.erd_code_class == ErdCodeClass.BATTERY + or self.device_class == DEVICE_CLASS_BATTERY + ): return "%" if self.device_class == DEVICE_CLASS_POWER_FACTOR: return "%" @@ -52,7 +58,10 @@ def _get_uom(self): 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]: + 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 diff --git a/custom_components/ge_home/entities/waterfilter/__init__.py b/custom_components/ge_home/entities/waterfilter/__init__.py index be45dce..96610ce 100644 --- a/custom_components/ge_home/entities/waterfilter/__init__.py +++ b/custom_components/ge_home/entities/waterfilter/__init__.py @@ -1,2 +1,3 @@ from .flow_rate_sensor import ErdFlowRateSensor from .filter_life_remaining import ErdFilterLifeRemainingSensor +from .filter_position import ErdFilterPositionSelect diff --git a/custom_components/ge_home/entities/waterfilter/filter_position.py b/custom_components/ge_home/entities/waterfilter/filter_position.py new file mode 100644 index 0000000..4eb2bb9 --- /dev/null +++ b/custom_components/ge_home/entities/waterfilter/filter_position.py @@ -0,0 +1,51 @@ +from homeassistant.components.ge_home.entities.common.ge_erd_select import GeErdSelect +from homeassistant.components.ge_home.entities.common.ge_erd_entity import GeErdEntity +from homeassistant.components.ge_home.devices.base import ApplianceApi +import logging +from typing import Optional, Dict, Any + +from gehomesdk import ErdCode, ErdCodeClass, ErdCodeType +from gehomesdk.erd.values.waterfilter.erd_waterfilter_position import ( + ErdWaterFilterPosition, +) + +from ...const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ErdFilterPositionSelect(GeErdEntity, GeErdSelect): + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + ): + super().__init__(api=api, erd_code=erd_code) + self._attr_hass = api._hass + self.hass = api._hass + self._attr_unique_id = self.unique_id + self._attr_name = self.name + self._attr_current_option = ErdWaterFilterPosition.UNKNOWN.name + self._attr_icon = self.icon + self._attr_device_class = self.device_class + self._attr_options = [ + ErdWaterFilterPosition.BYPASS.name, + ErdWaterFilterPosition.OFF.name, + ErdWaterFilterPosition.FILTERED.name, + ErdWaterFilterPosition.READY.name, + ] + self._attr_device_info = self.device_info + + async def async_select_option(self, option: str) -> None: + if ( + option == ErdWaterFilterPosition.READY.name + or option == ErdWaterFilterPosition.UNKNOWN.name + ): + return + await self.api.appliance.async_set_erd_value( + self.erd_code, ErdWaterFilterPosition[option] + ) + + @property + def icon(self) -> str: + return "mdi:water" diff --git a/custom_components/ge_home/select.py b/custom_components/ge_home/select.py new file mode 100644 index 0000000..22ff7ae --- /dev/null +++ b/custom_components/ge_home/select.py @@ -0,0 +1,38 @@ +"""GE Home Sensor Entities""" +import async_timeout +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +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] + + # This should be a NOP, but let's be safe + with async_timeout.timeout(20): + await coordinator.initialization_future + _LOGGER.debug("Coordinator init future finished") + + apis = list(coordinator.appliance_apis.values()) + _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 + ] + _LOGGER.debug(f"Found {len(entities):d} selectors") + async_add_entities(entities) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 12d41a6..1c75dc5 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -34,7 +34,7 @@ ) from .devices import ApplianceApi, get_appliance_api_type -PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater"] +PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater", "select"] _LOGGER = logging.getLogger(__name__) From 8867ade6d00f278dcfcaa341c1e14a50307224e3 Mon Sep 17 00:00:00 2001 From: bendavis Date: Tue, 29 Jun 2021 20:01:33 +0000 Subject: [PATCH 068/338] Update domains --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index 2790e10..4038478 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Appliances (SmartHQ)", "homeassistant": "2021.1.5", - "domains": ["binary_sensor", "sensor", "switch", "water_heater"], + "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select"], "iot_class": "Cloud Polling" } From 7ac74cf8c3de9d68976ae6a283efe089f9db803f Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 6 Jul 2021 13:49:21 -0400 Subject: [PATCH 069/338] - fixed issue with update coordinator setup --- custom_components/ge_home/update_coordinator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 4f1aef5..174fab1 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -164,6 +164,8 @@ async def async_setup(self): except (asyncio.CancelledError, asyncio.TimeoutError): raise HaCannotConnect('Initialization timed out') + return True + async def async_start_client(self): """Start a new GeClient in the HASS event loop.""" try: From a3b8044f3baa82f78f7bd447b6e5c4c742fded4a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 8 Jul 2021 11:50:41 -0400 Subject: [PATCH 070/338] - added additional dishwasher sensors --- .../ge_home/devices/dishwasher.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index e9a8632..92fd301 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -1,3 +1,5 @@ +from custom_components.ge_home.entities.common.ge_erd_binary_sensor import GeErdBinarySensor +from custom_components.ge_home.entities.common.ge_erd_property_binary_sensor import GeErdPropertyBinarySensor import logging from typing import List @@ -5,14 +7,14 @@ from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeDishwasherControlLockedSwitch +from ..entities import GeErdSensor, GeErdPropertySensor, GeDishwasherControlLockedSwitch _LOGGER = logging.getLogger(__name__) class DishwasherApi(ApplianceApi): """API class for dishwasher objects""" - APPLIANCE_TYPE = ErdApplianceType.WASHER + APPLIANCE_TYPE = ErdApplianceType.DISH_WASHER def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() @@ -25,6 +27,19 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE), GeErdSensor(self, ErdCode.DISHWASHER_RINSE_AGENT, icon_override="mdi:sparkles"), GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING), + GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS), + + #User Setttings + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "dry_option"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_zone"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours") ] entities = base_entities + dishwasher_entities return entities From 5ddee5c48957484e1c2b6a03aee4d0dae1d07a56 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 8 Jul 2021 14:08:40 -0400 Subject: [PATCH 071/338] - added entities to the washer/dryer devices - updated dependencies --- custom_components/ge_home/devices/base.py | 12 ++++- custom_components/ge_home/devices/dryer.py | 52 +++++++++++++++---- custom_components/ge_home/devices/washer.py | 39 +++++++++----- .../ge_home/devices/washer_dryer.py | 35 ++++++------- .../ge_home/entities/common/ge_erd_sensor.py | 2 + custom_components/ge_home/manifest.json | 4 +- 6 files changed, 97 insertions(+), 47 deletions(-) diff --git a/custom_components/ge_home/devices/base.py b/custom_components/ge_home/devices/base.py index 5a5ddc6..e08c514 100644 --- a/custom_components/ge_home/devices/base.py +++ b/custom_components/ge_home/devices/base.py @@ -98,12 +98,16 @@ def entities(self) -> List[Entity]: def get_all_entities(self) -> List[Entity]: """Create Entities for this device.""" + return self.get_base_entties() + + def get_base_entities(self) -> List[Entity]: + """Create base entities (i.e. common between all appliances).""" from ..entities import GeErdSensor, GeErdSwitch entities = [ GeErdSensor(self, ErdCode.CLOCK_TIME), GeErdSwitch(self, ErdCode.SABBATH_MODE), ] - return entities + return entities def build_entities_list(self) -> None: """Build the entities list, adding anything new.""" @@ -123,3 +127,9 @@ def try_get_erd_value(self, code: ErdCodeType): except: return None + def has_erd_code(self, code: ErdCodeType): + try: + self.appliance.get_erd_value(code) + return True + except: + return False diff --git a/custom_components/ge_home/devices/dryer.py b/custom_components/ge_home/devices/dryer.py index 33edcf5..be0a223 100644 --- a/custom_components/ge_home/devices/dryer.py +++ b/custom_components/ge_home/devices/dryer.py @@ -2,14 +2,13 @@ from typing import List from homeassistant.helpers.entity import Entity -from gehomesdk.erd import ErdCode, ErdApplianceType +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 @@ -17,19 +16,50 @@ class DryerApi(ApplianceApi): def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() - dryer_entities = [ + common_entities = [ GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_STATE), - GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_SUBCYCLE), + 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_CYCLE), GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), - GeErdSensor(self, ErdCode.LAUNDRY_DOOR), - GeErdSensor(self, ErdCode.LAUNDRY_DRYNESSNEW_LEVEL), - GeErdSensor(self, ErdCode.LAUNDRY_TEMPERATURENEW_OPTION), - GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS) - + GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS), ] - entities = base_entities + dryer_entities + + 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([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_SHEET_USAGE_CONFIGURATION)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_SHEET_INVENTORY): + dryer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_SHEET_INVENTORY)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_ECODRY_STATUS): + dryer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_ECODRY_STATUS)]) + + return dryer_entities diff --git a/custom_components/ge_home/devices/washer.py b/custom_components/ge_home/devices/washer.py index 53b1d74..128b46a 100644 --- a/custom_components/ge_home/devices/washer.py +++ b/custom_components/ge_home/devices/washer.py @@ -1,9 +1,8 @@ -from custom_components.ge_home.entities.common.ge_erd_binary_sensor import GeErdBinarySensor import logging from typing import List from homeassistant.helpers.entity import Entity -from gehomesdk.erd import ErdCode, ErdApplianceType +from gehomesdk import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import GeErdSensor, GeErdBinarySensor @@ -18,21 +17,35 @@ class WasherApi(ApplianceApi): def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() - washer_entities = [ + common_entities = [ GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_STATE), - GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_SUBCYCLE), + 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_CYCLE), GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), - GeErdSensor(self, ErdCode.LAUNDRY_DOOR), - GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR_LOCK), - GeErdSensor(self, ErdCode.LAUNDRY_SOIL_LEVEL), - GeErdSensor(self, ErdCode.LAUNDRY_WASHTEMP_LEVEL), - GeErdSensor(self, ErdCode.LAUNDRY_SPINTIME_LEVEL), - GeErdSensor(self, ErdCode.LAUNDRY_RINSE_OPTION), - GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS) + GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS), ] - entities = base_entities + washer_entities + + 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), + GeErdSensor(self, ErdCode.LAUNDRY_WASHER_WASHTEMP_LEVEL), + GeErdSensor(self, ErdCode.LAUNDRY_WASHER_SPINTIME_LEVEL), + GeErdSensor(self, ErdCode.LAUNDRY_WASHER_RINSE_OPTION), + ] + + 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)]) + + return washer_entities \ No newline at end of file diff --git a/custom_components/ge_home/devices/washer_dryer.py b/custom_components/ge_home/devices/washer_dryer.py index 5b3d720..f701ff2 100644 --- a/custom_components/ge_home/devices/washer_dryer.py +++ b/custom_components/ge_home/devices/washer_dryer.py @@ -2,40 +2,35 @@ from typing import List from homeassistant.helpers.entity import Entity -from gehomesdk.erd import ErdCode, ErdApplianceType +from gehomesdk import ErdCode, ErdApplianceType -from .base import ApplianceApi +from .washer import WasherApi +from .dryer import DryerApi from ..entities import GeErdSensor, GeErdBinarySensor _LOGGER = logging.getLogger(__name__) - -class WasherDryerApi(ApplianceApi): +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 = super().get_all_entities() - - washer_entities = [ + base_entities = self.get_base_entities() + + common_entities = [ GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_STATE), - GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_SUBCYCLE), + 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_CYCLE), GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), - GeErdSensor(self, ErdCode.LAUNDRY_DOOR), - GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR_LOCK), - GeErdSensor(self, ErdCode.LAUNDRY_SOIL_LEVEL), - GeErdSensor(self, ErdCode.LAUNDRY_WASHTEMP_LEVEL), - GeErdSensor(self, ErdCode.LAUNDRY_SPINTIME_LEVEL), - GeErdSensor(self, ErdCode.LAUNDRY_RINSE_OPTION), - GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS) + GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS), ] - dryer_entities = [ + washer_entities = self.get_washer_entities() + dryer_entities = self.get_dryer_entities() - ] - entities = base_entities + washer_entities + dryer_entities + entities = base_entities + common_entities + washer_entities + dryer_entities return entities - + diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index cdeee95..c647f40 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -45,6 +45,8 @@ def _get_uom(self): 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 "%" return None diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 5b668b4..fbcc363 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gehomesdk==0.3.12","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.3.14","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.3.12" + "version": "0.3.14" } From 47f35cc22ac2e870f384706d33f0c839ee1e2954 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 8 Jul 2021 17:03:52 -0400 Subject: [PATCH 072/338] - reworked the water filter to use property sensors - reworked the select to make it more generic - converted the filter position select to use generic entity --- custom_components/ge_home/devices/__init__.py | 2 +- .../{waterfilter.py => water_filter.py} | 14 +++-- .../ge_home/entities/__init__.py | 2 +- .../ge_home/entities/common/__init__.py | 2 +- .../ge_home/entities/common/ge_erd_entity.py | 3 +- .../ge_home/entities/common/ge_erd_select.py | 30 +++++++++-- .../ge_home/entities/common/ge_erd_sensor.py | 5 ++ .../ge_home/entities/water_filter/__init__.py | 1 + .../entities/water_filter/filter_position.py | 27 ++++++++++ .../ge_home/entities/waterfilter/__init__.py | 3 -- .../waterfilter/filter_life_remaining.py | 19 ------- .../entities/waterfilter/filter_position.py | 51 ------------------- .../entities/waterfilter/flow_rate_sensor.py | 21 -------- custom_components/ge_home/select.py | 2 +- 14 files changed, 72 insertions(+), 110 deletions(-) rename custom_components/ge_home/devices/{waterfilter.py => water_filter.py} (68%) create mode 100644 custom_components/ge_home/entities/water_filter/__init__.py create mode 100644 custom_components/ge_home/entities/water_filter/filter_position.py delete mode 100644 custom_components/ge_home/entities/waterfilter/__init__.py delete mode 100644 custom_components/ge_home/entities/waterfilter/filter_life_remaining.py delete mode 100644 custom_components/ge_home/entities/waterfilter/filter_position.py delete mode 100644 custom_components/ge_home/entities/waterfilter/flow_rate_sensor.py diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 6d1affd..a262944 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -10,7 +10,7 @@ from .washer import WasherApi from .dryer import DryerApi from .washer_dryer import WasherDryerApi -from .waterfilter import WaterFilterApi +from .water_filter import WaterFilterApi _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/ge_home/devices/waterfilter.py b/custom_components/ge_home/devices/water_filter.py similarity index 68% rename from custom_components/ge_home/devices/waterfilter.py rename to custom_components/ge_home/devices/water_filter.py index dcce390..6a59641 100644 --- a/custom_components/ge_home/devices/waterfilter.py +++ b/custom_components/ge_home/devices/water_filter.py @@ -1,17 +1,15 @@ -from homeassistant.components.select import SelectEntity import logging from typing import List from homeassistant.helpers.entity import Entity -from gehomesdk.erd import ErdCode, ErdApplianceType +from gehomesdk import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import ( GeErdSensor, + GeErdPropertySensor, GeErdBinarySensor, - ErdFlowRateSensor, - ErdFilterLifeRemainingSensor, - ErdFilterPositionSelect, + GeErdFilterPositionSelect, ) _LOGGER = logging.getLogger(__name__) @@ -28,11 +26,11 @@ def get_all_entities(self) -> List[Entity]: wf_entities = [ GeErdSensor(self, ErdCode.WH_FILTER_MODE), GeErdSensor(self, ErdCode.WH_FILTER_VALVE_STATE), - ErdFilterPositionSelect(self, ErdCode.WH_FILTER_POSITION), + GeErdFilterPositionSelect(self, ErdCode.WH_FILTER_POSITION), GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE), - ErdFlowRateSensor(self, ErdCode.WH_FILTER_FLOW_RATE), + GeErdPropertySensor(self, ErdCode.WH_FILTER_FLOW_RATE, 'flow_rate'), GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE), - ErdFilterLifeRemainingSensor(self, ErdCode.WH_FILTER_LIFE_REMAINING), + GeErdPropertySensor(self, ErdCode.WH_FILTER_LIFE_REMAINING, 'life_remaining'), GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT), ] entities = base_entities + wf_entities diff --git a/custom_components/ge_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index 3befece..2fc8567 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -2,4 +2,4 @@ from .dishwasher import * from .fridge import * from .oven import * -from .waterfilter import * +from .water_filter import * diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index eaff1ec..81a07ab 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -6,4 +6,4 @@ from .ge_erd_property_sensor import GeErdPropertySensor from .ge_erd_switch import GeErdSwitch from .ge_water_heater import GeWaterHeater -from .ge_erd_select import GeErdSelect +from .ge_erd_select import GeErdSelect, OptionsConverter \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index 799e924..7dbce1c 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -1,7 +1,6 @@ from datetime import timedelta from typing import Optional -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits from ...const import DOMAIN @@ -116,5 +115,7 @@ def _get_icon(self): return "mdi:dishwasher" if self.erd_code_class == ErdCodeClass.WATERFILTER_SENSOR: return "mdi:water" + if self.erd_code_class == ErdCodeClass.FLOW_RATE: + return "mdi:water" return None diff --git a/custom_components/ge_home/entities/common/ge_erd_select.py b/custom_components/ge_home/entities/common/ge_erd_select.py index 25e3699..1b8dfc1 100644 --- a/custom_components/ge_home/entities/common/ge_erd_select.py +++ b/custom_components/ge_home/entities/common/ge_erd_select.py @@ -1,11 +1,35 @@ + import logging +from typing import Any, List from homeassistant.components.select import SelectEntity +from gehomesdk import ErdCodeType + +from ...devices import ApplianceApi +from .ge_erd_entity import GeErdEntity _LOGGER = logging.getLogger(__name__) +class OptionsConverter: + @property + def options(self) -> List[str]: + return [] + def from_option_string(self, value: str) -> Any: + return value + +class GeErdSelect(GeErdEntity, SelectEntity): + """ERD-based selector entity""" + device_class = "select" -class GeErdSelect(SelectEntity): - """Switches for boolean ERD codes.""" + 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 - device_class = "select" + def options(self) -> List[str]: + "Return a list of options" + return self._converter.options + + async def async_select_option(self, option: str) -> None: + """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_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index 036e8e4..fffe853 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -55,6 +55,11 @@ def _get_uom(self): return "%" if self.device_class == DEVICE_CLASS_POWER_FACTOR: return "%" + if self.erd_code_class == ErdCodeClass.FLOW_RATE: + if self._temp_measurement_system == ErdMeasurementUnits.METRIC: + return "lpm" + return "gpm" + return None def _get_device_class(self) -> Optional[str]: diff --git a/custom_components/ge_home/entities/water_filter/__init__.py b/custom_components/ge_home/entities/water_filter/__init__.py new file mode 100644 index 0000000..1d37958 --- /dev/null +++ b/custom_components/ge_home/entities/water_filter/__init__.py @@ -0,0 +1 @@ +from .filter_position import GeErdFilterPositionSelect diff --git a/custom_components/ge_home/entities/water_filter/filter_position.py b/custom_components/ge_home/entities/water_filter/filter_position.py new file mode 100644 index 0000000..ad191f1 --- /dev/null +++ b/custom_components/ge_home/entities/water_filter/filter_position.py @@ -0,0 +1,27 @@ +import logging +from typing import List, Any + +from gehomesdk import ErdCodeType, ErdWaterFilterPosition +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class FilterPositionOptionsConverter(OptionsConverter): + def options(self) -> List[str]: + return [i.name.title() for i in ErdWaterFilterPosition].remove("Unknown") + def from_option_string(self, value: str) -> Any: + try: + return ErdWaterFilterPosition[value] + except: + return ErdWaterFilterPosition.UNKNOWN + +class GeErdFilterPositionSelect(GeErdSelect): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): + super().__init__(api, erd_code, FilterPositionOptionsConverter()) + + async def async_select_option(self, option: str) -> None: + value = self._converter.from_option_string(option) + if value in [ErdWaterFilterPosition.UNKNOWN, ErdWaterFilterPosition.READY]: + return + return await super().async_select_option(option) diff --git a/custom_components/ge_home/entities/waterfilter/__init__.py b/custom_components/ge_home/entities/waterfilter/__init__.py deleted file mode 100644 index 96610ce..0000000 --- a/custom_components/ge_home/entities/waterfilter/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .flow_rate_sensor import ErdFlowRateSensor -from .filter_life_remaining import ErdFilterLifeRemainingSensor -from .filter_position import ErdFilterPositionSelect diff --git a/custom_components/ge_home/entities/waterfilter/filter_life_remaining.py b/custom_components/ge_home/entities/waterfilter/filter_life_remaining.py deleted file mode 100644 index 8fd5697..0000000 --- a/custom_components/ge_home/entities/waterfilter/filter_life_remaining.py +++ /dev/null @@ -1,19 +0,0 @@ -from gehomesdk import ErdCode, ErdOperatingMode -from gehomesdk.erd.values.common.erd_measurement_units import ErdMeasurementUnits -from typing import Optional - -from ..common import GeErdSensor - - -class ErdFilterLifeRemainingSensor(GeErdSensor): - @property - def state(self) -> Optional[int]: - try: - value = self.appliance.get_erd_value(self.erd_code) - except KeyError: - return None - return value.life_remaining - - @property - def unit_of_measurement(self) -> Optional[str]: - return "%" diff --git a/custom_components/ge_home/entities/waterfilter/filter_position.py b/custom_components/ge_home/entities/waterfilter/filter_position.py deleted file mode 100644 index 4eb2bb9..0000000 --- a/custom_components/ge_home/entities/waterfilter/filter_position.py +++ /dev/null @@ -1,51 +0,0 @@ -from homeassistant.components.ge_home.entities.common.ge_erd_select import GeErdSelect -from homeassistant.components.ge_home.entities.common.ge_erd_entity import GeErdEntity -from homeassistant.components.ge_home.devices.base import ApplianceApi -import logging -from typing import Optional, Dict, Any - -from gehomesdk import ErdCode, ErdCodeClass, ErdCodeType -from gehomesdk.erd.values.waterfilter.erd_waterfilter_position import ( - ErdWaterFilterPosition, -) - -from ...const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class ErdFilterPositionSelect(GeErdEntity, GeErdSelect): - def __init__( - self, - api: ApplianceApi, - erd_code: ErdCodeType, - ): - super().__init__(api=api, erd_code=erd_code) - self._attr_hass = api._hass - self.hass = api._hass - self._attr_unique_id = self.unique_id - self._attr_name = self.name - self._attr_current_option = ErdWaterFilterPosition.UNKNOWN.name - self._attr_icon = self.icon - self._attr_device_class = self.device_class - self._attr_options = [ - ErdWaterFilterPosition.BYPASS.name, - ErdWaterFilterPosition.OFF.name, - ErdWaterFilterPosition.FILTERED.name, - ErdWaterFilterPosition.READY.name, - ] - self._attr_device_info = self.device_info - - async def async_select_option(self, option: str) -> None: - if ( - option == ErdWaterFilterPosition.READY.name - or option == ErdWaterFilterPosition.UNKNOWN.name - ): - return - await self.api.appliance.async_set_erd_value( - self.erd_code, ErdWaterFilterPosition[option] - ) - - @property - def icon(self) -> str: - return "mdi:water" diff --git a/custom_components/ge_home/entities/waterfilter/flow_rate_sensor.py b/custom_components/ge_home/entities/waterfilter/flow_rate_sensor.py deleted file mode 100644 index 065270b..0000000 --- a/custom_components/ge_home/entities/waterfilter/flow_rate_sensor.py +++ /dev/null @@ -1,21 +0,0 @@ -from gehomesdk import ErdCode, ErdOperatingMode -from gehomesdk.erd.values.common.erd_measurement_units import ErdMeasurementUnits -from typing import Optional - -from ..common import GeErdSensor - - -class ErdFlowRateSensor(GeErdSensor): - @property - def state(self) -> Optional[float]: - try: - value = self.appliance.get_erd_value(self.erd_code) - except KeyError: - return None - return value.flow_rate - - @property - def unit_of_measurement(self) -> Optional[str]: - if self._temp_measurement_system == ErdMeasurementUnits.METRIC: - return "lpm" - return "gpm" diff --git a/custom_components/ge_home/select.py b/custom_components/ge_home/select.py index 22ff7ae..bfa6b0b 100644 --- a/custom_components/ge_home/select.py +++ b/custom_components/ge_home/select.py @@ -1,4 +1,4 @@ -"""GE Home Sensor Entities""" +"""GE Home Select Entities""" import async_timeout import logging from typing import Callable From f92b58340af2d662172efb70287ba09bd2eed6b2 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 8 Jul 2021 18:11:10 -0400 Subject: [PATCH 073/338] - import fixes - formatting fixes --- custom_components/ge_home/devices/dishwasher.py | 14 ++++++-------- custom_components/ge_home/devices/water_filter.py | 7 ++++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index 92fd301..2586359 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -1,5 +1,3 @@ -from custom_components.ge_home.entities.common.ge_erd_binary_sensor import GeErdBinarySensor -from custom_components.ge_home.entities.common.ge_erd_property_binary_sensor import GeErdPropertyBinarySensor import logging from typing import List @@ -7,7 +5,7 @@ from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdPropertySensor, GeDishwasherControlLockedSwitch +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor _LOGGER = logging.getLogger(__name__) @@ -30,16 +28,16 @@ def get_all_entities(self) -> List[Entity]: GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS), #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound", icon_override="mdi:volume-high"), + 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"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", icon_override="mdi:coolant-temperature"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "dry_option"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_zone"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours") + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours", icon_override="mdi:clock-fast") ] entities = base_entities + dishwasher_entities return entities diff --git a/custom_components/ge_home/devices/water_filter.py b/custom_components/ge_home/devices/water_filter.py index 6a59641..641cce9 100644 --- a/custom_components/ge_home/devices/water_filter.py +++ b/custom_components/ge_home/devices/water_filter.py @@ -25,13 +25,14 @@ def get_all_entities(self) -> List[Entity]: wf_entities = [ GeErdSensor(self, ErdCode.WH_FILTER_MODE), - GeErdSensor(self, ErdCode.WH_FILTER_VALVE_STATE), + 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), + 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), + GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"), ] entities = base_entities + wf_entities return entities From 59b882f793ba21dcc38ecc241bf9885f96668d06 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 8 Jul 2021 18:14:30 -0400 Subject: [PATCH 074/338] - formatting fixes --- .../ge_home/update_coordinator.py | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index db8437e..ba75216 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -80,9 +80,7 @@ def create_ge_client( self._password, event_loop=event_loop, ) - client.add_event_handler( - EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update - ) + client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update) client.add_event_handler(EVENT_APPLIANCE_UPDATE_RECEIVED, self.on_device_update) client.add_event_handler(EVENT_GOT_APPLIANCE_LIST, self.on_appliance_list) client.add_event_handler(EVENT_DISCONNECTED, self.on_disconnect) @@ -125,9 +123,7 @@ def regenerate_appliance_apis(self): def maybe_add_appliance_api(self, appliance: GeAppliance): mac_addr = appliance.mac_addr if mac_addr not in self.appliance_apis: - _LOGGER.debug( - f"Adding appliance api for appliance {mac_addr} ({appliance.appliance_type})" - ) + _LOGGER.debug(f"Adding appliance api for appliance {mac_addr} ({appliance.appliance_type})") api = self._get_appliance_api(appliance) api.build_entities_list() self.appliance_apis[mac_addr] = api @@ -195,9 +191,7 @@ async def async_begin_session(self): _LOGGER.debug("Beginning session") session = self._hass.helpers.aiohttp_client.async_get_clientsession() await self.client.async_get_credentials(session) - fut = asyncio.ensure_future( - self.client.async_run_client(), loop=self._hass.loop - ) + fut = asyncio.ensure_future(self.client.async_run_client(), loop=self._hass.loop) _LOGGER.debug("Client running") return fut @@ -241,9 +235,7 @@ async def async_reconnect(self) -> None: with async_timeout.timeout(ASYNC_TIMEOUT): await self.async_start_client() except Exception as err: - _LOGGER.warn( - f"could not reconnect: {err}, will retry in {self._get_retry_delay()} seconds" - ) + _LOGGER.warn(f"could not reconnect: {err}, will retry in {self._get_retry_delay()} seconds") self.hass.loop.call_later(self._get_retry_delay(), self.reconnect) _LOGGER.debug("forcing a state refresh while disconnected") try: @@ -279,14 +271,10 @@ async def _refresh_ha_state(self): ] for entity in entities: try: - _LOGGER.debug( - f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}" - ) + _LOGGER.debug(f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}") entity.async_write_ha_state() except: - _LOGGER.debug( - f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}" - ) + _LOGGER.debug(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}") @property def all_appliances_updated(self) -> bool: @@ -300,9 +288,8 @@ async def on_appliance_list(self, _): if not self._got_roster: self._got_roster = True # TODO: Probably should have a better way of confirming we're good to go... - await asyncio.sleep( - 5 - ) # After the initial roster update, wait a bit and hit go + await asyncio.sleep(5) + # After the initial roster update, wait a bit and hit go await self.async_maybe_trigger_all_ready() async def on_device_initial_update(self, appliance: GeAppliance): @@ -321,9 +308,7 @@ async def on_device_initial_update(self, appliance: GeAppliance): async def on_disconnect(self, _): """Handle disconnection.""" - _LOGGER.debug( - f"Disconnected. Attempting to reconnect in {MIN_RETRY_DELAY} seconds" - ) + _LOGGER.debug(f"Disconnected. Attempting to reconnect in {MIN_RETRY_DELAY} seconds") self.last_update_success = False self.hass.loop.call_later(MIN_RETRY_DELAY, self.reconnect, True) @@ -338,9 +323,7 @@ async def async_maybe_trigger_all_ready(self): # Been here, done this return if self._got_roster and self.all_appliances_updated: - _LOGGER.debug( - "Ready to go. Waiting 2 seconds and setting init future result." - ) + _LOGGER.debug("Ready to go. Waiting 2 seconds and setting init future result.") # The the flag and wait to prevent two different fun race conditions self._init_done = True await asyncio.sleep(2) From a5b8640b10afda12d7dab0c9e91ec68e5c19e8a4 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 8 Jul 2021 23:33:58 -0400 Subject: [PATCH 075/338] - updated sensor types based on new library version --- custom_components/ge_home/devices/water_filter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/devices/water_filter.py b/custom_components/ge_home/devices/water_filter.py index 641cce9..dc7c492 100644 --- a/custom_components/ge_home/devices/water_filter.py +++ b/custom_components/ge_home/devices/water_filter.py @@ -29,9 +29,9 @@ def get_all_entities(self) -> List[Entity]: 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_FLOW_RATE), GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE), - GeErdPropertySensor(self, ErdCode.WH_FILTER_LIFE_REMAINING, 'life_remaining'), + GeErdSensor(self, ErdCode.WH_FILTER_LIFE_REMAINING), GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"), ] entities = base_entities + wf_entities From aa6ef1dd4002865ef16935f355758e10e8fed19c Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 8 Jul 2021 23:39:39 -0400 Subject: [PATCH 076/338] - updated imports --- custom_components/ge_home/devices/water_filter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/ge_home/devices/water_filter.py b/custom_components/ge_home/devices/water_filter.py index dc7c492..b926291 100644 --- a/custom_components/ge_home/devices/water_filter.py +++ b/custom_components/ge_home/devices/water_filter.py @@ -7,7 +7,6 @@ from .base import ApplianceApi from ..entities import ( GeErdSensor, - GeErdPropertySensor, GeErdBinarySensor, GeErdFilterPositionSelect, ) From 6efba9b345322f270d885dfc458b4baa73f5621b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 8 Jul 2021 23:40:24 -0400 Subject: [PATCH 077/338] - version bump --- custom_components/ge_home/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index fbcc363..f9198af 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gehomesdk==0.3.14","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.3.15","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.3.14" + "version": "0.3.15" } From fefc9ead74c9718e63a219e2696b76a4ae545452 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 10 Jul 2021 22:48:28 -0400 Subject: [PATCH 078/338] - added advantium support --- .../ge_home/devices/advantium.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 custom_components/ge_home/devices/advantium.py diff --git a/custom_components/ge_home/devices/advantium.py b/custom_components/ge_home/devices/advantium.py new file mode 100644 index 0000000..065dfa9 --- /dev/null +++ b/custom_components/ge_home/devices/advantium.py @@ -0,0 +1,44 @@ +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, 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), + + #Cook Status + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "cook_mode"), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "termination_reason"), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "preheat_status"), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "temperature"), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "power_level"), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "warm_status"), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "door_status"), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "sensing_active"), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "cooling_fan_status"), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "oven_light_status"), + ] + entities = base_entities + advantium_entities + return entities + + def _single_name(self, erd_code: ErdCode): + return erd_code.name.replace(UPPER_OVEN+"_","").replace("_", " ").title() + From c72cba97c48ed50dd1128a91573107dc4f2d75a2 Mon Sep 17 00:00:00 2001 From: Warren Rees Date: Mon, 12 Jul 2021 15:55:38 -0400 Subject: [PATCH 079/338] - fixed typo in base entities, updated ha version in hacs.json, added new washer ERDs --- custom_components/ge_home/devices/base.py | 2 +- custom_components/ge_home/devices/washer.py | 8 ++++++++ hacs.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/devices/base.py b/custom_components/ge_home/devices/base.py index e08c514..7ae5c4c 100644 --- a/custom_components/ge_home/devices/base.py +++ b/custom_components/ge_home/devices/base.py @@ -98,7 +98,7 @@ def entities(self) -> List[Entity]: def get_all_entities(self) -> List[Entity]: """Create Entities for this device.""" - return self.get_base_entties() + return self.get_base_entities() def get_base_entities(self) -> List[Entity]: """Create base entities (i.e. common between all appliances).""" diff --git a/custom_components/ge_home/devices/washer.py b/custom_components/ge_home/devices/washer.py index ddc2d52..b4e0456 100644 --- a/custom_components/ge_home/devices/washer.py +++ b/custom_components/ge_home/devices/washer.py @@ -47,5 +47,13 @@ def get_washer_entities(self) -> List[Entity]: 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)]) + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_POWERSTEAM): + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_POWERSTEAM)]) + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_PREWASH): + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_PREWASH)]) + if self.has_erd_code(ErdCode.LAUNDRY_WASHER_TUMBLECARE): + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_TUMBLECARE)]) return washer_entities diff --git a/hacs.json b/hacs.json index 4038478..0b373c0 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Appliances (SmartHQ)", - "homeassistant": "2021.1.5", + "homeassistant": "2021.7.1", "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select"], "iot_class": "Cloud Polling" } From b973bc3698e427b146228e7c40784a8054b0ac05 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 12 Jul 2021 21:00:50 -0400 Subject: [PATCH 080/338] - version bump --- custom_components/ge_home/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index f9198af..79ab208 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gehomesdk==0.3.15","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.3.16","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.3.15" + "version": "0.3.16" } From fe47488d65a6dc8ab895d97c4abec722a61214e3 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 13 Jul 2021 14:42:22 -0400 Subject: [PATCH 081/338] - added current_option override in select entity --- .../ge_home/entities/common/ge_erd_select.py | 7 ++++++- .../ge_home/entities/water_filter/filter_position.py | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_select.py b/custom_components/ge_home/entities/common/ge_erd_select.py index 1b8dfc1..298e22a 100644 --- a/custom_components/ge_home/entities/common/ge_erd_select.py +++ b/custom_components/ge_home/entities/common/ge_erd_select.py @@ -1,6 +1,6 @@ import logging -from typing import Any, List +from typing import Any, List, Optional from homeassistant.components.select import SelectEntity from gehomesdk import ErdCodeType @@ -16,6 +16,8 @@ def options(self) -> List[str]: return [] def from_option_string(self, value: str) -> Any: return value + def to_option_string(self, value: Any) -> Optional[str]: + return str(value) class GeErdSelect(GeErdEntity, SelectEntity): """ERD-based selector entity""" @@ -25,6 +27,9 @@ def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, converter: OptionsC super().__init__(api, erd_code, erd_override=erd_override, icon_override=icon_override, device_class_override=device_class_override) self._converter = converter + def current_option(self): + return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) + def options(self) -> List[str]: "Return a list of options" return self._converter.options diff --git a/custom_components/ge_home/entities/water_filter/filter_position.py b/custom_components/ge_home/entities/water_filter/filter_position.py index ad191f1..a7065a1 100644 --- a/custom_components/ge_home/entities/water_filter/filter_position.py +++ b/custom_components/ge_home/entities/water_filter/filter_position.py @@ -1,5 +1,5 @@ import logging -from typing import List, Any +from typing import List, Any, Optional from gehomesdk import ErdCodeType, ErdWaterFilterPosition from ...devices import ApplianceApi @@ -15,6 +15,13 @@ def from_option_string(self, value: str) -> Any: return ErdWaterFilterPosition[value] except: 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): From c4efb4195607a19e342eb8127fa388cf5cdd9e5f Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 13 Jul 2021 15:07:04 -0400 Subject: [PATCH 082/338] - added property decoratores to overrides in select entity --- custom_components/ge_home/entities/common/ge_erd_select.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/ge_home/entities/common/ge_erd_select.py b/custom_components/ge_home/entities/common/ge_erd_select.py index 298e22a..523a048 100644 --- a/custom_components/ge_home/entities/common/ge_erd_select.py +++ b/custom_components/ge_home/entities/common/ge_erd_select.py @@ -27,9 +27,11 @@ def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, converter: OptionsC 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 From ce3d5abeb0c8bc753bdbf72396772365076a5ed5 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 13 Jul 2021 17:12:59 -0400 Subject: [PATCH 083/338] - missed one more property decorator --- .../ge_home/entities/water_filter/filter_position.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/ge_home/entities/water_filter/filter_position.py b/custom_components/ge_home/entities/water_filter/filter_position.py index a7065a1..2016c3f 100644 --- a/custom_components/ge_home/entities/water_filter/filter_position.py +++ b/custom_components/ge_home/entities/water_filter/filter_position.py @@ -8,6 +8,7 @@ _LOGGER = logging.getLogger(__name__) class FilterPositionOptionsConverter(OptionsConverter): + @property def options(self) -> List[str]: return [i.name.title() for i in ErdWaterFilterPosition].remove("Unknown") def from_option_string(self, value: str) -> Any: From bca28bca5ebeae2627384c91c0f8b966c4fa5165 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Fri, 16 Jul 2021 18:53:30 -0400 Subject: [PATCH 084/338] - fixed water filter options --- .../ge_home/entities/water_filter/filter_position.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/water_filter/filter_position.py b/custom_components/ge_home/entities/water_filter/filter_position.py index 2016c3f..cc5b84b 100644 --- a/custom_components/ge_home/entities/water_filter/filter_position.py +++ b/custom_components/ge_home/entities/water_filter/filter_position.py @@ -10,7 +10,7 @@ class FilterPositionOptionsConverter(OptionsConverter): @property def options(self) -> List[str]: - return [i.name.title() for i in ErdWaterFilterPosition].remove("Unknown") + return [i.name.title() for i in ErdWaterFilterPosition if i != ErdWaterFilterPosition.UNKNOWN] def from_option_string(self, value: str) -> Any: try: return ErdWaterFilterPosition[value] From d860c11f0d3829e4fbc51d907ba06c3a5e16ab10 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Fri, 16 Jul 2021 23:11:11 -0400 Subject: [PATCH 085/338] - added initial support for controlling advantium devices --- .../ge_home/devices/advantium.py | 2 + .../ge_home/entities/advantium/__init__.py | 0 .../ge_home/entities/advantium/const.py | 8 + .../entities/advantium/ge_advantium.py | 279 ++++++++++++++++++ .../ge_home/entities/common/ge_erd_select.py | 1 + custom_components/ge_home/manifest.json | 4 +- 6 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 custom_components/ge_home/entities/advantium/__init__.py create mode 100644 custom_components/ge_home/entities/advantium/const.py create mode 100644 custom_components/ge_home/entities/advantium/ge_advantium.py diff --git a/custom_components/ge_home/devices/advantium.py b/custom_components/ge_home/devices/advantium.py index 065dfa9..3236d72 100644 --- a/custom_components/ge_home/devices/advantium.py +++ b/custom_components/ge_home/devices/advantium.py @@ -1,3 +1,4 @@ +from custom_components.ge_home.entities.advantium.ge_advantium import GeAdvantium import logging from typing import List @@ -23,6 +24,7 @@ def get_all_entities(self) -> List[Entity]: 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"), diff --git a/custom_components/ge_home/entities/advantium/__init__.py b/custom_components/ge_home/entities/advantium/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_components/ge_home/entities/advantium/const.py b/custom_components/ge_home/entities/advantium/const.py new file mode 100644 index 0000000..7c487be --- /dev/null +++ b/custom_components/ge_home/entities/advantium/const.py @@ -0,0 +1,8 @@ +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_home/entities/advantium/ge_advantium.py b/custom_components/ge_home/entities/advantium/ge_advantium.py new file mode 100644 index 0000000..eafc347 --- /dev/null +++ b/custom_components/ge_home/entities/advantium/ge_advantium.py @@ -0,0 +1,279 @@ +"""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 GeWaterHeater +from .const import * + +_LOGGER = logging.getLogger(__name__) + +class GeAdvantium(GeWaterHeater): + """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 [v for _, 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_home/entities/common/ge_erd_select.py b/custom_components/ge_home/entities/common/ge_erd_select.py index 523a048..e4d8b44 100644 --- a/custom_components/ge_home/entities/common/ge_erd_select.py +++ b/custom_components/ge_home/entities/common/ge_erd_select.py @@ -37,6 +37,7 @@ def options(self) -> List[str]: 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_home/manifest.json b/custom_components/ge_home/manifest.json index 79ab208..763eb0f 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gehomesdk==0.3.16","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.3.17","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.3.16" + "version": "0.3.17" } From 48b251af1f069d0c3b75d726ca4db08399efe44c Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 19 Jul 2021 22:08:15 -0400 Subject: [PATCH 086/338] - measurement system cleanup - fix to water filter position --- .../ge_home/entities/common/ge_erd_entity.py | 10 ++++++++-- .../ge_home/entities/common/ge_erd_sensor.py | 13 ++++++++----- .../entities/water_filter/filter_position.py | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index 7dbce1c..cca1cc8 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -75,7 +75,11 @@ def _stringify(self, value: any, **kwargs) -> Optional[str]: return self.appliance.stringify_erd_value(value, **kwargs) @property - def _temp_measurement_system(self) -> Optional[ErdMeasurementUnits]: + def _measurement_system(self) -> Optional[ErdMeasurementUnits]: + """ + Get the measurement system this appliance is using. For now, uses the + temperature unit if available, otherwise assumes imperial. + """ try: value = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) except KeyError: @@ -116,6 +120,8 @@ def _get_icon(self): if self.erd_code_class == ErdCodeClass.WATERFILTER_SENSOR: return "mdi:water" if self.erd_code_class == ErdCodeClass.FLOW_RATE: - return "mdi:water" + return "mdi:water" + if self.erd_code_class == ErdCodeClass.LIQUID_VOLUME: + return "mdi:water" return None diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index fffe853..0f3870d 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -32,7 +32,7 @@ def unit_of_measurement(self) -> Optional[str]: @property def _temp_units(self) -> Optional[str]: - if self._temp_measurement_system == ErdMeasurementUnits.METRIC: + if self._measurement_system == ErdMeasurementUnits.METRIC: return TEMP_CELSIUS return TEMP_FAHRENHEIT @@ -43,7 +43,7 @@ def _get_uom(self): in [ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE] or self.device_class == DEVICE_CLASS_TEMPERATURE ): - if self._temp_measurement_system == ErdMeasurementUnits.METRIC: + if self._measurement_system == ErdMeasurementUnits.METRIC: return TEMP_CELSIUS return TEMP_FAHRENHEIT if ( @@ -56,10 +56,13 @@ def _get_uom(self): if self.device_class == DEVICE_CLASS_POWER_FACTOR: return "%" if self.erd_code_class == ErdCodeClass.FLOW_RATE: - if self._temp_measurement_system == ErdMeasurementUnits.METRIC: + if self._measurement_system == ErdMeasurementUnits.METRIC: return "lpm" - return "gpm" - + return "gpm" + if self.erd_code_class == ErdCodeClass.LIQUID_VOLUME: + if self._measurement_system == ErdMeasurementUnits.METRIC: + return "l" + return "g" return None def _get_device_class(self) -> Optional[str]: diff --git a/custom_components/ge_home/entities/water_filter/filter_position.py b/custom_components/ge_home/entities/water_filter/filter_position.py index cc5b84b..bb691b3 100644 --- a/custom_components/ge_home/entities/water_filter/filter_position.py +++ b/custom_components/ge_home/entities/water_filter/filter_position.py @@ -13,7 +13,7 @@ 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] + return ErdWaterFilterPosition[value.upper()] except: return ErdWaterFilterPosition.UNKNOWN def to_option_string(self, value: Any) -> Optional[str]: From 88b91cedf0e285104bd2e1ac0a97010aea3374f5 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 19 Jul 2021 22:11:26 -0400 Subject: [PATCH 087/338] - fixed icon issue with binary sensors --- .../ge_home/entities/common/ge_erd_binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py b/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py index 8c6f613..edd8417 100644 --- a/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py @@ -19,7 +19,7 @@ def is_on(self) -> bool: """Return True if entity is on.""" return self._boolify(self.appliance.get_erd_value(self.erd_code)) - def _get_erd_icon(self): + 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: From fb6437e30a50fdc623903791e42901f46626c67f Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 19 Jul 2021 22:15:37 -0400 Subject: [PATCH 088/338] - version bump --- custom_components/ge_home/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 763eb0f..f6a3d5b 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gehomesdk==0.3.17","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.3.19","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.3.17" + "version": "0.3.19" } From 9f116aa2ab42ef08de1d52b99fc6d7dd136fcb13 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 19 Jul 2021 22:45:39 -0400 Subject: [PATCH 089/338] - added warning on selection of filter position when it cannot be set --- .../ge_home/entities/water_filter/filter_position.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/ge_home/entities/water_filter/filter_position.py b/custom_components/ge_home/entities/water_filter/filter_position.py index bb691b3..fea4c00 100644 --- a/custom_components/ge_home/entities/water_filter/filter_position.py +++ b/custom_components/ge_home/entities/water_filter/filter_position.py @@ -15,6 +15,7 @@ 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: From 5aad65e537f70b002a8e7809b3043fb4f8209926 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 20 Jul 2021 14:08:59 -0400 Subject: [PATCH 090/338] - updated cooktop detection in oven device --- custom_components/ge_home/devices/oven.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index 1e0c84d..d1067a0 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -31,7 +31,10 @@ class OvenApi(ApplianceApi): 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 = self.appliance.get_erd_value(ErdCode.COOKTOP_CONFIG) + + 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"Oven Config: {oven_config}") _LOGGER.debug(f"Cooktop Config: {cooktop_config}") From 09a039d995f9474fd040ad9f6444dd8ed057565e Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 20 Jul 2021 14:52:08 -0400 Subject: [PATCH 091/338] - additional oven fixes for optional ERDs --- .../ge_home/entities/oven/ge_oven.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index aefb42c..3a75f62 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -181,7 +181,9 @@ def display_state(self) -> Optional[str]: @property def device_state_attributes(self) -> Optional[Dict[str, Any]]: - probe_present = self.get_erd_value("PROBE_PRESENT") + 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, @@ -189,14 +191,18 @@ def device_state_attributes(self) -> Optional[Dict[str, Any]]: } if probe_present: data["probe_temperature"] = self.get_erd_value("PROBE_DISPLAY_TEMP") - elapsed_time = self.get_erd_value("ELAPSED_COOK_TIME") - cook_time_left = self.get_erd_value("COOK_TIME_REMAINING") - kitchen_timer = self.get_erd_value("KITCHEN_TIMER") - delay_time = self.get_erd_value("DELAY_TIME_REMAINING") + if self.api.has_erd_code(self.get_erd_value("ELAPSED_COOK_TIME")): + elapsed_time = self.get_erd_value("ELAPSED_COOK_TIME") + if self.api.has_erd_code(self.get_erd_value("COOK_TIME_REMAINING")): + cook_time_remaining = self.get_erd_value("COOK_TIME_REMAINING") + if self.api.has_erd_code(self.get_erd_value("KITCHEN_TIMER")): + kitchen_timer = self.get_erd_value("KITCHEN_TIMER") + if self.api.has_erd_code(self.get_erd_value("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_left: - data["cook_time_left"] = self._stringify(cook_time_left) + 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: From 8deacf88360bd6b88cc25f4a8d6c3547973bd093 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 20 Jul 2021 15:22:22 -0400 Subject: [PATCH 092/338] - fixed my quick and dirty fixes --- custom_components/ge_home/entities/oven/ge_oven.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index 3a75f62..675c97a 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -191,13 +191,13 @@ def device_state_attributes(self) -> Optional[Dict[str, Any]]: } if probe_present: data["probe_temperature"] = self.get_erd_value("PROBE_DISPLAY_TEMP") - if self.api.has_erd_code(self.get_erd_value("ELAPSED_COOK_TIME")): + 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_value("COOK_TIME_REMAINING")): + 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_value("KITCHEN_TIMER")): + 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_value("DELAY_TIME_REMAINING")): + 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) From 9b49edcee4ca238390aa9b11382082092c36ff1d Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 20 Jul 2021 15:24:24 -0400 Subject: [PATCH 093/338] - adjusted variable declaration --- custom_components/ge_home/entities/oven/ge_oven.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index 675c97a..cccf6ee 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -191,6 +191,11 @@ def device_state_attributes(self) -> Optional[Dict[str, Any]]: } 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")): From e2bddfc56acc55b942311c8846605d3eef72c5d7 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 20 Jul 2021 23:57:56 -0400 Subject: [PATCH 094/338] - version/requirements bump --- custom_components/ge_home/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index f6a3d5b..807bd95 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gehomesdk==0.3.19","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.3.20","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.3.19" + "version": "0.3.20" } From 3fa80c8f04ed6ab9b28193ebc1950956cb603aae Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 21 Jul 2021 13:51:02 -0400 Subject: [PATCH 095/338] - added additional protections for water filter position setting --- .../entities/water_filter/filter_position.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/water_filter/filter_position.py b/custom_components/ge_home/entities/water_filter/filter_position.py index fea4c00..d9eb8b0 100644 --- a/custom_components/ge_home/entities/water_filter/filter_position.py +++ b/custom_components/ge_home/entities/water_filter/filter_position.py @@ -1,7 +1,7 @@ import logging from typing import List, Any, Optional -from gehomesdk import ErdCodeType, ErdWaterFilterPosition +from gehomesdk import ErdCodeType, ErdWaterFilterPosition, ErdCode, ErdWaterFilterMode from ...devices import ApplianceApi from ..common import GeErdSelect, OptionsConverter @@ -29,8 +29,35 @@ class GeErdFilterPositionSelect(GeErdSelect): def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): super().__init__(api, erd_code, FilterPositionOptionsConverter()) + @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) From 703944870576505e48bae4645e996ab2fc763423 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 22 Jul 2021 20:25:07 -0400 Subject: [PATCH 096/338] - added changelog --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..95e40e7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ + +# GE Home Appliances (SmartHQ) Changelog + +## 0.3.x + +- Implemented Laundry Support (@warrenrees, @ssindsd) +- Implemented Water Filter Support (@bendavis, @tumtumsback, @rgabrielson11) +- Implemented Initial Advantium Support (@ssinsd) +- Bug fixes for ovens (@TKpizza) +- Additional authentication error handling (@rgabrielson11) +- Additional dishwasher functionality (@ssinsd) +- Introduced new select entity (@bendavis) +- Miscellaneous entity bug fixes/refinements +- Integrated new version of SDK + +## 0.3.12 + +- Initial tracked version \ No newline at end of file From 2c51691c73476eddb376c74333e80eabab432a4d Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 22 Jul 2021 20:25:26 -0400 Subject: [PATCH 097/338] - version bump --- custom_components/ge_home/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 807bd95..4b36a55 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gehomesdk==0.3.20","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.3.21","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.3.20" + "version": "0.3.21" } From d616f21ac5fc470887aad5fea7eda9a9c36a4063 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 22 Jul 2021 22:46:38 -0400 Subject: [PATCH 098/338] - added ability to have a uom override - fixed a few sensor configurations --- .../ge_home/devices/dishwasher.py | 4 ++-- custom_components/ge_home/devices/dryer.py | 6 +++--- .../ge_home/entities/common/ge_erd_sensor.py | 21 +++++++++++++++++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index 2586359..bb63a76 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -22,7 +22,7 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_NAME), GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_STATE), GeErdSensor(self, ErdCode.DISHWASHER_OPERATING_MODE), - GeErdSensor(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE), + GeErdSensor(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE, uom_override="pods"), GeErdSensor(self, ErdCode.DISHWASHER_RINSE_AGENT, icon_override="mdi:sparkles"), GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING), GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS), @@ -31,7 +31,7 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound", icon_override="mdi:volume-high"), 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"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode", icon_override="mdi:state-machine"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", icon_override="mdi:coolant-temperature"), diff --git a/custom_components/ge_home/devices/dryer.py b/custom_components/ge_home/devices/dryer.py index be0a223..dcda727 100644 --- a/custom_components/ge_home/devices/dryer.py +++ b/custom_components/ge_home/devices/dryer.py @@ -55,11 +55,11 @@ def get_dryer_entities(self): 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([GeErdBinarySensor(self, 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([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_SHEET_INVENTORY)]) + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_SHEET_INVENTORY, uom_override="sheets")]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_ECODRY_STATUS): - dryer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_ECODRY_STATUS)]) + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_ECODRY_STATUS)]) return dryer_entities diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index 0f3870d..f2a8e1d 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -8,14 +8,26 @@ TEMP_FAHRENHEIT, ) from homeassistant.helpers.entity import Entity -from gehomesdk import ErdCode, ErdCodeClass, ErdMeasurementUnits +from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits from .ge_erd_entity import GeErdEntity - +from ...devices import ApplianceApi class GeErdSensor(GeErdEntity, Entity): """GE Entity for sensors""" + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_override: str, + icon_override: str, + device_class_override: str, + uom_override: str + ): + super().__init__(api, erd_code, erd_override, icon_override, device_class_override) + self._uom_override = uom_override + @property def state(self) -> Optional[str]: try: @@ -38,6 +50,11 @@ def _temp_units(self) -> Optional[str]: 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] From bec1c1e00fe184891034aa3c7478491f6456a755 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 24 Jul 2021 11:58:24 -0400 Subject: [PATCH 099/338] - miscellaneous cleanup - changed from device_state_attributes to extra_state_attributes --- custom_components/ge_home/devices/__init__.py | 2 +- .../ge_home/entities/common/ge_erd_sensor.py | 4 +-- .../entities/common/ge_water_heater.py | 10 ------ .../entities/fridge/ge_abstract_fridge.py | 31 ++++++++++++------- .../ge_home/entities/fridge/ge_dispenser.py | 2 +- .../ge_home/entities/fridge/ge_fridge.py | 11 ++++--- .../ge_home/entities/oven/ge_oven.py | 2 +- 7 files changed, 29 insertions(+), 33 deletions(-) diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index a262944..122e1d5 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -16,8 +16,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: - _LOGGER.debug(f"Found device type: {appliance_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.FRIDGE: diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index f2a8e1d..d92fef3 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -60,9 +60,7 @@ def _get_uom(self): in [ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE] or self.device_class == DEVICE_CLASS_TEMPERATURE ): - if self._measurement_system == ErdMeasurementUnits.METRIC: - return TEMP_CELSIUS - return TEMP_FAHRENHEIT + return self._temp_units if ( self.erd_code_class == ErdCodeClass.BATTERY or self.device_class == DEVICE_CLASS_BATTERY diff --git a/custom_components/ge_home/entities/common/ge_water_heater.py b/custom_components/ge_home/entities/common/ge_water_heater.py index a99b6c6..1a7fb68 100644 --- a/custom_components/ge_home/entities/common/ge_water_heater.py +++ b/custom_components/ge_home/entities/common/ge_water_heater.py @@ -42,13 +42,3 @@ def temperature_unit(self): @property def supported_features(self): raise NotImplementedError - - @property - def other_state_attrs(self) -> Dict[str, Any]: - """State attributes to be optionally overridden in subclasses.""" - return {} - - @property - def device_state_attributes(self) -> Dict[str, Any]: - other_attrs = self.other_state_attrs - return {**other_attrs} diff --git a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py index 06f6948..5266fc4 100644 --- a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py @@ -139,15 +139,17 @@ def ice_maker_state_attrs(self) -> Dict[str, Any]: """Get state attributes for the ice maker, if applicable.""" data = {} - 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) - - 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) + 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 @@ -157,8 +159,13 @@ def door_state_attrs(self) -> Dict[str, Any]: return {} @property - def device_state_attributes(self) -> Dict[str, Any]: + 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_attrs = self.other_state_attrs - return {**door_attrs, **ice_maker_attrs, **other_attrs} + other_state_attrs = self.other_state_attrs + return {**door_attrs, **ice_maker_attrs, **other_state_attrs} diff --git a/custom_components/ge_home/entities/fridge/ge_dispenser.py b/custom_components/ge_home/entities/fridge/ge_dispenser.py index eef0e9d..759d3f6 100644 --- a/custom_components/ge_home/entities/fridge/ge_dispenser.py +++ b/custom_components/ge_home/entities/fridge/ge_dispenser.py @@ -110,7 +110,7 @@ def max_temp(self): return convert_temperature(self._max_temp, TEMP_FAHRENHEIT, self.temperature_unit) @property - def other_state_attrs(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> Dict[str, Any]: data = {} data["target_temperature"] = self.target_temperature diff --git a/custom_components/ge_home/entities/fridge/ge_fridge.py b/custom_components/ge_home/entities/fridge/ge_fridge.py index 42cc80e..56c0c09 100644 --- a/custom_components/ge_home/entities/fridge/ge_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_fridge.py @@ -26,11 +26,12 @@ class GeFridge(GeAbstractFridge): @property def other_state_attrs(self) -> Dict[str, Any]: - """Water filter state.""" - 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)} + 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]: diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index cccf6ee..1d444ce 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -180,7 +180,7 @@ def display_state(self) -> Optional[str]: return self._stringify(erd_value, temp_units=self.temperature_unit) @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: + 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") From be8fec022cb62b52c4ceb5b624c77d33f617a35d Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 24 Jul 2021 12:30:13 -0400 Subject: [PATCH 100/338] - bug fixes --- custom_components/ge_home/devices/dishwasher.py | 2 +- .../ge_home/entities/common/ge_erd_sensor.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index bb63a76..4be9af6 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -30,7 +30,7 @@ def get_all_entities(self) -> List[Entity]: #User Setttings GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound", icon_override="mdi:volume-high"), 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, "sabbath", icon_override="mdi:judaism"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode", icon_override="mdi:state-machine"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet"), diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index d92fef3..a54f91f 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -20,10 +20,10 @@ def __init__( self, api: ApplianceApi, erd_code: ErdCodeType, - erd_override: str, - icon_override: str, - device_class_override: str, - uom_override: str + erd_override: str = None, + icon_override: str = None, + device_class_override: str = None, + uom_override: str = None ): super().__init__(api, erd_code, erd_override, icon_override, device_class_override) self._uom_override = uom_override From bf197fcbb432d0be25608f558615c922460b92e5 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 24 Jul 2021 13:12:35 -0400 Subject: [PATCH 101/338] - iconography updates --- custom_components/ge_home/devices/advantium.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/custom_components/ge_home/devices/advantium.py b/custom_components/ge_home/devices/advantium.py index 3236d72..837b954 100644 --- a/custom_components/ge_home/devices/advantium.py +++ b/custom_components/ge_home/devices/advantium.py @@ -28,15 +28,15 @@ def get_all_entities(self) -> List[Entity]: #Cook Status GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "cook_mode"), - GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "termination_reason"), - GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "preheat_status"), - GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "temperature"), - GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "power_level"), - GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "warm_status"), - GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "door_status"), - GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "sensing_active"), - GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "cooling_fan_status"), - GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "oven_light_status"), + 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"), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "power_level", icon_override="mdi:gauge"), + 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 From 3310a4dba32953abbe2fc0028e324192ebbaa590 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 24 Jul 2021 13:12:50 -0400 Subject: [PATCH 102/338] - iconography updates --- custom_components/ge_home/devices/dishwasher.py | 10 +++++----- .../ge_home/entities/common/ge_erd_binary_sensor.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index 4be9af6..4700c78 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -20,7 +20,7 @@ def get_all_entities(self) -> List[Entity]: dishwasher_entities = [ #GeDishwasherControlLockedSwitch(self, ErdCode.USER_INTERFACE_LOCKED), GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_NAME), - GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_STATE), + 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"), GeErdSensor(self, ErdCode.DISHWASHER_RINSE_AGENT, icon_override="mdi:sparkles"), @@ -32,11 +32,11 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control", icon_override="mdi:lock"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath", icon_override="mdi:judaism"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode", icon_override="mdi:state-machine"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet"), + 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, "dry_option"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_zone"), + 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") ] entities = base_entities + dishwasher_entities diff --git a/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py b/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py index edd8417..55afc01 100644 --- a/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py @@ -28,7 +28,7 @@ def _get_icon(self): 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 None + return super()._get_icon() def _get_device_class(self) -> Optional[str]: if self._device_class_override: From d93949f2c47781e496aa6d469933d35f76ea50fe Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 24 Jul 2021 13:17:18 -0400 Subject: [PATCH 103/338] - iconography updates --- custom_components/ge_home/devices/fridge.py | 2 +- .../ge_home/entities/water_filter/filter_position.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index 10f17b4..cf3d620 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -91,7 +91,7 @@ def get_all_entities(self) -> List[Entity]: dispenser_entities.extend([ GeErdBinarySensor(self, ErdCode.HOT_WATER_IN_USE), GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), - GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "status"), + 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), GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=DEVICE_CLASS_PROBLEM), diff --git a/custom_components/ge_home/entities/water_filter/filter_position.py b/custom_components/ge_home/entities/water_filter/filter_position.py index d9eb8b0..a94bdc8 100644 --- a/custom_components/ge_home/entities/water_filter/filter_position.py +++ b/custom_components/ge_home/entities/water_filter/filter_position.py @@ -27,7 +27,7 @@ def to_option_string(self, value: Any) -> Optional[str]: class GeErdFilterPositionSelect(GeErdSelect): def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): - super().__init__(api, erd_code, FilterPositionOptionsConverter()) + super().__init__(api, erd_code, FilterPositionOptionsConverter(), icon_override="mdi:valve") @property def current_option(self): From f244f0d6c3774d637519115f972b2e96a73cb458 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 31 Jul 2021 19:06:21 -0400 Subject: [PATCH 104/338] - updated HACS information - updated README/CHANGELOG - version bump --- img/shark_vacuum_card.png | Bin 4542 -> 0 bytes img/shark_vacuum_control.png | Bin 30326 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 img/shark_vacuum_card.png delete mode 100644 img/shark_vacuum_control.png diff --git a/img/shark_vacuum_card.png b/img/shark_vacuum_card.png deleted file mode 100644 index 558c1d1dc48ef52b98019a3061feaa492598e9e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4542 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 IF1p427rJ+dyZ`_I diff --git a/img/shark_vacuum_control.png b/img/shark_vacuum_control.png deleted file mode 100644 index 0d41446a1b6a8ba9c374f53f04de117d8900ea71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30326 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А}ObU@35{ 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 From 2f413460a1e1f486b45e03ea6ccf1f27794bcbb9 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 31 Jul 2021 19:07:36 -0400 Subject: [PATCH 105/338] - updated HACS information - updated README/CHANGELOG - version bump --- CHANGELOG.md | 2 +- README.md | 31 +++++++++++++++++-- custom_components/ge_home/manifest.json | 2 +- img/appliance_entities.png | Bin 29593 -> 51608 bytes img/fridge_control.png | Bin 29999 -> 52850 bytes img/fridge_controls_dark.png | Bin 26952 -> 61993 bytes img/oven_controls.png | Bin 22900 -> 46089 bytes info.md | 38 ++++++++++++++++++++++-- 8 files changed, 67 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e40e7..04ade4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # GE Home Appliances (SmartHQ) Changelog -## 0.3.x +## 0.4.0 - Implemented Laundry Support (@warrenrees, @ssindsd) - Implemented Water Filter Support (@bendavis, @tumtumsback, @rgabrielson11) diff --git a/README.md b/README.md index e8cb73f..b74bed4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ # GE Home Appliances (SmartHQ) -## `ge_home` -Integration for GE WiFi-enabled appliances into Home Assistant. This integration currently contains fridge, oven, dishwasher, laundry washer, laundry dryer support. +Integration for GE WiFi-enabled appliances into Home Assistant. This integration currently supports the following devices: + +- Fridge +- Oven +- Dishwasher +- Laundry (Washer/Dryer) +- Whole Home Water Filter +- Advantium **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** +## Home Assistant UI Examples Entities card: ![Entities](https://raw.githubusercontent.com/simbaja/ha_components/master/img/appliance_entities.png) @@ -17,3 +24,23 @@ Oven Controls: ![Fridge controls](https://raw.githubusercontent.com/simbaja/ha_components/master/img/oven_controls.png) +## Installation (Manual) + +1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). +2. If you do not have a `custom_components` directory (folder) there, you need to create it. +3. In the `custom_components` directory (folder) create a new folder called `ge_home`. +4. Download _all_ the files from the `custom_components/ge_home/` directory (folder) in this repository. +5. Place the files you downloaded in the new directory (folder) you created. +6. Restart Home Assistant +7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "GE Home" + +## Installation (HACS) + +Please follow directions [here](https://hacs.xyz/docs/faq/custom_repositories/), and use https://github.com/simbaja/ha_gehome as the repository URL. +## Configuration + +Configuration is done via the HA user interface. + +## Change Log + +Please click [here](CHANGELOG.md) for change information. \ No newline at end of file diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 4b36a55..3358a8c 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://github.com/simbaja/ha_components", "requirements": ["gehomesdk==0.3.21","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.3.21" + "version": "0.4.0" } diff --git a/img/appliance_entities.png b/img/appliance_entities.png index 5f2920327d3a1e2ec6c6b7ed03bfeb98e93b6b51..fd1582998f6cd1ccff217182dfe0002d2c890f18 100644 GIT binary patch literal 51608 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 HN@xNA9L2pS literal 29593 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 z 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 literal 29999 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 elDoGae%n^abK+7^1k^V0m>OH2DmMJIoiy0#k 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 z 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 Dxhc51 literal 26952 zcmd?RcT`hb+b@b0MF9(niV)ovRFoh^loqV02#5-Z2-(>P2nZ+;0)!A$iVaW^kQx*P zl#)o1nna0$fOJAaNg`51LPAM_goL|-d+&F@@4e%eG0yqMIp6(*!AdgMTyw7Zl;88r z=Qkf-w6;)G*rp&SC#QJs?5Rs~a!bT=a!cn{tpvU)&yPI?{96)m$>M}uQOkBF@L{>f zajWBUav$Q?%)?dypI6^L>lh#>w;@pWw}kEv3YL?*NjrDy_?2Ln37Qb2Vn?W75G#if z8A!^iJhkTKn@*qJ^K#j5j~?Wmy?8Hc$AcYzEdPFN6j#O4<`f;9NNuw_%uWsZoK8b7 zJeiq5#O96$Hs>PWdsKN_+Y!Tq&D^WTuZ$VljXGB@cu)TD>HqQhWat+CRfnlYhp9f5 z8ijE|Dsm`<9{$J0c++RUu@tuSS|r_D!Z*yet3_Oc6cpFjP^ULyC3)7GjnPsZ8A ze1E#Lugd46xUw&(8Y(9j@lCNO$hh2Y_!G{TDqJg?=s(i8fhQ-Y@~{}$_QMBlv}|1@ z%4kg9LBVksF#oB`Km01;6YIm{aib43_!78ug=U9!t#|GKQi zNN(xLKI)1w3$|3m3NzWGR>NZ??o1r$h7$0MM^Uojsg9;s*E)wXQ@4$~C&oUjrOSt} z6Ws5&cajZodjR_mr+#4`b52qK`@=O-vP0G-CS4a5y_NSmyj$nP6bwwp$p-hW>E(yP z{?1mDQ7NBm#mPzYGiZ8K=n{}37Nv2Y$4&^0Il4`9=33W_Lw(|(3cFgj?L88;Mton% zsH+*``l<)JUNIY17=F5R(7L+9y9oaoFN|02FK0-e76d}(XNkUpIW{y9NRnVY|6sLg zbc&uILCl^3BMS;V4`dfJ2k__Wt%JDe$!EuI<&{qbt3r!=QGB?=ZQ_D7hBKBOf4C`X z6;iG+C%uAGrPiN^7ZwI}dWsyniT0fzqq@K*iCCCH9AW5+@!ML0@tEscWE5W$6P|hS zhx_I?jb2+6ouWTco=xUs37L_|ijF8VDdh~J?w+1^^kV~d?gi7sJ)Z5txlRnou;~r( z=B}OFss0gaV=5`AW^s_-RR*t%x9w$b=IF02JrRXh_5oY|5W2m5$$TLFpqW{)QbIKJ z^*3brBR9Oy*RJ@67YGgWCG-#>#=v40M)jjkT2J@rA_d#a>%Z9=bSg%{76vR9f-I8{ zaXW)W_i^%V=ZWZ{_;bjP^V@O_3h!Rb!zH`dR^K`f9>rFDL(P#58-#N|Nbw2TKXJsM zl`dh-#5W6ujBsvI#q_urWCc!HOko7)yH~hJ!mvcqtwe9lS`G7j{i;3!@sCO;;$Ai> zE8c*;$MQk);j!UokRnS$d>oyN{!#9pQs#}#TR8yLV3{2RR%f>b1~T?4Li z^2P7k>O%6&tc3-x{7FZ0Z8#xqfu}mJlm}&G$gPVxLm4`W2^T~m$C;xWM2rIoId_~o zzdP+DxonFHalG%M5-qZZhRu~Z6$hp`=45WErDGD;H9sDCM-O`oc8nw}3_!>n7?l+s z&1XTmwl4Zlq&Xx)yXm^|obUsKP0RMhI|r#J?XM`T=BUx$ay|Q*11nF!NG=5z!2xYC zJF7qVFE?@>xm9xA^l&5|gIG}<_c1p!_!4bS0qmGZBpMz^qe51bTs(}!ZnAV^I6O-1 zqpF~h(Vgl7Wi#Gw-6u8EY2eXV$ei%|0O-_@#pQ;vd=Q%7z4+ zv38Jo$W_B}rl`j5$Qxbez#o-|TLIfNw-p9efqfID^@S1LNd$T}!cil6&FCP>D4y;~ zpI;vattGU_#a@M>>2s=AY{X8TA8K84MqZQc8Q{y@1ORUp^&Nu7WQ(3p}wFKv- zX|%3sdb~+65L|kzlO(qfLOVqno2n+zvvTj$1>AQzP9oUmB_Eqq#o)V8(ytB;!d_m( zsBio0-n6ziYhhrsbiRKUVCM+#FX_H*oyCs9-9J-d1!}xNa0F-dMFeZl=}>p5nx5h4 zlHoVmeyndMRzxU9!=6@9{OX{`%IUUE0_VG+NJ+PE)1g5{@j;l&$fpHTl|_DhGPATS zZjzt(^2Ftxd7y+ThOtSl;>oghmOqI#kyKM_7RIx-U{jpYu9uFmvJuyms=8jemH5&y z`H(Ft(fF~iC&OO?b~na}Fkf?{exN2XzrT-Lle{)}V0Bnl`Zm)b)l0Wo65(?<`Q!S& z-l-v4*QtgZf4V7#(aasVZhAXuCb?NNq^MWDXN}f&PAedzytjPWsZJIHO6RTlHW5r- zkH70T;pT7!&^1+7U5)+2;WPLXQ7C9_b-`V5lS9FU%mqPw+{3s?{ z!$X2Zay*Rho0*-5LepD98>~$aq=hdBC#B82xd@DgMcAI zn!NWj)^Nad>PshiSCC|#GKI0^&3?@yqO>mcX}9O>848aKHnIE5(QMLY-#x=iAV_n} z9%~G);kwF2(Dg%WirQU64)0{!8r9pRjAw>$=7s|CMKh136nL0(YkBmNh*0YkGt$~n zTZs2d>J}K$0C4P|Y_`z~K}|FPevZ4a7k~mcZ}v=YYS=G+TIJ~GdLtTtFH`30w*wwf z?tI)LD*3;@yaZ!No|wv@!m)j9)Q?4|@ejOmJsu}KZbMxrcTq_OO!ipFF8%-ic-iaq zoqC7!J|7F;sHyHDP&NhZFo$nQ*A*|36UE`cd5QVjfuOM24`PlOM2laTg9wKmpw+4_ z!R_i0H!rKoKjDh)rMl^!QE1h%nGwp`jhEz{NFn8nJITk=%fr5&FJ~sS(t4YUbi^^h za@|y}B}8xtqlDf4ps+C*Vhm=nn`Ui{DnbsEDg728zk=0%QIbBclmx&o$1*29l}~^0 zy$t}w)_WfZe)-h{3|BKm{nW0WZ`6$6~Qh3twgqwfSZ>J_^Px4ZN zNaQ3;uGc^Sum=zn2#RAO?2ax^Xxj980ql(bSbwxETV`ew^fb1OjzYtRIe6}pascji zJyDi3S`UNju%Ke(h9~wA%S;$H0wqcr_42JNsg*uIe-$n2QRTE$bK%G*+Y?`)e?H8m zsY1h@HwsdP==m9x@TVnqx7!s#U7GKmg|LNXLT+r@`@ycbR+$w*qg_w>A3M1O-SF1? zdB*CAh}o$n=DEMI*PMY-`>gJ~vX4XspLE&Rzq&2V2S8f>k?KkqCFF_^+6HeBJGz|j zsz*!I5p-wMlk1Z3b4h&(Y0~+l*otW{p;Qd2x}1Z#cG=qFSgEUy+xSBR+LEl!b%GV0 z$vB7KufZgILk7QUW83# zQtYtRyVt0ZQrcp?a8E-bmzD712piD@uTV3VJ~i7)=sO#bei&ElLaSY;93Sm;=LVw@ zXM998820Xr)iKApl z9IXfQ)88~Lk*j*4%hps!yx#hwx$S7d9p%!VlF^Fzg|ZJE=aCOFc;b4nvptQ!@)Lkk zc4hkFW*A?qrO|2$#uJjG@cWsy=&7H342JcsX`ews%Ob2#Q7P-asgGSu^XvP06{*C| z>wX=bw9(%6Z8VOwIrKvmeyrV?W`$$fQS5uH$@KpbYPmL)hTWJLNbOQo!bLEPteK6DRS`iBzI5jR;dB*MG= zo~?KsoZg=A5B~zvXpUBUyr-#p%1K@DI(ht7KkuD*pnk!MaDS=ft~_Y`!|@m}*|rBk zm*iHD(#77^gOAbg;)3q+@VwU=8T(gZ=HwgC_GNU3_<%yAf~eqrDc=D@l78?$!X~>! zPQ312R;I<&3-dDW61=-`&2J)uuKZdJZNM?`24jP`eolS9>gxrB>ZK4i{VW^CXb4O? zL6N{EjB(|_4Isg5J@+-O*RD@c&cVIV3-h&Cyx%^?x))Eb5pb7qOiURggv;4DcsMiR_P5VS>6sMR4g zBd7Xe!jnxV2F0GCi_x3midT$>ZIWd&1XNdu;}5p2|41)+72!~BvB0}p9Po-YCUww~ zAoWBW1yZJZhI-e=3ouA?^+*Gzr_F>lGd(;?e&K!QDAx0EO+!f+41yZzA6js~xdn*j zY$IXlxlImxJZgE4u>8wEnK%cz@w3-==7Q3u2W_`_Zag#XXY6xkmIlbd}RbbEb(CBNj6}P{wy;>@SEqw9=747HNZ(LahDgL;W#yZ3% zL!6JK=Wrqy`W}NF;zv$ORwHeeVBKDMvj^Tj3=Ir9ZhU?q%OT4lIWN^Rs8ebcngx(_5*2Vv5avXg*Mr{wLR_uy0}h3PyoY| zcqUV&`UJGoUg%D8aS5HX69O2OchnhUR!oVzYdsNTZ1j4dfHCpy(XVMi+ie&*O*wC4 zRz$pfd%p`Iq-2q8!l?Psl6ZQV5xeaE00vurv;O2v+XX7?jwvnw2czkguQV2cC5S?s zEh_?DUk3&A#?m>xl4uw%I z^}u9LI5j^_BVeYJ!kiz4&N@wRCp0oQMACyP7pRZJ^-c}0V#Hz%eGhym`HSKxjN6y} z84(Loqo#I-1$UJ=9e#r9MaW9t%$2FZ2YkWXcB&gE)RDOn20qX zm3BN2NUBbD32#$+&(#cxsr>VZR-;>BN&W-#Ug_8(G^wzX-8gi~>Z+2Q=>4KQAKHx*4^Hk1JwpHBd4%my zP_tuoY6fDfyFjRtUUeZWLXa!w3z;4ZsmsNeTj$9cX2K^!>`$L#wkJi>mMKdJp&vgf zi0dm+j@a{Dt7wB*RNTEV!t-)-h z{Ati&lGk5Y&PZgNM8UX-PhD0sS#H6-%$euI{!KoJ#Gh-|M%;C1#DNHX-^PtO;u9oJ zG3$0f|6$L)>`kW>4y+)MA#dc!csb3?E{V9+-*!0fiUo$r%=qx5VCc-}Dfu9L{U;!G z!PU>};&0;L0c@!mv#$y>n+Ft8e!VJ2>1A>!N2T%vmYt55I#cL;O!z3F%`< zEu0Oe6K+wwRP+oYt*oDaU#5+*^ZG!tUsP8*@_GSa)vz(|u)4hHdU}r(4=vZZ4O_I^Nl~z9#5ihZ|o#jOtx~ z()jn@hv$=Lj8{&Cc`Y>`-3ON`VoQV5%&7$@x82^A)EkP(6*0bzowisq#A?Yl4OA$A z%rJ4pI809cdWnfwt_2M1&2Pm|Uh=;NH8d67A!lEnPA(#tRBpy1H_um)^fZ{m>lH|5 z_=Kl9#mZ*$4|8-_^;{lsdKj{!~K+o5w{YOm)~>v1C{LUc+{0 z#NZ**mYcyw8z`o0sxr;EPol_{E11j;0=Yj^8H((&GZ-$u zhP!R+|HXx?zWj1-X8LkTX+ka~&?e?%(Euwm;$ZF+JU#;6bGLI=?%G2f7TRu5-@_0rj6i#tP4iw`BRcRH+mCgIs3sxlUVZ*Oo z`+S+(RqD2bM7-!3IJZ6o)9>h;)(lx78 zPk1aXXp$eWgaKt?#eR+|aUNc#_N$BXeQc7toNSXs`|T>^ifZe2L!(%Xbgi6u?!G{(m^E27r;WM> z4f|wR^-$T|gX;0^qwo9=AKk4VnuA7;M!Y`o^%)-M;xdn!je#+ThTb=q-;VeB=(snw?(*ha7IRehJ`Cjs*?hkh zRjUiGf+dcGFDT=+GWN6hwhbeeF{V!dCvQ?aW0LoHcD-QA^;d(byIe=ZF}M8*|M82WxaI@w z%l)bcAQ%YpF}vji)#FR_dgbxo!Xr`h)HtIxFwFXY>njp<=GK;}EbX5b)cW{|ypnZ- zt^eC}Fl=Yx;P=7JV905!U~_)hvk&U<&9!E`jywszs5;eT%;r-{8r{lTpZa*$?TIvP zjKH-%P~4@_b$WhU37HQC)NbFhznkx357q~DIj#~vq3ZlR?))Vl7Z2*Q_1MYQ`-AjA z9=;w@YZiXXA|vYWK30im|LuAIvpU#+i>j=Cq_#QLpszW@*zm)sOLpjsxn_ZJ6|*zf zq3OcW&_CjJ9sh03E&=*?oD1mRrg2f4ww~|&-3AZPyfmQX-=?3(OPr8Ut`2(z)SNwtex#v zhs@DhdqH2AIAJFpP;rCdN6~va|12ask`w-K~Ci#E@5hp?sxG zud_4Yf#eaVE6b=Qjb)A0M#TDs9veNj{V6~rAGpQ5e+t89&+?eFI^R$!JjGB2bYgo; z#DT1>@AW5kT$MZp8(i!<-QVj7h*?gm#T>L#c zgqxpW7_t;SJcCWru{?5PkN1?i0%5(E>0nZFhewsgq5ed zX=D#nD;2hIKOniTNev{Nsi43y2*35C_*?3L)7IT21ij4t8p5x{XTwMP3?~xuew7Nmj${Ib#jlIQULV;4&_)pM=}B4+ zsiYeN^&gs*jQLlw2M0u6?JY$H83iA&2NfNoyO$cV%PYpc0$fVA=}&Dj4Cd1N65EnI z4(g1aZmK^3ZQYy<^#$hQ_$(P$j??OUd3UJ(sZ}irg%Ib;CdC09(>*LrlcF7cTP>*=-+v`FL zL1MP4cj})@kovn)!s(+L8HbBUEJ};sQXHDH!a+SCx481Hg-T-Jl52OY7+bJlPYhiN z3VUD*P8@bk#OAtL2jjafKLNja3TW~z4yVapuL^hxS(BhhQ&@Zj;2~vAigyA1e3AJBpvg0JSwGsq%FCLt zW&Pawzj>5`ngB>4+%9o%4hk4;v0_(J_LZa?0(P1+yZ>@OIDF2#1nu{)!TfE{7x9!; zGGAhsiQ^jAYBcYVjp41#lJS64K>;(TbzCE8V^sAC=@-w=RpRg+?Dn;blT!dZWE3h? zeh0h7b6&dhudmOq7Z_z=J9*5`+3})IF|jAyqLzSJh19<NLRuJoamoSkGh`Qk90kt%HLC)Hq zj~3-0AS+((aqd<#y7@ru@>Xj!s=O1ekaLhU`c>rH9VV!YZ&mJNAN8e&1t9F-PVokN z-s?i|-<#r}VUUy`hs_!lixPYC3gxTw4;a4#^uV)}Gg3wf@=_l@l%)Q?1K6doAVA&= znc;TkdRIJqkDo;_syig(hd$;F81@D_`dPaj%O^@jzV(XRuB%HC5O0$(mpX@_ zGx{LmmD!Lrqk38pGsvxy!8To`QA@=a#C|;-w%7mK~Qm+80%$ zZES0iAzNG4q|tdHWf1Up(?BTm0OzLxizmE}OSPb8X8`N^a~Z>1g?EB#7>~1n+FCU4 zVz*mI0XT5IASmUo`L0cn@w|{o)82o&K=3Y;T8)WQGHvN| zTe&p$%6wk-$a=v|O_|Ejwe{BBeDl*n-2fPx@p_rw{8tXCD;Y<8zo9n?d8Drf_CSOy zk8bX4PV;NOZ6>eqYoK#DN|1+7yk~;E;^F#)P4GqhC)yyuGY_#{k5jW9fp-u_^&m4Q zo*RuH1YXyl?pnRfCSMy1EYH8-U|zNW&??n`wowL_X~*CAaXb&T3dvjXH~zM@m^ewz z2D1Kt0ZtpJ#Ncz{CIK7iZ ze~q)@$^kWA!{z|s{xyHwM>q;IETdc5W-xayTS5<7H4gi?+g_2(YR+NH*EuXqZVUel z(HePSbY(kPx__iie4j2n64LfGl{sEwSLDc?YQ+B5qqJyhWq3}dVKBpj=0oA(2mJJ!g%do zC9CMN53MOiSsUx%fw<5KG4yy7@3m>Kd){wl|B48+<9-@Gg%91o9p+Yl+WuCh8^6Sk zuABSZVcQvWp)~e7{+X~29*_EGkMym zyBSe6L$Esz)^}Nq?bN@<0|q@?841LVQOCK^tc?Pp2*|h%ZjyzDUAFUpvmva5VGOrn zWw8@5dYOZa#NVyclEq7T*+A$Ccs9SRsYkL{XnbEd@X3a%cC%(NRx)}70zPdNto&c} z_Ohxt!PeqJHHWPpoB6fUv)Q*yWgg3y8c%gM4%osSX)c#}QngqIS(I&SG35z@cx))jPU)V6@P|*&Aoqi8S+KL zqhG-@APm186Zd4le2tk6#>{k%)im!eR!{IX`_W0#fBYZquyUB&yf3M~qU{=$tcAhc z3wMaK!&tSTrvL2F&z2fvEXQnpoh|n4%-Lfhfe8=(8L|Qr23ThqTrt32=1KJlasBY+ zAPweYcojtJSHLe5T3Lt%U=}588n-q(xR%iWYdN#yj4b!Zu}{Oe*m{rE;$QKiqfZ$> z>(OV3WQAN7;{JiT^(1RM&ssB77roB_d1&7K=BV7AwmMh1g~{TK=O(vqaBnx?cY{h< zGp)mRLK&L8~2<_@pU_*^A-&==pfnImRJec&v_Piata+{N>ZCd?Gl1uFFqz7JB}|OlPYu z?JCo7R{R=BlD=wwPu~u zuenZG827KoykDdB35AMg9AC)qdvmUS@1Sy1(?AT!b_^%{%uaq~S7!+ZL2gw@J@$7# zX!H*LFw#Kz@Ntv3b0?2XO!0ip2T)CbOVZxtKUiv@|Dv1KDb(|3dbbHO(b{fd*XBc7c#l-CkT4 z-K={(Nq4jNQLwYTw4_$^IyMY)4jXWKVANQWu?H`W9+r4OeXqHBdlnpHTMXE!|`mM!F+fc{4Wy zacd+9>RbrcvtJ7=GtPJQE8#(>cYzx#Bvz*6sI0 z^`)Urb`0oXJL8~REypu8ck(7Z6?=Qz0gyIAG1th}*)w#UE8YnOFw36)Syu(5bH24@ zJeXD?ni?-@boU8#wet&cWbAJ$1||uuFRa3iU+5r3b*_rvjl#`xPAiCcA6*xpVz734 z@e$17ylJK-d_cd}$3oK8j8GuAFf*ctW_Z ztN<4(rW=4x+xms@;XUqKi?D-AtlDvqLhiF7El6zO*{|BB#BTo!`Q_J16S|fuIOMn9 zr1{y}5G$LL68$GtV!cDnw~~SKF%48sY@gAFgfohTWSn><$#CVb2i1B;Dkk_9T-j@x zt)@k+=}<&LlYNo~t{vlq)*9J{NH7GIY@=JHN_D3g#~HNVxB|7GC%T1K=IpPR_?4n! z;E67+SCUgRa|RGu5(LmL3lE6v1z!)I^@X>oLQ%c=OcB<=$3bK=KZ^IirIy}?# z-LT>n;h<9kusn4cMUT&Pz+bn%FP}tuh2pLB+#M;#M1q*hxqtNfLdlDB-lg|PSo+TO z;XvXs*#33O#D)m~QM--%wAqtbYRl2r+6=chC!O=ou>~S`vX1DMVp>^y>@#RMVf{xD z+36!cdcvooz^$VL-hQ^qy5RQ%u7KhF9c7Ob4f~K0NxC}^RiEMKh^Ap2mhpCvP-$9^ zcDu$u$|o0E7sLyaA>tx?WXuE7SMx|#tQmQP5!Q4-wyUtA_w{q;>syP&kDanFp06p% zSZq@J>&sUWCE5GPwv}*hhSwuE=Q48PEa~;aT%3{`86@|@?&U<(T5&bE61=)ZR`gxXmCDn0p|2+E zOBD+AHm9e&n1U@xlo#GD??s$jwm{uzQw3~M*T!x&ZYhK=gm%ng(G%Q;;k!U>N7=Tg zu!{wF_M_eF80ZDk4UbPY-mZI2d9-FGT(Edm0wWop`{5GS;AP1FlJK4sqfhI!#<0T0 zZQSR#JD{HQ~+zM~FU~Bdg==PllofTcB{DQda&AfdbUIUT8EC z<<|ED1)vjzIe~Zxi*aB$4_*ff=r53<0TX=koKH|VVSb#o`+=RkBs5fySfB_8;0(ReAvJbi^_+bPX_KX zUA^64OvEO+DB`z;L9Jz!ncY+sZ6gtn29)6rq~e5wZMFk1-f7x5?O$gw4;0ZlOe85~ zoe4V{WxOd{sFuJE4x47G`T|vtWDY>tvAK8Eco)#0fvF8pQRBLyfXTV0A+j!~2O&uV zo9GI6)L(hKq-KC7RBn!bN^ZJl^V-PXIll#E2$Xbenhv)(p85_Vw=Yen&oZFdOnnRC zRkF^lFY(BU-rfv3h!Ut)8epX2{yjZo`_==2Vdc_jysybTuLS&l=Q#7i7l0Q6m1vk< zCwV@`P&6@Nb!<+qonP<-Z(sT*j!_kdJKvR1lp9KWLpOTr(%n6+0-+xS^uleUI}+G7 z$VGGULg|~&-M0DLLR;m}WCPT?s8Q1Am}uo^p|!X?L+i(ztiyYmp23bJGxe8DZr8qa zgMv5?|Hdiws6}yCLRT6BWsl%1R|C1uG7}Pqe|2W9oZEVL?v@O7RR2gnddU9hoF{9nH?><MoYQ-UwM9U(I7nc`%`MVK$zL&u$sHA5qJvjrz zR<{-25#O0@s}QBKp5r>4>9Av#Xi|7Xhg(D0B=At^48CJL%3CDPtrmM4D2z_(hoeU| zy-mriz5MRG1u(`1$rAIP$R3vpoN(av?+Pb}=wgktzVp*}+xM|c=vjzH-enq~sJnAt zku1vezgrnH#;wify1ZnoQxrv%v>+Rvm2t+Do1o!e?Z|0Ez{BvLcr;1r{3J82dp;H+ zU5S^r*mQ?J51k38m4j_U@Z? zvtl*1{*%fQv&umy4EYBTL%3_;I|p4u-&o##Wzoo6m6C%k2I_wMypClQ+XDq$p&vsZ z^$9I!jMdcW;S^d3#gyDO75{L5Aphv~qtL1;Ysx35O@eFo1A{k>6l@@sZJ4=+Yc|ER z?6(cBk3%(}O%&pub_8YCApV*(wA9*ojbp7#aW`OOtNtMoP1!{8tc7%9J{MEJKikEef!RdTeGTdW8dapKL!^H zI@fpvmRDoIrU^x9{VE0HjQ2Q7Tr}M{OUL12H}ah|bWz3{nOsn}Y3ldfM&^_WM5#F; z=UvDZbMG*tT?sM@+pne4@S!R%H+@{Blc&1Pt!-cW61ibO3N51XaRu-y)3DV~;-&-j zQi*D&2cdfb{~Q>t(`T&9h7TrL4@Xm8!zWxU_@N$Q*{q4v00qL_V^s;>)n*XbkMx=b z%Ie(HlWX@$|efaIZE+t%7!qp32sgo?YdENodF)nhH=O`L#zT;x|=d^xcQ0seRW zIg2-f`Orp#3-Rv6;$L{) zL5r_sUq)aXD6s9-Lu-ZkMf_s^?&@)_7=G!m8?tTNkQp0h7hhP!r7dMb0)8bvS!v!i z&a~yA!;N3xceFi2`5ouZeWK{tFTDPRi(WKpS1ajNJEiV-(*K|Qx-HZNxfdX0Q=p=F zk@zShtv0l-8|MN(hY3(FHv?VF`BwnExW%c;wT0B)iq~Zew;XFUl{rDcXa63Z znk$DIh*G5i4BYQikt5&JFznq|2jAsFU_|$cVNMg4z&zZ7B}lsq@oa?vpdMJR?EnCm zQ~^$IsinU@H-6~!s|7*z!fh6Xm^m*QXK}qwSksVe=E>gL0Z{a1@*l~>P(XgX}LaFOHG$9=uol(@pI?Fha57clM z6RrsWa*JbmL4SC?Gr4%ej5d**Xr`B?zINhf9;Pvpv$KkrK$B#UC!B?{F{vFwR^17c!|beW2<&&tVJ&`{Jm>1q-w8k;>L_B z;ZCoN6Mkg?TEm9g96t-xQr}ljApv#G5%z#*LL zeb26lT!iOD-_ZQhn(a%i782yg=}zW=5xTYq@E6heOgo3*11iXBHON|9+8#9ETr}g~ zF$1g6Uu+QHh%zF!PdOu{f5I;|7p%gTkw>-9@s7CsDM+dZ*qhR4Mh@M!F_{YF^YFFT zT89m*HWGWvNAWWDHLr5!`<=3lf6Yr-Jo&6rUpgX&CDBd8|B(kYHxvufEo)o=W=Cea=rz?yln0OGf`7+LPt5) zu;E0l0aDI9AMpD?>53G`X&tS|AO(qRu7|8Z;#)~ZP3KLi%S3`O2t+tQK;Zi{0W-@kHnecNW6=C zwvf$=<10f%2rQzITX9*~Cx`@UA|rlNC^3DHQ$~Na&K-rP(hrQf9g9Wt+Us{CCi9=v zp=J)n`c=#jk71|#v*hLWy!4)Ja{PsSMJ0g*zxbBXeOM+=tn0!P`D{xtXUL?{t8+t^Pu5d>SMwAGeub_iN& zhD{=ae=t2fNV=)fIh)K#kTx(GsKk~-ZIW37tV7qhyl2>thvAbWKoq12e%cOQIAUTm zj6|0Ch%`O;L~E{hx&m7LYs}!20eGeCC1L@ar_FZz`DXBW^$!V?7up5HQ1op!sYOIF zv@zdeu**Bx#;kIom2LR=FI9}j=a%;{?QkNiV9>lMzjeGw+I>6rX1zH@1Q7+hHt!b$ zHRpThtmd1viMo`_LRmDuXcg6ga}Too{tLqCx4K?jR5K^Zy>T-DkYw}J=TwcFEt3B3 z%By<`l;1sR1P%efyZ`OYYGq+1XkiBZkiL{5j|XF6h5n~E|MH=+b2F-fZL`cQN>4;! ziteh#!2iYh>`&YmS;@b=D)Z`H#<}C*Ki%kg`Ebn@_{d)_S!?aQwL<9XUpHObGMll1 zx8Ya6rYru{?7t0W`5ywtc7#F}zGC~8j5+L}eou!iosR%>^9HS~n`yH6Rv*xnjM;x2 z$uVbt#IoO9V+Cp~O4TX9n8x+b$xoMl=&bTC>sLIgy_P$(PWIY&gxW7t zP>CwD9^EO!Pv*RWl`8ubyfa!($|NOX%u#XQMU`%&jF`oRZ77A|+(>!RtM3Y8 zbY@ilJb~yeL{z8wRZ|M_KSL4UURk8LxlT1fC3P7({2PWr^Pk<(auQdAEOXndi(`vF zz!p+n!H79K!-^U3)j>fY`quOuuW}8IZ%AYAwKk$}ci(Z)6k%73riUcMe4B?Um3o`C z-|d&rGWf^%059nHIJwf2ay=u`gNowJm-M?ixNxJDK(?KztT!YLbL2mUpS@ZPoVUQy zt_Ftqvq3MEr=~Yzj+js^uRf`axhM%Eml`U(DLumLK zt%Il=7$vc6H`=$orzs(0m2*AlL*KU{c4W}ex+li__HVRa__jLXx$aEMRd!LO)ZWkw zBY8i#(wv%2{ZkG$3d}yK0cVYL7VQ!{9r)aXI+=dLg*LDBx%TvE#U?HA`FoV)6jW~* z;`7(!O4;@}Je^Q;OE?3qd_@WhAd2`(WZzU!;^h!VVL6V{QctGPekwuy|0sLXvajIi z!XM>B>4^;<-lA>Vs~584-UTiD*Rxdx-&`yvM69OByZJf}dE(BAo#-lhyq>9PMlnV4 zo>43}21sTYSWg6x-J~V2=lkhPLT8o-8s#tfO9haD6t+#OS-u4%stVpr@TD&Bh`zIz zBcg-oWvmg1)>ZfwtT*d%(m^bJcb+uTR0sDD0c?-N-@vO8wb*KyI*2%Z=KbgKrZIi| z0z<*xTl44Gxq3r&L34VYCi^b^pMt0$E@McTsg&y<%vjX`AssrgsGfcBE&G#|7W5X*37DO;gb9wej8A8C@yX!J5~M+_>P^%vGmPJcOEZ)MPw1^-j%Hwk3y zdGQovVbpAP($x^)WGgnA0Ni}QhyKRCD=Ju7O04~qR{i@!HKSYQsnTjC^?{Mh54)M@ zmPeNTX4&;!Jg{!98T6NF&0dQP0jTC$Ydn*y+yELxz*WVY=%~90`U-E_mvfq3JWY1{ zfj^rnim}AG7U!V3M0gXxjTpskYm+K0vuSY&x~o<>YDNZx^xD@&^NxJ+>-Zi9Y9Tws zh-ga%@&@mbviQvbicWhm2V|}LMHDH=8e@#jNw2-~>iuo}OcAm_$Hpu`7F2S&`+RLg zz@ZXOiFd-1!@4K8#f$s^x^lGuIM_&Imqn$#M%Xvmkt1Yg&1{EO;;#FHUOBtQ1wiiB zzDdL_^Cw3c3IJ&=le%`>Be($X>ahX!Vehcbo)88+q>vC(A z0~BSt`sxa|U?ZXnRqLP(uIYXU4%zr58U9dDlbuBpM*nq|${upD8GA8WyQoFoR4O}8 zrvnPZ4s_ zlHReG5m6OYdcP$XxHuiMq%Pw-F0MB)+%ocQ6WoT^)X4v?OE9getf)i{~TLPL$#tB+IeYycxS~wK!O8Uj z2^^;7T3Mn2HII$uJO&ek06KSm5D{S5({eGWfy}V!l+>xb>L6W!bxM|YC2#6B`(|gh776+O zv+>#7yOI%y#0kGveT4s@;4V7sS(%<7fQZp_gHNNR+Z z^Lc4OOO?!q@)|1;mT^)8?zkDUCB=8tIa)Z&iA0GVe_@RyZ2nxFG1vOuHH&&_AYqeW zW)ytkgq?+e*(qJjG{~aP#neA=7{Z%0Wn{)RiuLKME-C^A9*~yEXBM@N69y&_-f=(P z(see2*xUBfVsY}J#em+Om#4pqvAJOVmQ}L!I?OXlvSd)HNaHRhc)DQx9WNtDW5hb+ z7=#(lX(anDV5~D@U|97>&yW*N8S<-1(l%0jQIPVeO|C!^s1GvxWb#}$%S^{{4p})iqfRjH$*U9!s{CoA*LG=YjWRMS|HKhzLOD+PWG_(c%h}ad z24nPj`9Oy~7D4ap||O zcN|(Po_xSqLvooJs-~t*s|m8cX53*z$3|BzKJDQrkI4M2#U#8)w%QA#Py3N0zM@#LwSx_Sawd zaZ;J`g8v!FYWB!1q~qP(%fm)d+-l?pP)|%Qi@$2XCHstU%$lma{TGCdS|y$glmZ7A zyt7ClxRm%IsNSQbZSlT8JDbBIO`pb+BLoikPo2#n%xVS+$89wb2LZ=7c}5D=*9n%@ z|1uUkYhC7>h*`p1k3P*kCgQ6sD1BoFD+Q)865unQQw4g@zxK&J~m$K~=8r;0XW9LUD; zT6zxl6c0!`9->Io5{`412e#>F8@b+R~pq+vaJuI($Xr)lh=>d}gW8G+7(fvshB;zrLIQ#~Fa(4)A(0Rf z8Db(~O2QO;6~wmRTD{(TYu)?v{X98Yb*gsNsrtVC?Y)l<|7qBmV)S$S1zhz2-RT9g zaXUz{cFm^vJA&F4GGPp_V_+@03U_+ElJAG456AztDS&!#NTO>=k_31(T~T5n-MH8r zl$56RHMLrI-ysD!zO0cCA-lLBr_sN+BTvQUvK`2ln|rCr)Y=YmaH!l_-hqKdhR2{0 z^-(W~aaESfv#(dX*vcOp-q*b3)nRLGCTx!}urS(S%&q@oypzC9vhEum`QHxW|B-S0 zI|u-8gdqquWy+Uc&0&T;Fu-7ffBiS;&%f1~RZ`45XJx3?y!{$zKm7a^0!Ejr*zNeoi+j2v+8(p-rY(R?cKih}t3W9Ke{bPwcAas3 zRHiS%Z}~z<`RiD5_P66FyTPWS#$jr#fENEY4-7x$>#2>=tvdZHByK4~N4NM)BJ4*dk38@ zuCIoQb6S6pn)>!V>HODY_p@Uybp41-JL$=#$~j%}bxuQ`NWBx7N4UcP86p75WI<4Y%Swyd0lTHGgU+HZ=B! zph+?4q|!955-lXzNK@LbIbmwapW4GKgz+`V7|0HANUkbER7rkIXpWu(Ka-Q#uEy_S zB@6itLm!SnDP35t;5&t9Ri_X4ut8M_!f?;4a_6Yo(}1bp^HBFsbBU*Q^cx&$r0OR1 zF30PBj}4|c-_9vnh8I3WXMBejIKPW8grRiL?10*`KuJ#91Gq;B@#P9XPIy|SI)xda za}?RqVj^$Wo@jLmFHjscEf~t+v(hwsPi7e6(}89^om`ogBdVJWA|r|lDTd|NVOeqe z-#WK}VBr%wqCDI}mv)RewAE(cO_xGgd&ATyow89G>T`$Z$onj6X_-T3r$#iY6i_Rb%Y z%jl1Ba@MPci`K0-LcBC41nHkVBFm<{l{cB|P{65f5>yxXi@9cS*GbCf5gi-bgI=O& z9w{%Z+x;P8JmKn4bHjjqjT4r4`OrYs?sIC#F6(J@#}7`#EVpHD81#vbLsg!S1;)34 zyVsW@-9#3CLze-HJ@i@p2b%%!+tyS)G4c@~SGQt(tROrCfY+eImhMaiVj2>N-A6D4 z0E=(dmQZbEox%RpiMyv>?QuGzlN6dksC(AS>{Al-f|+V7dsWWGk!GfJ!()(9KE zw;GJ8n3fV?pMT62cf=@4{>C#@t*PSo)WC|uW|96NJ}#1;leQQGtJwOH#jdrbFdnyf zLr}Sg((5eP18eUJX7S0~R?UkqZ}?GpvLj~Xq+?^)JK&tg#cPEYW`=Y<);_t$ zNX6lxsRnurcWoW^_kizVE46zfYNG)_zI6(XDfoN}O<4P0_3)xRSav8n#KZ-mW@WQ) z?-KZ@A2cE+o;}D!zA2uxNqOmb=I+9oS0rYjp(pI;gGk}Z8a4cJWIL>?d^XLdG`?X(SbD1R9`lm>?i77}S$_YwxR?FS2t& zTXk1m%*p&SWDVI5)c7=8Krkw*iPs9WX$RU)q|9R=kJ>Sj(R$O?lx~8&Ph4=+5wQ{z zo~VY`e68D7v{CZ7x?**Jg2hBi_t)OlBKp2Ivc0o6w;%uGhqY1xF`+jFIY|;^IR!6% zpJSPFWC2p6X3EyA=hm+(9yM-O);xH1KK>=f%8^F>ofu~Nrr*=|$zIiPuaJ`^NhMe0 z?>=NtMf2gSupcDKuHK$qQ)fAxWkBd~gXa|GthgJ_z5L*wZ}HYt*S!Ky&Kp4Ay7HuIGH?e3x5Z3iwjOo>Gp0RG=!6w@SC6lgR#^n{<=)pa z;QR~%h_Z05D`iU}76nfoHjS;pEm+gWnwbb^d(95(#qGFWK>-PfAMASe`_$RzPB{NsVPn!Q_;C#y{xUy2h>x);KhDIkRJ zHr^Fdt{H*o6teJReP_oFF(!4ZFYz{85bEmCZgCnWI%L)45P-sz$)>q8K5lTNsE$NneoRw8fvVUCh%!0wxqT0x)8O&M6&5Nm2vY;D?uwcH%pIS0c#gh zQm;DT;wHIyo{Oh1KB3U8I;jT34H*JIR+^6tq^$(D&20=yB*!y*dl=53Q9IMyOS#we_DhPxlV9A%u-e5{v zdyZF|k*-NM#s<;_d8F6uQnsm5)%yB95jBDEUaz&f>?$X9PTpR}qOO)rWzWJhpyz}&a{CH@$F zMd~x6N?W07%mvK?gF7B*F&&4^yu>G;i;K~M&^kr69yc~aHem?zp>P}oV4*DvDk(LT~qWYH*xUv2z&Re(!a5Gm&5mIBCzpNurv-$36!#+xuRs;7-)64SWD z&=PDdA`hts(4Iew-zMdA+mbg-S_F9gEVYF5`BZs~jGFQ-_HMLcHO9GYv zHLphXwziF~Jkn0cr5E>*@uQt@3xab!8Rl8bcUMpOv+8$l2)P+&(+; zeteC6f77)4lbM+}Z~A^nacydY86#&4bXtO}b$eM8@)Q>`D&2G7TqLHSo8Hx+8d0zs zbo`-%>{(H|kXQjll<$p#i1ps)&KA)RwCY|Tf3o1v(+9g)YQ0H_!O|n?L%GP~KTlFC ze%u#ocsHxn@p`_p{gwiAdvo{5n5@B-m3sbdSP%41txtq&o-lg7f?FTiQ0N%gZ0fTf z)RKbr()$!mf$^XqD>edY^B~a6^99V6Zec4sIMJ8BsfOuelg^bdHhn|$)98x4R-C3& zVd(mn$~4`dp^(Td0w~ZHNoMWQck4;jQ&>aQ(j)e;GU@n zqB`W}q@nB0@bB7;eKG)MMb(W+Y3C$MLl;kTLh__RS$- zT(P^Sdoi&^BH1mXXBoP+4AxSVwajKv2yA9<(&ryVG^f}McCbegY$1gD3cx+w zNyM_2kx4XIIctY9vkd(rEucX_qUYSMS*fYG{JkKsbrXi)u&LC)5O$W}1(nWqOCaGi zJ~MEL+iR>+;%to8BjOh9bEO>ETo;QF|hWXq&HDZ zgoz8xO~2j=e$8aEUi~Q2uWwEo^p*5g!u0<;-3H#vZ_?++zssV))dwWgc_ra+ zr*gtyDg*cufe3d5dal98usz-k5-+6*)caXoa z(U-G$Y-v*U7)W|Mk- zgm~4Qig3%za3??X;eH!JG8KH=YHpIosOl)yDE$}coh z-N!R0m1akb7J3^eJ6biwYv4kTqFx`fsH$#=r!y=KZPytgK6x~){x-oynxLZ6*oH-S zQ{vVGeWWET*h8`eN_DQ9Sw#i0S>roEmLgy6`jA)x7Xh16{%+I!TJ=E?b{&)}INjGF zH+A)!O>gRI>*29`H+tc?k?D?q>tw?UDPHtPSdgJBE7zT{ee+Vg#2O3RUY1ee9@esm zmSZ%|=#|K*S?tT}zAi6!0#6O_!hzLi*oNU3W2NnHbHk>!0^F`yi8H=*LaIsuZkqVQ zXr;~mz@4F3Z^z6e$YTp#t|I`jQr?LM3W9E(p;8;uKBrGU<3;f?L^Siu!W}qhMt+Q(yj diff --git a/img/oven_controls.png b/img/oven_controls.png index 5c5b396e1bfd6dca0481d53a20086ea18b663426..c8e6a8ecea9425d910d602a24191b4342972677f 100644 GIT binary patch literal 46089 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 VKG&|1fd)L{;QpX}Y~SeY{{sF>vl{>a diff --git a/info.md b/info.md index e8cb73f..af2ad50 100644 --- a/info.md +++ b/info.md @@ -1,10 +1,17 @@ # GE Home Appliances (SmartHQ) -## `ge_home` -Integration for GE WiFi-enabled appliances into Home Assistant. This integration currently contains fridge, oven, dishwasher, laundry washer, laundry dryer support. +Integration for GE WiFi-enabled appliances into Home Assistant. This integration currently support the following devices: + +- Fridge +- Oven +- Dishwasher +- Laundry (Washer/Dryer) +- Whole Home Water Filter +- Advantium **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** +## Home Assistant UI Examples Entities card: ![Entities](https://raw.githubusercontent.com/simbaja/ha_components/master/img/appliance_entities.png) @@ -17,3 +24,30 @@ Oven Controls: ![Fridge controls](https://raw.githubusercontent.com/simbaja/ha_components/master/img/oven_controls.png) + +{% if installed %} +### Changes as compared to your installed version: + +#### Breaking Changes + +#### Changes + +#### Features + +{% if version_installed.split('.') | map('int') < '0.4.1'.split('.') | map('int') %} + +- Implemented Laundry Support (@warrenrees, @ssindsd) +- Implemented Water Filter Support (@bendavis, @tumtumsback, @rgabrielson11) +- Implemented Initial Advantium Support (@ssinsd) +- Bug fixes for ovens (@TKpizza) +- Additional authentication error handling (@rgabrielson11) +- Additional dishwasher functionality (@ssinsd) +- Introduced new select entity (@bendavis) +- Miscellaneous entity bug fixes/refinements +- Integrated new version of SDK + +{% endif %} + +#### Bugfixes + +{% endif %} From fdf6e38beb821b6cf0d0dcade10001459b1539ee Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 31 Jul 2021 19:10:56 -0400 Subject: [PATCH 106/338] - added badges --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b74bed4..e2f1517 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # GE Home Appliances (SmartHQ) +[![GitHub Release][releases-shield]][releases] +[![GitHub Activity][commits-shield]][commits] +[![License][license-shield]](LICENSE) +[![hacs][hacsbadge]][hacs] + Integration for GE WiFi-enabled appliances into Home Assistant. This integration currently supports the following devices: - Fridge @@ -43,4 +48,13 @@ Configuration is done via the HA user interface. ## Change Log -Please click [here](CHANGELOG.md) for change information. \ No newline at end of file +Please click [here](CHANGELOG.md) for change information. + +[commits-shield]: https://img.shields.io/github/commit-activity/y/simbaja/ha_gehome.svg?style=for-the-badge +[commits]: https://github.com/simbaja/ha_gehome/commits/master +[hacs]: https://github.com/custom-components/hacs +[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge +[license-shield]: https://img.shields.io/github/license/simbaja/ha_gehome.svg?style=for-the-badge +[maintenance-shield]: https://img.shields.io/badge/maintainer-Jack%20Simbach%20%40simbaja-blue.svg?style=for-the-badge +[releases-shield]: https://img.shields.io/github/release/simbaja/ha_gehome.svg?style=for-the-badge +[releases]: https://github.com/simbaja/ha_gehome/releases \ No newline at end of file From 45a65f1a271abb3ff50b228e4fa61f9f6a4f651a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 31 Jul 2021 19:15:45 -0400 Subject: [PATCH 107/338] - HACS updates --- info.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/info.md b/info.md index af2ad50..dd4e829 100644 --- a/info.md +++ b/info.md @@ -39,15 +39,20 @@ Oven Controls: - Implemented Laundry Support (@warrenrees, @ssindsd) - Implemented Water Filter Support (@bendavis, @tumtumsback, @rgabrielson11) - Implemented Initial Advantium Support (@ssinsd) -- Bug fixes for ovens (@TKpizza) - Additional authentication error handling (@rgabrielson11) - Additional dishwasher functionality (@ssinsd) - Introduced new select entity (@bendavis) -- Miscellaneous entity bug fixes/refinements - Integrated new version of SDK {% endif %} #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.4.1'.split('.') | map('int') %} + +- Bug fixes for ovens (@TKpizza) +- Miscellaneous entity bug fixes/refinements + +{% endif %} + {% endif %} From 984e25b27372dd059e026a65804770617a937e1a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 31 Jul 2021 20:15:11 -0400 Subject: [PATCH 108/338] - iconography updates - updated icon classes - added uom override to property sensor - version bump --- custom_components/ge_home/devices/dryer.py | 12 +++++----- custom_components/ge_home/devices/washer.py | 24 +++++++++++-------- .../ge_home/entities/common/ge_erd_entity.py | 8 +++++++ .../entities/common/ge_erd_property_sensor.py | 4 ++-- custom_components/ge_home/manifest.json | 2 +- 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/custom_components/ge_home/devices/dryer.py b/custom_components/ge_home/devices/dryer.py index dcda727..0835c74 100644 --- a/custom_components/ge_home/devices/dryer.py +++ b/custom_components/ge_home/devices/dryer.py @@ -17,14 +17,14 @@ 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), - GeErdSensor(self, ErdCode.LAUNDRY_SUB_CYCLE), - GeErdBinarySensor(self, ErdCode.LAUNDRY_END_OF_CYCLE), + 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_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), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, icon_override="mdi:tumble-dryer"), ] dryer_entities = self.get_dryer_entities() @@ -57,7 +57,7 @@ def get_dryer_entities(self): 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, uom_override="sheets")]) + 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)]) diff --git a/custom_components/ge_home/devices/washer.py b/custom_components/ge_home/devices/washer.py index b4e0456..9cc0372 100644 --- a/custom_components/ge_home/devices/washer.py +++ b/custom_components/ge_home/devices/washer.py @@ -5,7 +5,7 @@ from gehomesdk import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdBinarySensor +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor _LOGGER = logging.getLogger(__name__) @@ -19,8 +19,8 @@ def get_all_entities(self) -> List[Entity]: common_entities = [ GeErdSensor(self, ErdCode.LAUNDRY_MACHINE_STATE), - GeErdSensor(self, ErdCode.LAUNDRY_CYCLE), - GeErdSensor(self, ErdCode.LAUNDRY_SUB_CYCLE), + 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), @@ -35,10 +35,10 @@ def get_all_entities(self) -> List[Entity]: def get_washer_entities(self) -> List[Entity]: washer_entities = [ - GeErdSensor(self, ErdCode.LAUNDRY_WASHER_SOIL_LEVEL), + 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), - GeErdSensor(self, ErdCode.LAUNDRY_WASHER_RINSE_OPTION), + 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): @@ -48,12 +48,16 @@ def get_washer_entities(self) -> List[Entity]: 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)]) + 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)]) + 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)]) + 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_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index cca1cc8..b14236d 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -119,6 +119,14 @@ def _get_icon(self): 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: diff --git a/custom_components/ge_home/entities/common/ge_erd_property_sensor.py b/custom_components/ge_home/entities/common/ge_erd_property_sensor.py index 0092d42..9624871 100644 --- a/custom_components/ge_home/entities/common/ge_erd_property_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_property_sensor.py @@ -8,8 +8,8 @@ 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): - super().__init__(api, erd_code, erd_override=erd_override, icon_override=icon_override, device_class_override=device_class_override) + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str, erd_override: str = None, icon_override: str = None, device_class_override: str = None, uom_override: str = None): + super().__init__(api, erd_code, erd_override=erd_override, icon_override=icon_override, device_class_override=device_class_override, uom_override=uom_override) self.erd_property = erd_property self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 3358a8c..b31c783 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home", "config_flow": true, "documentation": "https://github.com/simbaja/ha_components", - "requirements": ["gehomesdk==0.3.21","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.0","magicattr==0.1.5"], "codeowners": ["@simbaja"], "version": "0.4.0" } From 09a7a311aac75854a907d011c5b9c5ec447eb7e3 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 31 Jul 2021 20:15:51 -0400 Subject: [PATCH 109/338] - updated name --- custom_components/ge_home/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index b31c783..143c0f9 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -1,8 +1,8 @@ { "domain": "ge_home", - "name": "GE Home", + "name": "GE Home (SmartHQ)", "config_flow": true, - "documentation": "https://github.com/simbaja/ha_components", + "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==0.4.0","magicattr==0.1.5"], "codeowners": ["@simbaja"], "version": "0.4.0" From f8919b170a8ade397c4633ca5aaa9850aa61c919 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 31 Jul 2021 20:16:39 -0400 Subject: [PATCH 110/338] - updated HACS name --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index 0b373c0..f508818 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { - "name": "GE Appliances (SmartHQ)", + "name": "GE Home (SmartHQ)", "homeassistant": "2021.7.1", "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select"], "iot_class": "Cloud Polling" From 43076479065def9d03c3242071b4341b52fb6b03 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 31 Jul 2021 20:28:41 -0400 Subject: [PATCH 111/338] - added advantium to api factory --- custom_components/ge_home/devices/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 122e1d5..696549e 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -1,3 +1,4 @@ +from custom_components.ge_home.devices.advantium import AdvantiumApi import logging from typing import Type @@ -32,6 +33,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return WasherDryerApi if appliance_type == ErdApplianceType.POE_WATER_FILTER: return WaterFilterApi + if appliance_type == ErdApplianceType.ADVANTIUM: + return AdvantiumApi # Fallback return ApplianceApi From b22babe05ce6309b490b25582eb517cd16a823a3 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 31 Jul 2021 20:29:09 -0400 Subject: [PATCH 112/338] - fixed import reference --- custom_components/ge_home/devices/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 696549e..6034f3b 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -1,4 +1,3 @@ -from custom_components.ge_home.devices.advantium import AdvantiumApi import logging from typing import Type @@ -12,6 +11,7 @@ from .dryer import DryerApi from .washer_dryer import WasherDryerApi from .water_filter import WaterFilterApi +from .advantium import AdvantiumApi _LOGGER = logging.getLogger(__name__) From e7f1fdfc9b1ed26d77c117f5b49cdd74f82ef265 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 1 Aug 2021 10:54:21 -0400 Subject: [PATCH 113/338] - initial climate support --- custom_components/ge_home/climate.py | 35 +++++ .../ge_home/entities/common/__init__.py | 4 +- .../ge_home/entities/common/ge_climate.py | 121 ++++++++++++++++++ .../ge_home/entities/common/ge_erd_select.py | 10 +- .../entities/common/options_converter.py | 10 ++ .../ge_home/update_coordinator.py | 2 +- 6 files changed, 171 insertions(+), 11 deletions(-) create mode 100644 custom_components/ge_home/climate.py create mode 100644 custom_components/ge_home/entities/common/ge_climate.py create mode 100644 custom_components/ge_home/entities/common/options_converter.py diff --git a/custom_components/ge_home/climate.py b/custom_components/ge_home/climate.py new file mode 100644 index 0000000..3694a09 --- /dev/null +++ b/custom_components/ge_home/climate.py @@ -0,0 +1,35 @@ +"""GE Home Climate Entities""" +import async_timeout +import logging +from typing import Callable + +from homeassistant.components.climate import ClimateEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .entities import GeClimate +from .const import DOMAIN +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 Climate Entities') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + # This should be a NOP, but let's be safe + with async_timeout.timeout(20): + await coordinator.initialization_future + _LOGGER.debug('Coordinator init future finished') + + apis = list(coordinator.appliance_apis.values()) + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeClimate) + ] + _LOGGER.debug(f'Found {len(entities):d} climate entities') + async_add_entities(entities) diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 81a07ab..3bb073a 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -1,3 +1,4 @@ +from .options_converter import OptionsConverter from .ge_entity import GeEntity from .ge_erd_entity import GeErdEntity from .ge_erd_binary_sensor import GeErdBinarySensor @@ -6,4 +7,5 @@ from .ge_erd_property_sensor import GeErdPropertySensor from .ge_erd_switch import GeErdSwitch from .ge_water_heater import GeWaterHeater -from .ge_erd_select import GeErdSelect, OptionsConverter \ No newline at end of file +from .ge_erd_select import GeErdSelect +from .ge_climate import GeClimate \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py new file mode 100644 index 0000000..9d2d0e9 --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -0,0 +1,121 @@ +import logging +from typing import Any, List, Optional +from gehomesdk.erd.erd_codes import ErdCodeType + +from homeassistant.components.climate import ClimateEntity +from homeassistant.const import ( + ATTR_TEMPERATURE, + TEMP_FAHRENHEIT, + TEMP_CELSIUS +) +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_FAN_MODE +) +from gehomesdk import ErdCode, ErdMeasurementUnits +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, + current_temperature_erd_code: ErdCodeType, + target_temperature_erd_code: ErdCodeType, + hvac_mode_erd_code: ErdCodeType, + fan_mode_erd_code: ErdCodeType, + hvac_mode_converter: OptionsConverter, + fan_mode_converter: OptionsConverter + ): + super().__init__(api) + 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) + self._hvac_mode_converter = hvac_mode_converter + self._fan_mode_converter = fan_mode_converter + + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.serial_number}_climate" + + @property + def name(self) -> Optional[str]: + return f"{self.serial_number} Climate" + + @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): + 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 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 hvac_mode(self): + 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 self._hvac_mode_converter.options + + @property + def fan_mode(self): + 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]: + 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: + await self.appliance.async_set_erd_value(self.hvac_mode_erd_code, self._converter.from_option_string(hvac_mode)) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + _LOGGER.debug(f"Setting HVAC mode from {self.fan_mode} to {fan_mode}") + if fan_mode != self.fan_mode: + await self.appliance.async_set_erd_value(self.fan_mode_erd_code, self._converter.from_option_string(fan_mode)) + + async def async_set_temperature(self, **kwargs) -> None: + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + _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) \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_select.py b/custom_components/ge_home/entities/common/ge_erd_select.py index e4d8b44..833dea2 100644 --- a/custom_components/ge_home/entities/common/ge_erd_select.py +++ b/custom_components/ge_home/entities/common/ge_erd_select.py @@ -7,17 +7,9 @@ from ...devices import ApplianceApi from .ge_erd_entity import GeErdEntity +from .options_converter import OptionsConverter _LOGGER = logging.getLogger(__name__) - -class OptionsConverter: - @property - def options(self) -> List[str]: - return [] - def from_option_string(self, value: str) -> Any: - return value - def to_option_string(self, value: Any) -> Optional[str]: - return str(value) class GeErdSelect(GeErdEntity, SelectEntity): """ERD-based selector entity""" diff --git a/custom_components/ge_home/entities/common/options_converter.py b/custom_components/ge_home/entities/common/options_converter.py new file mode 100644 index 0000000..5759f46 --- /dev/null +++ b/custom_components/ge_home/entities/common/options_converter.py @@ -0,0 +1,10 @@ +from typing import Any, List, Optional + +class OptionsConverter: + @property + def options(self) -> List[str]: + return [] + def from_option_string(self, value: str) -> Any: + return value + def to_option_string(self, value: Any) -> Optional[str]: + return str(value) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index ba75216..af8e631 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -34,7 +34,7 @@ ) from .devices import ApplianceApi, get_appliance_api_type -PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater", "select"] +PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater", "select", "climate"] _LOGGER = logging.getLogger(__name__) From 9db79646d558685208422b624163b37c07627027 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 3 Aug 2021 16:39:34 -0400 Subject: [PATCH 114/338] - bug fix for dryer entity icon overrides --- custom_components/ge_home/devices/dryer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/devices/dryer.py b/custom_components/ge_home/devices/dryer.py index 0835c74..9387f7e 100644 --- a/custom_components/ge_home/devices/dryer.py +++ b/custom_components/ge_home/devices/dryer.py @@ -20,11 +20,11 @@ def get_all_entities(self) -> List[Entity]: 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_override="mdi:tumble-dryer"), + 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_override="mdi:tumble-dryer"), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, icon_on_override="mdi:tumble-dryer", icon_off_override="mdi:tumble-dryer"), ] dryer_entities = self.get_dryer_entities() From e9eda3f88275990d3f2268ebf5b1e8454e0c5bb6 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 3 Aug 2021 22:22:30 -0400 Subject: [PATCH 115/338] - updated changelog/info --- CHANGELOG.md | 4 ++++ info.md | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04ade4b..1dd3f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.4.1 + +- Fixed an issue with dryer entities causing an error in HA (@steveredden) + ## 0.4.0 - Implemented Laundry Support (@warrenrees, @ssindsd) diff --git a/info.md b/info.md index dd4e829..1396175 100644 --- a/info.md +++ b/info.md @@ -34,7 +34,7 @@ Oven Controls: #### Features -{% if version_installed.split('.') | map('int') < '0.4.1'.split('.') | map('int') %} +{% if version_installed.split('.') | map('int') < '0.4.0'.split('.') | map('int') %} - Implemented Laundry Support (@warrenrees, @ssindsd) - Implemented Water Filter Support (@bendavis, @tumtumsback, @rgabrielson11) @@ -50,6 +50,12 @@ Oven Controls: {% if version_installed.split('.') | map('int') < '0.4.1'.split('.') | map('int') %} +- Fixed an issue with dryer entities causing an error in HA (@steveredden) + +{% endif %} + +{% if version_installed.split('.') | map('int') < '0.4.0'.split('.') | map('int') %} + - Bug fixes for ovens (@TKpizza) - Miscellaneous entity bug fixes/refinements From 70721aac79de067cfba7e28ee1214133fc73db15 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 7 Aug 2021 13:15:27 -0400 Subject: [PATCH 116/338] - updated base climate entity - modified entities to support serial/mac for uniqueness (mac when serial not available) --- .../ge_home/entities/common/ge_climate.py | 57 +++++++++++++------ .../ge_home/entities/common/ge_entity.py | 10 ++++ .../ge_home/entities/common/ge_erd_entity.py | 4 +- .../entities/common/ge_water_heater.py | 4 +- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 9d2d0e9..30fdc4b 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -1,18 +1,18 @@ import logging from typing import Any, List, Optional -from gehomesdk.erd.erd_codes import ErdCodeType from homeassistant.components.climate import ClimateEntity from homeassistant.const import ( ATTR_TEMPERATURE, TEMP_FAHRENHEIT, - TEMP_CELSIUS + TEMP_CELSIUS, ) from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE + SUPPORT_FAN_MODE, + HVAC_MODE_OFF ) -from gehomesdk import ErdCode, ErdMeasurementUnits +from gehomesdk import ErdCode, ErdCodeType, ErdMeasurementUnits, ErdOnOff from ...const import DOMAIN from ...devices import ApplianceApi from .ge_erd_entity import GeEntity @@ -28,28 +28,35 @@ class GeClimate(GeEntity, ClimateEntity): def __init__( self, api: ApplianceApi, - current_temperature_erd_code: ErdCodeType, - target_temperature_erd_code: ErdCodeType, - hvac_mode_erd_code: ErdCodeType, - fan_mode_erd_code: ErdCodeType, hvac_mode_converter: OptionsConverter, - fan_mode_converter: OptionsConverter + fan_mode_converter: OptionsConverter, + 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._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) - self._hvac_mode_converter = hvac_mode_converter - self._fan_mode_converter = fan_mode_converter @property def unique_id(self) -> str: - return f"{DOMAIN}_{self.serial_number}_climate" + return f"{DOMAIN}_{self.serial_or_mac}_climate" @property def name(self) -> Optional[str]: - return f"{self.serial_number} Climate" + 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): @@ -78,6 +85,10 @@ def temperature_unit(self): 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)) @@ -88,11 +99,14 @@ def current_temperature(self) -> Optional[float]: @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 self._hvac_mode_converter.options + return [HVAC_MODE_OFF] + self._hvac_mode_converter.options @property def fan_mode(self): @@ -105,12 +119,21 @@ def fan_modes(self) -> List[str]: 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: - await self.appliance.async_set_erd_value(self.hvac_mode_erd_code, self._converter.from_option_string(hvac_mode)) + if hvac_mode == HVAC_MODE_OFF: + await self.appliance.async_set_erd_value(self.power_status_erd_code, ErdOnOff.OFF) + else: + 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 HVAC mode from {self.fan_mode} to {fan_mode}") + _LOGGER.debug(f"Setting Fan mode from {self.fan_mode} to {fan_mode}") if fan_mode != self.fan_mode: - await self.appliance.async_set_erd_value(self.fan_mode_erd_code, self._converter.from_option_string(fan_mode)) + await self.appliance.async_set_erd_value( + self.fan_mode_erd_code, + self._fan_mode_converter.from_option_string(fan_mode) + ) async def async_set_temperature(self, **kwargs) -> None: temperature = kwargs.get(ATTR_TEMPERATURE) diff --git a/custom_components/ge_home/entities/common/ge_entity.py b/custom_components/ge_home/entities/common/ge_entity.py index ed9e090..977bf9a 100644 --- a/custom_components/ge_home/entities/common/ge_entity.py +++ b/custom_components/ge_home/entities/common/ge_entity.py @@ -36,6 +36,16 @@ def available(self) -> bool: def appliance(self) -> GeAppliance: return self.api.appliance + @property + def mac_addr(self) -> str: + return self.api.appliance.mac_addr + + @property + def serial_or_mac(self) -> str: + if self.serial_number and not self.serial_number.isspace(): + return self.serial_number + return self.mac_addr + @property def name(self) -> Optional[str]: raise NotImplementedError diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index b14236d..de5cfba 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -53,11 +53,11 @@ def name(self) -> Optional[str]: erd_string = self._erd_override erd_title = " ".join(erd_string.split("_")).title() - return f"{self.serial_number} {erd_title}" + return f"{self.serial_or_mac} {erd_title}" @property def unique_id(self) -> Optional[str]: - return f"{DOMAIN}_{self.serial_number}_{self.erd_string.lower()}" + return f"{DOMAIN}_{self.serial_or_mac}_{self.erd_string.lower()}" def _stringify(self, value: any, **kwargs) -> Optional[str]: """Stringify a value""" diff --git a/custom_components/ge_home/entities/common/ge_water_heater.py b/custom_components/ge_home/entities/common/ge_water_heater.py index 1a7fb68..e0d3e04 100644 --- a/custom_components/ge_home/entities/common/ge_water_heater.py +++ b/custom_components/ge_home/entities/common/ge_water_heater.py @@ -26,11 +26,11 @@ def operation_list(self) -> List[str]: @property def unique_id(self) -> str: - return f"{DOMAIN}_{self.serial_number}_{self.heater_type}" + return f"{DOMAIN}_{self.serial_or_mac}_{self.heater_type}" @property def name(self) -> Optional[str]: - return f"{self.serial_number} {self.heater_type.title()}" + return f"{self.serial_or_mac} {self.heater_type.title()}" @property def temperature_unit(self): From 284ae75343a6c11e1afb3bc0b6227689fc79409a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 7 Aug 2021 14:17:57 -0400 Subject: [PATCH 117/338] - added multiple fan mode converters for generic climate entity - added the wac climate entity --- .../ge_home/entities/ac/ge_wac_climate.py | 87 +++++++++++++++++++ .../ge_home/entities/common/ge_climate.py | 19 +++- 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 custom_components/ge_home/entities/ac/ge_wac_climate.py diff --git a/custom_components/ge_home/entities/ac/ge_wac_climate.py b/custom_components/ge_home/entities/ac/ge_wac_climate.py new file mode 100644 index 0000000..dbb1258 --- /dev/null +++ b/custom_components/ge_home/entities/ac/ge_wac_climate.py @@ -0,0 +1,87 @@ +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, ErdAcFanSetting +from ...devices import ApplianceApi +from ..common import GeClimate, OptionsConverter + +_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 WacFanModeOptionsConverter(OptionsConverter): + @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 ErdAcFanSetting.AUTO + 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 ErdAcFanSetting.AUTO.stringify() + +class WacFanOnlyFanModeOptionsConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [i.stringify() for i in [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 ErdAcFanSetting.LOW + def to_option_string(self, value: Any) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdAcFanSetting.LOW.stringify() + +class GeWacClimate(GeClimate): + """Class for Window AC units""" + def __init__(self, api: ApplianceApi): + super().__init__(api, WacHvacModeOptionsConverter(), WacFanModeOptionsConverter(), WacFanOnlyFanModeOptionsConverter()) diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 30fdc4b..2280e8e 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -8,6 +8,7 @@ TEMP_CELSIUS, ) from homeassistant.components.climate.const import ( + HVAC_MODE_FAN_ONLY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, HVAC_MODE_OFF @@ -29,7 +30,8 @@ def __init__( self, api: ApplianceApi, hvac_mode_converter: OptionsConverter, - fan_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, @@ -40,6 +42,10 @@ def __init__( 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) @@ -110,10 +116,14 @@ def hvac_modes(self) -> List[str]: @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: @@ -130,9 +140,14 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: 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, - self._fan_mode_converter.from_option_string(fan_mode) + converter.from_option_string(fan_mode) ) async def async_set_temperature(self, **kwargs) -> None: From f4f402e70abdab743c3af9676fbd0947c81f1006 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 7 Aug 2021 14:36:40 -0400 Subject: [PATCH 118/338] - fixed advantium imports - added wac support to the appliance creation - added ac sensor icon to base entity --- custom_components/ge_home/devices/__init__.py | 3 ++ .../ge_home/devices/advantium.py | 3 +- custom_components/ge_home/devices/wac.py | 33 +++++++++++++++++++ .../ge_home/entities/__init__.py | 2 ++ .../ge_home/entities/ac/__init__.py | 1 + .../ge_home/entities/advantium/__init__.py | 1 + .../ge_home/entities/common/ge_erd_entity.py | 4 ++- 7 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 custom_components/ge_home/devices/wac.py create mode 100644 custom_components/ge_home/entities/ac/__init__.py diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 6034f3b..e6eab58 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -1,3 +1,4 @@ +from custom_components.ge_home.devices.wac import WacApi import logging from typing import Type @@ -35,6 +36,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return WaterFilterApi if appliance_type == ErdApplianceType.ADVANTIUM: return AdvantiumApi + if appliance_type == ErdApplianceType.AIR_CONDITIONER: + return WacApi # Fallback return ApplianceApi diff --git a/custom_components/ge_home/devices/advantium.py b/custom_components/ge_home/devices/advantium.py index 837b954..828192a 100644 --- a/custom_components/ge_home/devices/advantium.py +++ b/custom_components/ge_home/devices/advantium.py @@ -1,4 +1,3 @@ -from custom_components.ge_home.entities.advantium.ge_advantium import GeAdvantium import logging from typing import List @@ -6,7 +5,7 @@ from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdPropertyBinarySensor, UPPER_OVEN +from ..entities import GeAdvantium, GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdPropertyBinarySensor, UPPER_OVEN _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/ge_home/devices/wac.py b/custom_components/ge_home/devices/wac.py new file mode 100644 index 0000000..6f50b25 --- /dev/null +++ b/custom_components/ge_home/devices/wac.py @@ -0,0 +1,33 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeWacClimate, GeErdSensor, GeErdBinarySensor, GeErdSwitch + +_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, 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), + ] + entities = base_entities + wac_entities + return entities + diff --git a/custom_components/ge_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index 2fc8567..245757e 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -3,3 +3,5 @@ from .fridge import * from .oven import * from .water_filter import * +from .advantium import * +from .ac import * \ No newline at end of file diff --git a/custom_components/ge_home/entities/ac/__init__.py b/custom_components/ge_home/entities/ac/__init__.py new file mode 100644 index 0000000..09ea404 --- /dev/null +++ b/custom_components/ge_home/entities/ac/__init__.py @@ -0,0 +1 @@ +from .ge_wac_climate import GeWacClimate \ No newline at end of file diff --git a/custom_components/ge_home/entities/advantium/__init__.py b/custom_components/ge_home/entities/advantium/__init__.py index e69de29..a4cfe30 100644 --- a/custom_components/ge_home/entities/advantium/__init__.py +++ b/custom_components/ge_home/entities/advantium/__init__.py @@ -0,0 +1 @@ +from .ge_advantium import GeAdvantium \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index de5cfba..aae33fc 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -130,6 +130,8 @@ def _get_icon(self): if self.erd_code_class == ErdCodeClass.FLOW_RATE: return "mdi:water" if self.erd_code_class == ErdCodeClass.LIQUID_VOLUME: - return "mdi:water" + return "mdi:water" + if self.erd_code_class == ErdCodeClass.AC_SENSOR: + return "mdi:air-conditioner" return None From ad3762955bb85b69173cf418d318e5de3ae681de Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 7 Aug 2021 14:46:29 -0400 Subject: [PATCH 119/338] - added min/max temps to climate --- .../ge_home/entities/common/ge_climate.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 2280e8e..32c18e1 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -103,6 +103,14 @@ def target_temperature(self) -> Optional[float]: 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: @@ -151,9 +159,20 @@ async def async_set_fan_mode(self, fan_mode: str) -> None: ) 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) \ No newline at end of file + await self.appliance.async_set_erd_value(self.target_temperature_erd_code, temperature) + + 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) \ No newline at end of file From ee63c0e365990ce744824446c9dda3b59beb21c8 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 7 Aug 2021 14:53:36 -0400 Subject: [PATCH 120/338] - version bump - documentation updates --- CHANGELOG.md | 7 +++++++ custom_components/ge_home/manifest.json | 4 ++-- info.md | 6 ++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd3f11..2ad850b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.4.x + +- Added support for Window A/C units (@mbrentrowe, @swcrawford1) +- Fixed multiple binary sensors (bad conversion from enum) (@steveredden) +- Fixed delay time interpretation for laundry (@steveredden, @sweichbr) +- Enabled support for appliances without serial numbers + ## 0.4.1 - Fixed an issue with dryer entities causing an error in HA (@steveredden) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 143c0f9..714a51a 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.0","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.2","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.4.0" + "version": "0.4.2" } diff --git a/info.md b/info.md index 1396175..d080eab 100644 --- a/info.md +++ b/info.md @@ -30,6 +30,12 @@ Oven Controls: #### Breaking Changes +{% if version_installed.split('.') | map('int') < '0.4.0'.split('.') | map('int') %} + +- Laundry support changes will cause entity names to be different, you will need to fix in HA + +{% endif %} + #### Changes #### Features From 71264ccf11ba2a3f422f32d4f37bef89c6ad7719 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 7 Aug 2021 19:06:51 -0400 Subject: [PATCH 121/338] - fixed import circular reference --- custom_components/ge_home/devices/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index e6eab58..0cceff7 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -1,4 +1,3 @@ -from custom_components.ge_home.devices.wac import WacApi import logging from typing import Type @@ -13,6 +12,7 @@ from .washer_dryer import WasherDryerApi from .water_filter import WaterFilterApi from .advantium import AdvantiumApi +from .wac import WacApi _LOGGER = logging.getLogger(__name__) From d89ba016efc5fffd98a9218f6869f5f0c956e325 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 7 Aug 2021 19:48:54 -0400 Subject: [PATCH 122/338] - modified switches to support ErdOnOff instead of just bools - modified climate control to turn on the A/C unit when setting modes - added async on/off methods to climate --- custom_components/ge_home/devices/wac.py | 4 ++-- .../ge_home/entities/common/__init__.py | 2 +- .../ge_home/entities/common/ge_climate.py | 12 ++++++++++++ .../ge_home/entities/common/ge_erd_switch.py | 19 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/devices/wac.py b/custom_components/ge_home/devices/wac.py index 6f50b25..585370d 100644 --- a/custom_components/ge_home/devices/wac.py +++ b/custom_components/ge_home/devices/wac.py @@ -5,7 +5,7 @@ from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeWacClimate, GeErdSensor, GeErdBinarySensor, GeErdSwitch +from ..entities import GeWacClimate, GeErdSensor, GeErdBinarySensor, GeErdOnOffSwitch _LOGGER = logging.getLogger(__name__) @@ -23,7 +23,7 @@ def get_all_entities(self) -> List[Entity]: 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, icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + GeErdOnOffSwitch(self, ErdCode.AC_POWER_STATUS, 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), diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 3bb073a..10b3c1e 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -5,7 +5,7 @@ from .ge_erd_property_binary_sensor import GeErdPropertyBinarySensor from .ge_erd_sensor import GeErdSensor from .ge_erd_property_sensor import GeErdPropertySensor -from .ge_erd_switch import GeErdSwitch +from .ge_erd_switch import GeErdSwitch, GeErdOnOffSwitch from .ge_water_heater import GeWaterHeater from .ge_erd_select import GeErdSelect from .ge_climate import GeClimate \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 32c18e1..21e6549 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -2,6 +2,7 @@ from typing import Any, List, Optional from homeassistant.components.climate import ClimateEntity +from homeassistant.components.switch import is_on from homeassistant.const import ( ATTR_TEMPERATURE, TEMP_FAHRENHEIT, @@ -140,6 +141,11 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: 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) @@ -171,6 +177,12 @@ async def async_set_temperature(self, **kwargs) -> None: 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) diff --git a/custom_components/ge_home/entities/common/ge_erd_switch.py b/custom_components/ge_home/entities/common/ge_erd_switch.py index 1afc311..d1dcf9e 100644 --- a/custom_components/ge_home/entities/common/ge_erd_switch.py +++ b/custom_components/ge_home/entities/common/ge_erd_switch.py @@ -1,5 +1,6 @@ import logging +from gehomesdk.erd.values import ErdOnOff from homeassistant.components.switch import SwitchEntity from .ge_erd_binary_sensor import GeErdBinarySensor @@ -24,3 +25,21 @@ async def async_turn_off(self, **kwargs): _LOGGER.debug(f"Turning on {self.unique_id}") await self.appliance.async_set_erd_value(self.erd_code, False) +class GeErdOnOffSwitch(GeErdBinarySensor, SwitchEntity): + """Switches for boolean ERD codes.""" + device_class = "switch" + + @property + def is_on(self) -> bool: + """Return True if switch is on.""" + return self.appliance.get_erd_value(self.erd_code) == ErdOnOff.ON + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + _LOGGER.debug(f"Turning on {self.unique_id}") + await self.appliance.async_set_erd_value(self.erd_code, ErdOnOff.ON) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + _LOGGER.debug(f"Turning on {self.unique_id}") + await self.appliance.async_set_erd_value(self.erd_code, ErdOnOff.OFF) \ No newline at end of file From 598bdbb5e175a35ecdf1e19bca37d28b3f53f556 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 7 Aug 2021 20:34:26 -0400 Subject: [PATCH 123/338] - genericized the ErdSwitch - import cleanup --- custom_components/ge_home/devices/wac.py | 4 +-- .../ge_home/entities/common/__init__.py | 3 +- .../ge_home/entities/common/bool_converter.py | 19 +++++++++++ .../ge_home/entities/common/ge_climate.py | 3 +- .../ge_home/entities/common/ge_erd_switch.py | 32 ++++++------------- 5 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 custom_components/ge_home/entities/common/bool_converter.py diff --git a/custom_components/ge_home/devices/wac.py b/custom_components/ge_home/devices/wac.py index 585370d..c03d51d 100644 --- a/custom_components/ge_home/devices/wac.py +++ b/custom_components/ge_home/devices/wac.py @@ -5,7 +5,7 @@ from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeWacClimate, GeErdSensor, GeErdBinarySensor, GeErdOnOffSwitch +from ..entities import GeWacClimate, GeErdSensor, GeErdBinarySensor, GeErdSwitch, ErdOnOffBoolConverter _LOGGER = logging.getLogger(__name__) @@ -23,7 +23,7 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE), GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), GeErdSensor(self, ErdCode.AC_OPERATION_MODE), - GeErdOnOffSwitch(self, ErdCode.AC_POWER_STATUS, icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + 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), diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 10b3c1e..691629a 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -1,11 +1,12 @@ from .options_converter import OptionsConverter +from .bool_converter import BoolConverter, ErdOnOffBoolConverter from .ge_entity import GeEntity from .ge_erd_entity import GeErdEntity from .ge_erd_binary_sensor import GeErdBinarySensor from .ge_erd_property_binary_sensor import GeErdPropertyBinarySensor from .ge_erd_sensor import GeErdSensor from .ge_erd_property_sensor import GeErdPropertySensor -from .ge_erd_switch import GeErdSwitch, GeErdOnOffSwitch +from .ge_erd_switch import GeErdSwitch from .ge_water_heater import GeWaterHeater from .ge_erd_select import GeErdSelect from .ge_climate import GeClimate \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/bool_converter.py b/custom_components/ge_home/entities/common/bool_converter.py new file mode 100644 index 0000000..b8dcd14 --- /dev/null +++ b/custom_components/ge_home/entities/common/bool_converter.py @@ -0,0 +1,19 @@ +from typing import Any + +from gehomesdk import ErdOnOff + +class BoolConverter: + def boolify(self, value: Any) -> bool: + return bool(value) + def true_value(self) -> Any: + return True + def false_value(self) -> Any: + return False + +class ErdOnOffBoolConverter(BoolConverter): + def boolify(self, value: ErdOnOff) -> bool: + return value.boolify() + def true_value(self) -> Any: + return ErdOnOff.ON + def false_value(self) -> Any: + return ErdOnOff.OFF \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 21e6549..5e99785 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -1,8 +1,7 @@ import logging -from typing import Any, List, Optional +from typing import List, Optional from homeassistant.components.climate import ClimateEntity -from homeassistant.components.switch import is_on from homeassistant.const import ( ATTR_TEMPERATURE, TEMP_FAHRENHEIT, diff --git a/custom_components/ge_home/entities/common/ge_erd_switch.py b/custom_components/ge_home/entities/common/ge_erd_switch.py index d1dcf9e..cf39f9a 100644 --- a/custom_components/ge_home/entities/common/ge_erd_switch.py +++ b/custom_components/ge_home/entities/common/ge_erd_switch.py @@ -1,8 +1,11 @@ import logging -from gehomesdk.erd.values import ErdOnOff +from gehomesdk import ErdCodeType from homeassistant.components.switch import SwitchEntity + +from ...devices import ApplianceApi from .ge_erd_binary_sensor import GeErdBinarySensor +from .bool_converter import BoolConverter _LOGGER = logging.getLogger(__name__) @@ -10,36 +13,21 @@ class GeErdSwitch(GeErdBinarySensor, SwitchEntity): """Switches for boolean ERD codes.""" device_class = "switch" - @property - def is_on(self) -> bool: - """Return True if switch is on.""" - return bool(self.appliance.get_erd_value(self.erd_code)) - - async def async_turn_on(self, **kwargs): - """Turn the switch on.""" - _LOGGER.debug(f"Turning on {self.unique_id}") - await self.appliance.async_set_erd_value(self.erd_code, True) - - async def async_turn_off(self, **kwargs): - """Turn the switch off.""" - _LOGGER.debug(f"Turning on {self.unique_id}") - await self.appliance.async_set_erd_value(self.erd_code, False) - -class GeErdOnOffSwitch(GeErdBinarySensor, SwitchEntity): - """Switches for boolean ERD codes.""" - device_class = "switch" + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, bool_converter: BoolConverter = BoolConverter(), erd_override: str = None, icon_on_override: str = None, icon_off_override: str = None, device_class_override: str = None): + super().__init__(api, erd_code, erd_override, icon_on_override, icon_off_override, device_class_override) + self._converter = bool_converter @property def is_on(self) -> bool: """Return True if switch is on.""" - return self.appliance.get_erd_value(self.erd_code) == ErdOnOff.ON + return self._converter.boolify(self.appliance.get_erd_value(self.erd_code)) async def async_turn_on(self, **kwargs): """Turn the switch on.""" _LOGGER.debug(f"Turning on {self.unique_id}") - await self.appliance.async_set_erd_value(self.erd_code, ErdOnOff.ON) + await self.appliance.async_set_erd_value(self.erd_code, self._converter.true_value()) async def async_turn_off(self, **kwargs): """Turn the switch off.""" _LOGGER.debug(f"Turning on {self.unique_id}") - await self.appliance.async_set_erd_value(self.erd_code, ErdOnOff.OFF) \ No newline at end of file + await self.appliance.async_set_erd_value(self.erd_code, self._converter.false_value()) From ceb24a8dbfec9813da89e708bc76ba355b44c8b4 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 8 Aug 2021 11:19:47 -0400 Subject: [PATCH 124/338] - abstracted ac fan modes so that they can be shared - added initial split ac climate entity support --- .../ge_home/entities/ac/fan_mode_options.py | 50 +++++++++++ .../ge_home/entities/ac/ge_sac_climate.py | 88 +++++++++++++++++++ .../ge_home/entities/ac/ge_wac_climate.py | 50 +---------- 3 files changed, 142 insertions(+), 46 deletions(-) create mode 100644 custom_components/ge_home/entities/ac/fan_mode_options.py create mode 100644 custom_components/ge_home/entities/ac/ge_sac_climate.py diff --git a/custom_components/ge_home/entities/ac/fan_mode_options.py b/custom_components/ge_home/entities/ac/fan_mode_options.py new file mode 100644 index 0000000..0bf78cd --- /dev/null +++ b/custom_components/ge_home/entities/ac/fan_mode_options.py @@ -0,0 +1,50 @@ +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_home/entities/ac/ge_sac_climate.py b/custom_components/ge_home/entities/ac/ge_sac_climate.py new file mode 100644 index 0000000..f5309fe --- /dev/null +++ b/custom_components/ge_home/entities/ac/ge_sac_climate.py @@ -0,0 +1,88 @@ +import logging +from typing import Any, List, Optional +from gehomesdk.erd.values.ac.sac_enums import ErdSacTargetTemperatureRange + +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 +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 Window AC units""" + def __init__(self, api: ApplianceApi): + #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) + + #initialize the climate control + super().__init__(api, SacHvacModeOptionsConverter(self._modes), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) + + @property + def temperature_unit(self): + #SAC appears to be hard coded to use Fahrenheit internally, no matter what the display shows + return TEMP_FAHRENHEIT + + @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_home/entities/ac/ge_wac_climate.py b/custom_components/ge_home/entities/ac/ge_wac_climate.py index dbb1258..a7b3980 100644 --- a/custom_components/ge_home/entities/ac/ge_wac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_wac_climate.py @@ -6,9 +6,10 @@ HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY, ) -from gehomesdk import ErdAcOperationMode, ErdAcFanSetting +from gehomesdk import ErdAcOperationMode from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter +from .fan_mode_options import AcFanModeOptionsConverter, AcFanOnlyFanModeOptionsConverter _LOGGER = logging.getLogger(__name__) @@ -37,51 +38,8 @@ def to_option_string(self, value: Any) -> Optional[str]: except: _LOGGER.warn(f"Could not determine operation mode mapping for {value}") return HVAC_MODE_COOL - -class WacFanModeOptionsConverter(OptionsConverter): - @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 ErdAcFanSetting.AUTO - 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 ErdAcFanSetting.AUTO.stringify() - -class WacFanOnlyFanModeOptionsConverter(OptionsConverter): - @property - def options(self) -> List[str]: - return [i.stringify() for i in [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 ErdAcFanSetting.LOW - def to_option_string(self, value: Any) -> Optional[str]: - try: - if value is not None: - return value.stringify() - except: - pass - return ErdAcFanSetting.LOW.stringify() - + class GeWacClimate(GeClimate): """Class for Window AC units""" def __init__(self, api: ApplianceApi): - super().__init__(api, WacHvacModeOptionsConverter(), WacFanModeOptionsConverter(), WacFanOnlyFanModeOptionsConverter()) + super().__init__(api, WacHvacModeOptionsConverter(), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) From 2cc5f3a361ac397e16e72def24052025a160b47a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 8 Aug 2021 11:39:32 -0400 Subject: [PATCH 125/338] - added support for split AC devices --- custom_components/ge_home/devices/__init__.py | 3 ++ custom_components/ge_home/devices/sac.py | 37 +++++++++++++++++++ custom_components/ge_home/devices/wac.py | 2 +- .../ge_home/entities/ac/__init__.py | 3 +- 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 custom_components/ge_home/devices/sac.py diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 0cceff7..8a1236e 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -13,6 +13,7 @@ from .water_filter import WaterFilterApi from .advantium import AdvantiumApi from .wac import WacApi +from .sac import SacApi _LOGGER = logging.getLogger(__name__) @@ -38,6 +39,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return AdvantiumApi if appliance_type == ErdApplianceType.AIR_CONDITIONER: return WacApi + if appliance_type == ErdApplianceType.SPLIT_AIR_CONDITIONER: + return SacApi # Fallback return ApplianceApi diff --git a/custom_components/ge_home/devices/sac.py b/custom_components/ge_home/devices/sac.py new file mode 100644 index 0000000..2edfac6 --- /dev/null +++ b/custom_components/ge_home/devices/sac.py @@ -0,0 +1,37 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeSacClimate, GeErdSensor, GeErdBinarySensor, 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): + 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): + 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_home/devices/wac.py b/custom_components/ge_home/devices/wac.py index c03d51d..c3c049e 100644 --- a/custom_components/ge_home/devices/wac.py +++ b/custom_components/ge_home/devices/wac.py @@ -11,7 +11,7 @@ class WacApi(ApplianceApi): - """API class for window AC objects""" + """API class for Window AC objects""" APPLIANCE_TYPE = ErdApplianceType.AIR_CONDITIONER def get_all_entities(self) -> List[Entity]: diff --git a/custom_components/ge_home/entities/ac/__init__.py b/custom_components/ge_home/entities/ac/__init__.py index 09ea404..79bc17f 100644 --- a/custom_components/ge_home/entities/ac/__init__.py +++ b/custom_components/ge_home/entities/ac/__init__.py @@ -1 +1,2 @@ -from .ge_wac_climate import GeWacClimate \ No newline at end of file +from .ge_wac_climate import GeWacClimate +from .ge_sac_climate import GeSacClimate \ No newline at end of file From fda58254587f8422be3eb4dde078910a0e7b9b4b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 8 Aug 2021 11:39:58 -0400 Subject: [PATCH 126/338] - version bump --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 714a51a..4cb82cc 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==0.4.2","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.4.2" + "version": "0.4.3" } From 2e9bc2c726a09aefd7c889de554e941271477a14 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 8 Aug 2021 11:44:47 -0400 Subject: [PATCH 127/338] - documentation updates --- CHANGELOG.md | 1 + info.md | 12 ++++-------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ad850b..68f6b1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.4.x +- Added support for Split A/C units (@RobertusIT) - Added support for Window A/C units (@mbrentrowe, @swcrawford1) - Fixed multiple binary sensors (bad conversion from enum) (@steveredden) - Fixed delay time interpretation for laundry (@steveredden, @sweichbr) diff --git a/info.md b/info.md index d080eab..181c542 100644 --- a/info.md +++ b/info.md @@ -31,17 +31,18 @@ Oven Controls: #### Breaking Changes {% if version_installed.split('.') | map('int') < '0.4.0'.split('.') | map('int') %} - - Laundry support changes will cause entity names to be different, you will need to fix in HA - {% endif %} #### Changes #### Features -{% if version_installed.split('.') | map('int') < '0.4.0'.split('.') | map('int') %} +{% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} +- Support for Split and Window AC units (@swcrawford1, @mbrentrowe, @RobertusIT) +{% endif %} +{% if version_installed.split('.') | map('int') < '0.4.0'.split('.') | map('int') %} - Implemented Laundry Support (@warrenrees, @ssindsd) - Implemented Water Filter Support (@bendavis, @tumtumsback, @rgabrielson11) - Implemented Initial Advantium Support (@ssinsd) @@ -49,22 +50,17 @@ Oven Controls: - Additional dishwasher functionality (@ssinsd) - Introduced new select entity (@bendavis) - Integrated new version of SDK - {% endif %} #### Bugfixes {% if version_installed.split('.') | map('int') < '0.4.1'.split('.') | map('int') %} - - Fixed an issue with dryer entities causing an error in HA (@steveredden) - {% endif %} {% if version_installed.split('.') | map('int') < '0.4.0'.split('.') | map('int') %} - - Bug fixes for ovens (@TKpizza) - Miscellaneous entity bug fixes/refinements - {% endif %} {% endif %} From 6bd56b4dceda4b01a62dbca8390cdd6484e0e2c4 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 8 Aug 2021 16:58:08 -0400 Subject: [PATCH 128/338] - updated documentation - added icon for climate entities --- CHANGELOG.md | 2 +- README.md | 4 ++++ .../ge_home/entities/common/ge_climate.py | 5 ++++- img/ac_controls.png | Bin 0 -> 14007 bytes info.md | 10 +++++++++- 5 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 img/ac_controls.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f6b1b..7ca6828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # GE Home Appliances (SmartHQ) Changelog -## 0.4.x +## 0.4.3 - Added support for Split A/C units (@RobertusIT) - Added support for Window A/C units (@mbrentrowe, @swcrawford1) diff --git a/README.md b/README.md index e2f1517..bfd268e 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ Oven Controls: ![Fridge controls](https://raw.githubusercontent.com/simbaja/ha_components/master/img/oven_controls.png) +A/C Controls: + +![A/C controls](https://raw.githubusercontent.com/simbaja/ha_components/master/img/ac_controls.png) + ## Installation (Manual) 1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 5e99785..2b28532 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -186,4 +186,7 @@ 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) \ No newline at end of file + return (temperature_f - 32.0) * (5/9) + + def _get_icon(self) -> Optional[str]: + return "mdi:air-conditioner" \ No newline at end of file diff --git a/img/ac_controls.png b/img/ac_controls.png new file mode 100644 index 0000000000000000000000000000000000000000..ee1e2a5cbf18864e1873c154dcb5b5dc6b20269d GIT binary patch literal 14007 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 literal 0 HcmV?d00001 diff --git a/info.md b/info.md index 181c542..51933d9 100644 --- a/info.md +++ b/info.md @@ -24,6 +24,10 @@ Oven Controls: ![Fridge controls](https://raw.githubusercontent.com/simbaja/ha_components/master/img/oven_controls.png) +A/C Controls: + +![A/C controls](https://raw.githubusercontent.com/simbaja/ha_components/master/img/ac_controls.png) + {% if installed %} ### Changes as compared to your installed version: @@ -31,7 +35,7 @@ Oven Controls: #### Breaking Changes {% if version_installed.split('.') | map('int') < '0.4.0'.split('.') | map('int') %} -- Laundry support changes will cause entity names to be different, you will need to fix in HA +- Laundry support changes will cause entity names to be different, you will need to fix in HA (uninstall, reboot, delete leftover entitites, install, reboot) {% endif %} #### Changes @@ -54,6 +58,10 @@ Oven Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} +- Bug fixes for laundry (@steveredden, @sweichbr) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.4.1'.split('.') | map('int') %} - Fixed an issue with dryer entities causing an error in HA (@steveredden) {% endif %} From 442850aad0fac7041d46f04386feb028a35d1fe3 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 9 Aug 2021 11:54:15 -0400 Subject: [PATCH 129/338] - fixed an order of operations issue with the split ac climate control --- custom_components/ge_home/entities/ac/ge_sac_climate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/entities/ac/ge_sac_climate.py b/custom_components/ge_home/entities/ac/ge_sac_climate.py index f5309fe..8777019 100644 --- a/custom_components/ge_home/entities/ac/ge_sac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_sac_climate.py @@ -61,12 +61,15 @@ def to_option_string(self, value: Any) -> Optional[str]: class GeSacClimate(GeClimate): """Class for Window 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) - #initialize the climate control - super().__init__(api, SacHvacModeOptionsConverter(self._modes), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) @property def temperature_unit(self): From 68ef65ccdc613cda0a347af13e42c10d451b67bc Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 9 Aug 2021 12:33:52 -0400 Subject: [PATCH 130/338] - fixed missing entities for Split A/C - fixed some iconography --- custom_components/ge_home/devices/sac.py | 4 ++-- custom_components/ge_home/entities/common/ge_erd_entity.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/devices/sac.py b/custom_components/ge_home/devices/sac.py index 2edfac6..a183fe9 100644 --- a/custom_components/ge_home/devices/sac.py +++ b/custom_components/ge_home/devices/sac.py @@ -27,9 +27,9 @@ def get_all_entities(self) -> List[Entity]: ] if self.has_erd_code(ErdCode.SAC_SLEEP_MODE): - GeErdSwitch(self, ErdCode.SAC_SLEEP_MODE, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:sleep", icon_off_override="mdi:sleep-off"), + 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): - 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"), + 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 diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index aae33fc..ab45351 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -132,6 +132,8 @@ def _get_icon(self): if self.erd_code_class == ErdCodeClass.LIQUID_VOLUME: return "mdi:water" if self.erd_code_class == ErdCodeClass.AC_SENSOR: - return "mdi:air-conditioner" + return "mdi:air-conditioner" + if self.erd_code_class == ErdCodeClass.TEMPERATURE_CONTROL: + return "mdi:thermometer" return None From a25ee66b10599c8cad8ea2f3190ac49f7f9cb5bf Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 10 Aug 2021 13:31:37 -0400 Subject: [PATCH 131/338] - updated interpretation of WAC Demand Power - updated SDK version to resolve a few issues --- custom_components/ge_home/devices/wac.py | 2 +- custom_components/ge_home/entities/common/ge_erd_sensor.py | 6 ++++++ custom_components/ge_home/manifest.json | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/devices/wac.py b/custom_components/ge_home/devices/wac.py index c3c049e..6208a82 100644 --- a/custom_components/ge_home/devices/wac.py +++ b/custom_components/ge_home/devices/wac.py @@ -26,7 +26,7 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdSensor(self, ErdCode.WAC_DEMAND_RESPONSE_POWER, uom_override="kW"), ] entities = base_entities + wac_entities return entities diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index a54f91f..77800b3 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -1,6 +1,8 @@ from typing import Optional from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER_FACTOR, @@ -90,6 +92,10 @@ def _get_device_class(self) -> Optional[str]: 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 diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 4cb82cc..69bcb7e 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.2","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.3","magicattr==0.1.5"], "codeowners": ["@simbaja"], "version": "0.4.3" } From 25af2b08ea6294899f1d14ae9bbe68829273aef7 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 10 Aug 2021 13:35:30 -0400 Subject: [PATCH 132/338] - updated documentation --- CHANGELOG.md | 4 +++- info.md | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ca6828..af4ada4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,13 @@ ## 0.4.3 +- Enabled support for appliances without serial numbers - Added support for Split A/C units (@RobertusIT) - Added support for Window A/C units (@mbrentrowe, @swcrawford1) - Fixed multiple binary sensors (bad conversion from enum) (@steveredden) - Fixed delay time interpretation for laundry (@steveredden, @sweichbr) -- Enabled support for appliances without serial numbers +- Fixed startup issue when encountering an unknown unit type(@chansearrington, @opie546) +- Fixed interpretation of A/C demand response power (@garulf) ## 0.4.1 diff --git a/info.md b/info.md index 51933d9..0f27978 100644 --- a/info.md +++ b/info.md @@ -60,6 +60,8 @@ A/C Controls: {% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} - Bug fixes for laundry (@steveredden, @sweichbr) +- Fixed startup issue when encountering an unknown unit type(@chansearrington, @opie546) +- Fixed interpretation of A/C demand response power (@garulf) {% endif %} {% if version_installed.split('.') | map('int') < '0.4.1'.split('.') | map('int') %} From c6f30085ffd90275736f51f1798571122c0206d9 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 17 Aug 2021 22:30:01 -0400 Subject: [PATCH 133/338] - initial portable A/C support --- custom_components/ge_home/devices/__init__.py | 3 ++ custom_components/ge_home/devices/pac.py | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 custom_components/ge_home/devices/pac.py diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 8a1236e..b6a89f6 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -14,6 +14,7 @@ from .advantium import AdvantiumApi from .wac import WacApi from .sac import SacApi +from .pac import PacApi _LOGGER = logging.getLogger(__name__) @@ -41,6 +42,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return WacApi if appliance_type == ErdApplianceType.SPLIT_AIR_CONDITIONER: return SacApi + if appliance_type == ErdApplianceType.PORTABLE_AIR_CONDITIONER: + return PacApi # Fallback return ApplianceApi diff --git a/custom_components/ge_home/devices/pac.py b/custom_components/ge_home/devices/pac.py new file mode 100644 index 0000000..38b6d0f --- /dev/null +++ b/custom_components/ge_home/devices/pac.py @@ -0,0 +1,31 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeSacClimate, GeErdSensor, GeErdBinarySensor, 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 = [ + 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"), + ] + + entities = base_entities + pac_entities + return entities + From e8ef5daa12dcc8dfa4439164414a483f9b4e8166 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 17 Aug 2021 22:31:52 -0400 Subject: [PATCH 134/338] - updated documentation --- CHANGELOG.md | 1 + info.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af4ada4..686df18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Enabled support for appliances without serial numbers - Added support for Split A/C units (@RobertusIT) - Added support for Window A/C units (@mbrentrowe, @swcrawford1) +- Added support for Portable A/C units (@luddystefenson) - Fixed multiple binary sensors (bad conversion from enum) (@steveredden) - Fixed delay time interpretation for laundry (@steveredden, @sweichbr) - Fixed startup issue when encountering an unknown unit type(@chansearrington, @opie546) diff --git a/info.md b/info.md index 0f27978..6d3c68e 100644 --- a/info.md +++ b/info.md @@ -43,7 +43,7 @@ A/C Controls: #### Features {% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} -- Support for Split and Window AC units (@swcrawford1, @mbrentrowe, @RobertusIT) +- Support for Portable, Split, and Window AC units (@swcrawford1, @mbrentrowe, @RobertusIT, @luddystefenson) {% endif %} {% if version_installed.split('.') | map('int') < '0.4.0'.split('.') | map('int') %} From 5d233788740ae9996f77285bc65093a7396f0e13 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 17 Aug 2021 23:25:05 -0400 Subject: [PATCH 135/338] - updated support for portable A/C units --- custom_components/ge_home/devices/pac.py | 4 +- .../ge_home/entities/ac/__init__.py | 3 +- .../ge_home/entities/ac/ge_pac_climate.py | 86 +++++++++++++++++++ .../ge_home/entities/ac/ge_sac_climate.py | 5 +- 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 custom_components/ge_home/entities/ac/ge_pac_climate.py diff --git a/custom_components/ge_home/devices/pac.py b/custom_components/ge_home/devices/pac.py index 38b6d0f..fa2da9d 100644 --- a/custom_components/ge_home/devices/pac.py +++ b/custom_components/ge_home/devices/pac.py @@ -5,7 +5,7 @@ from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeSacClimate, GeErdSensor, GeErdBinarySensor, GeErdSwitch, ErdOnOffBoolConverter +from ..entities import GePacClimate, GeErdSensor, GeErdSwitch, ErdOnOffBoolConverter _LOGGER = logging.getLogger(__name__) @@ -18,7 +18,7 @@ def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() pac_entities = [ - GeSacClimate(self), + GePacClimate(self), GeErdSensor(self, ErdCode.AC_TARGET_TEMPERATURE), GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE), GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), diff --git a/custom_components/ge_home/entities/ac/__init__.py b/custom_components/ge_home/entities/ac/__init__.py index 79bc17f..0f2e6ad 100644 --- a/custom_components/ge_home/entities/ac/__init__.py +++ b/custom_components/ge_home/entities/ac/__init__.py @@ -1,2 +1,3 @@ from .ge_wac_climate import GeWacClimate -from .ge_sac_climate import GeSacClimate \ No newline at end of file +from .ge_sac_climate import GeSacClimate +from .ge_pac_climate import GePacClimate \ No newline at end of file diff --git a/custom_components/ge_home/entities/ac/ge_pac_climate.py b/custom_components/ge_home/entities/ac/ge_pac_climate.py new file mode 100644 index 0000000..13659e8 --- /dev/null +++ b/custom_components/ge_home/entities/ac/ge_pac_climate.py @@ -0,0 +1,86 @@ +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 temperature_unit(self): + #SAC appears to be hard coded to use Fahrenheit internally, no matter what the display shows + return TEMP_FAHRENHEIT + + @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_home/entities/ac/ge_sac_climate.py b/custom_components/ge_home/entities/ac/ge_sac_climate.py index 8777019..722a483 100644 --- a/custom_components/ge_home/entities/ac/ge_sac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_sac_climate.py @@ -1,6 +1,5 @@ import logging from typing import Any, List, Optional -from gehomesdk.erd.values.ac.sac_enums import ErdSacTargetTemperatureRange from homeassistant.const import ( TEMP_FAHRENHEIT @@ -12,7 +11,7 @@ HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, ) -from gehomesdk import ErdCode, ErdAcOperationMode, ErdSacAvailableModes +from gehomesdk import ErdCode, ErdAcOperationMode, ErdSacAvailableModes, ErdSacTargetTemperatureRange from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter from .fan_mode_options import AcFanOnlyFanModeOptionsConverter, AcFanModeOptionsConverter @@ -59,7 +58,7 @@ def to_option_string(self, value: Any) -> Optional[str]: return HVAC_MODE_COOL class GeSacClimate(GeClimate): - """Class for Window AC units""" + """Class for Split AC units""" def __init__(self, api: ApplianceApi): #initialize the climate control super().__init__(api, None, AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) From e0a19f85d9f709a410028cb9cc949d35da703479 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 19 Aug 2021 20:37:44 -0400 Subject: [PATCH 136/338] - prevent attempted updates of disabled entities (resolves part of #34) --- custom_components/ge_home/update_coordinator.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index af8e631..126a320 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -262,19 +262,21 @@ async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any] except KeyError: return for entity in api.entities: - _LOGGER.debug(f"Updating {entity} ({entity.unique_id}, {entity.entity_id})") - entity.async_write_ha_state() + if entity.enabled: + _LOGGER.debug(f"Updating {entity} ({entity.unique_id}, {entity.entity_id})") + entity.async_write_ha_state() async def _refresh_ha_state(self): entities = [ entity for api in self.appliance_apis.values() for entity in api.entities ] for entity in entities: - try: - _LOGGER.debug(f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}") - entity.async_write_ha_state() - except: - _LOGGER.debug(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}") + if entity.enabled: + try: + _LOGGER.debug(f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}") + entity.async_write_ha_state() + except: + _LOGGER.debug(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}") @property def all_appliances_updated(self) -> bool: From e9d15b2bc926a2edba0e39befdb27648de8ae1c3 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 19 Aug 2021 20:58:15 -0400 Subject: [PATCH 137/338] - fixed advantium operation list (hopefully resolves #34) --- custom_components/ge_home/entities/advantium/ge_advantium.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/advantium/ge_advantium.py b/custom_components/ge_home/entities/advantium/ge_advantium.py index eafc347..0be5f41 100644 --- a/custom_components/ge_home/entities/advantium/ge_advantium.py +++ b/custom_components/ge_home/entities/advantium/ge_advantium.py @@ -82,7 +82,10 @@ def operation_list(self) -> List[str]: if not self._remote_config.warm_enable: invalid.append(CookMode.WARM) - return [v for _, v in ADVANTIUM_OPERATION_MODE_COOK_SETTING_MAPPING.items() if v.cook_mode not in invalid] + 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: From ed759ef7c2ec434c74b54b54be44669267563f4a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 19 Aug 2021 21:01:26 -0400 Subject: [PATCH 138/338] - documentation updates --- CHANGELOG.md | 2 ++ info.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 686df18..3627ce0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - Fixed delay time interpretation for laundry (@steveredden, @sweichbr) - Fixed startup issue when encountering an unknown unit type(@chansearrington, @opie546) - Fixed interpretation of A/C demand response power (@garulf) +- Fixed issues with updating disabled entities (@willhayslett) +- Advantium fixes (@willhayslett) ## 0.4.1 diff --git a/info.md b/info.md index 6d3c68e..c6104e7 100644 --- a/info.md +++ b/info.md @@ -62,6 +62,8 @@ A/C Controls: - Bug fixes for laundry (@steveredden, @sweichbr) - Fixed startup issue when encountering an unknown unit type(@chansearrington, @opie546) - Fixed interpretation of A/C demand response power (@garulf) +- Fixed issues with updating disabled entities (@willhayslett) +- Advantium fixes (@willhayslett) {% endif %} {% if version_installed.split('.') | map('int') < '0.4.1'.split('.') | map('int') %} From 7648228ed5023ef69ac512452b2d9fd97214f298 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 19 Aug 2021 22:02:22 -0400 Subject: [PATCH 139/338] - fixed extra parens in advantium (#34) --- custom_components/ge_home/entities/advantium/ge_advantium.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/entities/advantium/ge_advantium.py b/custom_components/ge_home/entities/advantium/ge_advantium.py index 0be5f41..917a7d0 100644 --- a/custom_components/ge_home/entities/advantium/ge_advantium.py +++ b/custom_components/ge_home/entities/advantium/ge_advantium.py @@ -82,10 +82,10 @@ def operation_list(self) -> List[str]: if not self._remote_config.warm_enable: invalid.append(CookMode.WARM) - return [( + return [ k.stringify() for k, v in ADVANTIUM_OPERATION_MODE_COOK_SETTING_MAPPING.items() - if v.cook_mode not in invalid)] + if v.cook_mode not in invalid] @property def current_cook_setting(self) -> ErdAdvantiumCookSetting: From 3c045c02f593a6aab9f11c31e1bf3630f300d237 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 21 Aug 2021 11:33:18 -0400 Subject: [PATCH 140/338] - version bump --- custom_components/ge_home/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 69bcb7e..f196f64 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.3","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.4","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.4.3" + "version": "0.4.4" } From 920da8a788ced489dc93bdbf5e13f38297c9c38b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 21 Aug 2021 12:12:12 -0400 Subject: [PATCH 141/338] - initial oven hood support - documentation updates - version bump --- CHANGELOG.md | 4 ++ custom_components/ge_home/devices/__init__.py | 3 ++ custom_components/ge_home/devices/hood.py | 52 +++++++++++++++++++ .../ge_home/entities/__init__.py | 3 +- .../ge_home/entities/hood/__init__.py | 2 + .../entities/hood/ge_hood_fan_speed.py | 46 ++++++++++++++++ .../entities/hood/ge_hood_light_level.py | 42 +++++++++++++++ custom_components/ge_home/manifest.json | 4 +- info.md | 4 ++ 9 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 custom_components/ge_home/devices/hood.py create mode 100644 custom_components/ge_home/entities/hood/__init__.py create mode 100644 custom_components/ge_home/entities/hood/ge_hood_fan_speed.py create mode 100644 custom_components/ge_home/entities/hood/ge_hood_light_level.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3627ce0..47dd841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.5.0 + +- Initial support for oven hoods (@digitalbites) + ## 0.4.3 - Enabled support for appliances without serial numbers diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index b6a89f6..293fe3c 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -15,6 +15,7 @@ from .wac import WacApi from .sac import SacApi from .pac import PacApi +from .hood import HoodApi _LOGGER = logging.getLogger(__name__) @@ -44,6 +45,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return SacApi if appliance_type == ErdApplianceType.PORTABLE_AIR_CONDITIONER: return PacApi + if appliance_type == ErdApplianceType.HOOD: + return HoodApi # Fallback return ApplianceApi diff --git a/custom_components/ge_home/devices/hood.py b/custom_components/ge_home/devices/hood.py new file mode 100644 index 0000000..e57b590 --- /dev/null +++ b/custom_components/ge_home/devices/hood.py @@ -0,0 +1,52 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdHoodFanSpeedAvailability, + ErdHoodLightLevelAvailability, + ErdOnOff +) + +from .base import ApplianceApi +from ..entities import ( + GeHoodLightLevelSelect, + GeHoodFanSpeedSelect, + GeErdSensor, + 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(GeErdSensor(self, ErdCode.HOOD_TIMER)) + + entities = base_entities + hood_entities + return entities + diff --git a/custom_components/ge_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index 245757e..b03b590 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -4,4 +4,5 @@ from .oven import * from .water_filter import * from .advantium import * -from .ac import * \ No newline at end of file +from .ac import * +from .hood import * \ No newline at end of file diff --git a/custom_components/ge_home/entities/hood/__init__.py b/custom_components/ge_home/entities/hood/__init__.py new file mode 100644 index 0000000..abba26b --- /dev/null +++ b/custom_components/ge_home/entities/hood/__init__.py @@ -0,0 +1,2 @@ +from .ge_hood_fan_speed import GeHoodFanSpeedSelect +from .ge_hood_light_level import GeHoodLightLevelSelect \ No newline at end of file diff --git a/custom_components/ge_home/entities/hood/ge_hood_fan_speed.py b/custom_components/ge_home/entities/hood/ge_hood_fan_speed.py new file mode 100644 index 0000000..e38196c --- /dev/null +++ b/custom_components/ge_home/entities/hood/ge_hood_fan_speed.py @@ -0,0 +1,46 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCodeType, ErdHoodFanSpeedAvailability, ErdHoodFanSpeed, ErdCode +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class HoodFanSpeedOptionsConverter(OptionsConverter): + def __init__(self, availability: ErdHoodFanSpeedAvailability): + super().__init__() + self.availability = availability + self.excluded_speeds = [] + if not availability.off_available: + self.excluded_speeds.append(ErdHoodFanSpeed.OFF) + if not availability.low_available: + self.excluded_speeds.append(ErdHoodFanSpeed.LOW) + if not availability.med_available: + self.excluded_speeds.append(ErdHoodFanSpeed.MEDIUM) + if not availability.high_available: + self.excluded_speeds.append(ErdHoodFanSpeed.HIGH) + if not availability.boost_available: + self.excluded_speeds.append(ErdHoodFanSpeed.BOOST) + + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdHoodFanSpeed if i not in self.excluded_speeds] + def from_option_string(self, value: str) -> Any: + try: + return ErdHoodFanSpeed[value.upper()] + except: + _LOGGER.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_home/entities/hood/ge_hood_light_level.py b/custom_components/ge_home/entities/hood/ge_hood_light_level.py new file mode 100644 index 0000000..aa3f839 --- /dev/null +++ b/custom_components/ge_home/entities/hood/ge_hood_light_level.py @@ -0,0 +1,42 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCodeType, ErdHoodLightLevelAvailability, ErdHoodLightLevel, ErdCode +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class HoodLightLevelOptionsConverter(OptionsConverter): + def __init__(self, availability: ErdHoodLightLevelAvailability): + super().__init__() + self.availability = availability + self.excluded_levels = [] + if not availability.off_available: + self.excluded_levels.append(ErdHoodLightLevel.OFF) + if not availability.dim_available: + self.excluded_levels.append(ErdHoodLightLevel.DIM) + if not availability.on_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_home/manifest.json b/custom_components/ge_home/manifest.json index f196f64..540c45a 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.4","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.5","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.4.4" + "version": "0.5.0" } diff --git a/info.md b/info.md index c6104e7..490c459 100644 --- a/info.md +++ b/info.md @@ -42,6 +42,10 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} +- Support for Oven Hood units (@digitalbites) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} - Support for Portable, Split, and Window AC units (@swcrawford1, @mbrentrowe, @RobertusIT, @luddystefenson) {% endif %} From e3b8c12894032efbc60fe6817f5ac2bf5ed35d79 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 21 Aug 2021 12:19:02 -0400 Subject: [PATCH 142/338] - added extended cook modes to oven support --- custom_components/ge_home/entities/oven/const.py | 4 +++- custom_components/ge_home/entities/oven/ge_oven.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/oven/const.py b/custom_components/ge_home/entities/oven/const.py index 11bd991..2070538 100644 --- a/custom_components/ge_home/entities/oven/const.py +++ b/custom_components/ge_home/entities/oven/const.py @@ -20,6 +20,7 @@ OP_MODE_BAKED_GOODS = "Baked Goods" OP_MODE_FROZEN_PIZZA_MULTI = "Frozen Pizza Multi" OP_MODE_FROZEN_SNACKS_MULTI = "Frozen Snacks Multi" +OP_MODE_AIRFRY = "Air Fry" UPPER_OVEN = "UPPER_OVEN" LOWER_OVEN = "LOWER_OVEN" @@ -34,6 +35,7 @@ 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.FROZEN_SNACKS_MULTI: OP_MODE_FROZEN_SNACKS_MULTI, + ErdOvenCookMode.AIRFRY: OP_MODE_AIRFRY }) diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index 1d444ce..545973a 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -99,6 +99,13 @@ 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) + + #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) + if ext_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()) From 0b0f77d759238acdb3649cb025c28d56398d46e8 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 21 Aug 2021 12:43:00 -0400 Subject: [PATCH 143/338] - additional debugging for oven modes --- custom_components/ge_home/entities/oven/ge_oven.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index 545973a..2010a74 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -99,17 +99,19 @@ 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.union(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"found cook modes {cook_modes}") + _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 From 77e017d4fcc1ff7d5c2a0f4208d971707597d2ed Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 21 Aug 2021 12:44:08 -0400 Subject: [PATCH 144/338] - documentation updates --- CHANGELOG.md | 2 ++ info.md | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47dd841..ab75275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## 0.5.0 - Initial support for oven hoods (@digitalbites) +- Added extended mode support for ovens + ## 0.4.3 diff --git a/info.md b/info.md index 490c459..50589c6 100644 --- a/info.md +++ b/info.md @@ -44,6 +44,7 @@ A/C Controls: {% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} - Support for Oven Hood units (@digitalbites) +- Added extended mode support for ovens {% endif %} {% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} From 4ef16d7a381e6a870c6c58c6de64aeec1576b558 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 21 Aug 2021 14:02:13 -0400 Subject: [PATCH 145/338] - added logic to prevent configuring the same account multiple times --- custom_components/ge_home/config_flow.py | 26 ++++++++++++++----- custom_components/ge_home/exceptions.py | 2 ++ .../ge_home/translations/en.json | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/custom_components/ge_home/config_flow.py b/custom_components/ge_home/config_flow.py index b272f07..e303312 100644 --- a/custom_components/ge_home/config_flow.py +++ b/custom_components/ge_home/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DOMAIN # pylint:disable=unused-import -from .exceptions import HaAuthError, HaCannotConnect +from .exceptions import HaAuthError, HaCannotConnect, HaAlreadyConfigured _LOGGER = logging.getLogger(__name__) @@ -42,7 +42,7 @@ async def validate_input(hass: core.HomeAssistant, data): raise HaCannotConnect('Unknown connection failure') # Return info that you want to store in the config entry. - return {"title": f"GE Home ({data[CONF_USERNAME]:s})"} + return {"title": f"{data[CONF_USERNAME]:s}"} class GeHomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for GE Home.""" @@ -62,19 +62,33 @@ async def _async_validate_input(self, user_input): except HaCannotConnect: errors["base"] = "cannot_connect" except HaAuthError: - errors["base"] = "invalid_auth" + errors["base"] = "invalid_auth" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" return info, errors + def _ensure_not_configured(self, username: str): + """Ensure that we haven't configured this account""" + existing_accounts = { + entry.data[CONF_USERNAME] for entry in self._async_current_entries() + } + _LOGGER.debug(f"Existing accounts: {existing_accounts}") + if username in existing_accounts: + raise HaAlreadyConfigured + async def async_step_user(self, user_input: Optional[Dict] = None): """Handle the initial step.""" errors = {} if user_input is not None: - info, errors = await self._async_validate_input(user_input) - if info: - return self.async_create_entry(title=info["title"], data=user_input) + try: + self._ensure_not_configured(user_input[CONF_USERNAME]) + info, errors = await self._async_validate_input(user_input) + if info: + return self.async_create_entry(title=info["title"], data=user_input) + except HaAlreadyConfigured: + return self.async_abort(reason="already_configured_account") + return self.async_show_form( step_id="user", data_schema=GEHOME_SCHEMA, errors=errors diff --git a/custom_components/ge_home/exceptions.py b/custom_components/ge_home/exceptions.py index b262a6a..fe7946e 100644 --- a/custom_components/ge_home/exceptions.py +++ b/custom_components/ge_home/exceptions.py @@ -6,3 +6,5 @@ class HaCannotConnect(ha_exc.HomeAssistantError): """Error to indicate we cannot connect.""" class HaAuthError(ha_exc.HomeAssistantError): """Error to indicate authentication failure.""" +class HaAlreadyConfigured(ha_exc.HomeAssistantError): + """Error to indicate that the account is already configured""" \ No newline at end of file diff --git a/custom_components/ge_home/translations/en.json b/custom_components/ge_home/translations/en.json index ce1c350..1184d95 100644 --- a/custom_components/ge_home/translations/en.json +++ b/custom_components/ge_home/translations/en.json @@ -21,7 +21,7 @@ "unknown": "Unexpected error" }, "abort": { - "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured_account": "Account is already configured" } } } From 6868561bec6885c1400ce833aa79053f1f8b538d Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 21 Aug 2021 14:05:19 -0400 Subject: [PATCH 146/338] - documentation updates --- CHANGELOG.md | 2 +- info.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab75275..1bb6467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - Initial support for oven hoods (@digitalbites) - Added extended mode support for ovens - +- Added logic to prevent multiple configurations of the same GE account ## 0.4.3 diff --git a/info.md b/info.md index 50589c6..de7eb1d 100644 --- a/info.md +++ b/info.md @@ -40,6 +40,10 @@ A/C Controls: #### Changes +{% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} +- Added logic to prevent multiple configurations of the same GE account +{% endif %} + #### Features {% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} @@ -63,6 +67,10 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} +- Advantium fixes (@willhayslett) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} - Bug fixes for laundry (@steveredden, @sweichbr) - Fixed startup issue when encountering an unknown unit type(@chansearrington, @opie546) From e07a1043a1c81cba7806caa0e9794d20942e0818 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 21 Aug 2021 14:08:23 -0400 Subject: [PATCH 147/338] - added icons for new code classes --- custom_components/ge_home/entities/common/ge_erd_entity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index ab45351..7b9b67e 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -134,6 +134,10 @@ def _get_icon(self): if self.erd_code_class == ErdCodeClass.AC_SENSOR: return "mdi:air-conditioner" if self.erd_code_class == ErdCodeClass.TEMPERATURE_CONTROL: - return "mdi:thermometer" + return "mdi:thermometer" + if self.erd_code_class == ErdCodeClass.FAN: + return "mdi:fan" + if self.erd_code_class == ErdCodeClass.LIGHT: + return "mdi:lightbulb" return None From ff72fa2bc9976314bd96950c4739dc84010e0ff0 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 21 Aug 2021 22:17:54 -0400 Subject: [PATCH 148/338] - updated devices to utilize serial or mac address --- custom_components/ge_home/devices/base.py | 14 ++++++++++++-- .../ge_home/entities/common/ge_entity.py | 6 ++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/custom_components/ge_home/devices/base.py b/custom_components/ge_home/devices/base.py index 7ae5c4c..bbc7a0d 100644 --- a/custom_components/ge_home/devices/base.py +++ b/custom_components/ge_home/devices/base.py @@ -60,6 +60,16 @@ def available(self) -> bool: def serial_number(self) -> str: return self.appliance.get_erd_value(ErdCode.SERIAL_NUMBER) + @property + def mac_addr(self) -> str: + return self.appliance.mac_addr + + @property + def serial_or_mac(self) -> str: + if self.serial_number and not self.serial_number.isspace(): + return self.serial_number + return self.mac_addr + @property def model_number(self) -> str: return self.appliance.get_erd_value(ErdCode.MODEL_NUMBER) @@ -78,14 +88,14 @@ def name(self) -> str: appliance_type = "Appliance" else: appliance_type = appliance_type.name.replace("_", " ").title() - return f"GE {appliance_type} {self.serial_number}" + return f"GE {appliance_type} {self.serial_or_mac}" @property def device_info(self) -> Dict: """Device info dictionary.""" return { - "identifiers": {(DOMAIN, self.serial_number)}, + "identifiers": {(DOMAIN, self.serial_or_mac)}, "name": self.name, "manufacturer": "GE", "model": self.model_number, diff --git a/custom_components/ge_home/entities/common/ge_entity.py b/custom_components/ge_home/entities/common/ge_entity.py index 977bf9a..34f4037 100644 --- a/custom_components/ge_home/entities/common/ge_entity.py +++ b/custom_components/ge_home/entities/common/ge_entity.py @@ -38,13 +38,11 @@ def appliance(self) -> GeAppliance: @property def mac_addr(self) -> str: - return self.api.appliance.mac_addr + return self.api.mac_addr @property def serial_or_mac(self) -> str: - if self.serial_number and not self.serial_number.isspace(): - return self.serial_number - return self.mac_addr + return self.api.serial_or_mac @property def name(self) -> Optional[str]: From 898a3f7d2908baaa31433d37381a36048ffe459a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 21 Aug 2021 22:18:58 -0400 Subject: [PATCH 149/338] - updated documentation --- CHANGELOG.md | 1 + info.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bb6467..b9a214a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Initial support for oven hoods (@digitalbites) - Added extended mode support for ovens - Added logic to prevent multiple configurations of the same GE account +- Fixed device info when serial not present (@Xe138) ## 0.4.3 diff --git a/info.md b/info.md index de7eb1d..d4cd3c0 100644 --- a/info.md +++ b/info.md @@ -69,6 +69,7 @@ A/C Controls: {% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} - Advantium fixes (@willhayslett) +- Fixed device info when serial not present (@Xe138) {% endif %} {% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} From a4b917a3dad5c0c1c6bab2d397b0ff4aac3c3405 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 22 Aug 2021 11:01:22 -0400 Subject: [PATCH 150/338] - initial support for state class functionality --- .../entities/common/ge_erd_property_sensor.py | 13 ++++++-- .../ge_home/entities/common/ge_erd_sensor.py | 32 ++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_property_sensor.py b/custom_components/ge_home/entities/common/ge_erd_property_sensor.py index 9624871..53d9a92 100644 --- a/custom_components/ge_home/entities/common/ge_erd_property_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_property_sensor.py @@ -8,8 +8,17 @@ 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, uom_override: str = None): - super().__init__(api, erd_code, erd_override=erd_override, icon_override=icon_override, device_class_override=device_class_override, uom_override=uom_override) + 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 + ): + 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 + ) self.erd_property = erd_property self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index 77800b3..ce975b2 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -1,4 +1,5 @@ from typing import Optional +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.const import ( DEVICE_CLASS_ENERGY, @@ -9,6 +10,16 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +#from homeassistant.components.sensor import ( +# STATE_CLASS_MEASUREMENT, +# STATE_CLASS_TOTAL_INCREASING +#) +# For now, let's not force the newer version, we'll use the same constants +# but it'll be optional. +# TODO: Force the usage of new HA +STATE_CLASS_MEASUREMENT = "measurement" +STATE_CLASS_TOTAL_INCREASING = 'total_increasing' + from homeassistant.helpers.entity import Entity from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits @@ -25,10 +36,12 @@ def __init__( erd_override: str = None, icon_override: str = None, device_class_override: str = None, - uom_override: str = None + state_class_override: str = None, + uom_override: str = 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 @property def state(self) -> Optional[str]: @@ -44,6 +57,10 @@ def state(self) -> Optional[str]: def unit_of_measurement(self) -> Optional[str]: return self._get_uom() + @property + def state_class(self) -> Optional[str]: + return self._get_state_class() + @property def _temp_units(self) -> Optional[str]: if self._measurement_system == ErdMeasurementUnits.METRIC: @@ -99,6 +116,19 @@ def _get_device_class(self) -> Optional[str]: 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 STATE_CLASS_MEASUREMENT + if self.erd_code_class in [ErdCodeClass.FLOW_RATE, ErdCodeClass.PERCENTAGE]: + return STATE_CLASS_MEASUREMENT + if self.erd_code_class in [ErdCodeClass.LIQUID_VOLUME]: + return STATE_CLASS_TOTAL_INCREASING + + return None + def _get_icon(self): if self.erd_code_class == ErdCodeClass.DOOR: if self.state.lower().endswith("open"): From c62165bf3768584e2f972a34650cdcdd95bd58a2 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 25 Aug 2021 12:45:22 -0400 Subject: [PATCH 151/338] - updated oven to allow for display temperature if raw temperature is unavailable --- custom_components/ge_home/devices/oven.py | 25 ++++++++++++++----- .../ge_home/entities/oven/ge_oven.py | 13 +++++++--- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index d1067a0..83a61bf 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -36,6 +36,9 @@ def get_all_entities(self) -> List[Entity]: 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) + _LOGGER.debug(f"Oven Config: {oven_config}") _LOGGER.debug(f"Cooktop Config: {cooktop_config}") oven_entities = [] @@ -47,29 +50,36 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING), GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER), GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET), - GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE), + 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), GeErdSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER), GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET), - GeErdSensor(self, ErdCode.LOWER_OVEN_RAW_TEMPERATURE), + GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE), GeErdBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED), - GeOven(self, LOWER_OVEN, True), - GeOven(self, UPPER_OVEN, True), + 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)) 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)), GeErdSensor(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_RAW_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_RAW_TEMPERATURE)), + 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) + 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 cooktop_config == ErdCooktopConfig.PRESENT: cooktop_status: CooktopStatus = self.appliance.get_erd_value(ErdCode.COOKTOP_STATUS) @@ -90,3 +100,6 @@ def _single_name(self, erd_code: ErdCode): 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_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index 2010a74..54c0643 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -23,12 +23,13 @@ class GeOven(GeWaterHeater): icon = "mdi:stove" - def __init__(self, api: ApplianceApi, oven_select: str = UPPER_OVEN, two_cavity: bool = False): + 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 @@ -76,11 +77,13 @@ def remote_enabled(self) -> bool: 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. + #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("RAW_TEMPERATURE") + return self.get_erd_value(self._temperature_erd_code) @property def current_operation(self) -> Optional[str]: @@ -196,8 +199,10 @@ def extra_state_attributes(self) -> Optional[Dict[str, Any]]: data = { "display_state": self.display_state, "probe_present": probe_present, - "raw_temperature": self.get_erd_value("RAW_TEMPERATURE"), + "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") From 08704be5f6a96072f83a1c308114d4433963c8be Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 25 Aug 2021 12:50:18 -0400 Subject: [PATCH 152/338] - documentation updates --- CHANGELOG.md | 1 + README.md | 2 ++ info.md | 1 + 3 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9a214a..a2a90a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added extended mode support for ovens - Added logic to prevent multiple configurations of the same GE account - Fixed device info when serial not present (@Xe138) +- Fixed issue with ovens when raw temperature not available (@chadohalloran) ## 0.4.3 diff --git a/README.md b/README.md index bfd268e..ad30d3d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio - Dishwasher - Laundry (Washer/Dryer) - Whole Home Water Filter +- A/C (Portable, Split, Window) +- Range Hoods - Advantium **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** diff --git a/info.md b/info.md index d4cd3c0..54b86d8 100644 --- a/info.md +++ b/info.md @@ -70,6 +70,7 @@ A/C Controls: {% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} - Advantium fixes (@willhayslett) - Fixed device info when serial not present (@Xe138) +- Fixed issue with ovens when raw temperature not available (@chadohalloran) {% endif %} {% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} From 281879f66d5c7b354c0ecbc61cd185546c18b294 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 25 Aug 2021 18:36:51 -0400 Subject: [PATCH 153/338] - sdk version bump to resolve errors relating to #39 --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 540c45a..53cb259 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.5","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.6","magicattr==0.1.5"], "codeowners": ["@simbaja"], "version": "0.5.0" } From 0844dc31d216c20b8b8d7f2cb85c9d31145db692 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 1 Sep 2021 18:15:48 -0400 Subject: [PATCH 154/338] - added fix for SAC temperature sensors (#40) --- CHANGELOG.md | 2 +- custom_components/ge_home/devices/sac.py | 6 +++--- custom_components/ge_home/entities/ac/__init__.py | 3 ++- .../entities/ac/ge_sac_temperature_sensor.py | 15 +++++++++++++++ info.md | 1 + 5 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a2a90a0..a954c8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Added logic to prevent multiple configurations of the same GE account - Fixed device info when serial not present (@Xe138) - Fixed issue with ovens when raw temperature not available (@chadohalloran) - +- Fixed issue where Split A/C temperature sensors report UOM incorrectly (@RobertusIT) ## 0.4.3 - Enabled support for appliances without serial numbers diff --git a/custom_components/ge_home/devices/sac.py b/custom_components/ge_home/devices/sac.py index a183fe9..40f87e2 100644 --- a/custom_components/ge_home/devices/sac.py +++ b/custom_components/ge_home/devices/sac.py @@ -5,7 +5,7 @@ from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeSacClimate, GeErdSensor, GeErdBinarySensor, GeErdSwitch, ErdOnOffBoolConverter +from ..entities import GeSacClimate, GeSacTemperatureSensor, GeErdSensor, GeErdSwitch, ErdOnOffBoolConverter _LOGGER = logging.getLogger(__name__) @@ -19,8 +19,8 @@ def get_all_entities(self) -> List[Entity]: sac_entities = [ GeSacClimate(self), - GeErdSensor(self, ErdCode.AC_TARGET_TEMPERATURE), - GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE), + GeSacTemperatureSensor(self, ErdCode.AC_TARGET_TEMPERATURE), + GeSacTemperatureSensor(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"), diff --git a/custom_components/ge_home/entities/ac/__init__.py b/custom_components/ge_home/entities/ac/__init__.py index 0f2e6ad..0b54100 100644 --- a/custom_components/ge_home/entities/ac/__init__.py +++ b/custom_components/ge_home/entities/ac/__init__.py @@ -1,3 +1,4 @@ from .ge_wac_climate import GeWacClimate from .ge_sac_climate import GeSacClimate -from .ge_pac_climate import GePacClimate \ No newline at end of file +from .ge_pac_climate import GePacClimate +from .ge_sac_temperature_sensor import GeSacTemperatureSensor \ No newline at end of file diff --git a/custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py b/custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py new file mode 100644 index 0000000..087ded1 --- /dev/null +++ b/custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py @@ -0,0 +1,15 @@ +import logging +from typing import Any, List, Optional + +from homeassistant.const import ( + TEMP_FAHRENHEIT +) +from ..common import GeErdSensor + +class GeSacTemperatureSensor(GeErdSensor): + """Class for Split A/C temperature sensors""" + + @property + def temperature_unit(self): + #SAC appears to be hard coded to use Fahrenheit internally, no matter what the display shows + return TEMP_FAHRENHEIT diff --git a/info.md b/info.md index 54b86d8..bf36f3e 100644 --- a/info.md +++ b/info.md @@ -71,6 +71,7 @@ A/C Controls: - Advantium fixes (@willhayslett) - Fixed device info when serial not present (@Xe138) - Fixed issue with ovens when raw temperature not available (@chadohalloran) +- Fixed issue where Split A/C temperature sensors report UOM incorrectly (@RobertusIT) {% endif %} {% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} From a8df74178c3b87f75c479192c9725aca395085ca Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 1 Sep 2021 18:17:44 -0400 Subject: [PATCH 155/338] - fixed property name for SAC sensor --- .../ge_home/entities/ac/ge_sac_temperature_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py b/custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py index 087ded1..854a239 100644 --- a/custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py +++ b/custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py @@ -10,6 +10,6 @@ class GeSacTemperatureSensor(GeErdSensor): """Class for Split A/C temperature sensors""" @property - def temperature_unit(self): + def _temp_units(self) -> Optional[str]: #SAC appears to be hard coded to use Fahrenheit internally, no matter what the display shows return TEMP_FAHRENHEIT From a6f157692ea314aa9473c31c5180cb9e0fe9c449 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 5 Sep 2021 10:08:12 -0400 Subject: [PATCH 156/338] - added additional oven states --- custom_components/ge_home/entities/oven/const.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/custom_components/ge_home/entities/oven/const.py b/custom_components/ge_home/entities/oven/const.py index 2070538..af9d602 100644 --- a/custom_components/ge_home/entities/oven/const.py +++ b/custom_components/ge_home/entities/oven/const.py @@ -20,6 +20,11 @@ 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" @@ -30,7 +35,11 @@ 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, From 9d2e738469e16084b2bc7541a152af97605a6e13 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 5 Sep 2021 14:12:20 -0400 Subject: [PATCH 157/338] - added initial services (timers) - added timer sensor to allow for service usage --- custom_components/ge_home/const.py | 2 ++ custom_components/ge_home/devices/oven.py | 7 +++-- .../ge_home/entities/common/__init__.py | 1 + .../ge_home/entities/common/ge_erd_sensor.py | 3 ++ .../entities/common/ge_erd_timer_sensor.py | 30 ++++++++++++++++++ custom_components/ge_home/sensor.py | 27 +++++++++++++++- custom_components/ge_home/services.yaml | 31 +++++++++++++++++++ 7 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 custom_components/ge_home/entities/common/ge_erd_timer_sensor.py create mode 100644 custom_components/ge_home/services.yaml diff --git a/custom_components/ge_home/const.py b/custom_components/ge_home/const.py index 87fe381..84fa94b 100644 --- a/custom_components/ge_home/const.py +++ b/custom_components/ge_home/const.py @@ -11,3 +11,5 @@ MAX_RETRY_DELAY = 1800 RETRY_OFFLINE_COUNT = 5 +SERVICE_SET_TIMER = "set_timer" +SERVICE_CLEAR_TIMER = "clear_timer" \ No newline at end of file diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index 83a61bf..f2a2074 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -14,6 +14,7 @@ from .base import ApplianceApi from ..entities import ( GeErdSensor, + GeErdTimerSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdPropertyBinarySensor, @@ -48,14 +49,14 @@ def get_all_entities(self) -> List[Entity]: oven_entities.extend([ GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE), GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING), - GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER), + 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), - GeErdSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER), + GeErdTimerSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER), GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET), GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE), GeErdBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED), @@ -71,7 +72,7 @@ def get_all_entities(self) -> List[Entity]: 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)), - GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER)), + 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)), diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 691629a..0094d25 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -5,6 +5,7 @@ from .ge_erd_binary_sensor import GeErdBinarySensor from .ge_erd_property_binary_sensor import GeErdPropertyBinarySensor from .ge_erd_sensor import GeErdSensor +from .ge_erd_timer_sensor import GeErdTimerSensor from .ge_erd_property_sensor import GeErdPropertySensor from .ge_erd_switch import GeErdSwitch from .ge_water_heater import GeWaterHeater diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index ce975b2..04de12a 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -7,6 +7,7 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_TIMESTAMP, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -113,6 +114,8 @@ def _get_device_class(self) -> Optional[str]: return DEVICE_CLASS_POWER if self.erd_code_class == ErdCodeClass.ENERGY: return DEVICE_CLASS_ENERGY + if self.erd_code_class == ErdCodeClass.TIMER: + return DEVICE_CLASS_TIMESTAMP return None diff --git a/custom_components/ge_home/entities/common/ge_erd_timer_sensor.py b/custom_components/ge_home/entities/common/ge_erd_timer_sensor.py new file mode 100644 index 0000000..33cdfee --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_erd_timer_sensor.py @@ -0,0 +1,30 @@ +import asyncio +from datetime import timedelta +from typing import Optional +import logging +import async_timeout + +from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits + +from .ge_erd_sensor import GeErdSensor +from ...devices import ApplianceApi + + +_LOGGER = logging.getLogger(__name__) + +class GeErdTimerSensor(GeErdSensor): + """GE Entity for timer sensors""" + + async def set_timer(self, duration: timedelta): + try: + await self.appliance.async_set_erd_value(self.erd_code, duration) + except: + _LOGGER.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_home/sensor.py b/custom_components/ge_home/sensor.py index 17a037d..a36b566 100644 --- a/custom_components/ge_home/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -2,20 +2,28 @@ import async_timeout 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 +from homeassistant.helpers import entity_platform -from .const import DOMAIN +from .const import DOMAIN, SERVICE_SET_TIMER, SERVICE_CLEAR_TIMER from .entities import GeErdSensor from .update_coordinator import GeHomeUpdateCoordinator +ATTR_DURATION = "duration" + _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): """GE Home sensors.""" _LOGGER.debug('Adding GE Home sensors') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + # Get the platform + platform = entity_platform.async_get_current_platform() # This should be a NOP, but let's be safe with async_timeout.timeout(20): @@ -32,3 +40,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn ] _LOGGER.debug(f'Found {len(entities):d} sensors') async_add_entities(entities) + + # 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 set_timer entity service + platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, 'clear_timer') + +async def set_timer(entity, service_call): + ts = timedelta(minutes=int(service_call.data['duration'])) + await entity.set_timer(ts) diff --git a/custom_components/ge_home/services.yaml b/custom_components/ge_home/services.yaml new file mode 100644 index 0000000..7ec2fd0 --- /dev/null +++ b/custom_components/ge_home/services.yaml @@ -0,0 +1,31 @@ +# GE Home Services + +set_timer: + name: Set Timer + description: Sets a GE Home timer value + target: + entity: + integration: "ge_home" + domain: "sensor" + device_class: "timestamp" + 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: Turns off a GE Home timer + target: + entity: + integration: "ge_home" + domain: "sensor" + device_class: "timestamp" From 1cbfe63268cfdf0904654d77209168e4e0aed02c Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 5 Sep 2021 22:25:18 -0400 Subject: [PATCH 158/338] - updates to better support HA UI --- custom_components/ge_home/entities/common/ge_erd_entity.py | 2 +- custom_components/ge_home/entities/common/ge_erd_sensor.py | 2 -- custom_components/ge_home/services.yaml | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index 7b9b67e..a23a36c 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -69,7 +69,7 @@ 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 "" + return str(value)[:-3] if value else "Off" if value is None: return None return self.appliance.stringify_erd_value(value, **kwargs) diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index 04de12a..bd54c03 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -114,8 +114,6 @@ def _get_device_class(self) -> Optional[str]: return DEVICE_CLASS_POWER if self.erd_code_class == ErdCodeClass.ENERGY: return DEVICE_CLASS_ENERGY - if self.erd_code_class == ErdCodeClass.TIMER: - return DEVICE_CLASS_TIMESTAMP return None diff --git a/custom_components/ge_home/services.yaml b/custom_components/ge_home/services.yaml index 7ec2fd0..b35b335 100644 --- a/custom_components/ge_home/services.yaml +++ b/custom_components/ge_home/services.yaml @@ -7,7 +7,6 @@ set_timer: entity: integration: "ge_home" domain: "sensor" - device_class: "timestamp" fields: duration: name: Duration @@ -28,4 +27,3 @@ clear_timer: entity: integration: "ge_home" domain: "sensor" - device_class: "timestamp" From 533ae26de2b435c193b9e3c9efdc9b95af515836 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Sep 2021 02:22:48 -0400 Subject: [PATCH 159/338] - added a service to set int values (pods, delays, etc) --- custom_components/ge_home/const.py | 3 ++- .../ge_home/entities/common/ge_erd_sensor.py | 10 ++++++++ custom_components/ge_home/sensor.py | 23 +++++++++++++++++-- custom_components/ge_home/services.yaml | 22 ++++++++++++++++-- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/custom_components/ge_home/const.py b/custom_components/ge_home/const.py index 84fa94b..e8511f5 100644 --- a/custom_components/ge_home/const.py +++ b/custom_components/ge_home/const.py @@ -12,4 +12,5 @@ RETRY_OFFLINE_COUNT = 5 SERVICE_SET_TIMER = "set_timer" -SERVICE_CLEAR_TIMER = "clear_timer" \ No newline at end of file +SERVICE_CLEAR_TIMER = "clear_timer" +SERVICE_SET_INT_VALUE = "set_int_value" \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index bd54c03..b1b9163 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -1,3 +1,4 @@ +import logging from typing import Optional from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT @@ -27,6 +28,8 @@ from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi +_LOGGER = logging.getLogger(__name__) + class GeErdSensor(GeErdEntity, Entity): """GE Entity for sensors""" @@ -137,3 +140,10 @@ def _get_icon(self): if self.state.lower().endswith("closed"): return "mdi:door-closed" return super()._get_icon() + + async def set_value(self, value): + """Sets the ERD value, assumes that the data type is correct""" + try: + await self.appliance.async_set_erd_value(self.erd_code, value) + except: + _LOGGER.warning(f"Could not set {self.name} to {value}") \ No newline at end of file diff --git a/custom_components/ge_home/sensor.py b/custom_components/ge_home/sensor.py index a36b566..2dea9a1 100644 --- a/custom_components/ge_home/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -9,11 +9,17 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform -from .const import DOMAIN, SERVICE_SET_TIMER, SERVICE_CLEAR_TIMER +from .const import ( + DOMAIN, + SERVICE_SET_TIMER, + SERVICE_CLEAR_TIMER, + SERVICE_SET_INT_VALUE +) from .entities import GeErdSensor from .update_coordinator import GeHomeUpdateCoordinator ATTR_DURATION = "duration" +ATTR_VALUE = "value" _LOGGER = logging.getLogger(__name__) @@ -51,9 +57,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn }, set_timer) - # register set_timer entity service + # 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_home/services.yaml b/custom_components/ge_home/services.yaml index b35b335..1ca48ea 100644 --- a/custom_components/ge_home/services.yaml +++ b/custom_components/ge_home/services.yaml @@ -2,7 +2,7 @@ set_timer: name: Set Timer - description: Sets a GE Home timer value + description: Sets a timer value (timespan) target: entity: integration: "ge_home" @@ -22,8 +22,26 @@ set_timer: mode: slider clear_timer: name: Clear Timer - description: Turns off a GE Home timer + description: Clears a timer value (sets to zero) target: entity: integration: "ge_home" domain: "sensor" + +set_int_value: + name: Set Int Value + description: Sets an integer value (also can be used with ERD enums) + target: + entity: + integration: "ge_home" + domain: "sensor" + fields: + value: + name: Value + description: The value to set + required: true + selector: + number: + min: 0 + max: 65535 + \ No newline at end of file From 842d5522642056ee3e45dc782a643992904e6d47 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Sep 2021 22:48:21 -0400 Subject: [PATCH 160/338] - dependency bump - updated fridge to include interior/proximity light control --- custom_components/ge_home/devices/fridge.py | 11 ++++++++++- custom_components/ge_home/manifest.json | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index cf3d620..798f856 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -1,3 +1,4 @@ +from custom_components.ge_home.entities.common.bool_converter import ErdOnOffBoolConverter from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM from homeassistant.const import DEVICE_CLASS_TEMPERATURE import logging @@ -49,6 +50,10 @@ def get_all_entities(self) -> List[Entity]: 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) + + interior_light: ErdOnOff = self.try_get_erd_value(ErdCode.INTERIOR_LIGHT) + proximity_light: ErdOnOff = self.try_get_erd_value(ErdCode.PROXIMITY_LIGHT) + # Common entities common_entities = [ @@ -74,7 +79,11 @@ def get_all_entities(self) -> List[Entity]: 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 != ErdOnOff.NA): + fridge_entities.append(GeErdSwitch(self, ErdCode.INTERIOR_LIGHT, ErdOnOffBoolConverter(), icon_on_override="mdi:lightbulb-on", icon_off_override="mdi:lightbulb")) + 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")) + # Freezer entities if fridge_model_info is None or fridge_model_info.has_freezer: freezer_entities.extend([ diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 53cb259..d9a461e 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.6","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.9","magicattr==0.1.5"], "codeowners": ["@simbaja"], "version": "0.5.0" } From a69938528677fd31b056cffd41781960f1c06966 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Sep 2021 22:56:22 -0400 Subject: [PATCH 161/338] - updated fridge interior light entity --- custom_components/ge_home/devices/fridge.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index 798f856..2a99b6d 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -51,7 +51,7 @@ def get_all_entities(self) -> List[Entity]: 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) - interior_light: ErdOnOff = self.try_get_erd_value(ErdCode.INTERIOR_LIGHT) + interior_light: int = self.try_get_erd_value(ErdCode.INTERIOR_LIGHT) proximity_light: ErdOnOff = self.try_get_erd_value(ErdCode.PROXIMITY_LIGHT) @@ -79,8 +79,8 @@ def get_all_entities(self) -> List[Entity]: 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 != ErdOnOff.NA): - fridge_entities.append(GeErdSwitch(self, ErdCode.INTERIOR_LIGHT, ErdOnOffBoolConverter(), icon_on_override="mdi:lightbulb-on", icon_off_override="mdi:lightbulb")) + if(interior_light and interior_light != 255): + fridge_entities.append(GeErdSensor(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")) From a354677ac162e3190cf0c2c713bd9c1264f334ff Mon Sep 17 00:00:00 2001 From: Bryson Steadman Date: Wed, 29 Sep 2021 16:56:41 -0700 Subject: [PATCH 162/338] Fix range hood light level crash It looks like `high_available` is what's currently used in gehomesdk --- custom_components/ge_home/entities/hood/ge_hood_light_level.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/hood/ge_hood_light_level.py b/custom_components/ge_home/entities/hood/ge_hood_light_level.py index aa3f839..52e2516 100644 --- a/custom_components/ge_home/entities/hood/ge_hood_light_level.py +++ b/custom_components/ge_home/entities/hood/ge_hood_light_level.py @@ -16,7 +16,7 @@ def __init__(self, availability: ErdHoodLightLevelAvailability): self.excluded_levels.append(ErdHoodLightLevel.OFF) if not availability.dim_available: self.excluded_levels.append(ErdHoodLightLevel.DIM) - if not availability.on_available: + if not availability.high_available: self.excluded_levels.append(ErdHoodLightLevel.HIGH) @property From 438d120700afe44e3a49c0cf09e84df957326cb1 Mon Sep 17 00:00:00 2001 From: elwing00 <56235263+elwing00@users.noreply.github.com> Date: Wed, 6 Oct 2021 22:23:52 -0400 Subject: [PATCH 163/338] Update ge_fridge.py Fixed drawer open status --- custom_components/ge_home/entities/fridge/ge_fridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/fridge/ge_fridge.py b/custom_components/ge_home/entities/fridge/ge_fridge.py index 56c0c09..ef9e708 100644 --- a/custom_components/ge_home/entities/fridge/ge_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_fridge.py @@ -49,7 +49,7 @@ def door_state_attrs(self) -> Dict[str, Any]: 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.fridge_left.name.title() + data["drawer"] = door_status.drawer.name.title() if data: all_closed = all(v == "Closed" for v in data.values()) From 64c92d38c525da15a9fc334eb7fdb0e7f78ab354 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Fri, 8 Oct 2021 22:10:38 -0400 Subject: [PATCH 164/338] - added convertable drawer to fridge --- custom_components/ge_home/devices/fridge.py | 17 ++++++---- .../ge_home/entities/fridge/__init__.py | 3 +- .../fridge/convertable_drawer_mode_options.py | 34 +++++++++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index 2a99b6d..6fa4cea 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -1,4 +1,3 @@ -from custom_components.ge_home.entities.common.bool_converter import ErdOnOffBoolConverter from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM from homeassistant.const import DEVICE_CLASS_TEMPERATURE import logging @@ -14,19 +13,23 @@ IceMakerControlStatus, ErdFilterStatus, HotWaterStatus, - FridgeModelInfo + FridgeModelInfo, + ErdConvertableDrawerMode ) from .base import ApplianceApi from ..entities import ( + ErdOnOffBoolConverter, GeErdSensor, GeErdBinarySensor, GeErdSwitch, + GeErdSelect, GeFridge, GeFreezer, GeDispenser, GeErdPropertySensor, - GeErdPropertyBinarySensor + GeErdPropertyBinarySensor, + ConvertableDrawerModeOptionsConverter ) _LOGGER = logging.getLogger(__name__) @@ -50,11 +53,11 @@ def get_all_entities(self) -> List[Entity]: 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) - # Common entities common_entities = [ GeErdSensor(self, ErdCode.FRIDGE_MODEL_INFO), @@ -83,7 +86,9 @@ def get_all_entities(self) -> List[Entity]: fridge_entities.append(GeErdSensor(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())) + # Freezer entities if fridge_model_info is None or fridge_model_info.has_freezer: freezer_entities.extend([ diff --git a/custom_components/ge_home/entities/fridge/__init__.py b/custom_components/ge_home/entities/fridge/__init__.py index 5dde001..b277fcf 100644 --- a/custom_components/ge_home/entities/fridge/__init__.py +++ b/custom_components/ge_home/entities/fridge/__init__.py @@ -1,3 +1,4 @@ from .ge_fridge import GeFridge from .ge_freezer import GeFreezer -from .ge_dispenser import GeDispenser \ No newline at end of file +from .ge_dispenser import GeDispenser +from .convertable_drawer_mode_options import ConvertableDrawerModeOptionsConverter \ No newline at end of file diff --git a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py new file mode 100644 index 0000000..6cb36c1 --- /dev/null +++ b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py @@ -0,0 +1,34 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdConvertableDrawerMode +from ..common import OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class ConvertableDrawerModeOptionsConverter(OptionsConverter): + def __init__(self): + super().__init__() + self._excluded_options = [ + ErdConvertableDrawerMode.UNKNOWN0, + ErdConvertableDrawerMode.UNKNOWN1, + ErdConvertableDrawerMode.NA + ] + + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdConvertableDrawerMode if i not in self._excluded_options] + def from_option_string(self, value: str) -> Any: + try: + return ErdConvertableDrawerMode[value.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: + return value.stringify() + except: + pass + return ErdConvertableDrawerMode.NA.stringify() + From 2f5ae91150029e82ff8260af0f5899bf2e4ae435 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Fri, 8 Oct 2021 22:12:07 -0400 Subject: [PATCH 165/338] - updated documentation --- CHANGELOG.md | 1 + info.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a954c8b..1695382 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Fixed device info when serial not present (@Xe138) - Fixed issue with ovens when raw temperature not available (@chadohalloran) - Fixed issue where Split A/C temperature sensors report UOM incorrectly (@RobertusIT) +- Added convertable drawer mode, proximity light, and interior lights to fridge (@grotto27, @elwing00) ## 0.4.3 - Enabled support for appliances without serial numbers diff --git a/info.md b/info.md index bf36f3e..75ed07d 100644 --- a/info.md +++ b/info.md @@ -72,6 +72,7 @@ A/C Controls: - Fixed device info when serial not present (@Xe138) - Fixed issue with ovens when raw temperature not available (@chadohalloran) - Fixed issue where Split A/C temperature sensors report UOM incorrectly (@RobertusIT) +- Added convertable drawer mode, proximity light, and interior lights to fridge (@grotto27, @elwing00) {% endif %} {% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} From 0aa02e3f715b6073ef8dac51e648a814d5493963 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Fri, 8 Oct 2021 22:48:17 -0400 Subject: [PATCH 166/338] - added initial version of the erd light entity --- custom_components/ge_home/devices/fridge.py | 3 +- .../ge_home/entities/common/__init__.py | 1 + .../ge_home/entities/common/ge_erd_light.py | 65 +++++++++++++++++++ custom_components/ge_home/light.py | 38 +++++++++++ custom_components/ge_home/manifest.json | 2 +- .../ge_home/update_coordinator.py | 2 +- 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 custom_components/ge_home/entities/common/ge_erd_light.py create mode 100644 custom_components/ge_home/light.py diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index 6fa4cea..43a1936 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -24,6 +24,7 @@ GeErdBinarySensor, GeErdSwitch, GeErdSelect, + GeErdLight, GeFridge, GeFreezer, GeDispenser, @@ -83,7 +84,7 @@ def get_all_entities(self) -> List[Entity]: 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(GeErdSensor(self, ErdCode.INTERIOR_LIGHT)) + 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): diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 0094d25..7db556b 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -5,6 +5,7 @@ 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 diff --git a/custom_components/ge_home/entities/common/ge_erd_light.py b/custom_components/ge_home/entities/common/ge_erd_light.py new file mode 100644 index 0000000..b07281f --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_erd_light.py @@ -0,0 +1,65 @@ +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._attr_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 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_home/light.py b/custom_components/ge_home/light.py new file mode 100644 index 0000000..310cabf --- /dev/null +++ b/custom_components/ge_home/light.py @@ -0,0 +1,38 @@ +"""GE Home Select Entities""" +import async_timeout +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .entities import GeErdLight +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] + + # This should be a NOP, but let's be safe + with async_timeout.timeout(20): + await coordinator.initialization_future + _LOGGER.debug("Coordinator init future finished") + + apis = list(coordinator.appliance_apis.values()) + _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 + ] + _LOGGER.debug(f"Found {len(entities):d} lights") + async_add_entities(entities) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index d9a461e..b229ce3 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.9","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.11","magicattr==0.1.5"], "codeowners": ["@simbaja"], "version": "0.5.0" } diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 126a320..b3ee44b 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -34,7 +34,7 @@ ) from .devices import ApplianceApi, get_appliance_api_type -PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater", "select", "climate"] +PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater", "select", "climate", "light"] _LOGGER = logging.getLogger(__name__) From 18143fe254db57f21cfc63f31a4efe741f59150a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 9 Oct 2021 14:07:47 -0400 Subject: [PATCH 167/338] - fixed issue with light brightness setting --- custom_components/ge_home/entities/common/ge_erd_light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_light.py b/custom_components/ge_home/entities/common/ge_erd_light.py index b07281f..8f3dc05 100644 --- a/custom_components/ge_home/entities/common/ge_erd_light.py +++ b/custom_components/ge_home/entities/common/ge_erd_light.py @@ -57,9 +57,9 @@ async def async_turn_on(self, **kwargs): brightness = kwargs.pop(ATTR_BRIGHTNESS, 255) _LOGGER.debug(f"Turning on {self.unique_id}") - await self._set_brightness(brightness, kwargs) + 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) + await self._set_brightness(0, **kwargs) From 3eb1dadaad25596b2a4b4a9ed7830881e5462002 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 12 Oct 2021 20:16:47 -0400 Subject: [PATCH 168/338] - updated version of gehomesdk - updated the erd light to hopefully make dimming work --- custom_components/ge_home/entities/common/ge_erd_light.py | 7 ++++++- custom_components/ge_home/manifest.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_light.py b/custom_components/ge_home/entities/common/ge_erd_light.py index 8f3dc05..5ecf457 100644 --- a/custom_components/ge_home/entities/common/ge_erd_light.py +++ b/custom_components/ge_home/entities/common/ge_erd_light.py @@ -27,7 +27,7 @@ class GeErdLight(GeErdEntity, LightEntity): 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._attr_color_mode = color_mode + self._color_mode = color_mode @property def supported_features(self): @@ -38,6 +38,11 @@ def supported_features(self): 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): diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index b229ce3..9d6b89c 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.11","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.12","magicattr==0.1.5"], "codeowners": ["@simbaja"], "version": "0.5.0" } From a0f49828340339d00f999916bc8d5df42f8831b6 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 12 Oct 2021 22:15:42 -0400 Subject: [PATCH 169/338] - maybe fixed dimmability? --- custom_components/ge_home/entities/common/ge_erd_light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_light.py b/custom_components/ge_home/entities/common/ge_erd_light.py index 5ecf457..1d9d714 100644 --- a/custom_components/ge_home/entities/common/ge_erd_light.py +++ b/custom_components/ge_home/entities/common/ge_erd_light.py @@ -37,7 +37,7 @@ def supported_features(self): @property def supported_color_modes(self): """Flag supported color modes.""" - return COLOR_MODE_BRIGHTNESS + return {COLOR_MODE_BRIGHTNESS} @property def color_mode(self): From a8e161c92187246c5c63678a47e88da687193c34 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 17 Oct 2021 10:54:43 -0400 Subject: [PATCH 170/338] - added temperature label for fridge drawer options --- custom_components/ge_home/devices/fridge.py | 4 ++- .../fridge/convertable_drawer_mode_options.py | 30 ++++++++++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index 43a1936..6dd2f4c 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -59,6 +59,8 @@ def get_all_entities(self) -> List[Entity]: interior_light: int = self.try_get_erd_value(ErdCode.INTERIOR_LIGHT) proximity_light: ErdOnOff = self.try_get_erd_value(ErdCode.PROXIMITY_LIGHT) + units = self.hass.config.units + # Common entities common_entities = [ GeErdSensor(self, ErdCode.FRIDGE_MODEL_INFO), @@ -88,7 +90,7 @@ def get_all_entities(self) -> List[Entity]: 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())) + fridge_entities.append(GeErdSelect(self, ErdCode.CONVERTABLE_DRAWER_MODE, ConvertableDrawerModeOptionsConverter(units))) # Freezer entities if fridge_model_info is None or fridge_model_info.has_freezer: diff --git a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py index 6cb36c1..b9b933c 100644 --- a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py +++ b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py @@ -2,33 +2,55 @@ 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): + 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 [i.stringify() for i in ErdConvertableDrawerMode if i not in self._excluded_options] + 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: - return ErdConvertableDrawerMode[value.upper()] + 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: - return value.stringify() + 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() From 12f3bca1e7f6e175c6560961e7952192d3ab3956 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 17 Oct 2021 15:08:05 -0400 Subject: [PATCH 171/338] - documentation update --- CHANGELOG.md | 2 +- info.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1695382..e06ce42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - Fixed device info when serial not present (@Xe138) - Fixed issue with ovens when raw temperature not available (@chadohalloran) - Fixed issue where Split A/C temperature sensors report UOM incorrectly (@RobertusIT) -- Added convertable drawer mode, proximity light, and interior lights to fridge (@grotto27, @elwing00) +- Added convertable drawer mode, proximity light, and interior lights to fridge (@groto27, @elwing00) ## 0.4.3 - Enabled support for appliances without serial numbers diff --git a/info.md b/info.md index 75ed07d..63cc22b 100644 --- a/info.md +++ b/info.md @@ -72,7 +72,7 @@ A/C Controls: - Fixed device info when serial not present (@Xe138) - Fixed issue with ovens when raw temperature not available (@chadohalloran) - Fixed issue where Split A/C temperature sensors report UOM incorrectly (@RobertusIT) -- Added convertable drawer mode, proximity light, and interior lights to fridge (@grotto27, @elwing00) +- Added convertable drawer mode, proximity light, and interior lights to fridge (@groto27, @elwing00) {% endif %} {% if version_installed.split('.') | map('int') < '0.4.3'.split('.') | map('int') %} From 65b6efc8269051ec091c0cae392699960da1251f Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Dec 2021 00:18:28 -0500 Subject: [PATCH 172/338] - updated sensors to uses native value/uom - changed temp uoms to always report fahrenheit - updated version of sdk in manifest --- CHANGELOG.md | 5 ++ .../ge_home/devices/advantium.py | 6 +-- custom_components/ge_home/devices/fridge.py | 5 +- custom_components/ge_home/devices/oven.py | 3 +- custom_components/ge_home/devices/sac.py | 4 +- .../ge_home/entities/ac/ge_pac_climate.py | 5 -- .../ge_home/entities/ac/ge_sac_climate.py | 6 --- .../entities/ac/ge_sac_temperature_sensor.py | 15 ------ .../ge_home/entities/common/ge_climate.py | 8 +-- .../entities/common/ge_erd_property_sensor.py | 21 +++++--- .../ge_home/entities/common/ge_erd_sensor.py | 50 ++++++++++++++----- .../entities/common/ge_water_heater.py | 7 +-- custom_components/ge_home/manifest.json | 2 +- 13 files changed, 77 insertions(+), 60 deletions(-) delete mode 100644 custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e06ce42..c5cd65b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.0 + +- Changed the sensors to use native value/uom +- Changed the temperatures to always be natively fahrenheit (API appears to always use this system) + ## 0.5.0 - Initial support for oven hoods (@digitalbites) diff --git a/custom_components/ge_home/devices/advantium.py b/custom_components/ge_home/devices/advantium.py index 828192a..23775ec 100644 --- a/custom_components/ge_home/devices/advantium.py +++ b/custom_components/ge_home/devices/advantium.py @@ -2,7 +2,7 @@ from typing import List from homeassistant.helpers.entity import Entity -from gehomesdk.erd import ErdCode, ErdApplianceType +from gehomesdk.erd import ErdCode, ErdApplianceType, ErdDataType from .base import ApplianceApi from ..entities import GeAdvantium, GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdPropertyBinarySensor, UPPER_OVEN @@ -29,8 +29,8 @@ def get_all_entities(self) -> List[Entity]: 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"), - GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "power_level", icon_override="mdi:gauge"), + 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"), diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index 6dd2f4c..b2c42d2 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -14,7 +14,8 @@ ErdFilterStatus, HotWaterStatus, FridgeModelInfo, - ErdConvertableDrawerMode + ErdConvertableDrawerMode, + ErdDataType ) from .base import ApplianceApi @@ -110,7 +111,7 @@ def get_all_entities(self) -> List[Entity]: 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), + 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) ]) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index f2a2074..0dde6d5 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -1,5 +1,6 @@ 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 @@ -92,7 +93,7 @@ def get_all_entities(self) -> List[Entity]: 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)) + 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 diff --git a/custom_components/ge_home/devices/sac.py b/custom_components/ge_home/devices/sac.py index 40f87e2..98686e0 100644 --- a/custom_components/ge_home/devices/sac.py +++ b/custom_components/ge_home/devices/sac.py @@ -19,8 +19,8 @@ def get_all_entities(self) -> List[Entity]: sac_entities = [ GeSacClimate(self), - GeSacTemperatureSensor(self, ErdCode.AC_TARGET_TEMPERATURE), - GeSacTemperatureSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE), + 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"), diff --git a/custom_components/ge_home/entities/ac/ge_pac_climate.py b/custom_components/ge_home/entities/ac/ge_pac_climate.py index 13659e8..ba8eb75 100644 --- a/custom_components/ge_home/entities/ac/ge_pac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_pac_climate.py @@ -66,11 +66,6 @@ def __init__(self, api: ApplianceApi): #construct the converter based on the available modes self._hvac_mode_converter = PacHvacModeOptionsConverter(self._modes) - @property - def temperature_unit(self): - #SAC appears to be hard coded to use Fahrenheit internally, no matter what the display shows - return TEMP_FAHRENHEIT - @property def min_temp(self) -> float: temp = 64 diff --git a/custom_components/ge_home/entities/ac/ge_sac_climate.py b/custom_components/ge_home/entities/ac/ge_sac_climate.py index 722a483..7c7ed54 100644 --- a/custom_components/ge_home/entities/ac/ge_sac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_sac_climate.py @@ -69,12 +69,6 @@ def __init__(self, api: ApplianceApi): #construct the converter based on the available modes self._hvac_mode_converter = SacHvacModeOptionsConverter(self._modes) - - @property - def temperature_unit(self): - #SAC appears to be hard coded to use Fahrenheit internally, no matter what the display shows - return TEMP_FAHRENHEIT - @property def min_temp(self) -> float: temp = 60 diff --git a/custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py b/custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py deleted file mode 100644 index 854a239..0000000 --- a/custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py +++ /dev/null @@ -1,15 +0,0 @@ -import logging -from typing import Any, List, Optional - -from homeassistant.const import ( - TEMP_FAHRENHEIT -) -from ..common import GeErdSensor - -class GeSacTemperatureSensor(GeErdSensor): - """Class for Split A/C temperature sensors""" - - @property - def _temp_units(self) -> Optional[str]: - #SAC appears to be hard coded to use Fahrenheit internally, no matter what the display shows - return TEMP_FAHRENHEIT diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 2b28532..fc48f93 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -82,10 +82,12 @@ def fan_mode_erd_code(self): @property def temperature_unit(self): - measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) - if measurement_system == ErdMeasurementUnits.METRIC: - return TEMP_CELSIUS + #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): diff --git a/custom_components/ge_home/entities/common/ge_erd_property_sensor.py b/custom_components/ge_home/entities/common/ge_erd_property_sensor.py index 53d9a92..70938d0 100644 --- a/custom_components/ge_home/entities/common/ge_erd_property_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_property_sensor.py @@ -1,7 +1,7 @@ from typing import Optional import magicattr -from gehomesdk import ErdCode, ErdCodeType, ErdMeasurementUnits +from gehomesdk import ErdCode, ErdCodeType, ErdMeasurementUnits, ErdDataType from ...devices import ApplianceApi from .ge_erd_sensor import GeErdSensor @@ -11,13 +11,14 @@ class GeErdPropertySensor(GeErdSensor): 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 + state_class_override: str = None, uom_override: str = None, data_type_override: ErdDataType = None ): super().__init__( api, erd_code, erd_override=erd_override, icon_override=icon_override, device_class_override=device_class_override, state_class_override=state_class_override, - uom_override=uom_override + uom_override=uom_override, + data_type_override=data_type_override ) self.erd_property = erd_property self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") @@ -33,9 +34,17 @@ def name(self) -> Optional[str]: return f"{base_string} {property_name}" @property - def state(self) -> Optional[str]: + def native_value(self): try: value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) + + # if it's a numeric data type, return it directly + if self._data_type in [ErdDataType.INT, ErdDataType.FLOAT]: + return value + + # otherwise, return a stringified version + # TODO: perhaps enhance so that there's a list of variables available + # for the stringify function to consume... + return self._stringify(value, temp_units=self._temp_units) except KeyError: - return None - return self._stringify(value, temp_units=self._temp_units) + return None \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index b1b9163..264b86a 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -1,5 +1,6 @@ import logging from typing import Optional +from gehomesdk.erd.erd_data_type import ErdDataType from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.const import ( @@ -42,34 +43,54 @@ def __init__( device_class_override: str = None, state_class_override: str = None, uom_override: str = None, + data_type_override: ErdDataType = None ): super().__init__(api, erd_code, erd_override, icon_override, device_class_override) self._uom_override = uom_override self._state_class_override = state_class_override + self._data_type_override = data_type_override @property - def state(self) -> Optional[str]: + def native_value(self): try: value = self.appliance.get_erd_value(self.erd_code) + + # if it's a numeric data type, return it directly + if self._data_type in [ErdDataType.INT, ErdDataType.FLOAT]: + return value + + # otherwise, return a stringified version + # TODO: perhaps enhance so that there's a list of variables available + # for the stringify function to consume... + return self._stringify(value, temp_units=self._temp_units) except KeyError: return None - # TODO: perhaps enhance so that there's a list of variables available - # for the stringify function to consume... - return self._stringify(value, temp_units=self._temp_units) @property - def unit_of_measurement(self) -> Optional[str]: + def native_unit_of_measurement(self) -> Optional[str]: return self._get_uom() @property def state_class(self) -> Optional[str]: return self._get_state_class() + @property + def _data_type(self) -> ErdDataType: + if self._data_type_override is not None: + return self._data_type_override + + return self.appliance.get_erd_code_data_type(self.erd_code) + @property def _temp_units(self) -> Optional[str]: - if self._measurement_system == ErdMeasurementUnits.METRIC: - return TEMP_CELSIUS - return TEMP_FAHRENHEIT + #based on testing, all API values are in Fahrenheit, so we'll redefine + #this property to be the configured temperature unit and set the native + #unit differently + return self.api.hass.config.units.temperature_unit + + #if self._measurement_system == ErdMeasurementUnits.METRIC: + # return TEMP_CELSIUS + #return TEMP_FAHRENHEIT def _get_uom(self): """Select appropriate units""" @@ -83,7 +104,10 @@ def _get_uom(self): in [ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE] or self.device_class == DEVICE_CLASS_TEMPERATURE ): - return self._temp_units + #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 @@ -94,12 +118,12 @@ def _get_uom(self): 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" + #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" + #if self._measurement_system == ErdMeasurementUnits.METRIC: + # return "l" return "g" return None diff --git a/custom_components/ge_home/entities/common/ge_water_heater.py b/custom_components/ge_home/entities/common/ge_water_heater.py index e0d3e04..87bd091 100644 --- a/custom_components/ge_home/entities/common/ge_water_heater.py +++ b/custom_components/ge_home/entities/common/ge_water_heater.py @@ -34,9 +34,10 @@ def name(self) -> Optional[str]: @property def temperature_unit(self): - measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) - if measurement_system == ErdMeasurementUnits.METRIC: - return TEMP_CELSIUS + #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 diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 9d6b89c..d670012 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.12","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.16","magicattr==0.1.5"], "codeowners": ["@simbaja"], "version": "0.5.0" } From 468a8496db8728a0630655e68afbd2746e4ebf4b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Dec 2021 00:36:30 -0500 Subject: [PATCH 173/338] - initial support for water softeners --- CHANGELOG.md | 1 + custom_components/ge_home/devices/__init__.py | 4 ++ .../ge_home/devices/water_softener.py | 36 ++++++++++ .../ge_home/entities/__init__.py | 3 +- .../entities/water_softener/__init__.py | 1 + .../water_softener/shutoff_position.py | 65 +++++++++++++++++++ 6 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 custom_components/ge_home/devices/water_softener.py create mode 100644 custom_components/ge_home/entities/water_softener/__init__.py create mode 100644 custom_components/ge_home/entities/water_softener/shutoff_position.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c5cd65b..7640c63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Changed the sensors to use native value/uom - Changed the temperatures to always be natively fahrenheit (API appears to always use this system) +- Initial support for Water Softeners (@npentell) ## 0.5.0 diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 293fe3c..a7c2e00 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -3,6 +3,8 @@ from gehomesdk.erd import ErdApplianceType +from custom_components.ge_home.devices.water_softener import WaterSoftenerApi + from .base import ApplianceApi from .oven import OvenApi from .fridge import FridgeApi @@ -37,6 +39,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return WasherDryerApi if appliance_type == ErdApplianceType.POE_WATER_FILTER: return WaterFilterApi + if appliance_type == ErdApplianceType.WATER_SOFTENER: + return WaterSoftenerApi if appliance_type == ErdApplianceType.ADVANTIUM: return AdvantiumApi if appliance_type == ErdApplianceType.AIR_CONDITIONER: diff --git a/custom_components/ge_home/devices/water_softener.py b/custom_components/ge_home/devices/water_softener.py new file mode 100644 index 0000000..5490082 --- /dev/null +++ b/custom_components/ge_home/devices/water_softener.py @@ -0,0 +1,36 @@ +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, + 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"), + GeErdSensor(self, ErdCode.WH_FILTER_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"), + GeErdSensor(self, ErdCode.WH_SOFTENER_LOW_SALT, icon_override="mdi:grain"), + GeErdSensor(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_STATE, icon_override="mdi:state-machine"), + GeErdShutoffPositionSelect(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_CONTROL), + ] + entities = base_entities + ws_entities + return entities diff --git a/custom_components/ge_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index b03b590..0502aff 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -5,4 +5,5 @@ from .water_filter import * from .advantium import * from .ac import * -from .hood import * \ No newline at end of file +from .hood import * +from .water_softener import * \ No newline at end of file diff --git a/custom_components/ge_home/entities/water_softener/__init__.py b/custom_components/ge_home/entities/water_softener/__init__.py new file mode 100644 index 0000000..7ae738e --- /dev/null +++ b/custom_components/ge_home/entities/water_softener/__init__.py @@ -0,0 +1 @@ +from .shutoff_position import GeErdShutoffPositionSelect \ No newline at end of file diff --git a/custom_components/ge_home/entities/water_softener/shutoff_position.py b/custom_components/ge_home/entities/water_softener/shutoff_position.py new file mode 100644 index 0000000..38b0929 --- /dev/null +++ b/custom_components/ge_home/entities/water_softener/shutoff_position.py @@ -0,0 +1,65 @@ +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) From 8c64ef78cecd0c0fcdc01cdf0314bf3feb53f636 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Dec 2021 00:57:41 -0500 Subject: [PATCH 174/338] - initial support for opal ice makers --- CHANGELOG.md | 1 + custom_components/ge_home/devices/__init__.py | 6 ++- custom_components/ge_home/devices/oim.py | 39 +++++++++++++++++++ .../ge_home/entities/__init__.py | 3 +- .../ge_home/entities/common/ge_erd_entity.py | 6 ++- .../entities/opal_ice_maker/__init__.py | 1 + .../opal_ice_maker/oim_light_level_options.py | 26 +++++++++++++ 7 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 custom_components/ge_home/devices/oim.py create mode 100644 custom_components/ge_home/entities/opal_ice_maker/__init__.py create mode 100644 custom_components/ge_home/entities/opal_ice_maker/oim_light_level_options.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7640c63..0938980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Changed the sensors to use native value/uom - Changed the temperatures to always be natively fahrenheit (API appears to always use this system) - Initial support for Water Softeners (@npentell) +- Initial support for Opal Ice Makers (@mbcomer, @knobunc) ## 0.5.0 diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index a7c2e00..1208a0d 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -3,8 +3,6 @@ from gehomesdk.erd import ErdApplianceType -from custom_components.ge_home.devices.water_softener import WaterSoftenerApi - from .base import ApplianceApi from .oven import OvenApi from .fridge import FridgeApi @@ -18,6 +16,8 @@ from .sac import SacApi from .pac import PacApi from .hood import HoodApi +from .water_softener import WaterSoftenerApi +from .oim import OimApi _LOGGER = logging.getLogger(__name__) @@ -51,6 +51,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return PacApi if appliance_type == ErdApplianceType.HOOD: return HoodApi + if appliance_type == ErdApplianceType.OPAL_ICE_MAKER: + return OimApi # Fallback return ApplianceApi diff --git a/custom_components/ge_home/devices/oim.py b/custom_components/ge_home/devices/oim.py new file mode 100644 index 0000000..c2703cc --- /dev/null +++ b/custom_components/ge_home/devices/oim.py @@ -0,0 +1,39 @@ +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, + GeErdSelect, + GeErdSwitch, + ErdOnOffBoolConverter +) + +_LOGGER = logging.getLogger(__name__) + + +class OimApi(ApplianceApi): + """API class for Oven Hood objects""" + APPLIANCE_TYPE = ErdApplianceType.HOOD + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + oim_entities = [ + GeErdSensor(self, ErdCode.OIM_STATUS), + GeErdSensor(self, ErdCode.OIM_FILTER_STATUS), + 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_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index 0502aff..b1da8ef 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -6,4 +6,5 @@ from .advantium import * from .ac import * from .hood import * -from .water_softener import * \ No newline at end of file +from .water_softener import * +from .opal_ice_maker import * \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index a23a36c..a93a1fb 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -138,6 +138,10 @@ def _get_icon(self): if self.erd_code_class == ErdCodeClass.FAN: return "mdi:fan" if self.erd_code_class == ErdCodeClass.LIGHT: - return "mdi:lightbulb" + 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" return None diff --git a/custom_components/ge_home/entities/opal_ice_maker/__init__.py b/custom_components/ge_home/entities/opal_ice_maker/__init__.py new file mode 100644 index 0000000..5ec3f31 --- /dev/null +++ b/custom_components/ge_home/entities/opal_ice_maker/__init__.py @@ -0,0 +1 @@ +from .oim_light_level_options import OimLightLevelOptionsConverter \ No newline at end of file diff --git a/custom_components/ge_home/entities/opal_ice_maker/oim_light_level_options.py b/custom_components/ge_home/entities/opal_ice_maker/oim_light_level_options.py new file mode 100644 index 0000000..019500d --- /dev/null +++ b/custom_components/ge_home/entities/opal_ice_maker/oim_light_level_options.py @@ -0,0 +1,26 @@ +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() From 52afe3c044d7fe3daba4287a67785356e028dc22 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Dec 2021 00:59:28 -0500 Subject: [PATCH 175/338] - updated deprecated icons --- custom_components/ge_home/devices/dishwasher.py | 4 ++-- custom_components/ge_home/entities/common/ge_erd_entity.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index 4700c78..d51913e 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -23,14 +23,14 @@ def get_all_entities(self) -> List[Entity]: 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"), - GeErdSensor(self, ErdCode.DISHWASHER_RINSE_AGENT, icon_override="mdi:sparkles"), + GeErdSensor(self, ErdCode.DISHWASHER_RINSE_AGENT, icon_override="mdi:shimmer"), GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING), GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS), #User Setttings GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound", icon_override="mdi:volume-high"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control", icon_override="mdi:lock"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath", icon_override="mdi:judaism"), + 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"), diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index a93a1fb..32f7b66 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -104,7 +104,7 @@ def _get_icon(self): if self.erd_code_class == ErdCodeClass.LOCK_CONTROL: return "mdi:lock-outline" if self.erd_code_class == ErdCodeClass.SABBATH_CONTROL: - return "mdi:judaism" + return "mdi:star-david" if self.erd_code_class == ErdCodeClass.COOLING_CONTROL: return "mdi:snowflake" if self.erd_code_class == ErdCodeClass.OVEN_SENSOR: From 739c272e41f6320cfa602d097a6fc67e5cb1a7b3 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Dec 2021 11:06:30 -0500 Subject: [PATCH 176/338] - updated versions and documentation --- CHANGELOG.md | 1 + README.md | 5 ++++- custom_components/ge_home/manifest.json | 4 ++-- hacs.json | 2 +- info.md | 21 ++++++++++++++++++++- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0938980..19f908e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Changed the temperatures to always be natively fahrenheit (API appears to always use this system) - Initial support for Water Softeners (@npentell) - Initial support for Opal Ice Makers (@mbcomer, @knobunc) +- Updated deprecated icons (@mjmeli, @schmittx) ## 0.5.0 diff --git a/README.md b/README.md index ad30d3d..59ec6e0 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,12 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio - Dishwasher - Laundry (Washer/Dryer) - Whole Home Water Filter +- Whole Home Water Softener - A/C (Portable, Split, Window) -- Range Hoods +- Range Hood - Advantium +- Microwave +- Opal Ice Maker **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index d670012..5d2e346 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.16","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.17","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.5.0" + "version": "0.6.0" } diff --git a/hacs.json b/hacs.json index f508818..25fb813 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Home (SmartHQ)", - "homeassistant": "2021.7.1", + "homeassistant": "2021.11.0", "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select"], "iot_class": "Cloud Polling" } diff --git a/info.md b/info.md index 63cc22b..e77e223 100644 --- a/info.md +++ b/info.md @@ -2,12 +2,17 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integration currently support the following devices: -- Fridge +- Fridge - Oven - Dishwasher - Laundry (Washer/Dryer) - Whole Home Water Filter +- Whole Home Water Softener +- A/C (Portable, Split, Window) +- Range Hood - Advantium +- Microwave +- Opal Ice Maker **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** @@ -34,6 +39,11 @@ A/C Controls: #### Breaking Changes +{% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} +- Changed the sensors to use native value/uom +- Changed the temperatures to always be natively fahrenheit (API appears to always use this system) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.4.0'.split('.') | map('int') %} - Laundry support changes will cause entity names to be different, you will need to fix in HA (uninstall, reboot, delete leftover entitites, install, reboot) {% endif %} @@ -46,6 +56,11 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} +- Initial support for Water Softeners (@npentell) +- Initial support for Opal Ice Makers (@mbcomer, @knobunc) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} - Support for Oven Hood units (@digitalbites) - Added extended mode support for ovens @@ -67,6 +82,10 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} +- Updated deprecated icons (@mjmeli, @schmittx) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} - Advantium fixes (@willhayslett) - Fixed device info when serial not present (@Xe138) From d0e46ed6077a0de7a9378709244eb1b213a1b340 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Dec 2021 11:10:26 -0500 Subject: [PATCH 177/338] - added water softener life remaining sensor --- custom_components/ge_home/devices/water_softener.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/ge_home/devices/water_softener.py b/custom_components/ge_home/devices/water_softener.py index 5490082..fed8841 100644 --- a/custom_components/ge_home/devices/water_softener.py +++ b/custom_components/ge_home/devices/water_softener.py @@ -30,6 +30,7 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.WH_SOFTENER_ERROR_CODE, icon_override="mdi:alert-circle"), GeErdSensor(self, ErdCode.WH_SOFTENER_LOW_SALT, icon_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:wrench-clock"), GeErdShutoffPositionSelect(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_CONTROL), ] entities = base_entities + ws_entities From 034d0df55885e25ba9e3f0be86abb9498d231c0d Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Dec 2021 18:41:07 -0500 Subject: [PATCH 178/338] - added oven light selections --- custom_components/ge_home/devices/oven.py | 11 +++- .../ge_home/entities/oven/__init__.py | 1 + .../oven/ge_oven_light_level_select.py | 65 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 custom_components/ge_home/entities/oven/ge_oven_light_level_select.py diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index 0dde6d5..ff2d077 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -9,7 +9,8 @@ ErdApplianceType, OvenConfiguration, ErdCooktopConfig, - CooktopStatus + CooktopStatus, + ErdOvenLightLevelAvailability ) from .base import ApplianceApi @@ -20,6 +21,7 @@ GeErdPropertySensor, GeErdPropertyBinarySensor, GeOven, + GeOvenLightLevelSelect, UPPER_OVEN, LOWER_OVEN ) @@ -41,6 +43,9 @@ def get_all_entities(self) -> List[Entity]: 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_availability: ErdOvenLightLevelAvailability = self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT_AVAILABILITY) + 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 = [] @@ -69,6 +74,8 @@ def get_all_entities(self) -> List[Entity]: 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: + 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)), @@ -81,6 +88,8 @@ def get_all_entities(self) -> List[Entity]: ]) 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: + oven_entities.append(GeOvenLightLevelSelect(self, ErdCode.UPPER_OVEN_LIGHT, self._single_name(ErdCode.UPPER_OVEN_LIGHT))) if cooktop_config == ErdCooktopConfig.PRESENT: diff --git a/custom_components/ge_home/entities/oven/__init__.py b/custom_components/ge_home/entities/oven/__init__.py index a5e7f85..e4166e8 100644 --- a/custom_components/ge_home/entities/oven/__init__.py +++ b/custom_components/ge_home/entities/oven/__init__.py @@ -1,2 +1,3 @@ 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_home/entities/oven/ge_oven_light_level_select.py b/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py new file mode 100644 index 0000000..c94a3c0 --- /dev/null +++ b/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py @@ -0,0 +1,65 @@ +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.has_dimmed: + 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): + 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)) + + 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 From c6155d15b6c1c89367bc6e096e59c9a6ac4e82a9 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Dec 2021 18:42:07 -0500 Subject: [PATCH 179/338] - fixed issue with removed entity type --- custom_components/ge_home/devices/sac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/sac.py b/custom_components/ge_home/devices/sac.py index 98686e0..a1dfad5 100644 --- a/custom_components/ge_home/devices/sac.py +++ b/custom_components/ge_home/devices/sac.py @@ -5,7 +5,7 @@ from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeSacClimate, GeSacTemperatureSensor, GeErdSensor, GeErdSwitch, ErdOnOffBoolConverter +from ..entities import GeSacClimate, GeErdSensor, GeErdSwitch, ErdOnOffBoolConverter _LOGGER = logging.getLogger(__name__) From b07e8f9dee9d394e417495f8c8f446cbd5a941cc Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Dec 2021 19:07:21 -0500 Subject: [PATCH 180/338] - updated gehomesdk version requirement --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 5d2e346..4bf6cc5 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.17","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.19","magicattr==0.1.5"], "codeowners": ["@simbaja"], "version": "0.6.0" } From 180b11f66727b2eb992378fef61a98ced106416b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Dec 2021 19:55:09 -0500 Subject: [PATCH 181/338] - fixed interitance and state classes in GeErdSensor --- .../ge_home/entities/common/ge_erd_sensor.py | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index 264b86a..240de5b 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -1,7 +1,7 @@ import logging from typing import Optional from gehomesdk.erd.erd_data_type import ErdDataType -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.const import ( DEVICE_CLASS_ENERGY, @@ -13,25 +13,13 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -#from homeassistant.components.sensor import ( -# STATE_CLASS_MEASUREMENT, -# STATE_CLASS_TOTAL_INCREASING -#) -# For now, let's not force the newer version, we'll use the same constants -# but it'll be optional. -# TODO: Force the usage of new HA -STATE_CLASS_MEASUREMENT = "measurement" -STATE_CLASS_TOTAL_INCREASING = 'total_increasing' - -from homeassistant.helpers.entity import Entity from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits - from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi _LOGGER = logging.getLogger(__name__) -class GeErdSensor(GeErdEntity, Entity): +class GeErdSensor(GeErdEntity, SensorEntity): """GE Entity for sensors""" def __init__( @@ -149,11 +137,11 @@ def _get_state_class(self) -> Optional[str]: return self._state_class_override if self.device_class in [DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_ENERGY]: - return STATE_CLASS_MEASUREMENT + return SensorStateClass.MEASUREMENT if self.erd_code_class in [ErdCodeClass.FLOW_RATE, ErdCodeClass.PERCENTAGE]: - return STATE_CLASS_MEASUREMENT + return SensorStateClass.MEASUREMENT if self.erd_code_class in [ErdCodeClass.LIQUID_VOLUME]: - return STATE_CLASS_TOTAL_INCREASING + return SensorStateClass.TOTAL_INCREASING return None From 8fc846ebb7e319f33d5debc64b5a1a32782b84b7 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 11 Dec 2021 20:28:53 -0500 Subject: [PATCH 182/338] - fixed issues with oven light control --- .../ge_home/entities/oven/ge_oven_light_level_select.py | 7 ++++--- custom_components/ge_home/manifest.json | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py b/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py index c94a3c0..8c63973 100644 --- a/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py +++ b/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py @@ -13,7 +13,7 @@ def __init__(self, availability: ErdOvenLightLevelAvailability): self.availability = availability self.excluded_levels = [ErdOvenLightLevel.NOT_AVAILABLE] - if not availability or not availability.has_dimmed: + if not availability or not availability.dim_available: self.excluded_levels.append(ErdOvenLightLevel.DIM) @property @@ -35,7 +35,7 @@ def to_option_string(self, value: ErdOvenLightLevel) -> Optional[str]: class GeOvenLightLevelSelect(GeErdSelect): - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): + 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 @@ -43,8 +43,9 @@ def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): 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)) + super().__init__(api, erd_code, OvenLightLevelOptionsConverter(self._availability), erd_override=erd_override) + @property def assumed_state(self) -> bool: return not self._has_status diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 4bf6cc5..af54f66 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.19","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.20","magicattr==0.1.5"], "codeowners": ["@simbaja"], "version": "0.6.0" } From d77062cdf9960d253cab6ed275b6aa0a8b3c0c93 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 12 Dec 2021 11:10:11 -0500 Subject: [PATCH 183/338] - fixes for flow rate and icons --- custom_components/ge_home/devices/water_filter.py | 3 ++- custom_components/ge_home/devices/water_softener.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/devices/water_filter.py b/custom_components/ge_home/devices/water_filter.py index b926291..d938df1 100644 --- a/custom_components/ge_home/devices/water_filter.py +++ b/custom_components/ge_home/devices/water_filter.py @@ -7,6 +7,7 @@ from .base import ApplianceApi from ..entities import ( GeErdSensor, + GeErdPropertySensor, GeErdBinarySensor, GeErdFilterPositionSelect, ) @@ -28,7 +29,7 @@ def get_all_entities(self) -> List[Entity]: 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"), - GeErdSensor(self, ErdCode.WH_FILTER_FLOW_RATE), + GeErdPropertySensor(self, ErdCode.WH_FILTER_FLOW_RATE, "flow_rate"), GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE), GeErdSensor(self, ErdCode.WH_FILTER_LIFE_REMAINING), GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"), diff --git a/custom_components/ge_home/devices/water_softener.py b/custom_components/ge_home/devices/water_softener.py index fed8841..5e5feed 100644 --- a/custom_components/ge_home/devices/water_softener.py +++ b/custom_components/ge_home/devices/water_softener.py @@ -7,6 +7,7 @@ from .base import ApplianceApi from ..entities import ( GeErdSensor, + GeErdPropertySensor, GeErdBinarySensor, GeErdShutoffPositionSelect, ) @@ -24,13 +25,13 @@ def get_all_entities(self) -> List[Entity]: ws_entities = [ GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE, icon_on_override="mdi:human", icon_off_override="mdi:robot"), - GeErdSensor(self, ErdCode.WH_FILTER_FLOW_RATE), + 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"), GeErdSensor(self, ErdCode.WH_SOFTENER_LOW_SALT, icon_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:wrench-clock"), + 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 From a9cd736d5d7daeeebaed2e9337fc18a87ee2f3e5 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 12 Dec 2021 11:23:13 -0500 Subject: [PATCH 184/338] - documentation updates --- CHANGELOG.md | 1 + hacs.json | 2 +- info.md | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f908e..78ab853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.6.0 +- Requires HA 2021.12.x or later - Changed the sensors to use native value/uom - Changed the temperatures to always be natively fahrenheit (API appears to always use this system) - Initial support for Water Softeners (@npentell) diff --git a/hacs.json b/hacs.json index 25fb813..e9e1a0b 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Home (SmartHQ)", - "homeassistant": "2021.11.0", + "homeassistant": "2021.12.0", "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select"], "iot_class": "Cloud Polling" } diff --git a/info.md b/info.md index e77e223..a096566 100644 --- a/info.md +++ b/info.md @@ -40,6 +40,7 @@ A/C Controls: #### Breaking Changes {% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} +- Requires HA version 2021.12.0 or later - Changed the sensors to use native value/uom - Changed the temperatures to always be natively fahrenheit (API appears to always use this system) {% endif %} From b870725afbea10aba55c003b4995f86faa582c71 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 12 Dec 2021 11:24:45 -0500 Subject: [PATCH 185/338] - documentation --- CHANGELOG.md | 2 +- info.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ab853..f2d32e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - Requires HA 2021.12.x or later - Changed the sensors to use native value/uom -- Changed the temperatures to always be natively fahrenheit (API appears to always use this system) +- Changed the temperatures to always be natively fahrenheit (API appears to always use this system) (@vignatyuk) - Initial support for Water Softeners (@npentell) - Initial support for Opal Ice Makers (@mbcomer, @knobunc) - Updated deprecated icons (@mjmeli, @schmittx) diff --git a/info.md b/info.md index a096566..e88b544 100644 --- a/info.md +++ b/info.md @@ -42,7 +42,7 @@ A/C Controls: {% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} - Requires HA version 2021.12.0 or later - Changed the sensors to use native value/uom -- Changed the temperatures to always be natively fahrenheit (API appears to always use this system) +- Changed the temperatures to always be natively fahrenheit (API appears to always use this system) (@vignatyuk) {% endif %} {% if version_installed.split('.') | map('int') < '0.4.0'.split('.') | map('int') %} From 4c2cf74cb64143dc892e51d851497e1cebf0a1ff Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 12 Dec 2021 12:37:29 -0500 Subject: [PATCH 186/338] - sdk version bump - initial support for microwaves - updated support for hoods - documentation updates --- CHANGELOG.md | 3 +- custom_components/ge_home/devices/__init__.py | 3 + custom_components/ge_home/devices/hood.py | 4 +- .../ge_home/devices/microwave.py | 56 +++++++++++++++++++ custom_components/ge_home/manifest.json | 2 +- info.md | 3 +- 6 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 custom_components/ge_home/devices/microwave.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d32e0..7f1930d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ - Requires HA 2021.12.x or later - Changed the sensors to use native value/uom - Changed the temperatures to always be natively fahrenheit (API appears to always use this system) (@vignatyuk) -- Initial support for Water Softeners (@npentell) +- Initial support for Microwaves (@mbcomer, @mnestor) +- Initial support for Water Softeners (@npentell, @drjeff) - Initial support for Opal Ice Makers (@mbcomer, @knobunc) - Updated deprecated icons (@mjmeli, @schmittx) diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 1208a0d..9e17d3b 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -16,6 +16,7 @@ from .sac import SacApi from .pac import PacApi from .hood import HoodApi +from .microwave import MicrowaveApi from .water_softener import WaterSoftenerApi from .oim import OimApi @@ -51,6 +52,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return PacApi if appliance_type == ErdApplianceType.HOOD: return HoodApi + if appliance_type == ErdApplianceType.MICROWAVE: + return MicrowaveApi if appliance_type == ErdApplianceType.OPAL_ICE_MAKER: return OimApi diff --git a/custom_components/ge_home/devices/hood.py b/custom_components/ge_home/devices/hood.py index e57b590..439c775 100644 --- a/custom_components/ge_home/devices/hood.py +++ b/custom_components/ge_home/devices/hood.py @@ -14,7 +14,7 @@ from ..entities import ( GeHoodLightLevelSelect, GeHoodFanSpeedSelect, - GeErdSensor, + GeErdTimerSensor, GeErdSwitch, ErdOnOffBoolConverter ) @@ -45,7 +45,7 @@ def get_all_entities(self) -> List[Entity]: 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(GeErdSensor(self, ErdCode.HOOD_TIMER)) + hood_entities.append(GeErdTimerSensor(self, ErdCode.HOOD_TIMER)) entities = base_entities + hood_entities return entities diff --git a/custom_components/ge_home/devices/microwave.py b/custom_components/ge_home/devices/microwave.py new file mode 100644 index 0000000..ec943fa --- /dev/null +++ b/custom_components/ge_home/devices/microwave.py @@ -0,0 +1,56 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdHoodFanSpeedAvailability, + ErdHoodLightLevelAvailability, + ErdOnOff +) + +from .base import ApplianceApi +from ..entities import ( + GeHoodLightLevelSelect, + GeHoodFanSpeedSelect, + GeErdPropertySensor, + GeErdPropertyBinarySensor, + GeErdBinarySensor, + GeErdTimerSensor +) + +_LOGGER = logging.getLogger(__name__) + + +class MicrowaveApi(ApplianceApi): + """API class for Microwave objects""" + APPLIANCE_TYPE = ErdApplianceType.MICROWAVE + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + #get the availabilities + fan_availability: ErdHoodFanSpeedAvailability = self.try_get_erd_value(ErdCode.HOOD_FAN_SPEED_AVAILABILITY) + light_availability: ErdHoodLightLevelAvailability = self.try_get_erd_value(ErdCode.HOOD_LIGHT_LEVEL_AVAILABILITY) + + mwave_entities = [ + GeErdBinarySensor(self, ErdCode.MICROWAVE_REMOTE_ENABLE), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "status"), + GeErdPropertyBinarySensor(self, ErdCode.MICROWAVE_STATE, "door_status", device_class_override="door"), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "cook_mode", icon_override="mdi:food-turkey"), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "power_level", icon_override="mdi:gauge"), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "temperature", icon_override="mdi:thermometer"), + GeErdTimerSensor(self, ErdCode.MICROWAVE_COOK_TIMER), + GeErdTimerSensor(self, ErdCode.MICROWAVE_KITCHEN_TIMER) + ] + + if fan_availability and fan_availability.is_available: + mwave_entities.append(GeHoodFanSpeedSelect(self, ErdCode.HOOD_FAN_SPEED)) + #for now, represent as a select + if light_availability and light_availability.is_available: + mwave_entities.append(GeHoodLightLevelSelect(self, ErdCode.HOOD_LIGHT_LEVEL)) + + entities = base_entities + mwave_entities + return entities + diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index af54f66..b821356 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.20","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.21","magicattr==0.1.5"], "codeowners": ["@simbaja"], "version": "0.6.0" } diff --git a/info.md b/info.md index e88b544..ae9a5b2 100644 --- a/info.md +++ b/info.md @@ -58,8 +58,9 @@ A/C Controls: #### Features {% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} -- Initial support for Water Softeners (@npentell) +- Initial support for Water Softeners (@npentell, @drjeff) - Initial support for Opal Ice Makers (@mbcomer, @knobunc) +- Initial support for Microwaves (@mbcomer, @mnestor) {% endif %} {% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} From 42fc0c4e5ab4ef90cc6bcaf0b806fc752afd90ff Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 12 Dec 2021 12:45:04 -0500 Subject: [PATCH 187/338] - updated filter status entity type for oim --- custom_components/ge_home/devices/oim.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/oim.py b/custom_components/ge_home/devices/oim.py index c2703cc..ad1bd06 100644 --- a/custom_components/ge_home/devices/oim.py +++ b/custom_components/ge_home/devices/oim.py @@ -12,6 +12,7 @@ from ..entities import ( OimLightLevelOptionsConverter, GeErdSensor, + GeErdBinarySensor, GeErdSelect, GeErdSwitch, ErdOnOffBoolConverter @@ -29,7 +30,7 @@ def get_all_entities(self) -> List[Entity]: oim_entities = [ GeErdSensor(self, ErdCode.OIM_STATUS), - GeErdSensor(self, ErdCode.OIM_FILTER_STATUS), + GeErdBinarySensor(self, ErdCode.OIM_FILTER_STATUS, 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"), ] From 9d27a3ea365966d2ecba46ea469de0bfcc44d699 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 12 Dec 2021 13:25:43 -0500 Subject: [PATCH 188/338] - fixed issue with missing GeSacTemperatureSensor - added initial coffee maker support --- custom_components/ge_home/devices/__init__.py | 3 ++ .../ge_home/devices/coffee_maker.py | 40 +++++++++++++++++++ .../ge_home/entities/__init__.py | 3 +- .../ge_home/entities/ac/__init__.py | 3 +- .../ge_home/entities/ccm/__init__.py | 1 + .../ge_ccm_pot_not_present_binary_sensor.py | 8 ++++ 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 custom_components/ge_home/devices/coffee_maker.py create mode 100644 custom_components/ge_home/entities/ccm/__init__.py create mode 100644 custom_components/ge_home/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 9e17d3b..1acc228 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -19,6 +19,7 @@ from .microwave import MicrowaveApi from .water_softener import WaterSoftenerApi from .oim import OimApi +from .coffee_maker import CcmApi _LOGGER = logging.getLogger(__name__) @@ -56,6 +57,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return MicrowaveApi if appliance_type == ErdApplianceType.OPAL_ICE_MAKER: return OimApi + if appliance_type == ErdApplianceType.CAFE_COFFEE_MAKER: + return CcmApi # Fallback return ApplianceApi diff --git a/custom_components/ge_home/devices/coffee_maker.py b/custom_components/ge_home/devices/coffee_maker.py new file mode 100644 index 0000000..9fe9a62 --- /dev/null +++ b/custom_components/ge_home/devices/coffee_maker.py @@ -0,0 +1,40 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType +) + +from .base import ApplianceApi +from ..entities import ( + GeCcmPotNotPresentBinarySensor, + GeErdSensor, + GeErdBinarySensor +) + +_LOGGER = logging.getLogger(__name__) + + +class CcmApi(ApplianceApi): + """API class for Cafe Coffee Maker objects""" + APPLIANCE_TYPE = ErdApplianceType.CAFE_COFFEE_MAKER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + ccm_entities = [ + GeErdBinarySensor(self, ErdCode.CCM_IS_BREWING, device_class_override="heat"), + GeErdBinarySensor(self, ErdCode.CCM_IS_DESCALING), + GeErdSensor(self, ErdCode.CCM_BREW_STRENGTH), + GeErdSensor(self, ErdCode.CCM_BREW_CUPS), + GeErdSensor(self, ErdCode.CCM_BREW_TEMPERATURE), + 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 + diff --git a/custom_components/ge_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index b1da8ef..eabcc59 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -7,4 +7,5 @@ from .ac import * from .hood import * from .water_softener import * -from .opal_ice_maker import * \ No newline at end of file +from .opal_ice_maker import * +from .ccm import * \ No newline at end of file diff --git a/custom_components/ge_home/entities/ac/__init__.py b/custom_components/ge_home/entities/ac/__init__.py index 0b54100..0f2e6ad 100644 --- a/custom_components/ge_home/entities/ac/__init__.py +++ b/custom_components/ge_home/entities/ac/__init__.py @@ -1,4 +1,3 @@ from .ge_wac_climate import GeWacClimate from .ge_sac_climate import GeSacClimate -from .ge_pac_climate import GePacClimate -from .ge_sac_temperature_sensor import GeSacTemperatureSensor \ No newline at end of file +from .ge_pac_climate import GePacClimate \ No newline at end of file diff --git a/custom_components/ge_home/entities/ccm/__init__.py b/custom_components/ge_home/entities/ccm/__init__.py new file mode 100644 index 0000000..1bb8e10 --- /dev/null +++ b/custom_components/ge_home/entities/ccm/__init__.py @@ -0,0 +1 @@ +from .ge_ccm_pot_not_present_binary_sensor import GeCcmPotNotPresentBinarySensor \ No newline at end of file diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py b/custom_components/ge_home/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py new file mode 100644 index 0000000..124914a --- /dev/null +++ b/custom_components/ge_home/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py @@ -0,0 +1,8 @@ +from ..common import GeErdBinarySensor + +class GeCcmPotNotPresentBinarySensor(GeErdBinarySensor): + @property + def is_on(self) -> bool: + """Return True if entity is not pot present.""" + return not self._boolify(self.appliance.get_erd_value(self.erd_code)) + From 53dcf9b41940d090bf61f20439f3dae50e467d6a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 12 Dec 2021 13:27:10 -0500 Subject: [PATCH 189/338] - changed the water softener low salt to a binary sensor --- custom_components/ge_home/devices/water_softener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/water_softener.py b/custom_components/ge_home/devices/water_softener.py index 5e5feed..a730471 100644 --- a/custom_components/ge_home/devices/water_softener.py +++ b/custom_components/ge_home/devices/water_softener.py @@ -29,7 +29,7 @@ def get_all_entities(self) -> List[Entity]: 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"), - GeErdSensor(self, ErdCode.WH_SOFTENER_LOW_SALT, icon_override="mdi:grain"), + 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), From 01e56ea6fec32b4bddec1f900affe18d04aed061 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 12 Dec 2021 18:38:17 -0500 Subject: [PATCH 190/338] - added coffee maker control --- .../ge_home/devices/coffee_maker.py | 4 +- .../ge_home/entities/ccm/__init__.py | 3 +- .../ge_home/entities/ccm/const.py | 8 + .../ge_home/entities/ccm/ge_ccm.py | 149 ++++++++++++++++++ .../ge_home/entities/ccm/ge_ccm_options.py | 58 +++++++ 5 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 custom_components/ge_home/entities/ccm/const.py create mode 100644 custom_components/ge_home/entities/ccm/ge_ccm.py create mode 100644 custom_components/ge_home/entities/ccm/ge_ccm_options.py diff --git a/custom_components/ge_home/devices/coffee_maker.py b/custom_components/ge_home/devices/coffee_maker.py index 9fe9a62..8d248da 100644 --- a/custom_components/ge_home/devices/coffee_maker.py +++ b/custom_components/ge_home/devices/coffee_maker.py @@ -11,7 +11,8 @@ from ..entities import ( GeCcmPotNotPresentBinarySensor, GeErdSensor, - GeErdBinarySensor + GeErdBinarySensor, + GeCcm ) _LOGGER = logging.getLogger(__name__) @@ -33,6 +34,7 @@ def get_all_entities(self) -> List[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"), + GeCcm(self) ] entities = base_entities + ccm_entities diff --git a/custom_components/ge_home/entities/ccm/__init__.py b/custom_components/ge_home/entities/ccm/__init__.py index 1bb8e10..dc2da57 100644 --- a/custom_components/ge_home/entities/ccm/__init__.py +++ b/custom_components/ge_home/entities/ccm/__init__.py @@ -1 +1,2 @@ -from .ge_ccm_pot_not_present_binary_sensor import GeCcmPotNotPresentBinarySensor \ No newline at end of file +from .ge_ccm_pot_not_present_binary_sensor import GeCcmPotNotPresentBinarySensor +from .ge_ccm import GeCcm \ No newline at end of file diff --git a/custom_components/ge_home/entities/ccm/const.py b/custom_components/ge_home/entities/ccm/const.py new file mode 100644 index 0000000..64ed669 --- /dev/null +++ b/custom_components/ge_home/entities/ccm/const.py @@ -0,0 +1,8 @@ +from homeassistant.components.water_heater import ( + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE +) + +GE_CCM = SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE +MAX_CUPS = 10 +MIN_CUPS = 2 \ No newline at end of file diff --git a/custom_components/ge_home/entities/ccm/ge_ccm.py b/custom_components/ge_home/entities/ccm/ge_ccm.py new file mode 100644 index 0000000..aaa5d9f --- /dev/null +++ b/custom_components/ge_home/entities/ccm/ge_ccm.py @@ -0,0 +1,149 @@ +"""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, + ErdCcmBrewSettings, + ErdCcmBrewStrength +) + +from homeassistant.const import ATTR_TEMPERATURE + +from ...const import DOMAIN +from ...devices import ApplianceApi +from ..common import GeWaterHeater +from .const import * +from .ge_ccm_options import CcmBrewOptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class GeCcm(GeWaterHeater): + """GE Appliance Cafe Coffee Maker""" + + icon = "mdi:coffee-maker" + + def __init__(self, api: ApplianceApi): + super().__init__(api) + self._options = CcmBrewOptionsConverter() + + @property + def supported_features(self): + return GE_CCM + + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.serial_number}" + + @property + def name(self) -> Optional[str]: + return f"{self.serial_number} Coffee Maker" + + @property + def unit_type(self) -> Optional[ErdUnitType]: + try: + return self.appliance.get_erd_value(ErdCode.UNIT_TYPE) + except: + return None + + @property + def current_temperature(self) -> Optional[int]: + return self.appliance.get_erd_value(ErdCode.CCM_CURRENT_WATER_TEMPERATURE) + + @property + def is_brewing(self) -> bool: + return self.appliance.get_erd_value(ErdCode.CCM_IS_BREWING) + + @property + def is_descaling(self) -> bool: + return self.appliance.get_erd_value(ErdCode.CCM_IS_DESCALING) + + @property + def current_operation(self) -> Optional[str]: + try: + settings: ErdCcmBrewSettings = self.appliance.get_erd_value(ErdCode.CCM_BREW_SETTINGS) + if self.is_descaling: + return "Descale" + if not self.is_brewing: + return "Off" + return self._options.to_option_string(settings) + except: + return None + + @property + def operation_list(self) -> List[str]: + return self._options.options + + @property + def current_brew_setting(self) -> ErdCcmBrewSettings: + """Get the current brew setting.""" + return self.appliance.get_erd_value(ErdCode.CCM_BREW_SETTINGS) + + @property + def target_temperature(self) -> Optional[int]: + """Return the temperature we try to reach.""" + try: + return self.appliance.get_erd_value(ErdCode.CCM_BREW_TEMPERATURE) + except: + pass + return None + + @property + def min_temp(self) -> int: + """Return the minimum temperature.""" + min_temp, _, _ = self.appliance.get_erd_value(ErdCode.CCM_BREW_TEMPERATURE_RANGE) + return min_temp + + @property + def max_temp(self) -> int: + """Return the maximum temperature.""" + _, max_temp, _ = self.appliance.get_erd_value(ErdCode.CCM_BREW_TEMPERATURE_RANGE) + return max_temp + + @property + def extra_state_attributes(self) -> Optional[Mapping[str, Any]]: + data = {} + + data["unit_type"] = self._stringify(self.unit_type) + + return data + + @property + def can_set_temperature(self) -> bool: + """Indicates whether we can set the temperature based on the current mode""" + return not self.is_descaling + + async def async_set_operation_mode(self, operation_mode: str): + """Set the operation mode.""" + + #try to get the mode/setting for the selection + try: + if operation_mode not in ["Off","Descale"]: + new_mode = self._options.from_option_string(operation_mode) + new_mode.brew_temperature = self.target_temperature + + await self.appliance.async_set_erd_value(ErdCode.CCM_BREW_SETTINGS, new_mode) + elif operation_mode == "Off": + await self.appliance.async_set_erd_value(ErdCode.CCM_CANCEL_BREWING, True) + await self.appliance.async_set_erd_value(ErdCode.CCM_CANCEL_DESCALING, True) + elif operation_mode == "Descale": + await self.appliance.async_set_erd_value(ErdCode.CCM_START_DESCALING, True) + except: + _LOGGER.debug(f"Error Attempting to set mode to {operation_mode}.") + + async def async_set_temperature(self, **kwargs): + """Set the brew temperature""" + + target_temp = kwargs.get(ATTR_TEMPERATURE) + if target_temp is None: + return + + # get the current strength/cups + strength: ErdCcmBrewStrength = self.appliance.get_erd_value(ErdCode.CCM_BREW_STRENGTH) + cups: int = self.appliance.get_erd_value(ErdCode.CCM_BREW_CUPS) + new_mode = ErdCcmBrewSettings(cups, strength, target_temp) + + await self.appliance.async_set_erd_value(ErdCode.CCM_BREW_SETTINGS, new_mode) + diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_options.py b/custom_components/ge_home/entities/ccm/ge_ccm_options.py new file mode 100644 index 0000000..54c2f17 --- /dev/null +++ b/custom_components/ge_home/entities/ccm/ge_ccm_options.py @@ -0,0 +1,58 @@ +import logging +from typing import List, Any, NamedTuple, Optional + +from gehomesdk import ErdCcmBrewStrength, ErdCcmBrewSettings +from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.util.unit_system import UnitSystem + +from custom_components.ge_home.entities.ccm.const import MAX_CUPS, MIN_CUPS +from ..common import OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class CcmBrewOption(NamedTuple): + strength: ErdCcmBrewStrength + cups: int + + def stringify(self): + return f"{self.strength.stringify()} -- {self.cups} cups" + +class CcmBrewOptionsConverter(OptionsConverter): + def __init__(self, units: UnitSystem): + super().__init__() + self._units = units + self._options = self._build_options() + + @property + def options(self) -> List[str]: + return ( + ["Off"] + .extend([i.stringify() for i in self._options]) + .extend("Descale") + ) + + def from_option_string(self, value: str) -> Optional[ErdCcmBrewSettings]: + try: + if value in ["Off","Descale"]: + return None + s = value.split(" -- ")[0] + c = value.split(" -- ")[1].replace(" cups","") + return ErdCcmBrewSettings(int(c), ErdCcmBrewStrength(s), 200) + except: + #return a default if we can't interpret it + return ErdCcmBrewSettings(4, ErdCcmBrewStrength.MEDIUM, 200) + + def to_option_string(self, value: ErdCcmBrewSettings) -> Optional[str]: + try: + o = CcmBrewOption(value.brew_strength, value.number_of_cups) + return o.stringify() + except: + #return a default if we can't interpret it + return CcmBrewOption(ErdCcmBrewStrength.MEDIUM, 4) + + def _build_options(self) -> List[CcmBrewOption]: + options = [] + for s in ErdCcmBrewStrength: + for c in range(MIN_CUPS, MAX_CUPS, 2): + options.append(CcmBrewOption(s, c)) + return options From 807ee17110f2ba7bbd2871b166e5e356c729a468 Mon Sep 17 00:00:00 2001 From: alexanv1 <44785744+alexanv1@users.noreply.github.com> Date: Mon, 13 Dec 2021 02:43:00 -0800 Subject: [PATCH 191/338] CoffeeMaker fixes --- .../ge_home/devices/coffee_maker.py | 4 +++- .../ge_home/entities/ccm/ge_ccm.py | 9 +++++---- .../ge_home/entities/ccm/ge_ccm_options.py | 19 +++++++++++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/custom_components/ge_home/devices/coffee_maker.py b/custom_components/ge_home/devices/coffee_maker.py index 8d248da..a018a47 100644 --- a/custom_components/ge_home/devices/coffee_maker.py +++ b/custom_components/ge_home/devices/coffee_maker.py @@ -25,6 +25,8 @@ class CcmApi(ApplianceApi): def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() + units = self.hass.config.units + ccm_entities = [ GeErdBinarySensor(self, ErdCode.CCM_IS_BREWING, device_class_override="heat"), GeErdBinarySensor(self, ErdCode.CCM_IS_DESCALING), @@ -34,7 +36,7 @@ def get_all_entities(self) -> List[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"), - GeCcm(self) + GeCcm(self, units) ] entities = base_entities + ccm_entities diff --git a/custom_components/ge_home/entities/ccm/ge_ccm.py b/custom_components/ge_home/entities/ccm/ge_ccm.py index aaa5d9f..9783e2a 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm.py @@ -11,6 +11,7 @@ ) from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.util.unit_system import UnitSystem from ...const import DOMAIN from ...devices import ApplianceApi @@ -25,9 +26,9 @@ class GeCcm(GeWaterHeater): icon = "mdi:coffee-maker" - def __init__(self, api: ApplianceApi): + def __init__(self, api: ApplianceApi, units: UnitSystem): super().__init__(api) - self._options = CcmBrewOptionsConverter() + self._options = CcmBrewOptionsConverter(units) @property def supported_features(self): @@ -122,7 +123,7 @@ async def async_set_operation_mode(self, operation_mode: str): try: if operation_mode not in ["Off","Descale"]: new_mode = self._options.from_option_string(operation_mode) - new_mode.brew_temperature = self.target_temperature + new_mode = ErdCcmBrewSettings(new_mode.number_of_cups, new_mode.brew_strength, self.target_temperature) await self.appliance.async_set_erd_value(ErdCode.CCM_BREW_SETTINGS, new_mode) elif operation_mode == "Off": @@ -131,7 +132,7 @@ async def async_set_operation_mode(self, operation_mode: str): elif operation_mode == "Descale": await self.appliance.async_set_erd_value(ErdCode.CCM_START_DESCALING, True) except: - _LOGGER.debug(f"Error Attempting to set mode to {operation_mode}.") + _LOGGER.error(f"Error Attempting to set mode to {operation_mode}.", exc_info=True) async def async_set_temperature(self, **kwargs): """Set the brew temperature""" diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_options.py b/custom_components/ge_home/entities/ccm/ge_ccm_options.py index 54c2f17..e3f17cf 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_options.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_options.py @@ -25,20 +25,22 @@ def __init__(self, units: UnitSystem): @property def options(self) -> List[str]: - return ( - ["Off"] - .extend([i.stringify() for i in self._options]) - .extend("Descale") - ) + options = ["Off"] + options.extend([i.stringify() for i in self._options]) + options.extend(["Descale"]) + + return options def from_option_string(self, value: str) -> Optional[ErdCcmBrewSettings]: try: if value in ["Off","Descale"]: return None - s = value.split(" -- ")[0] + s = value.split(" -- ")[0].upper() c = value.split(" -- ")[1].replace(" cups","") - return ErdCcmBrewSettings(int(c), ErdCcmBrewStrength(s), 200) + return ErdCcmBrewSettings(int(c), ErdCcmBrewStrength[s], 200) except: + _LOGGER.error(f"Could not convert brew options '{value}'", exc_info=True) + #return a default if we can't interpret it return ErdCcmBrewSettings(4, ErdCcmBrewStrength.MEDIUM, 200) @@ -52,7 +54,8 @@ def to_option_string(self, value: ErdCcmBrewSettings) -> Optional[str]: def _build_options(self) -> List[CcmBrewOption]: options = [] - for s in ErdCcmBrewStrength: + for s in filter(lambda x: x != ErdCcmBrewStrength.UNKNOWN, ErdCcmBrewStrength): for c in range(MIN_CUPS, MAX_CUPS, 2): options.append(CcmBrewOption(s, c)) + return options From 244778edc00166eb8b240a94acfaa574fba122b5 Mon Sep 17 00:00:00 2001 From: alexanv1 <44785744+alexanv1@users.noreply.github.com> Date: Sat, 29 Jan 2022 21:06:43 -0800 Subject: [PATCH 192/338] Updates to Coffee Maker support --- custom_components/ge_home/button.py | 33 ++++ custom_components/ge_home/const.py | 8 +- custom_components/ge_home/devices/base.py | 4 +- .../ge_home/devices/coffee_maker.py | 48 ++++-- .../ge_home/entities/ccm/__init__.py | 5 +- .../ge_home/entities/ccm/const.py | 8 - .../ge_home/entities/ccm/ge_ccm.py | 150 ------------------ .../ge_home/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 | 29 ++++ .../entities/ccm/ge_ccm_cached_value.py | 20 +++ .../ge_home/entities/ccm/ge_ccm_options.py | 61 ------- .../ge_home/entities/common/__init__.py | 2 + .../ge_home/entities/common/ge_erd_button.py | 17 ++ .../ge_home/entities/common/ge_erd_entity.py | 6 +- .../ge_home/entities/common/ge_erd_number.py | 139 ++++++++++++++++ .../ge_home/entities/common/ge_erd_sensor.py | 36 +++-- custom_components/ge_home/manifest.json | 2 +- custom_components/ge_home/number.py | 33 ++++ .../ge_home/update_coordinator.py | 3 +- 21 files changed, 426 insertions(+), 257 deletions(-) create mode 100644 custom_components/ge_home/button.py delete mode 100644 custom_components/ge_home/entities/ccm/const.py delete mode 100644 custom_components/ge_home/entities/ccm/ge_ccm.py create mode 100644 custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py create mode 100644 custom_components/ge_home/entities/ccm/ge_ccm_brew_settings.py create mode 100644 custom_components/ge_home/entities/ccm/ge_ccm_brew_strength.py create mode 100644 custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py create mode 100644 custom_components/ge_home/entities/ccm/ge_ccm_cached_value.py delete mode 100644 custom_components/ge_home/entities/ccm/ge_ccm_options.py create mode 100644 custom_components/ge_home/entities/common/ge_erd_button.py create mode 100644 custom_components/ge_home/entities/common/ge_erd_number.py create mode 100644 custom_components/ge_home/number.py diff --git a/custom_components/ge_home/button.py b/custom_components/ge_home/button.py new file mode 100644 index 0000000..8e594bd --- /dev/null +++ b/custom_components/ge_home/button.py @@ -0,0 +1,33 @@ +"""GE Home Button Entities""" +import async_timeout +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +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.""" + + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + # This should be a NOP, but let's be safe + with async_timeout.timeout(20): + await coordinator.initialization_future + + apis = coordinator.appliance_apis.values() + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdButton) + ] + _LOGGER.debug(f'Found {len(entities):d} buttons ') + async_add_entities(entities) diff --git a/custom_components/ge_home/const.py b/custom_components/ge_home/const.py index e8511f5..ac908dc 100644 --- a/custom_components/ge_home/const.py +++ b/custom_components/ge_home/const.py @@ -1,5 +1,4 @@ """Constants for the gehome integration.""" -from gehomesdk.clients.const import LOGIN_URL DOMAIN = "ge_home" @@ -13,4 +12,9 @@ SERVICE_SET_TIMER = "set_timer" SERVICE_CLEAR_TIMER = "clear_timer" -SERVICE_SET_INT_VALUE = "set_int_value" \ No newline at end of file +SERVICE_SET_INT_VALUE = "set_int_value" + +# Prevent Home Assistant automatic temperature conversions by overriding TEMP_CELCIUS, TEMP_FAHRENHEIT +# This makes sure that the values shows in the UI match device preferences bypassing the automatic conversion to whatever the Home Assistant default is set to +TEMP_CELSIUS = "\u2103" +TEMP_FAHRENHEIT = "\u2109" diff --git a/custom_components/ge_home/devices/base.py b/custom_components/ge_home/devices/base.py index bbc7a0d..54bbe35 100644 --- a/custom_components/ge_home/devices/base.py +++ b/custom_components/ge_home/devices/base.py @@ -121,10 +121,10 @@ def get_base_entities(self) -> List[Entity]: def build_entities_list(self) -> None: """Build the entities list, adding anything new.""" - from ..entities import GeErdEntity + from ..entities import GeErdEntity, GeErdButton entities = [ e for e in self.get_all_entities() - if not isinstance(e, GeErdEntity) or e.erd_code in self.appliance.known_properties + if not isinstance(e, GeErdEntity) or isinstance(e, GeErdButton) or e.erd_code in self.appliance.known_properties ] for entity in entities: diff --git a/custom_components/ge_home/devices/coffee_maker.py b/custom_components/ge_home/devices/coffee_maker.py index a018a47..d95af55 100644 --- a/custom_components/ge_home/devices/coffee_maker.py +++ b/custom_components/ge_home/devices/coffee_maker.py @@ -3,16 +3,23 @@ from homeassistant.helpers.entity import Entity from gehomesdk import ( + GeAppliance, ErdCode, - ErdApplianceType + ErdApplianceType, + ErdCcmBrewSettings ) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .base import ApplianceApi from ..entities import ( GeCcmPotNotPresentBinarySensor, GeErdSensor, GeErdBinarySensor, - GeCcm + GeErdButton, + GeCcmBrewStrengthSelect, + GeCcmBrewTemperatureNumber, + GeCcmBrewCupsNumber, + GeCcmBrewSettingsButton ) _LOGGER = logging.getLogger(__name__) @@ -22,23 +29,38 @@ 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() - units = self.hass.config.units - ccm_entities = [ - GeErdBinarySensor(self, ErdCode.CCM_IS_BREWING, device_class_override="heat"), - GeErdBinarySensor(self, ErdCode.CCM_IS_DESCALING), - GeErdSensor(self, ErdCode.CCM_BREW_STRENGTH), - GeErdSensor(self, ErdCode.CCM_BREW_CUPS), - GeErdSensor(self, ErdCode.CCM_BREW_TEMPERATURE), + 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"), - GeCcm(self, units) + 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.value, + self._brew_strengh_entity.brew_strength, + self._brew_temperature_entity.brew_temperature) + 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_home/entities/ccm/__init__.py b/custom_components/ge_home/entities/ccm/__init__.py index dc2da57..614d130 100644 --- a/custom_components/ge_home/entities/ccm/__init__.py +++ b/custom_components/ge_home/entities/ccm/__init__.py @@ -1,2 +1,5 @@ from .ge_ccm_pot_not_present_binary_sensor import GeCcmPotNotPresentBinarySensor -from .ge_ccm import GeCcm \ No newline at end of file +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_home/entities/ccm/const.py b/custom_components/ge_home/entities/ccm/const.py deleted file mode 100644 index 64ed669..0000000 --- a/custom_components/ge_home/entities/ccm/const.py +++ /dev/null @@ -1,8 +0,0 @@ -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) - -GE_CCM = SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE -MAX_CUPS = 10 -MIN_CUPS = 2 \ No newline at end of file diff --git a/custom_components/ge_home/entities/ccm/ge_ccm.py b/custom_components/ge_home/entities/ccm/ge_ccm.py deleted file mode 100644 index 9783e2a..0000000 --- a/custom_components/ge_home/entities/ccm/ge_ccm.py +++ /dev/null @@ -1,150 +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, - ErdCcmBrewSettings, - ErdCcmBrewStrength -) - -from homeassistant.const import ATTR_TEMPERATURE -from homeassistant.util.unit_system import UnitSystem - -from ...const import DOMAIN -from ...devices import ApplianceApi -from ..common import GeWaterHeater -from .const import * -from .ge_ccm_options import CcmBrewOptionsConverter - -_LOGGER = logging.getLogger(__name__) - -class GeCcm(GeWaterHeater): - """GE Appliance Cafe Coffee Maker""" - - icon = "mdi:coffee-maker" - - def __init__(self, api: ApplianceApi, units: UnitSystem): - super().__init__(api) - self._options = CcmBrewOptionsConverter(units) - - @property - def supported_features(self): - return GE_CCM - - @property - def unique_id(self) -> str: - return f"{DOMAIN}_{self.serial_number}" - - @property - def name(self) -> Optional[str]: - return f"{self.serial_number} Coffee Maker" - - @property - def unit_type(self) -> Optional[ErdUnitType]: - try: - return self.appliance.get_erd_value(ErdCode.UNIT_TYPE) - except: - return None - - @property - def current_temperature(self) -> Optional[int]: - return self.appliance.get_erd_value(ErdCode.CCM_CURRENT_WATER_TEMPERATURE) - - @property - def is_brewing(self) -> bool: - return self.appliance.get_erd_value(ErdCode.CCM_IS_BREWING) - - @property - def is_descaling(self) -> bool: - return self.appliance.get_erd_value(ErdCode.CCM_IS_DESCALING) - - @property - def current_operation(self) -> Optional[str]: - try: - settings: ErdCcmBrewSettings = self.appliance.get_erd_value(ErdCode.CCM_BREW_SETTINGS) - if self.is_descaling: - return "Descale" - if not self.is_brewing: - return "Off" - return self._options.to_option_string(settings) - except: - return None - - @property - def operation_list(self) -> List[str]: - return self._options.options - - @property - def current_brew_setting(self) -> ErdCcmBrewSettings: - """Get the current brew setting.""" - return self.appliance.get_erd_value(ErdCode.CCM_BREW_SETTINGS) - - @property - def target_temperature(self) -> Optional[int]: - """Return the temperature we try to reach.""" - try: - return self.appliance.get_erd_value(ErdCode.CCM_BREW_TEMPERATURE) - except: - pass - return None - - @property - def min_temp(self) -> int: - """Return the minimum temperature.""" - min_temp, _, _ = self.appliance.get_erd_value(ErdCode.CCM_BREW_TEMPERATURE_RANGE) - return min_temp - - @property - def max_temp(self) -> int: - """Return the maximum temperature.""" - _, max_temp, _ = self.appliance.get_erd_value(ErdCode.CCM_BREW_TEMPERATURE_RANGE) - return max_temp - - @property - def extra_state_attributes(self) -> Optional[Mapping[str, Any]]: - data = {} - - data["unit_type"] = self._stringify(self.unit_type) - - return data - - @property - def can_set_temperature(self) -> bool: - """Indicates whether we can set the temperature based on the current mode""" - return not self.is_descaling - - async def async_set_operation_mode(self, operation_mode: str): - """Set the operation mode.""" - - #try to get the mode/setting for the selection - try: - if operation_mode not in ["Off","Descale"]: - new_mode = self._options.from_option_string(operation_mode) - new_mode = ErdCcmBrewSettings(new_mode.number_of_cups, new_mode.brew_strength, self.target_temperature) - - await self.appliance.async_set_erd_value(ErdCode.CCM_BREW_SETTINGS, new_mode) - elif operation_mode == "Off": - await self.appliance.async_set_erd_value(ErdCode.CCM_CANCEL_BREWING, True) - await self.appliance.async_set_erd_value(ErdCode.CCM_CANCEL_DESCALING, True) - elif operation_mode == "Descale": - await self.appliance.async_set_erd_value(ErdCode.CCM_START_DESCALING, True) - except: - _LOGGER.error(f"Error Attempting to set mode to {operation_mode}.", exc_info=True) - - async def async_set_temperature(self, **kwargs): - """Set the brew temperature""" - - target_temp = kwargs.get(ATTR_TEMPERATURE) - if target_temp is None: - return - - # get the current strength/cups - strength: ErdCcmBrewStrength = self.appliance.get_erd_value(ErdCode.CCM_BREW_STRENGTH) - cups: int = self.appliance.get_erd_value(ErdCode.CCM_BREW_CUPS) - new_mode = ErdCcmBrewSettings(cups, strength, target_temp) - - await self.appliance.async_set_erd_value(ErdCode.CCM_BREW_SETTINGS, new_mode) - diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py new file mode 100644 index 0000000..dc876b5 --- /dev/null +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py @@ -0,0 +1,19 @@ +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_value(self, value): + GeCcmCachedValue.set_value(self, value) + self.schedule_update_ha_state() + + @property + def value(self): + return self.get_value(device_value = super().value) \ No newline at end of file diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_settings.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_settings.py new file mode 100644 index 0000000..121392b --- /dev/null +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_settings.py @@ -0,0 +1,13 @@ +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_home/entities/ccm/ge_ccm_brew_strength.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_strength.py new file mode 100644 index 0000000..b6faa93 --- /dev/null +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_strength.py @@ -0,0 +1,47 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCode, ErdCcmBrewStrength +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter +from .ge_ccm_cached_value import GeCcmCachedValue + +_LOGGER = logging.getLogger(__name__) + +class GeCcmBrewStrengthOptionsConverter(OptionsConverter): + def __init__(self): + self._default = ErdCcmBrewStrength.MEDIUM + + @property + def options(self) -> List[str]: + return [i.stringify() for i in [ErdCcmBrewStrength.LIGHT, ErdCcmBrewStrength.MEDIUM, ErdCcmBrewStrength.BOLD, ErdCcmBrewStrength.GOLD]] + + def from_option_string(self, value: str) -> Any: + try: + return ErdCcmBrewStrength[value.upper()] + except: + _LOGGER.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_home/entities/ccm/ge_ccm_brew_temperature.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py new file mode 100644 index 0000000..bff5127 --- /dev/null +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py @@ -0,0 +1,29 @@ +from gehomesdk import ErdCode +from ...devices import ApplianceApi +from ..common import GeErdNumber +from ...const import TEMP_CELSIUS +from .ge_ccm_cached_value import GeCcmCachedValue + +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_value(self, value): + GeCcmCachedValue.set_value(self, value) + self.schedule_update_ha_state() + + @property + def value(self): + return int(self.get_value(device_value = super().value)) + + @property + def brew_temperature(self) -> int: + + value = self.value + if self.unit_of_measurement == TEMP_CELSIUS: + # Convert to Fahrenheit + value = int(round(value * 9/5) + 32) + + return value \ No newline at end of file diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_cached_value.py b/custom_components/ge_home/entities/ccm/ge_ccm_cached_value.py new file mode 100644 index 0000000..95c2b94 --- /dev/null +++ b/custom_components/ge_home/entities/ccm/ge_ccm_cached_value.py @@ -0,0 +1,20 @@ +class GeCcmCachedValue(): + def __init__(self): + self._set_value = None + self._last_device_value = None + + def get_value(self, device_value): + + # If the last device value is different from the current one, return the device value which overrides the set value + if self._last_device_value != device_value: + self._last_device_value = device_value + self._set_value = None + return device_value + + if self._set_value is not None: + return self._set_value + + return device_value + + def set_value(self, set_value): + self._set_value = set_value \ No newline at end of file diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_options.py b/custom_components/ge_home/entities/ccm/ge_ccm_options.py deleted file mode 100644 index e3f17cf..0000000 --- a/custom_components/ge_home/entities/ccm/ge_ccm_options.py +++ /dev/null @@ -1,61 +0,0 @@ -import logging -from typing import List, Any, NamedTuple, Optional - -from gehomesdk import ErdCcmBrewStrength, ErdCcmBrewSettings -from homeassistant.const import TEMP_FAHRENHEIT -from homeassistant.util.unit_system import UnitSystem - -from custom_components.ge_home.entities.ccm.const import MAX_CUPS, MIN_CUPS -from ..common import OptionsConverter - -_LOGGER = logging.getLogger(__name__) - -class CcmBrewOption(NamedTuple): - strength: ErdCcmBrewStrength - cups: int - - def stringify(self): - return f"{self.strength.stringify()} -- {self.cups} cups" - -class CcmBrewOptionsConverter(OptionsConverter): - def __init__(self, units: UnitSystem): - super().__init__() - self._units = units - self._options = self._build_options() - - @property - def options(self) -> List[str]: - options = ["Off"] - options.extend([i.stringify() for i in self._options]) - options.extend(["Descale"]) - - return options - - def from_option_string(self, value: str) -> Optional[ErdCcmBrewSettings]: - try: - if value in ["Off","Descale"]: - return None - s = value.split(" -- ")[0].upper() - c = value.split(" -- ")[1].replace(" cups","") - return ErdCcmBrewSettings(int(c), ErdCcmBrewStrength[s], 200) - except: - _LOGGER.error(f"Could not convert brew options '{value}'", exc_info=True) - - #return a default if we can't interpret it - return ErdCcmBrewSettings(4, ErdCcmBrewStrength.MEDIUM, 200) - - def to_option_string(self, value: ErdCcmBrewSettings) -> Optional[str]: - try: - o = CcmBrewOption(value.brew_strength, value.number_of_cups) - return o.stringify() - except: - #return a default if we can't interpret it - return CcmBrewOption(ErdCcmBrewStrength.MEDIUM, 4) - - def _build_options(self) -> List[CcmBrewOption]: - options = [] - for s in filter(lambda x: x != ErdCcmBrewStrength.UNKNOWN, ErdCcmBrewStrength): - for c in range(MIN_CUPS, MAX_CUPS, 2): - options.append(CcmBrewOption(s, c)) - - return options diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 7db556b..edd0b63 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -9,6 +9,8 @@ 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 GeWaterHeater from .ge_erd_select import GeErdSelect from .ge_climate import GeClimate \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_button.py b/custom_components/ge_home/entities/common/ge_erd_button.py new file mode 100644 index 0000000..ef28295 --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_erd_button.py @@ -0,0 +1,17 @@ +from typing import Optional + +from homeassistant.components.button import ButtonEntity + +from gehomesdk import ErdCodeType +from ...devices import ApplianceApi +from .ge_erd_entity import GeErdEntity + + +class GeErdButton(GeErdEntity, ButtonEntity): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): + super().__init__(api, erd_code, erd_override=erd_override) + + """GE Entity for buttons""" + async def async_press(self) -> None: + """Handle the button press.""" + await self.appliance.async_set_erd_value(self.erd_code, True) diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index 32f7b66..36b7883 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -83,7 +83,7 @@ def _measurement_system(self) -> Optional[ErdMeasurementUnits]: try: value = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) except KeyError: - return None + return ErdMeasurementUnits.Imperial return value def _get_icon(self): @@ -142,6 +142,8 @@ def _get_icon(self): if self.erd_code_class == ErdCodeClass.OIM_SENSOR: return "mdi:snowflake" if self.erd_code_class == ErdCodeClass.WATERSOFTENER_SENSOR: - return "mdi:water" + return "mdi:water" + if self.erd_code_class == ErdCodeClass.CCM_SENSOR: + return "mdi:coffee-maker" return None diff --git a/custom_components/ge_home/entities/common/ge_erd_number.py b/custom_components/ge_home/entities/common/ge_erd_number.py new file mode 100644 index 0000000..50fb63a --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_erd_number.py @@ -0,0 +1,139 @@ +import logging +from typing import Optional +from gehomesdk.erd.erd_data_type import ErdDataType +from homeassistant.components.number import NumberEntity + +from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, +) +from gehomesdk import ErdCodeType, ErdCodeClass, ErdMeasurementUnits +from .ge_erd_entity import GeErdEntity +from ...devices import ApplianceApi +from ...const import TEMP_CELSIUS, TEMP_FAHRENHEIT + +_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._min_value = min_value + self._max_value = max_value + self._step_value = step_value + self._mode = mode + + @property + def 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 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 min_value(self) -> float: + return self._convert_value_from_device(self._min_value) + + @property + def max_value(self) -> float: + return self._convert_value_from_device(self._max_value) + + @property + def step(self) -> float: + return self._step_value + + @property + def mode(self) -> float: + return self._mode + + def _convert_value_from_device(self, value): + """Convert to expected temperature units and data type""" + + if (self._get_uom() == TEMP_CELSIUS): + # Convert to Celcius + value = (value - 32 ) * 5/9 + + 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 == DEVICE_CLASS_TEMPERATURE: + if self._measurement_system == ErdMeasurementUnits.METRIC: + + # Actual data from API is always in Fahrenhreit but since Device preferences are set to Celcius + # we return Celcius here and will do the conversion ourselves + return TEMP_CELSIUS + else: + 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 DEVICE_CLASS_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_value(self, value): + """Sets the ERD value, assumes that the data type is correct""" + + if self._get_uom() == TEMP_CELSIUS: + # Convert to Fahrenheit + value = (value * 9/5) + 32 + + 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}") \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index 240de5b..061a4e8 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -9,13 +9,11 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_TIMESTAMP, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, ) -from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits +from gehomesdk import ErdCodeType, ErdCodeClass, ErdMeasurementUnits from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi +from ...const import TEMP_CELSIUS, TEMP_FAHRENHEIT _LOGGER = logging.getLogger(__name__) @@ -45,7 +43,7 @@ def native_value(self): # if it's a numeric data type, return it directly if self._data_type in [ErdDataType.INT, ErdDataType.FLOAT]: - return value + return self._convert_numeric_value_from_device(value) # otherwise, return a stringified version # TODO: perhaps enhance so that there's a list of variables available @@ -71,14 +69,23 @@ def _data_type(self) -> ErdDataType: @property def _temp_units(self) -> Optional[str]: - #based on testing, all API values are in Fahrenheit, so we'll redefine - #this property to be the configured temperature unit and set the native - #unit differently - return self.api.hass.config.units.temperature_unit + # Return the unit from device preferences + if self._measurement_system == ErdMeasurementUnits.METRIC: + return TEMP_CELSIUS - #if self._measurement_system == ErdMeasurementUnits.METRIC: - # return TEMP_CELSIUS - #return TEMP_FAHRENHEIT + return TEMP_FAHRENHEIT + + def _convert_numeric_value_from_device(self, value): + """Convert to expected temperature units and data type""" + + if (self._get_uom() == TEMP_CELSIUS): + # Convert to Celcius since device always returns temperature in Fahrenheit regardless of device preferences + value = (value - 32 ) * 5/9 + + if self._data_type == ErdDataType.INT: + return int(round(value)) + else: + return value def _get_uom(self): """Select appropriate units""" @@ -92,10 +99,7 @@ def _get_uom(self): 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 + return self._temp_units if ( self.erd_code_class == ErdCodeClass.BATTERY or self.device_class == DEVICE_CLASS_BATTERY diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index b821356..3a68d43 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.21","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.22","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], "version": "0.6.0" } diff --git a/custom_components/ge_home/number.py b/custom_components/ge_home/number.py new file mode 100644 index 0000000..eed2189 --- /dev/null +++ b/custom_components/ge_home/number.py @@ -0,0 +1,33 @@ +"""GE Home Number Entities""" +import async_timeout +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +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.""" + + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + # This should be a NOP, but let's be safe + with async_timeout.timeout(20): + await coordinator.initialization_future + + apis = coordinator.appliance_apis.values() + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdNumber) + ] + _LOGGER.debug(f'Found {len(entities):d} numbers ') + async_add_entities(entities) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index b3ee44b..8711d98 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -34,7 +34,7 @@ ) from .devices import ApplianceApi, get_appliance_api_type -PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater", "select", "climate", "light"] +PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater", "select", "climate", "light", "button", "number"] _LOGGER = logging.getLogger(__name__) @@ -261,6 +261,7 @@ async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any] api = self.appliance_apis[appliance.mac_addr] except KeyError: return + for entity in api.entities: if entity.enabled: _LOGGER.debug(f"Updating {entity} ({entity.unique_id}, {entity.entity_id})") From 5a1f2fc69f436ae894ea5e1bc2ddd1a27e4f52f7 Mon Sep 17 00:00:00 2001 From: alexanv1 <44785744+alexanv1@users.noreply.github.com> Date: Tue, 15 Feb 2022 23:26:24 -0800 Subject: [PATCH 193/338] Remove temperature unit changes --- custom_components/ge_home/const.py | 5 ---- .../entities/ccm/ge_ccm_brew_temperature.py | 9 +------ .../ge_home/entities/common/ge_erd_number.py | 25 +++++------------- .../ge_home/entities/common/ge_erd_sensor.py | 26 ++++++++++--------- 4 files changed, 22 insertions(+), 43 deletions(-) diff --git a/custom_components/ge_home/const.py b/custom_components/ge_home/const.py index ac908dc..76cef76 100644 --- a/custom_components/ge_home/const.py +++ b/custom_components/ge_home/const.py @@ -13,8 +13,3 @@ SERVICE_SET_TIMER = "set_timer" SERVICE_CLEAR_TIMER = "clear_timer" SERVICE_SET_INT_VALUE = "set_int_value" - -# Prevent Home Assistant automatic temperature conversions by overriding TEMP_CELCIUS, TEMP_FAHRENHEIT -# This makes sure that the values shows in the UI match device preferences bypassing the automatic conversion to whatever the Home Assistant default is set to -TEMP_CELSIUS = "\u2103" -TEMP_FAHRENHEIT = "\u2109" diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py index bff5127..4dc20e9 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py @@ -1,7 +1,6 @@ from gehomesdk import ErdCode from ...devices import ApplianceApi from ..common import GeErdNumber -from ...const import TEMP_CELSIUS from .ge_ccm_cached_value import GeCcmCachedValue class GeCcmBrewTemperatureNumber(GeErdNumber, GeCcmCachedValue): @@ -20,10 +19,4 @@ def value(self): @property def brew_temperature(self) -> int: - - value = self.value - if self.unit_of_measurement == TEMP_CELSIUS: - # Convert to Fahrenheit - value = int(round(value * 9/5) + 32) - - return value \ No newline at end of file + return self.value \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_number.py b/custom_components/ge_home/entities/common/ge_erd_number.py index 50fb63a..bea3f69 100644 --- a/custom_components/ge_home/entities/common/ge_erd_number.py +++ b/custom_components/ge_home/entities/common/ge_erd_number.py @@ -5,11 +5,11 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, + TEMP_FAHRENHEIT, ) -from gehomesdk import ErdCodeType, ErdCodeClass, ErdMeasurementUnits +from gehomesdk import ErdCodeType, ErdCodeClass from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi -from ...const import TEMP_CELSIUS, TEMP_FAHRENHEIT _LOGGER = logging.getLogger(__name__) @@ -74,11 +74,7 @@ def mode(self) -> float: return self._mode def _convert_value_from_device(self, value): - """Convert to expected temperature units and data type""" - - if (self._get_uom() == TEMP_CELSIUS): - # Convert to Celcius - value = (value - 32 ) * 5/9 + """Convert to expected data type""" if self._data_type == ErdDataType.INT: return int(round(value)) @@ -93,13 +89,10 @@ def _get_uom(self): return self._uom_override if self.device_class == DEVICE_CLASS_TEMPERATURE: - if self._measurement_system == ErdMeasurementUnits.METRIC: - - # Actual data from API is always in Fahrenhreit but since Device preferences are set to Celcius - # we return Celcius here and will do the conversion ourselves - return TEMP_CELSIUS - else: - return TEMP_FAHRENHEIT + #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 @@ -126,10 +119,6 @@ def _get_icon(self): async def async_set_value(self, value): """Sets the ERD value, assumes that the data type is correct""" - if self._get_uom() == TEMP_CELSIUS: - # Convert to Fahrenheit - value = (value * 9/5) + 32 - if self._data_type == ErdDataType.INT: value = int(round(value)) diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index 061a4e8..ffb16f0 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -9,11 +9,11 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER_FACTOR, + TEMP_FAHRENHEIT, ) -from gehomesdk import ErdCodeType, ErdCodeClass, ErdMeasurementUnits +from gehomesdk import ErdCodeType, ErdCodeClass from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi -from ...const import TEMP_CELSIUS, TEMP_FAHRENHEIT _LOGGER = logging.getLogger(__name__) @@ -69,18 +69,17 @@ def _data_type(self) -> ErdDataType: @property def _temp_units(self) -> Optional[str]: - # Return the unit from device preferences - if self._measurement_system == ErdMeasurementUnits.METRIC: - return TEMP_CELSIUS + #based on testing, all API values are in Fahrenheit, so we'll redefine + #this property to be the configured temperature unit and set the native + #unit differently + return self.api.hass.config.units.temperature_unit - return TEMP_FAHRENHEIT + #if self._measurement_system == ErdMeasurementUnits.METRIC: + # return TEMP_CELSIUS + #return TEMP_FAHRENHEIT def _convert_numeric_value_from_device(self, value): - """Convert to expected temperature units and data type""" - - if (self._get_uom() == TEMP_CELSIUS): - # Convert to Celcius since device always returns temperature in Fahrenheit regardless of device preferences - value = (value - 32 ) * 5/9 + """Convert to expected data type""" if self._data_type == ErdDataType.INT: return int(round(value)) @@ -99,7 +98,10 @@ def _get_uom(self): in [ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE] or self.device_class == DEVICE_CLASS_TEMPERATURE ): - return self._temp_units + #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 From 37cf69b99d23d72b88f81ed58428a70d598614b0 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Fri, 18 Feb 2022 23:51:43 -0500 Subject: [PATCH 194/338] - documentation updates --- CHANGELOG.md | 1 + README.md | 1 + info.md | 2 ++ 3 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f1930d..50d1833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Initial support for Microwaves (@mbcomer, @mnestor) - Initial support for Water Softeners (@npentell, @drjeff) - Initial support for Opal Ice Makers (@mbcomer, @knobunc) +- Initial support for Coffee Makers (@alexanv1) - Updated deprecated icons (@mjmeli, @schmittx) ## 0.5.0 diff --git a/README.md b/README.md index 59ec6e0..f669bf3 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio - Advantium - Microwave - Opal Ice Maker +- Coffee Maker **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** diff --git a/info.md b/info.md index ae9a5b2..b449127 100644 --- a/info.md +++ b/info.md @@ -13,6 +13,7 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio - Advantium - Microwave - Opal Ice Maker +- Coffee Maker **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** @@ -61,6 +62,7 @@ A/C Controls: - Initial support for Water Softeners (@npentell, @drjeff) - Initial support for Opal Ice Makers (@mbcomer, @knobunc) - Initial support for Microwaves (@mbcomer, @mnestor) +- Initial support for Coffee Makers (@alexanv1) {% endif %} {% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} From 9b901f82f786354690aa0107470006c967ba5074 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 19 Feb 2022 15:03:14 -0500 Subject: [PATCH 195/338] - implemented us/eu authorization fixes --- custom_components/ge_home/__init__.py | 19 +++++++++++++++++++ custom_components/ge_home/config_flow.py | 20 +++++++++++++++----- custom_components/ge_home/manifest.json | 2 +- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/custom_components/ge_home/__init__.py b/custom_components/ge_home/__init__.py index 36c2c0b..7b3f082 100644 --- a/custom_components/ge_home/__init__.py +++ b/custom_components/ge_home/__init__.py @@ -1,19 +1,38 @@ """The ge_home integration.""" +import logging from homeassistant.const import EVENT_HOMEASSISTANT_STOP import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_REGION from .const import DOMAIN from .update_coordinator import GeHomeUpdateCoordinator +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) async def async_setup(hass: HomeAssistant, config: dict): return True +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version == 1: + + new = {**config_entry.data} + new[CONF_REGION] = "US" + + config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry, data=new) + + _LOGGER.info("Migration to version %s successful", config_entry.version) + + return True + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up the ge_home component.""" diff --git a/custom_components/ge_home/config_flow.py b/custom_components/ge_home/config_flow.py index e303312..3116627 100644 --- a/custom_components/ge_home/config_flow.py +++ b/custom_components/ge_home/config_flow.py @@ -7,11 +7,17 @@ import asyncio import async_timeout -from gehomesdk import GeAuthFailedError, GeNotAuthenticatedError, GeGeneralServerError, async_get_oauth2_token +from gehomesdk import ( + GeAuthFailedError, + GeNotAuthenticatedError, + GeGeneralServerError, + async_get_oauth2_token, + LOGIN_REGIONS +) import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION from .const import DOMAIN # pylint:disable=unused-import from .exceptions import HaAuthError, HaCannotConnect, HaAlreadyConfigured @@ -19,7 +25,11 @@ _LOGGER = logging.getLogger(__name__) GEHOME_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_REGION): vol.In(LOGIN_REGIONS.keys()) + } ) async def validate_input(hass: core.HomeAssistant, data): @@ -30,7 +40,7 @@ async def validate_input(hass: core.HomeAssistant, data): # noinspection PyBroadException try: with async_timeout.timeout(10): - _ = await async_get_oauth2_token(session, data[CONF_USERNAME], data[CONF_PASSWORD]) + _ = await async_get_oauth2_token(session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_REGION]) except (asyncio.TimeoutError, aiohttp.ClientError): raise HaCannotConnect('Connection failure') except (GeAuthFailedError, GeNotAuthenticatedError): @@ -47,7 +57,7 @@ async def validate_input(hass: core.HomeAssistant, data): class GeHomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for GE Home.""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH async def _async_validate_input(self, user_input): diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 3a68d43..ec5a90e 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.22","magicattr==0.1.5","slixmpp==1.7.1"], + "requirements": ["gehomesdk==0.4.24","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], "version": "0.6.0" } From d99dbe2a4bcf49576cf767e7f8b7c120422a054f Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 19 Feb 2022 15:05:53 -0500 Subject: [PATCH 196/338] - documentation updates --- CHANGELOG.md | 1 + hacs.json | 2 +- info.md | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50d1833..18a3b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## 0.6.0 - Requires HA 2021.12.x or later +- Enabled authentication to both US and EU regions - Changed the sensors to use native value/uom - Changed the temperatures to always be natively fahrenheit (API appears to always use this system) (@vignatyuk) - Initial support for Microwaves (@mbcomer, @mnestor) diff --git a/hacs.json b/hacs.json index e9e1a0b..7c919dc 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Home (SmartHQ)", "homeassistant": "2021.12.0", - "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select"], + "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select", "button", "climate", "light", "number"], "iot_class": "Cloud Polling" } diff --git a/info.md b/info.md index b449127..d8d24e2 100644 --- a/info.md +++ b/info.md @@ -42,6 +42,7 @@ A/C Controls: {% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} - Requires HA version 2021.12.0 or later +- Enabled authentication to both US and EU regions (may require re-auth) - Changed the sensors to use native value/uom - Changed the temperatures to always be natively fahrenheit (API appears to always use this system) (@vignatyuk) {% endif %} From 5cf7e6a67672007f0476313786a6e9ac0ef5f295 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 30 Apr 2022 19:48:20 -0400 Subject: [PATCH 197/338] - bumped gehomesdk version --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index ec5a90e..aca93d4 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.24","magicattr==0.1.5","slixmpp==1.7.1"], + "requirements": ["gehomesdk==0.4.25","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], "version": "0.6.0" } From a02819589995bae68e3072eb8aea332384466e69 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 30 Apr 2022 19:51:24 -0400 Subject: [PATCH 198/338] - updated readme --- README.md | 2 ++ info.md | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index f669bf3..42ad817 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio - Coffee Maker **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** +## Updates +Unfortunately, I'm pretty much at the end of what I can do without assistance from others with these devices that can help provide logs. I'll do what I can to make updates if there's something broken, but I am not really able to add new functionality if I can't get a little help to do so. ## Home Assistant UI Examples Entities card: diff --git a/info.md b/info.md index d8d24e2..f99e99d 100644 --- a/info.md +++ b/info.md @@ -17,6 +17,10 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** +## Updates + +Unfortunately, I'm pretty much at the end of what I can do without assistance from others with these devices that can help provide logs. I'll do what I can to make updates if there's something broken, but I am not really able to add new functionality if I can't get a little help to do so. + ## Home Assistant UI Examples Entities card: From 61ce622e1395324174bfa0f126bb341719dabfbc Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 7 May 2022 19:53:38 -0400 Subject: [PATCH 199/338] - fixed issue with water life remaining sensor --- custom_components/ge_home/devices/water_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/water_filter.py b/custom_components/ge_home/devices/water_filter.py index d938df1..d466f67 100644 --- a/custom_components/ge_home/devices/water_filter.py +++ b/custom_components/ge_home/devices/water_filter.py @@ -31,7 +31,7 @@ def get_all_entities(self) -> List[Entity]: 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), - GeErdSensor(self, ErdCode.WH_FILTER_LIFE_REMAINING), + 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 From 7852885adf99d5ecfb745d686ae649196e40cc86 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 7 May 2022 19:57:03 -0400 Subject: [PATCH 200/338] - version bump/doc update --- CHANGELOG.md | 4 ++++ custom_components/ge_home/manifest.json | 2 +- info.md | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18a3b52..9260877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.1 + +- Fixed issue with water filter life sensor (@rgabrielson11) + ## 0.6.0 - Requires HA 2021.12.x or later diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index aca93d4..048a3cc 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==0.4.25","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], - "version": "0.6.0" + "version": "0.6.1" } diff --git a/info.md b/info.md index f99e99d..b07663e 100644 --- a/info.md +++ b/info.md @@ -91,6 +91,10 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.1'.split('.') | map('int') %} +- Fixed issue with water filter life sensor (@rgabrielson11) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} - Updated deprecated icons (@mjmeli, @schmittx) {% endif %} From 0a207c573a6bc82f039979000dc135c404d8c2f2 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 11 May 2022 09:20:57 -0400 Subject: [PATCH 201/338] - updated water heater naming --- CHANGELOG.md | 4 ++++ .../ge_home/entities/fridge/ge_abstract_fridge.py | 2 +- custom_components/ge_home/entities/oven/ge_oven.py | 4 ++-- info.md | 4 ++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9260877..44024cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.2 + +- Fixed issue with water heater naming when no serial is present + ## 0.6.1 - Fixed issue with water filter life sensor (@rgabrielson11) diff --git a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py index 5266fc4..31a5fda 100644 --- a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py @@ -48,7 +48,7 @@ def unique_id(self) -> str: @property def name(self) -> Optional[str]: - return f"{self.serial_number} {self.heater_type.title()}" + return f"{self.serial_or_mac} {self.heater_type.title()}" @property def target_temps(self) -> FridgeSetPoints: diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index 54c0643..4bb5210 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -41,7 +41,7 @@ def supported_features(self): @property def unique_id(self) -> str: - return f"{DOMAIN}_{self.serial_number}_{self.oven_select.lower()}" + return f"{DOMAIN}_{self.serial_or_mac}_{self.oven_select.lower()}" @property def name(self) -> Optional[str]: @@ -50,7 +50,7 @@ def name(self) -> Optional[str]: else: oven_title = "Oven" - return f"{self.serial_number} {oven_title}" + return f"{self.serial_or_mac} {oven_title}" @property def temperature_unit(self): diff --git a/info.md b/info.md index b07663e..2c50876 100644 --- a/info.md +++ b/info.md @@ -91,6 +91,10 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.2'.split('.') | map('int') %} +- Fixed issue with water heater naming when no serial is present +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.1'.split('.') | map('int') %} - Fixed issue with water filter life sensor (@rgabrielson11) {% endif %} From 4cf40bc055ba96bdf0903fa76cc161a19f87a1ac Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 22 Jun 2022 22:42:16 -0400 Subject: [PATCH 202/338] - initial support for built-in AC units --- CHANGELOG.md | 1 + custom_components/ge_home/devices/__init__.py | 3 ++ custom_components/ge_home/devices/base.py | 6 ++-- custom_components/ge_home/devices/biac.py | 34 +++++++++++++++++++ custom_components/ge_home/manifest.json | 4 +-- info.md | 4 +++ 6 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 custom_components/ge_home/devices/biac.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 44024cf..12b017d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## 0.6.2 - Fixed issue with water heater naming when no serial is present +- Initial support for built-in air conditioners (@DaveZheng) ## 0.6.1 diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 1acc228..5c8332f 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -15,6 +15,7 @@ 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 @@ -51,6 +52,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: 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: diff --git a/custom_components/ge_home/devices/base.py b/custom_components/ge_home/devices/base.py index 54bbe35..85b2687 100644 --- a/custom_components/ge_home/devices/base.py +++ b/custom_components/ge_home/devices/base.py @@ -66,9 +66,11 @@ def mac_addr(self) -> str: @property def serial_or_mac(self) -> str: - if self.serial_number and not self.serial_number.isspace(): + if (self.serial_number and not + self.serial_number.isspace() and not + self.serial_number == "00000000"): return self.serial_number - return self.mac_addr + return self.mac_addr @property def model_number(self) -> str: diff --git a/custom_components/ge_home/devices/biac.py b/custom_components/ge_home/devices/biac.py new file mode 100644 index 0000000..8ddcfd2 --- /dev/null +++ b/custom_components/ge_home/devices/biac.py @@ -0,0 +1,34 @@ +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_home/manifest.json b/custom_components/ge_home/manifest.json index 048a3cc..0df25cc 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.25","magicattr==0.1.5","slixmpp==1.7.1"], + "requirements": ["gehomesdk==0.4.27","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], - "version": "0.6.1" + "version": "0.6.2" } diff --git a/info.md b/info.md index 2c50876..17d5229 100644 --- a/info.md +++ b/info.md @@ -63,6 +63,10 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} +- Initial support for built-in air conditioners (@DaveZheng) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} - Initial support for Water Softeners (@npentell, @drjeff) - Initial support for Opal Ice Makers (@mbcomer, @knobunc) From 207a82937a51c7db9e9ab0561caea78217136f40 Mon Sep 17 00:00:00 2001 From: Rob Schmidt Date: Sun, 10 Jul 2022 16:36:26 -0700 Subject: [PATCH 203/338] Add support for Built-In AC Unit Built-In AC unit operation and function is similar to a WAC. `gehomesdk` version bumped from 0.4.25 to 0.4.27 to include latest ErdApplianceType enum. --- custom_components/ge_home/climate.py | 2 +- custom_components/ge_home/devices/__init__.py | 3 ++ custom_components/ge_home/devices/biac.py | 33 ++++++++++++++ .../ge_home/entities/ac/__init__.py | 3 +- .../ge_home/entities/ac/ge_biac_climate.py | 45 +++++++++++++++++++ .../ge_home/entities/common/ge_climate.py | 2 +- custom_components/ge_home/manifest.json | 2 +- 7 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 custom_components/ge_home/devices/biac.py create mode 100644 custom_components/ge_home/entities/ac/ge_biac_climate.py diff --git a/custom_components/ge_home/climate.py b/custom_components/ge_home/climate.py index 3694a09..fbdb94d 100644 --- a/custom_components/ge_home/climate.py +++ b/custom_components/ge_home/climate.py @@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home Water Heaters.""" + """GE Climate Devices.""" _LOGGER.debug('Adding GE Climate Entities') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 1acc228..5c8332f 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -15,6 +15,7 @@ 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 @@ -51,6 +52,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: 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: diff --git a/custom_components/ge_home/devices/biac.py b/custom_components/ge_home/devices/biac.py new file mode 100644 index 0000000..d22eb17 --- /dev/null +++ b/custom_components/ge_home/devices/biac.py @@ -0,0 +1,33 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeBiacClimate, GeErdSensor, GeErdBinarySensor, GeErdSwitch, ErdOnOffBoolConverter + +_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() + + biac_entities = [ + GeBiacClimate(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 + biac_entities + return entities + diff --git a/custom_components/ge_home/entities/ac/__init__.py b/custom_components/ge_home/entities/ac/__init__.py index 0f2e6ad..aefb995 100644 --- a/custom_components/ge_home/entities/ac/__init__.py +++ b/custom_components/ge_home/entities/ac/__init__.py @@ -1,3 +1,4 @@ from .ge_wac_climate import GeWacClimate from .ge_sac_climate import GeSacClimate -from .ge_pac_climate import GePacClimate \ No newline at end of file +from .ge_pac_climate import GePacClimate +from .ge_biac_climate import GeBiacClimate diff --git a/custom_components/ge_home/entities/ac/ge_biac_climate.py b/custom_components/ge_home/entities/ac/ge_biac_climate.py new file mode 100644 index 0000000..f3b7453 --- /dev/null +++ b/custom_components/ge_home/entities/ac/ge_biac_climate.py @@ -0,0 +1,45 @@ +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_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index fc48f93..7f44edb 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -191,4 +191,4 @@ def _convert_temp(self, temperature_f: int): return (temperature_f - 32.0) * (5/9) def _get_icon(self) -> Optional[str]: - return "mdi:air-conditioner" \ No newline at end of file + return "mdi:air-conditioner" diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 048a3cc..330faec 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.25","magicattr==0.1.5","slixmpp==1.7.1"], + "requirements": ["gehomesdk==0.4.27","magicattr==0.1.6","slixmpp==1.8.2"], "codeowners": ["@simbaja"], "version": "0.6.1" } From c31396d33e8654e4997da06658ca04c54fef6d57 Mon Sep 17 00:00:00 2001 From: Rob Schmidt Date: Sun, 10 Jul 2022 16:42:31 -0700 Subject: [PATCH 204/338] Update README.md Update README.md to include Built-In AC as supported device --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 42ad817..319ddd0 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio - Laundry (Washer/Dryer) - Whole Home Water Filter - Whole Home Water Softener -- A/C (Portable, Split, Window) +- A/C (Portable, Split, Window, Built-In) - Range Hood - Advantium - Microwave @@ -69,4 +69,4 @@ Please click [here](CHANGELOG.md) for change information. [license-shield]: https://img.shields.io/github/license/simbaja/ha_gehome.svg?style=for-the-badge [maintenance-shield]: https://img.shields.io/badge/maintainer-Jack%20Simbach%20%40simbaja-blue.svg?style=for-the-badge [releases-shield]: https://img.shields.io/github/release/simbaja/ha_gehome.svg?style=for-the-badge -[releases]: https://github.com/simbaja/ha_gehome/releases \ No newline at end of file +[releases]: https://github.com/simbaja/ha_gehome/releases From 113f49eef3714d1fc3ad08beb0025f3041d04d22 Mon Sep 17 00:00:00 2001 From: elwing00 <56235263+elwing00@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:10:36 -0400 Subject: [PATCH 205/338] Update ge_ccm_brew_cups.py Updated for NumberEntity refactoring --- custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py index dc876b5..b1ee283 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py @@ -10,10 +10,10 @@ def __init__(self, api: ApplianceApi): self._set_value = None - async def async_set_value(self, value): + async def async_set_native_value(self, value): GeCcmCachedValue.set_value(self, value) self.schedule_update_ha_state() @property - def value(self): - return self.get_value(device_value = super().value) \ No newline at end of file + def native_value(self): + return self.get_value(device_value = super().value) From 347fb84a4dc2a483564d11edcfd24e310402d5da Mon Sep 17 00:00:00 2001 From: elwing00 <56235263+elwing00@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:11:28 -0400 Subject: [PATCH 206/338] Update ge_ccm_brew_temperature.py Updated for NumberEntity refactoring --- .../ge_home/entities/ccm/ge_ccm_brew_temperature.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py index 4dc20e9..56bf968 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py @@ -9,14 +9,18 @@ def __init__(self, api: ApplianceApi): 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_value(self, value): + async def async_set_native_value(self, value): GeCcmCachedValue.set_value(self, value) self.schedule_update_ha_state() @property - def value(self): + def native_value(self): return int(self.get_value(device_value = super().value)) + + @property + def native_unit_of_measurement(self): + return TEMP_FAHRENHEIT @property def brew_temperature(self) -> int: - return self.value \ No newline at end of file + return self.value From eb50e37c9c2d6b467a2b5efd4e5c70b340bf93f9 Mon Sep 17 00:00:00 2001 From: elwing00 <56235263+elwing00@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:14:18 -0400 Subject: [PATCH 207/338] Update ge_erd_number.py Updated for NumberEntityRefactoring --- .../ge_home/entities/common/ge_erd_number.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_number.py b/custom_components/ge_home/entities/common/ge_erd_number.py index bea3f69..bb5dde4 100644 --- a/custom_components/ge_home/entities/common/ge_erd_number.py +++ b/custom_components/ge_home/entities/common/ge_erd_number.py @@ -33,9 +33,9 @@ def __init__( 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._min_value = min_value - self._max_value = max_value - self._step_value = step_value + self._native_min_value = min_value + self._native_max_value = max_value + self._native_step = step_value self._mode = mode @property @@ -47,7 +47,7 @@ def value(self): return None @property - def unit_of_measurement(self) -> Optional[str]: + def native_unit_of_measurement(self) -> Optional[str]: return self._get_uom() @property @@ -58,15 +58,15 @@ def _data_type(self) -> ErdDataType: return self.appliance.get_erd_code_data_type(self.erd_code) @property - def min_value(self) -> float: - return self._convert_value_from_device(self._min_value) + def native_min_value(self) -> float: + return self._convert_value_from_device(self._native_min_value) @property - def max_value(self) -> float: - return self._convert_value_from_device(self._max_value) + def native_max_value(self) -> float: + return self._convert_value_from_device(self._native_max_value) @property - def step(self) -> float: + def native_step(self) -> float: return self._step_value @property @@ -116,7 +116,7 @@ def _get_icon(self): return "mdi:door-closed" return super()._get_icon() - async def async_set_value(self, value): + 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: @@ -125,4 +125,4 @@ async def async_set_value(self, value): try: await self.appliance.async_set_erd_value(self.erd_code, value) except: - _LOGGER.warning(f"Could not set {self.name} to {value}") \ No newline at end of file + _LOGGER.warning(f"Could not set {self.name} to {value}") From f9019b37c37750c92f58f9267f7faad11200d5e5 Mon Sep 17 00:00:00 2001 From: elwing00 <56235263+elwing00@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:19:45 -0400 Subject: [PATCH 208/338] Update ge_erd_number.py --- custom_components/ge_home/entities/common/ge_erd_number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_number.py b/custom_components/ge_home/entities/common/ge_erd_number.py index bb5dde4..f7350e3 100644 --- a/custom_components/ge_home/entities/common/ge_erd_number.py +++ b/custom_components/ge_home/entities/common/ge_erd_number.py @@ -39,7 +39,7 @@ def __init__( self._mode = mode @property - def value(self): + def native_value(self): try: value = self.appliance.get_erd_value(self.erd_code) return self._convert_value_from_device(value) From bafd8ad1417f3f320bb361602390e6aa8e223095 Mon Sep 17 00:00:00 2001 From: elwing00 <56235263+elwing00@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:22:16 -0400 Subject: [PATCH 209/338] Update ge_erd_number.py --- custom_components/ge_home/entities/common/ge_erd_number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_number.py b/custom_components/ge_home/entities/common/ge_erd_number.py index f7350e3..c6de900 100644 --- a/custom_components/ge_home/entities/common/ge_erd_number.py +++ b/custom_components/ge_home/entities/common/ge_erd_number.py @@ -67,7 +67,7 @@ def native_max_value(self) -> float: @property def native_step(self) -> float: - return self._step_value + return self._native_step @property def mode(self) -> float: From 6235d4af1dfa342d44b50fc283279032a3d4c779 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 17 Jul 2022 10:14:55 -0400 Subject: [PATCH 210/338] - updated zero serial number detection (resolves #89) --- custom_components/ge_home/devices/base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/base.py b/custom_components/ge_home/devices/base.py index 85b2687..179e3e9 100644 --- a/custom_components/ge_home/devices/base.py +++ b/custom_components/ge_home/devices/base.py @@ -66,9 +66,16 @@ def mac_addr(self) -> str: @property def serial_or_mac(self) -> str: + def is_zero(val: str) -> bool: + try: + intVal = int(val) + return intVal == 0 + except: + return False + if (self.serial_number and not self.serial_number.isspace() and not - self.serial_number == "00000000"): + is_zero(self.serial_number)): return self.serial_number return self.mac_addr From b71ee0933c86e9afe91df30a77a553e28fd0f4a5 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 17 Jul 2022 10:50:12 -0400 Subject: [PATCH 211/338] - updated version - updated changelog --- CHANGELOG.md | 5 +++++ custom_components/ge_home/manifest.json | 2 +- info.md | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12b017d..15f002e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.3 + +- Updated detection of invalid serial numbers (#89) +- Updated implementation of number entities to fix deprecation warnings (#85) + ## 0.6.2 - Fixed issue with water heater naming when no serial is present diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 0df25cc..fee7cae 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==0.4.27","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], - "version": "0.6.2" + "version": "0.6.3" } diff --git a/info.md b/info.md index 17d5229..0868ea8 100644 --- a/info.md +++ b/info.md @@ -95,6 +95,11 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.3'.split('.') | map('int') %} +- Updated detection of invalid serial numbers (#89) +- Updated implementation of number entities to fix deprecation warnings (#85) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.2'.split('.') | map('int') %} - Fixed issue with water heater naming when no serial is present {% endif %} From 84aade4a8bcf4da83c1ec4b3b3c44d8dbd61e02c Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 23 Jul 2022 11:03:18 -0400 Subject: [PATCH 212/338] - hopefully fixed recursion bug with numbers --- custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py | 2 +- .../ge_home/entities/ccm/ge_ccm_brew_temperature.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py index b1ee283..5792f12 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py @@ -16,4 +16,4 @@ async def async_set_native_value(self, value): @property def native_value(self): - return self.get_value(device_value = super().value) + return self.get_value(device_value = super().native_value) diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py index 56bf968..ecda169 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py @@ -2,6 +2,7 @@ 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): @@ -15,7 +16,7 @@ async def async_set_native_value(self, value): @property def native_value(self): - return int(self.get_value(device_value = super().value)) + return int(self.get_value(device_value = super().native_value)) @property def native_unit_of_measurement(self): From b3a73bbc28b5930f238f61289df38ee93d3b9392 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 2 Aug 2022 15:46:16 -0400 Subject: [PATCH 213/338] - added cooktop support --- custom_components/ge_home/devices/__init__.py | 4 ++ custom_components/ge_home/devices/cooktop.py | 52 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 custom_components/ge_home/devices/cooktop.py diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 5c8332f..f9651be 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -3,6 +3,8 @@ from gehomesdk.erd import ErdApplianceType +from custom_components.ge_home.devices.cooktop import CooktopApi + from .base import ApplianceApi from .oven import OvenApi from .fridge import FridgeApi @@ -30,6 +32,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> 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.DISH_WASHER: diff --git a/custom_components/ge_home/devices/cooktop.py b/custom_components/ge_home/devices/cooktop.py new file mode 100644 index 0000000..6fb3453 --- /dev/null +++ b/custom_components/ge_home/devices/cooktop.py @@ -0,0 +1,52 @@ +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('_') From bda47eea1fc7ce45237efae5629d55cef6a50165 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 2 Aug 2022 16:36:50 -0400 Subject: [PATCH 214/338] - fixed circular reference --- custom_components/ge_home/devices/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index f9651be..0bd862b 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -3,10 +3,10 @@ from gehomesdk.erd import ErdApplianceType -from custom_components.ge_home.devices.cooktop import CooktopApi 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 0afb05f9eefda75b6c4292054cd06e5522256df5 Mon Sep 17 00:00:00 2001 From: Federico Sevilla Date: Fri, 12 Aug 2022 17:09:00 +1000 Subject: [PATCH 215/338] Add support for fridges with reduced support for ERD codes (no turbo mode, no current temperature reporting, no temperature setpoint limit reporting, no door status reporting). This change has been tested on a Fisher & Paykel RF610AA. --- .../entities/fridge/ge_abstract_fridge.py | 52 ++++++++++++++----- .../ge_home/entities/fridge/ge_freezer.py | 9 ++-- .../ge_home/entities/fridge/ge_fridge.py | 42 ++++++++------- 3 files changed, 69 insertions(+), 34 deletions(-) diff --git a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py index 5266fc4..eca4771 100644 --- a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.util.temperature import convert as convert_temperature from gehomesdk import ( ErdCode, @@ -26,6 +27,14 @@ class GeAbstractFridge(GeWaterHeater): """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 @@ -40,7 +49,11 @@ def turbo_mode(self) -> str: @property def operation_list(self) -> List[str]: - return [OP_MODE_NORMAL, OP_MODE_SABBATH, self.turbo_mode] + 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: @@ -63,11 +76,15 @@ def target_temperature(self) -> int: @property def current_temperature(self) -> int: """Return the current temperature.""" - 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 + 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) @@ -95,21 +112,32 @@ def setpoint_limits(self) -> FridgeSetPointLimits: @property def min_temp(self): - """Return the minimum temperature.""" - return getattr(self.setpoint_limits, f"{self.heater_type}_min") + """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 convert_temperature(self.temp_limits[f"{self.heater_type}_min"], TEMP_FAHRENHEIT, self.temperature_unit) @property def max_temp(self): - """Return the maximum temperature.""" - return getattr(self.setpoint_limits, f"{self.heater_type}_max") + """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 convert_temperature(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 - if self.appliance.get_erd_value(self.turbo_erd_code): - return self.turbo_mode + 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): diff --git a/custom_components/ge_home/entities/fridge/ge_freezer.py b/custom_components/ge_home/entities/fridge/ge_freezer.py index 4b178fc..005dba9 100644 --- a/custom_components/ge_home/entities/fridge/ge_freezer.py +++ b/custom_components/ge_home/entities/fridge/ge_freezer.py @@ -26,7 +26,10 @@ class GeFreezer(GeAbstractFridge): @property def door_state_attrs(self) -> Optional[Dict[str, Any]]: - door_status = self.door_status.freezer - if door_status and door_status != ErdDoorStatus.NA: - return {ATTR_DOOR_STATUS: self._stringify(door_status)} + 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_home/entities/fridge/ge_fridge.py b/custom_components/ge_home/entities/fridge/ge_fridge.py index ef9e708..e24c3e0 100644 --- a/custom_components/ge_home/entities/fridge/ge_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_fridge.py @@ -36,23 +36,27 @@ def other_state_attrs(self) -> Dict[str, Any]: @property def door_state_attrs(self) -> Dict[str, Any]: """Get state attributes for the doors.""" - data = {} - door_status = self.door_status - if not door_status: + 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 {} - 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 From ac005af4285cd583e76dc24dfce59a4d9fd07772 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 4 Sep 2022 16:21:30 -0400 Subject: [PATCH 216/338] - added dual dishwasher support - updated documentation - version bumps --- CHANGELOG.md | 7 +++ README.md | 6 +- custom_components/ge_home/devices/__init__.py | 5 ++ .../ge_home/devices/dual_dishwasher.py | 61 +++++++++++++++++++ custom_components/ge_home/devices/fridge.py | 6 ++ custom_components/ge_home/manifest.json | 4 +- info.md | 13 +++- 7 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 custom_components/ge_home/devices/dual_dishwasher.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f002e..d32298c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.5 + +- Added beverage cooler support (@kksligh) +- Added dual dishwasher support (@jkili) +- Added initial espresso maker support (@datagen24) +- Added whole home water heater support (@seantibor) + ## 0.6.3 - Updated detection of invalid serial numbers (#89) diff --git a/README.md b/README.md index 319ddd0..e21f84b 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,18 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio - Fridge - Oven -- Dishwasher +- 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 +- Coffee Maker / Espresso Maker +- Beverage Center **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** ## Updates diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 0bd862b..842adc9 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -23,6 +23,7 @@ from .water_softener import WaterSoftenerApi from .oim import OimApi from .coffee_maker import CcmApi +from .dual_dishwasher import DualDishwasherApi _LOGGER = logging.getLogger(__name__) @@ -36,8 +37,12 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return CooktopApi if appliance_type == ErdApplianceType.FRIDGE: return FridgeApi + if appliance_type == ErdApplianceType.BEVERAGE_CENTER: + return FridgeApi if appliance_type == ErdApplianceType.DISH_WASHER: return DishwasherApi + if appliance_type == ErdApplianceType.DUAL_DISH_WASHER: + return DualDishwasherApi if appliance_type == ErdApplianceType.WASHER: return WasherApi if appliance_type == ErdApplianceType.DRYER: diff --git a/custom_components/ge_home/devices/dual_dishwasher.py b/custom_components/ge_home/devices/dual_dishwasher.py new file mode 100644 index 0000000..63c8b29 --- /dev/null +++ b/custom_components/ge_home/devices/dual_dishwasher.py @@ -0,0 +1,61 @@ +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_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index b2c42d2..24f0657 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -59,6 +59,8 @@ def get_all_entities(self) -> List[Entity]: 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 @@ -92,6 +94,10 @@ def get_all_entities(self) -> List[Entity]: 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: diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index fee7cae..32d35b9 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.27","magicattr==0.1.5","slixmpp==1.7.1"], + "requirements": ["gehomesdk==0.5.5","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], - "version": "0.6.3" + "version": "0.6.5" } diff --git a/info.md b/info.md index 0868ea8..33beba7 100644 --- a/info.md +++ b/info.md @@ -4,16 +4,18 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio - Fridge - Oven -- Dishwasher +- 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) - Range Hood - Advantium - Microwave - Opal Ice Maker -- Coffee Maker +- Coffee Maker / Espresso Maker +- Beverage Center **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** @@ -63,6 +65,13 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '0.6.5'.split('.') | map('int') %} +- Added beverage cooler support (@kksligh) +- Added dual dishwasher support (@jkili) +- Added initial espresso maker support (@datagen24) +- Added whole home water heater support (@seantibor) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} - Initial support for built-in air conditioners (@DaveZheng) {% endif %} From 2938b26cccd0a21e1d58facd85a3a6d157a255e6 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 4 Sep 2022 22:31:12 -0400 Subject: [PATCH 217/338] - added water heater support --- custom_components/ge_home/devices/__init__.py | 3 + custom_components/ge_home/devices/oim.py | 4 +- .../ge_home/devices/water_heater.py | 41 +++++++++ .../ge_home/entities/__init__.py | 1 + .../ge_home/entities/common/__init__.py | 2 +- .../entities/common/ge_water_heater.py | 2 +- .../entities/fridge/ge_abstract_fridge.py | 4 +- .../ge_home/entities/fridge/ge_dispenser.py | 4 +- .../ge_home/entities/oven/ge_oven.py | 4 +- .../ge_home/entities/water_heater/__init__.py | 2 + .../entities/water_heater/ge_water_heater.py | 91 +++++++++++++++++++ .../entities/water_heater/heater_modes.py | 26 ++++++ custom_components/ge_home/water_heater.py | 4 +- 13 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 custom_components/ge_home/devices/water_heater.py create mode 100644 custom_components/ge_home/entities/water_heater/__init__.py create mode 100644 custom_components/ge_home/entities/water_heater/ge_water_heater.py create mode 100644 custom_components/ge_home/entities/water_heater/heater_modes.py diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 842adc9..bef308e 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -21,6 +21,7 @@ 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 @@ -53,6 +54,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: 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: diff --git a/custom_components/ge_home/devices/oim.py b/custom_components/ge_home/devices/oim.py index ad1bd06..124ad3d 100644 --- a/custom_components/ge_home/devices/oim.py +++ b/custom_components/ge_home/devices/oim.py @@ -22,8 +22,8 @@ class OimApi(ApplianceApi): - """API class for Oven Hood objects""" - APPLIANCE_TYPE = ErdApplianceType.HOOD + """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() diff --git a/custom_components/ge_home/devices/water_heater.py b/custom_components/ge_home/devices/water_heater.py new file mode 100644 index 0000000..0c77340 --- /dev/null +++ b/custom_components/ge_home/devices/water_heater.py @@ -0,0 +1,41 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdOnOff +) + +from custom_components.ge_home.entities.water_heater.ge_water_heater import GeWaterHeater + +from .base import ApplianceApi +from ..entities import ( + GeErdSensor, + GeErdBinarySensor, + GeErdSelect, + GeErdSwitch, + ErdOnOffBoolConverter +) + +_LOGGER = logging.getLogger(__name__) + + +class WaterHeaterApi(ApplianceApi): + """API class for Water Heater objects""" + APPLIANCE_TYPE = ErdApplianceType.WATER_HEATER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + wh_entities = [ + GeErdSensor(self, ErdCode.WH_HEATER_TARGET_TEMPERATURE), + GeErdSensor(self, ErdCode.WH_HEATER_TEMPERATURE), + GeErdSensor(self, ErdCode.WH_HEATER_MODE_HOURS_REMAINING), + GeWaterHeater(self) + ] + + entities = base_entities + wh_entities + return entities + diff --git a/custom_components/ge_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index eabcc59..2306cae 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -7,5 +7,6 @@ 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_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index edd0b63..0b555ca 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -11,6 +11,6 @@ from .ge_erd_switch import GeErdSwitch from .ge_erd_button import GeErdButton from .ge_erd_number import GeErdNumber -from .ge_water_heater import GeWaterHeater +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_home/entities/common/ge_water_heater.py b/custom_components/ge_home/entities/common/ge_water_heater.py index 87bd091..55ae4d9 100644 --- a/custom_components/ge_home/entities/common/ge_water_heater.py +++ b/custom_components/ge_home/entities/common/ge_water_heater.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) -class GeWaterHeater(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): +class GeAbstractWaterHeater(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): """Mock temperature/operation mode supporting device as a water heater""" @property diff --git a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py index 31a5fda..3492cb4 100644 --- a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py @@ -18,12 +18,12 @@ IceMakerControlStatus ) from ...const import DOMAIN -from ..common import GeWaterHeater +from ..common import GeAbstractWaterHeater from .const import * _LOGGER = logging.getLogger(__name__) -class GeAbstractFridge(GeWaterHeater): +class GeAbstractFridge(GeAbstractWaterHeater): """Mock a fridge or freezer as a water heater.""" @property diff --git a/custom_components/ge_home/entities/fridge/ge_dispenser.py b/custom_components/ge_home/entities/fridge/ge_dispenser.py index 759d3f6..a961df3 100644 --- a/custom_components/ge_home/entities/fridge/ge_dispenser.py +++ b/custom_components/ge_home/entities/fridge/ge_dispenser.py @@ -15,7 +15,7 @@ HotWaterStatus ) -from ..common import GeWaterHeater +from ..common import GeAbstractWaterHeater from .const import ( HEATER_TYPE_DISPENSER, OP_MODE_NORMAL, @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) -class GeDispenser(GeWaterHeater): +class GeDispenser(GeAbstractWaterHeater): """Entity for in-fridge dispensers""" # These values are from FridgeHotWaterFragment.smali in the android app (in imperial units) diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index 4bb5210..297b2c1 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -13,12 +13,12 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from ...const import DOMAIN from ...devices import ApplianceApi -from ..common import GeWaterHeater +from ..common import GeAbstractWaterHeater from .const import * _LOGGER = logging.getLogger(__name__) -class GeOven(GeWaterHeater): +class GeOven(GeAbstractWaterHeater): """GE Appliance Oven""" icon = "mdi:stove" diff --git a/custom_components/ge_home/entities/water_heater/__init__.py b/custom_components/ge_home/entities/water_heater/__init__.py new file mode 100644 index 0000000..c0fa79f --- /dev/null +++ b/custom_components/ge_home/entities/water_heater/__init__.py @@ -0,0 +1,2 @@ +from .heater_modes import WhHeaterModeConverter +from .ge_water_heater import GeWaterHeater \ No newline at end of file diff --git a/custom_components/ge_home/entities/water_heater/ge_water_heater.py b/custom_components/ge_home/entities/water_heater/ge_water_heater.py new file mode 100644 index 0000000..135e383 --- /dev/null +++ b/custom_components/ge_home/entities/water_heater/ge_water_heater.py @@ -0,0 +1,91 @@ +"""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.OVEN_MODE_MIN_MAX_TEMP) + #return min_temp + return 100 + + @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 + return 200 + + 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_home/entities/water_heater/heater_modes.py b/custom_components/ge_home/entities/water_heater/heater_modes.py new file mode 100644 index 0000000..cd2e39a --- /dev/null +++ b/custom_components/ge_home/entities/water_heater/heater_modes.py @@ -0,0 +1,26 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdWaterHeaterMode +from ..common import OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class WhHeaterModeConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdWaterHeaterMode] + def from_option_string(self, value: str) -> Any: + enum_val = value.upper().replace(" ","_") + try: + return ErdWaterHeaterMode[enum_val] + except: + _LOGGER.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_home/water_heater.py b/custom_components/ge_home/water_heater.py index ac0aa85..72e4602 100644 --- a/custom_components/ge_home/water_heater.py +++ b/custom_components/ge_home/water_heater.py @@ -7,7 +7,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .entities import GeWaterHeater +from .entities import GeAbstractWaterHeater from .const import DOMAIN from .update_coordinator import GeHomeUpdateCoordinator @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn entity for api in apis for entity in api.entities - if isinstance(entity, GeWaterHeater) + if isinstance(entity, GeAbstractWaterHeater) ] _LOGGER.debug(f'Found {len(entities):d} "water heaters"') async_add_entities(entities) From 669772fd519b4615282d0b28d74feee240c381f7 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 4 Sep 2022 22:44:34 -0400 Subject: [PATCH 218/338] - added basic espresso maker device --- custom_components/ge_home/devices/__init__.py | 5 ++- .../ge_home/devices/espresso_maker.py | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 custom_components/ge_home/devices/espresso_maker.py diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index bef308e..b8a07ef 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -3,7 +3,6 @@ from gehomesdk.erd import ErdApplianceType - from .base import ApplianceApi from .oven import OvenApi from .cooktop import CooktopApi @@ -25,6 +24,8 @@ from .oim import OimApi from .coffee_maker import CcmApi from .dual_dishwasher import DualDishwasherApi +from .espresso_maker import EspressoMakerApi + _LOGGER = logging.getLogger(__name__) @@ -74,6 +75,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: 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_home/devices/espresso_maker.py b/custom_components/ge_home/devices/espresso_maker.py new file mode 100644 index 0000000..efb184e --- /dev/null +++ b/custom_components/ge_home/devices/espresso_maker.py @@ -0,0 +1,34 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType +) + +from .base import ApplianceApi +from ..entities import ( + GeErdBinarySensor, + GeErdButton +) + +_LOGGER = logging.getLogger(__name__) + + +class EspressoMakerApi(ApplianceApi): + """API class for Espresso Maker objects""" + APPLIANCE_TYPE = ErdApplianceType.ESPRESSO_MAKER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + em_entities = [ + GeErdBinarySensor(self, ErdCode.CCM_IS_DESCALING), + GeErdButton(self, ErdCode.CCM_CANCEL_DESCALING), + GeErdButton(self, ErdCode.CCM_START_DESCALING), + GeErdBinarySensor(self, ErdCode.CCM_OUT_OF_WATER, device_class_override="problem"), + ] + + entities = base_entities + em_entities + return entities From c3729685f91cf1ade4de0933718a5d2d0c45c41e Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 4 Sep 2022 22:55:51 -0400 Subject: [PATCH 219/338] - bugfixes --- custom_components/ge_home/entities/advantium/ge_advantium.py | 4 ++-- custom_components/ge_home/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/entities/advantium/ge_advantium.py b/custom_components/ge_home/entities/advantium/ge_advantium.py index 917a7d0..2d1372b 100644 --- a/custom_components/ge_home/entities/advantium/ge_advantium.py +++ b/custom_components/ge_home/entities/advantium/ge_advantium.py @@ -18,12 +18,12 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from ...const import DOMAIN from ...devices import ApplianceApi -from ..common import GeWaterHeater +from ..common import GeAbstractWaterHeater from .const import * _LOGGER = logging.getLogger(__name__) -class GeAdvantium(GeWaterHeater): +class GeAdvantium(GeAbstractWaterHeater): """GE Appliance Advantium""" icon = "mdi:microwave" diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 32d35b9..8df34b9 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.5","magicattr==0.1.5","slixmpp==1.7.1"], + "requirements": ["gehomesdk==0.5.6","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], "version": "0.6.5" } From f2553c9bbca624334615a1109a95b846ebaaff36 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 5 Sep 2022 16:08:56 -0400 Subject: [PATCH 220/338] - rewrote initialization (resolves #99) --- custom_components/ge_home/__init__.py | 13 +++++-- custom_components/ge_home/binary_sensor.py | 34 ++++++++++-------- custom_components/ge_home/button.py | 30 ++++++++-------- custom_components/ge_home/climate.py | 31 ++++++++-------- custom_components/ge_home/light.py | 35 ++++++++++--------- custom_components/ge_home/number.py | 33 +++++++++-------- custom_components/ge_home/select.py | 32 ++++++++--------- custom_components/ge_home/sensor.py | 30 ++++++++-------- custom_components/ge_home/switch.py | 31 ++++++++-------- .../ge_home/update_coordinator.py | 24 ++++++------- custom_components/ge_home/water_heater.py | 31 ++++++++-------- 11 files changed, 172 insertions(+), 152 deletions(-) diff --git a/custom_components/ge_home/__init__.py b/custom_components/ge_home/__init__.py index 7b3f082..f088fd7 100644 --- a/custom_components/ge_home/__init__.py +++ b/custom_components/ge_home/__init__.py @@ -7,7 +7,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.const import CONF_REGION +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN +from .exceptions import HaAuthError, HaCannotConnect from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -42,9 +44,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coordinator = GeHomeUpdateCoordinator(hass, entry) hass.data[DOMAIN][entry.entry_id] = coordinator - if not await coordinator.async_setup(): - return False - + try: + if not await coordinator.async_setup(): + return False + except HaCannotConnect: + raise ConfigEntryNotReady("Could not connect to SmartHQ") + except HaAuthError: + raise ConfigEntryAuthFailed("Could not authenticate to SmartHQ") + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown) return True diff --git a/custom_components/ge_home/binary_sensor.py b/custom_components/ge_home/binary_sensor.py index addce09..9172f27 100644 --- a/custom_components/ge_home/binary_sensor.py +++ b/custom_components/ge_home/binary_sensor.py @@ -5,30 +5,34 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN +from .devices import ApplianceApi from .entities import GeErdBinarySensor from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home sensors.""" + """GE Home binary sensors.""" + _LOGGER.debug('Adding GE Binary Sensor Entities') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + #apis = coordinator.appliance_apis.values() - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - apis = coordinator.appliance_apis.values() - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdBinarySensor) and not isinstance(entity, SwitchEntity) - ] - _LOGGER.debug(f'Found {len(entities):d} binary sensors ') - async_add_entities(entities) + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdBinarySensor) and not isinstance(entity, SwitchEntity) + ] + _LOGGER.debug(f'Found {len(entities):d} binary sensors') + async_add_entities(entities) + + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/button.py b/custom_components/ge_home/button.py index 8e594bd..724bb17 100644 --- a/custom_components/ge_home/button.py +++ b/custom_components/ge_home/button.py @@ -4,9 +4,11 @@ from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN +from .devices import ApplianceApi from .entities import GeErdButton from .update_coordinator import GeHomeUpdateCoordinator @@ -15,19 +17,19 @@ 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] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future + @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) + ] + _LOGGER.debug(f'Found {len(entities):d} buttons ') + async_add_entities(entities) - apis = coordinator.appliance_apis.values() - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdButton) - ] - _LOGGER.debug(f'Found {len(entities):d} 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_home/climate.py b/custom_components/ge_home/climate.py index fbdb94d..c74a24b 100644 --- a/custom_components/ge_home/climate.py +++ b/custom_components/ge_home/climate.py @@ -5,31 +5,32 @@ from homeassistant.components.climate import ClimateEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect 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] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @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) + ] + _LOGGER.debug(f'Found {len(entities):d} climate entities') + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeClimate) - ] - _LOGGER.debug(f'Found {len(entities):d} 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_home/light.py b/custom_components/ge_home/light.py index 310cabf..0d6a787 100644 --- a/custom_components/ge_home/light.py +++ b/custom_components/ge_home/light.py @@ -4,10 +4,13 @@ from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .const import DOMAIN from .entities import GeErdLight +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -20,19 +23,17 @@ async def async_setup_entry( _LOGGER.debug("Adding GE Home lights") coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug("Coordinator init future finished") - - apis = list(coordinator.appliance_apis.values()) - _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 - ] - _LOGGER.debug(f"Found {len(entities):d} lights") - async_add_entities(entities) + @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 + ] + _LOGGER.debug(f"Found {len(entities):d} lights") + async_add_entities(entities) + + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/number.py b/custom_components/ge_home/number.py index eed2189..ff95109 100644 --- a/custom_components/ge_home/number.py +++ b/custom_components/ge_home/number.py @@ -4,9 +4,12 @@ from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .const import DOMAIN +from .devices import ApplianceApi from .entities import GeErdNumber from .update_coordinator import GeHomeUpdateCoordinator @@ -15,19 +18,19 @@ 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] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - - apis = coordinator.appliance_apis.values() - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdNumber) - ] - _LOGGER.debug(f'Found {len(entities):d} numbers ') - async_add_entities(entities) + @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) + ] + _LOGGER.debug(f'Found {len(entities):d} numbers ') + async_add_entities(entities) + + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/select.py b/custom_components/ge_home/select.py index bfa6b0b..27c3903 100644 --- a/custom_components/ge_home/select.py +++ b/custom_components/ge_home/select.py @@ -4,9 +4,11 @@ from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN +from .devices import ApplianceApi from .entities import GeErdSelect from .update_coordinator import GeHomeUpdateCoordinator @@ -20,19 +22,17 @@ async def async_setup_entry( _LOGGER.debug("Adding GE Home selects") coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug("Coordinator init future finished") + @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 + ] + _LOGGER.debug(f"Found {len(entities):d} selectors") + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _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 - ] - _LOGGER.debug(f"Found {len(entities):d} selectors") - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/sensor.py b/custom_components/ge_home/sensor.py index 2dea9a1..662d5d1 100644 --- a/custom_components/ge_home/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -6,7 +6,8 @@ from datetime import timedelta from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers import entity_platform from .const import ( @@ -16,6 +17,7 @@ SERVICE_SET_INT_VALUE ) from .entities import GeErdSensor +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator ATTR_DURATION = "duration" @@ -31,21 +33,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn # Get the platform platform = entity_platform.async_get_current_platform() - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdSensor) and entity.erd_code in api.appliance._property_cache + ] + _LOGGER.debug(f'Found {len(entities):d} sensors') + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdSensor) and entity.erd_code in api.appliance._property_cache - ] - _LOGGER.debug(f'Found {len(entities):d} sensors') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) # register set_timer entity service platform.async_register_entity_service( diff --git a/custom_components/ge_home/switch.py b/custom_components/ge_home/switch.py index 78cf896..452fc21 100644 --- a/custom_components/ge_home/switch.py +++ b/custom_components/ge_home/switch.py @@ -4,10 +4,13 @@ from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .entities import GeErdSwitch from .const import DOMAIN +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -17,18 +20,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn _LOGGER.debug('Adding GE Home switches') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdSwitch) and entity.erd_code in api.appliance._property_cache + ] + _LOGGER.debug(f'Found {len(entities):d} switches') + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdSwitch) and entity.erd_code in api.appliance._property_cache - ] - _LOGGER.debug(f'Found {len(entities):d} switches') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 8711d98..545a82c 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -21,6 +21,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -64,7 +65,6 @@ def _reset_initialization(self): self._got_roster = False self._init_done = False self._retry_count = 0 - self.initialization_future = asyncio.Future() def create_ge_client( self, event_loop: Optional[asyncio.AbstractEventLoop] @@ -95,6 +95,11 @@ def appliances(self) -> Iterable[GeAppliance]: def appliance_apis(self) -> Dict[str, ApplianceApi]: return self._appliance_apis + @property + def signal_ready(self) -> str: + """Event specific per entry to signal readiness""" + return f"{DOMAIN}-ready-{self._config_entry.entry_id}" + @property def online(self) -> bool: """ @@ -167,12 +172,6 @@ async def async_setup(self): except Exception: raise HaCannotConnect("Unknown connection failure") - try: - with async_timeout.timeout(ASYNC_TIMEOUT): - await self.initialization_future - except (asyncio.CancelledError, asyncio.TimeoutError): - raise HaCannotConnect("Initialization timed out") - return True async def async_start_client(self): @@ -322,16 +321,17 @@ async def on_connect(self, _): async def async_maybe_trigger_all_ready(self): """See if we're all ready to go, and if so, let the games begin.""" - if self._init_done or self.initialization_future.done(): + if self._init_done: # Been here, done this return if self._got_roster and self.all_appliances_updated: - _LOGGER.debug("Ready to go. Waiting 2 seconds and setting init future result.") - # The the flag and wait to prevent two different fun race conditions + _LOGGER.debug("Ready to go, sending ready signal") self._init_done = True - await asyncio.sleep(2) - self.initialization_future.set_result(True) await self.client.async_event(EVENT_ALL_APPLIANCES_READY, None) + async_dispatcher_send( + self.hass, + self.signal_ready, + list(self.appliance_apis.values())) def _get_retry_delay(self) -> int: delay = MIN_RETRY_DELAY * 2 ** (self._retry_count - 1) diff --git a/custom_components/ge_home/water_heater.py b/custom_components/ge_home/water_heater.py index 72e4602..94a8357 100644 --- a/custom_components/ge_home/water_heater.py +++ b/custom_components/ge_home/water_heater.py @@ -5,10 +5,13 @@ from homeassistant.components.water_heater import WaterHeaterEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .entities import GeAbstractWaterHeater from .const import DOMAIN +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -18,18 +21,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn _LOGGER.debug('Adding GE "Water Heaters"') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @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) + ] + _LOGGER.debug(f'Found {len(entities):d} "water heaters"') + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeAbstractWaterHeater) - ] - _LOGGER.debug(f'Found {len(entities):d} "water heaters"') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) From 992623bb94a3aafe3234807ae828a7230c72d618 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 17 Sep 2022 11:05:58 -0400 Subject: [PATCH 221/338] - added logic to prevent double registration of entities --- custom_components/ge_home/binary_sensor.py | 7 ++++--- custom_components/ge_home/button.py | 6 ++++-- custom_components/ge_home/climate.py | 7 +++++-- custom_components/ge_home/light.py | 7 ++++--- custom_components/ge_home/number.py | 7 ++++--- custom_components/ge_home/select.py | 6 ++++-- custom_components/ge_home/sensor.py | 8 +++++--- custom_components/ge_home/switch.py | 7 ++++--- custom_components/ge_home/water_heater.py | 6 ++++-- 9 files changed, 38 insertions(+), 23 deletions(-) diff --git a/custom_components/ge_home/binary_sensor.py b/custom_components/ge_home/binary_sensor.py index 9172f27..fb808f8 100644 --- a/custom_components/ge_home/binary_sensor.py +++ b/custom_components/ge_home/binary_sensor.py @@ -1,5 +1,4 @@ """GE Home Sensor Entities""" -import async_timeout import logging from typing import Callable @@ -7,6 +6,7 @@ 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 @@ -20,7 +20,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn _LOGGER.debug('Adding GE Binary Sensor Entities') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - #apis = coordinator.appliance_apis.values() + registry = er.async_get(hass) @callback def async_devices_discovered(apis: list[ApplianceApi]): @@ -31,8 +31,9 @@ def async_devices_discovered(apis: list[ApplianceApi]): for api in apis for entity in api.entities if isinstance(entity, GeErdBinarySensor) and not isinstance(entity, SwitchEntity) + if not registry.async_is_registered(entity.entity_id) ] - _LOGGER.debug(f'Found {len(entities):d} binary sensors') + _LOGGER.debug(f'Found {len(entities):d} unregistered binary sensors') async_add_entities(entities) async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/button.py b/custom_components/ge_home/button.py index 724bb17..cfbb843 100644 --- a/custom_components/ge_home/button.py +++ b/custom_components/ge_home/button.py @@ -1,11 +1,11 @@ """GE Home Button Entities""" -import async_timeout 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 @@ -19,6 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn _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]): @@ -28,8 +29,9 @@ def async_devices_discovered(apis: list[ApplianceApi]): 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} buttons ') + _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_home/climate.py b/custom_components/ge_home/climate.py index c74a24b..8255fb8 100644 --- a/custom_components/ge_home/climate.py +++ b/custom_components/ge_home/climate.py @@ -1,5 +1,4 @@ """GE Home Climate Entities""" -import async_timeout import logging from typing import Callable @@ -7,6 +6,7 @@ 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 @@ -20,17 +20,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn _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} climate entities') + _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_home/light.py b/custom_components/ge_home/light.py index 0d6a787..b652d02 100644 --- a/custom_components/ge_home/light.py +++ b/custom_components/ge_home/light.py @@ -1,12 +1,11 @@ """GE Home Select Entities""" -import async_timeout 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 @@ -22,6 +21,7 @@ async def async_setup_entry( """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]): @@ -32,8 +32,9 @@ def async_devices_discovered(apis: list[ApplianceApi]): 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} lights") + _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_home/number.py b/custom_components/ge_home/number.py index ff95109..2b4213c 100644 --- a/custom_components/ge_home/number.py +++ b/custom_components/ge_home/number.py @@ -1,12 +1,11 @@ """GE Home Number Entities""" -import async_timeout 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 @@ -20,6 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn _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]): @@ -29,8 +29,9 @@ def async_devices_discovered(apis: list[ApplianceApi]): 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} numbers ') + _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_home/select.py b/custom_components/ge_home/select.py index 27c3903..613b03b 100644 --- a/custom_components/ge_home/select.py +++ b/custom_components/ge_home/select.py @@ -1,11 +1,11 @@ """GE Home Select Entities""" -import async_timeout 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 @@ -21,6 +21,7 @@ async def async_setup_entry( """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]): @@ -31,8 +32,9 @@ def async_devices_discovered(apis: list[ApplianceApi]): 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} selectors") + _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_home/sensor.py b/custom_components/ge_home/sensor.py index 662d5d1..9732dbf 100644 --- a/custom_components/ge_home/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -1,5 +1,4 @@ """GE Home Sensor Entities""" -import async_timeout import logging from typing import Callable import voluptuous as vol @@ -9,6 +8,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers import entity_platform +from homeassistant.helpers import entity_registry as er from .const import ( DOMAIN, @@ -29,7 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn """GE Home sensors.""" _LOGGER.debug('Adding GE Home sensors') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - + registry = er.async_get(hass) + # Get the platform platform = entity_platform.async_get_current_platform() @@ -41,8 +42,9 @@ def async_devices_discovered(apis: list[ApplianceApi]): for api in apis for entity in api.entities if isinstance(entity, GeErdSensor) and entity.erd_code in api.appliance._property_cache + if not registry.async_is_registered(entity.entity_id) ] - _LOGGER.debug(f'Found {len(entities):d} sensors') + _LOGGER.debug(f'Found {len(entities):d} unregistered sensors') async_add_entities(entities) async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/switch.py b/custom_components/ge_home/switch.py index 452fc21..3aa6f11 100644 --- a/custom_components/ge_home/switch.py +++ b/custom_components/ge_home/switch.py @@ -1,12 +1,11 @@ """GE Home Switch Entities""" -import async_timeout import logging from typing import Callable from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect - +from homeassistant.helpers import entity_registry as er from .entities import GeErdSwitch from .const import DOMAIN @@ -19,6 +18,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn """GE Home sensors.""" _LOGGER.debug('Adding GE Home switches') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) @callback def async_devices_discovered(apis: list[ApplianceApi]): @@ -28,8 +28,9 @@ def async_devices_discovered(apis: list[ApplianceApi]): for api in apis for entity in api.entities if isinstance(entity, GeErdSwitch) and entity.erd_code in api.appliance._property_cache + if not registry.async_is_registered(entity.entity_id) ] - _LOGGER.debug(f'Found {len(entities):d} switches') + _LOGGER.debug(f'Found {len(entities):d} unregistered switches') async_add_entities(entities) async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/water_heater.py b/custom_components/ge_home/water_heater.py index 94a8357..19e8b4d 100644 --- a/custom_components/ge_home/water_heater.py +++ b/custom_components/ge_home/water_heater.py @@ -7,7 +7,7 @@ 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 @@ -20,6 +20,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn """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]): @@ -29,8 +30,9 @@ def async_devices_discovered(apis: list[ApplianceApi]): 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} "water heaters"') + _LOGGER.debug(f'Found {len(entities):d} unregistered water heaters') async_add_entities(entities) async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) From 15a8a1d2ba1e16703f9c4092bd41a801a8eee9ef Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 17 Sep 2022 11:22:19 -0400 Subject: [PATCH 222/338] - added correct min/max temps for water heaters --- custom_components/ge_home/devices/water_heater.py | 2 ++ .../ge_home/entities/water_heater/ge_water_heater.py | 10 ++++------ custom_components/ge_home/manifest.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/ge_home/devices/water_heater.py b/custom_components/ge_home/devices/water_heater.py index 0c77340..aad2aa9 100644 --- a/custom_components/ge_home/devices/water_heater.py +++ b/custom_components/ge_home/devices/water_heater.py @@ -33,6 +33,8 @@ def get_all_entities(self) -> List[Entity]: 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) ] diff --git a/custom_components/ge_home/entities/water_heater/ge_water_heater.py b/custom_components/ge_home/entities/water_heater/ge_water_heater.py index 135e383..7954055 100644 --- a/custom_components/ge_home/entities/water_heater/ge_water_heater.py +++ b/custom_components/ge_home/entities/water_heater/ge_water_heater.py @@ -61,16 +61,14 @@ def target_temperature(self) -> Optional[int]: @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 - return 100 + 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.OVEN_MODE_MIN_MAX_TEMP) - #return max_temp - return 200 + _, 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.""" diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 8df34b9..20a0297 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.6","magicattr==0.1.5","slixmpp==1.7.1"], + "requirements": ["gehomesdk==0.5.7","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], "version": "0.6.5" } From 17e7c4a95d5c56fd0fe03c4037cb8e6cfb460684 Mon Sep 17 00:00:00 2001 From: alexanv1 <44785744+alexanv1@users.noreply.github.com> Date: Thu, 29 Sep 2022 23:30:54 -0700 Subject: [PATCH 223/338] Fix CoffeeMaker after the NumberEntity refactoring --- custom_components/ge_home/devices/coffee_maker.py | 4 ++-- .../ge_home/entities/ccm/ge_ccm_brew_temperature.py | 8 -------- .../ge_home/entities/common/ge_erd_number.py | 13 ++++++------- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/custom_components/ge_home/devices/coffee_maker.py b/custom_components/ge_home/devices/coffee_maker.py index d95af55..d3f39c9 100644 --- a/custom_components/ge_home/devices/coffee_maker.py +++ b/custom_components/ge_home/devices/coffee_maker.py @@ -60,7 +60,7 @@ def get_all_entities(self) -> List[Entity]: async def start_brewing(self) -> None: """Aggregate brew settings and start brewing.""" - new_mode = ErdCcmBrewSettings(self._brew_cups_entity.value, + new_mode = ErdCcmBrewSettings(self._brew_cups_entity.native_value, self._brew_strengh_entity.brew_strength, - self._brew_temperature_entity.brew_temperature) + 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_home/entities/ccm/ge_ccm_brew_temperature.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py index ecda169..86cac03 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py @@ -17,11 +17,3 @@ async def async_set_native_value(self, value): @property def native_value(self): return int(self.get_value(device_value = super().native_value)) - - @property - def native_unit_of_measurement(self): - return TEMP_FAHRENHEIT - - @property - def brew_temperature(self) -> int: - return self.value diff --git a/custom_components/ge_home/entities/common/ge_erd_number.py b/custom_components/ge_home/entities/common/ge_erd_number.py index c6de900..d773ad1 100644 --- a/custom_components/ge_home/entities/common/ge_erd_number.py +++ b/custom_components/ge_home/entities/common/ge_erd_number.py @@ -1,12 +1,11 @@ import logging from typing import Optional from gehomesdk.erd.erd_data_type import ErdDataType -from homeassistant.components.number import NumberEntity - -from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, - TEMP_FAHRENHEIT, +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 @@ -88,7 +87,7 @@ def _get_uom(self): if self._uom_override: return self._uom_override - if self.device_class == DEVICE_CLASS_TEMPERATURE: + 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 @@ -104,7 +103,7 @@ def _get_device_class(self) -> Optional[str]: ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE, ]: - return DEVICE_CLASS_TEMPERATURE + return NumberDeviceClass.TEMPERATURE return None From d555ff103c343a7e596998ad9b8832dadd5fe14a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 16 Oct 2022 09:48:31 -0400 Subject: [PATCH 224/338] - added fix for CGY366P2TS1 (#116) to try to get the cooktop status, but fail more gracefully if it isn't supported --- custom_components/ge_home/devices/oven.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index ff2d077..ff5e418 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -93,16 +93,17 @@ def get_all_entities(self) -> List[Entity]: if cooktop_config == ErdCooktopConfig.PRESENT: - cooktop_status: CooktopStatus = self.appliance.get_erd_value(ErdCode.COOKTOP_STATUS) - cooktop_entities.append(GeErdBinarySensor(self, ErdCode.COOKTOP_STATUS)) + 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)) + 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 From c8d77120c9702690ce01e334220df986580d20c0 Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sun, 16 Oct 2022 10:44:14 -0400 Subject: [PATCH 225/338] Merge Development Changes (0.6.5) (#120) * - updated water heater naming * - initial support for built-in AC units * Add support for Built-In AC Unit Built-In AC unit operation and function is similar to a WAC. `gehomesdk` version bumped from 0.4.25 to 0.4.27 to include latest ErdApplianceType enum. * Update README.md Update README.md to include Built-In AC as supported device * - updated zero serial number detection (resolves #89) * - updated version - updated changelog * - hopefully fixed recursion bug with numbers * - added cooktop support * - fixed circular reference * Add support for fridges with reduced support for ERD codes (no turbo mode, no current temperature reporting, no temperature setpoint limit reporting, no door status reporting). This change has been tested on a Fisher & Paykel RF610AA. * - added dual dishwasher support - updated documentation - version bumps * - added water heater support * - added basic espresso maker device * - bugfixes * - rewrote initialization (resolves #99) * - added logic to prevent double registration of entities * - added correct min/max temps for water heaters * Fix CoffeeMaker after the NumberEntity refactoring * - added fix for CGY366P2TS1 (#116) to try to get the cooktop status, but fail more gracefully if it isn't supported Co-authored-by: Rob Schmidt Co-authored-by: Federico Sevilla Co-authored-by: alexanv1 <44785744+alexanv1@users.noreply.github.com> --- CHANGELOG.md | 17 ++++ README.md | 10 ++- custom_components/ge_home/__init__.py | 13 ++- custom_components/ge_home/binary_sensor.py | 37 ++++---- custom_components/ge_home/button.py | 34 +++---- custom_components/ge_home/climate.py | 38 ++++---- custom_components/ge_home/devices/__init__.py | 18 ++++ custom_components/ge_home/devices/base.py | 13 ++- custom_components/ge_home/devices/biac.py | 35 ++++++++ .../ge_home/devices/coffee_maker.py | 4 +- custom_components/ge_home/devices/cooktop.py | 52 +++++++++++ .../ge_home/devices/dual_dishwasher.py | 61 +++++++++++++ .../ge_home/devices/espresso_maker.py | 34 +++++++ custom_components/ge_home/devices/fridge.py | 6 ++ custom_components/ge_home/devices/oim.py | 4 +- custom_components/ge_home/devices/oven.py | 19 ++-- .../ge_home/devices/water_heater.py | 43 +++++++++ .../ge_home/entities/__init__.py | 1 + .../ge_home/entities/ac/__init__.py | 3 +- .../ge_home/entities/ac/ge_biac_climate.py | 45 ++++++++++ .../entities/advantium/ge_advantium.py | 4 +- .../ge_home/entities/ccm/ge_ccm_brew_cups.py | 2 +- .../entities/ccm/ge_ccm_brew_temperature.py | 11 +-- .../ge_home/entities/common/__init__.py | 2 +- .../ge_home/entities/common/ge_climate.py | 2 +- .../ge_home/entities/common/ge_erd_number.py | 13 ++- .../entities/common/ge_water_heater.py | 2 +- .../entities/fridge/ge_abstract_fridge.py | 58 ++++++++---- .../ge_home/entities/fridge/ge_dispenser.py | 4 +- .../ge_home/entities/fridge/ge_freezer.py | 9 +- .../ge_home/entities/fridge/ge_fridge.py | 42 +++++---- .../ge_home/entities/oven/ge_oven.py | 8 +- .../ge_home/entities/water_heater/__init__.py | 2 + .../entities/water_heater/ge_water_heater.py | 89 +++++++++++++++++++ .../entities/water_heater/heater_modes.py | 26 ++++++ custom_components/ge_home/light.py | 36 ++++---- custom_components/ge_home/manifest.json | 4 +- custom_components/ge_home/number.py | 34 +++---- custom_components/ge_home/select.py | 36 ++++---- custom_components/ge_home/sensor.py | 36 ++++---- custom_components/ge_home/switch.py | 34 +++---- .../ge_home/update_coordinator.py | 24 ++--- custom_components/ge_home/water_heater.py | 35 ++++---- info.md | 26 +++++- 44 files changed, 776 insertions(+), 250 deletions(-) create mode 100644 custom_components/ge_home/devices/biac.py create mode 100644 custom_components/ge_home/devices/cooktop.py create mode 100644 custom_components/ge_home/devices/dual_dishwasher.py create mode 100644 custom_components/ge_home/devices/espresso_maker.py create mode 100644 custom_components/ge_home/devices/water_heater.py create mode 100644 custom_components/ge_home/entities/ac/ge_biac_climate.py create mode 100644 custom_components/ge_home/entities/water_heater/__init__.py create mode 100644 custom_components/ge_home/entities/water_heater/ge_water_heater.py create mode 100644 custom_components/ge_home/entities/water_heater/heater_modes.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9260877..d32298c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.5 + +- Added beverage cooler support (@kksligh) +- Added dual dishwasher support (@jkili) +- Added initial espresso maker support (@datagen24) +- Added whole home water heater support (@seantibor) + +## 0.6.3 + +- Updated detection of invalid serial numbers (#89) +- Updated implementation of number entities to fix deprecation warnings (#85) + +## 0.6.2 + +- Fixed issue with water heater naming when no serial is present +- Initial support for built-in air conditioners (@DaveZheng) + ## 0.6.1 - Fixed issue with water filter life sensor (@rgabrielson11) diff --git a/README.md b/README.md index 42ad817..e21f84b 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,18 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio - Fridge - Oven -- Dishwasher +- Dishwasher / F&P Dual Dishwasher - Laundry (Washer/Dryer) - Whole Home Water Filter - Whole Home Water Softener -- A/C (Portable, Split, Window) +- Whole Home Water Heater +- A/C (Portable, Split, Window, Built-In) - Range Hood - Advantium - Microwave - Opal Ice Maker -- Coffee Maker +- Coffee Maker / Espresso Maker +- Beverage Center **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** ## Updates @@ -69,4 +71,4 @@ Please click [here](CHANGELOG.md) for change information. [license-shield]: https://img.shields.io/github/license/simbaja/ha_gehome.svg?style=for-the-badge [maintenance-shield]: https://img.shields.io/badge/maintainer-Jack%20Simbach%20%40simbaja-blue.svg?style=for-the-badge [releases-shield]: https://img.shields.io/github/release/simbaja/ha_gehome.svg?style=for-the-badge -[releases]: https://github.com/simbaja/ha_gehome/releases \ No newline at end of file +[releases]: https://github.com/simbaja/ha_gehome/releases diff --git a/custom_components/ge_home/__init__.py b/custom_components/ge_home/__init__.py index 7b3f082..f088fd7 100644 --- a/custom_components/ge_home/__init__.py +++ b/custom_components/ge_home/__init__.py @@ -7,7 +7,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.const import CONF_REGION +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN +from .exceptions import HaAuthError, HaCannotConnect from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -42,9 +44,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coordinator = GeHomeUpdateCoordinator(hass, entry) hass.data[DOMAIN][entry.entry_id] = coordinator - if not await coordinator.async_setup(): - return False - + try: + if not await coordinator.async_setup(): + return False + except HaCannotConnect: + raise ConfigEntryNotReady("Could not connect to SmartHQ") + except HaAuthError: + raise ConfigEntryAuthFailed("Could not authenticate to SmartHQ") + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown) return True diff --git a/custom_components/ge_home/binary_sensor.py b/custom_components/ge_home/binary_sensor.py index addce09..fb808f8 100644 --- a/custom_components/ge_home/binary_sensor.py +++ b/custom_components/ge_home/binary_sensor.py @@ -1,34 +1,39 @@ """GE Home Sensor Entities""" -import async_timeout import logging from typing import Callable from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er from .const import DOMAIN +from .devices import ApplianceApi from .entities import GeErdBinarySensor from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home sensors.""" + """GE Home binary sensors.""" + _LOGGER.debug('Adding GE Binary Sensor Entities') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - apis = coordinator.appliance_apis.values() - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdBinarySensor) and not isinstance(entity, SwitchEntity) - ] - _LOGGER.debug(f'Found {len(entities):d} binary sensors ') - async_add_entities(entities) + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdBinarySensor) and not isinstance(entity, SwitchEntity) + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered binary sensors') + async_add_entities(entities) + + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/button.py b/custom_components/ge_home/button.py index 8e594bd..cfbb843 100644 --- a/custom_components/ge_home/button.py +++ b/custom_components/ge_home/button.py @@ -1,12 +1,14 @@ """GE Home Button Entities""" -import async_timeout import logging from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +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 @@ -15,19 +17,21 @@ 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) - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future + @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) - apis = coordinator.appliance_apis.values() - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdButton) - ] - _LOGGER.debug(f'Found {len(entities):d} 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_home/climate.py b/custom_components/ge_home/climate.py index 3694a09..8255fb8 100644 --- a/custom_components/ge_home/climate.py +++ b/custom_components/ge_home/climate.py @@ -1,35 +1,39 @@ """GE Home Climate Entities""" -import async_timeout import logging from typing import Callable from homeassistant.components.climate import ClimateEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +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 Home Water Heaters.""" + """GE Climate Devices.""" + _LOGGER.debug('Adding GE Climate Entities') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeClimate) - ] - _LOGGER.debug(f'Found {len(entities):d} climate entities') - async_add_entities(entities) + 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_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 1acc228..b8a07ef 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -5,6 +5,7 @@ from .base import ApplianceApi from .oven import OvenApi +from .cooktop import CooktopApi from .fridge import FridgeApi from .dishwasher import DishwasherApi from .washer import WasherApi @@ -15,11 +16,16 @@ 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__) @@ -29,10 +35,16 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> 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: return DishwasherApi + if appliance_type == ErdApplianceType.DUAL_DISH_WASHER: + return DualDishwasherApi if appliance_type == ErdApplianceType.WASHER: return WasherApi if appliance_type == ErdApplianceType.DRYER: @@ -43,6 +55,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: 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: @@ -51,6 +65,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: 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: @@ -59,6 +75,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: 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_home/devices/base.py b/custom_components/ge_home/devices/base.py index 54bbe35..179e3e9 100644 --- a/custom_components/ge_home/devices/base.py +++ b/custom_components/ge_home/devices/base.py @@ -66,9 +66,18 @@ def mac_addr(self) -> str: @property def serial_or_mac(self) -> str: - if self.serial_number and not self.serial_number.isspace(): + def is_zero(val: str) -> bool: + try: + intVal = int(val) + return intVal == 0 + except: + return False + + if (self.serial_number and not + self.serial_number.isspace() and not + is_zero(self.serial_number)): return self.serial_number - return self.mac_addr + return self.mac_addr @property def model_number(self) -> str: diff --git a/custom_components/ge_home/devices/biac.py b/custom_components/ge_home/devices/biac.py new file mode 100644 index 0000000..916b6cb --- /dev/null +++ b/custom_components/ge_home/devices/biac.py @@ -0,0 +1,35 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk.erd import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import GeSacClimate, GeErdSensor, GeErdSwitch, ErdOnOffBoolConverter, GeErdBinarySensor + + +_LOGGER = logging.getLogger(__name__) + + +class BiacApi(ApplianceApi): + """API class for Built-In AC objects""" + APPLIANCE_TYPE = ErdApplianceType.BUILT_IN_AIR_CONDITIONER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + sac_entities = [ + GeSacClimate(self), + GeErdSensor(self, ErdCode.AC_TARGET_TEMPERATURE), + GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE), + GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), + GeErdSensor(self, ErdCode.AC_OPERATION_MODE), + GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + GeErdBinarySensor(self, ErdCode.AC_FILTER_STATUS, device_class_override="problem"), + GeErdSensor(self, ErdCode.WAC_DEMAND_RESPONSE_STATE), + GeErdSensor(self, ErdCode.WAC_DEMAND_RESPONSE_POWER, uom_override="kW"), + ] + + entities = base_entities + sac_entities + return entities + diff --git a/custom_components/ge_home/devices/coffee_maker.py b/custom_components/ge_home/devices/coffee_maker.py index d95af55..d3f39c9 100644 --- a/custom_components/ge_home/devices/coffee_maker.py +++ b/custom_components/ge_home/devices/coffee_maker.py @@ -60,7 +60,7 @@ def get_all_entities(self) -> List[Entity]: async def start_brewing(self) -> None: """Aggregate brew settings and start brewing.""" - new_mode = ErdCcmBrewSettings(self._brew_cups_entity.value, + new_mode = ErdCcmBrewSettings(self._brew_cups_entity.native_value, self._brew_strengh_entity.brew_strength, - self._brew_temperature_entity.brew_temperature) + 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_home/devices/cooktop.py b/custom_components/ge_home/devices/cooktop.py new file mode 100644 index 0000000..6fb3453 --- /dev/null +++ b/custom_components/ge_home/devices/cooktop.py @@ -0,0 +1,52 @@ +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_home/devices/dual_dishwasher.py b/custom_components/ge_home/devices/dual_dishwasher.py new file mode 100644 index 0000000..63c8b29 --- /dev/null +++ b/custom_components/ge_home/devices/dual_dishwasher.py @@ -0,0 +1,61 @@ +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_home/devices/espresso_maker.py b/custom_components/ge_home/devices/espresso_maker.py new file mode 100644 index 0000000..efb184e --- /dev/null +++ b/custom_components/ge_home/devices/espresso_maker.py @@ -0,0 +1,34 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType +) + +from .base import ApplianceApi +from ..entities import ( + GeErdBinarySensor, + GeErdButton +) + +_LOGGER = logging.getLogger(__name__) + + +class EspressoMakerApi(ApplianceApi): + """API class for Espresso Maker objects""" + APPLIANCE_TYPE = ErdApplianceType.ESPRESSO_MAKER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + em_entities = [ + GeErdBinarySensor(self, ErdCode.CCM_IS_DESCALING), + GeErdButton(self, ErdCode.CCM_CANCEL_DESCALING), + GeErdButton(self, ErdCode.CCM_START_DESCALING), + GeErdBinarySensor(self, ErdCode.CCM_OUT_OF_WATER, device_class_override="problem"), + ] + + entities = base_entities + em_entities + return entities diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index b2c42d2..24f0657 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -59,6 +59,8 @@ def get_all_entities(self) -> List[Entity]: 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 @@ -92,6 +94,10 @@ def get_all_entities(self) -> List[Entity]: 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: diff --git a/custom_components/ge_home/devices/oim.py b/custom_components/ge_home/devices/oim.py index ad1bd06..124ad3d 100644 --- a/custom_components/ge_home/devices/oim.py +++ b/custom_components/ge_home/devices/oim.py @@ -22,8 +22,8 @@ class OimApi(ApplianceApi): - """API class for Oven Hood objects""" - APPLIANCE_TYPE = ErdApplianceType.HOOD + """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() diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index ff2d077..ff5e418 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -93,16 +93,17 @@ def get_all_entities(self) -> List[Entity]: if cooktop_config == ErdCooktopConfig.PRESENT: - cooktop_status: CooktopStatus = self.appliance.get_erd_value(ErdCode.COOKTOP_STATUS) - cooktop_entities.append(GeErdBinarySensor(self, ErdCode.COOKTOP_STATUS)) + 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)) + 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 diff --git a/custom_components/ge_home/devices/water_heater.py b/custom_components/ge_home/devices/water_heater.py new file mode 100644 index 0000000..aad2aa9 --- /dev/null +++ b/custom_components/ge_home/devices/water_heater.py @@ -0,0 +1,43 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdOnOff +) + +from custom_components.ge_home.entities.water_heater.ge_water_heater import GeWaterHeater + +from .base import ApplianceApi +from ..entities import ( + GeErdSensor, + GeErdBinarySensor, + GeErdSelect, + GeErdSwitch, + ErdOnOffBoolConverter +) + +_LOGGER = logging.getLogger(__name__) + + +class WaterHeaterApi(ApplianceApi): + """API class for Water Heater objects""" + APPLIANCE_TYPE = ErdApplianceType.WATER_HEATER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + 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_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index eabcc59..2306cae 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -7,5 +7,6 @@ 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_home/entities/ac/__init__.py b/custom_components/ge_home/entities/ac/__init__.py index 0f2e6ad..aefb995 100644 --- a/custom_components/ge_home/entities/ac/__init__.py +++ b/custom_components/ge_home/entities/ac/__init__.py @@ -1,3 +1,4 @@ from .ge_wac_climate import GeWacClimate from .ge_sac_climate import GeSacClimate -from .ge_pac_climate import GePacClimate \ No newline at end of file +from .ge_pac_climate import GePacClimate +from .ge_biac_climate import GeBiacClimate diff --git a/custom_components/ge_home/entities/ac/ge_biac_climate.py b/custom_components/ge_home/entities/ac/ge_biac_climate.py new file mode 100644 index 0000000..f3b7453 --- /dev/null +++ b/custom_components/ge_home/entities/ac/ge_biac_climate.py @@ -0,0 +1,45 @@ +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_home/entities/advantium/ge_advantium.py b/custom_components/ge_home/entities/advantium/ge_advantium.py index 917a7d0..2d1372b 100644 --- a/custom_components/ge_home/entities/advantium/ge_advantium.py +++ b/custom_components/ge_home/entities/advantium/ge_advantium.py @@ -18,12 +18,12 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from ...const import DOMAIN from ...devices import ApplianceApi -from ..common import GeWaterHeater +from ..common import GeAbstractWaterHeater from .const import * _LOGGER = logging.getLogger(__name__) -class GeAdvantium(GeWaterHeater): +class GeAdvantium(GeAbstractWaterHeater): """GE Appliance Advantium""" icon = "mdi:microwave" diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py index b1ee283..5792f12 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py @@ -16,4 +16,4 @@ async def async_set_native_value(self, value): @property def native_value(self): - return self.get_value(device_value = super().value) + return self.get_value(device_value = super().native_value) diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py index 56bf968..86cac03 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py @@ -2,6 +2,7 @@ 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): @@ -15,12 +16,4 @@ async def async_set_native_value(self, value): @property def native_value(self): - return int(self.get_value(device_value = super().value)) - - @property - def native_unit_of_measurement(self): - return TEMP_FAHRENHEIT - - @property - def brew_temperature(self) -> int: - return self.value + return int(self.get_value(device_value = super().native_value)) diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index edd0b63..0b555ca 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -11,6 +11,6 @@ from .ge_erd_switch import GeErdSwitch from .ge_erd_button import GeErdButton from .ge_erd_number import GeErdNumber -from .ge_water_heater import GeWaterHeater +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_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index fc48f93..7f44edb 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -191,4 +191,4 @@ def _convert_temp(self, temperature_f: int): return (temperature_f - 32.0) * (5/9) def _get_icon(self) -> Optional[str]: - return "mdi:air-conditioner" \ No newline at end of file + return "mdi:air-conditioner" diff --git a/custom_components/ge_home/entities/common/ge_erd_number.py b/custom_components/ge_home/entities/common/ge_erd_number.py index c6de900..d773ad1 100644 --- a/custom_components/ge_home/entities/common/ge_erd_number.py +++ b/custom_components/ge_home/entities/common/ge_erd_number.py @@ -1,12 +1,11 @@ import logging from typing import Optional from gehomesdk.erd.erd_data_type import ErdDataType -from homeassistant.components.number import NumberEntity - -from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, - TEMP_FAHRENHEIT, +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 @@ -88,7 +87,7 @@ def _get_uom(self): if self._uom_override: return self._uom_override - if self.device_class == DEVICE_CLASS_TEMPERATURE: + 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 @@ -104,7 +103,7 @@ def _get_device_class(self) -> Optional[str]: ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE, ]: - return DEVICE_CLASS_TEMPERATURE + return NumberDeviceClass.TEMPERATURE return None diff --git a/custom_components/ge_home/entities/common/ge_water_heater.py b/custom_components/ge_home/entities/common/ge_water_heater.py index 87bd091..55ae4d9 100644 --- a/custom_components/ge_home/entities/common/ge_water_heater.py +++ b/custom_components/ge_home/entities/common/ge_water_heater.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) -class GeWaterHeater(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): +class GeAbstractWaterHeater(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): """Mock temperature/operation mode supporting device as a water heater""" @property diff --git a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py index 5266fc4..167692f 100644 --- a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.util.temperature import convert as convert_temperature from gehomesdk import ( ErdCode, @@ -18,14 +19,22 @@ IceMakerControlStatus ) from ...const import DOMAIN -from ..common import GeWaterHeater +from ..common import GeAbstractWaterHeater from .const import * _LOGGER = logging.getLogger(__name__) -class GeAbstractFridge(GeWaterHeater): +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 @@ -40,7 +49,11 @@ def turbo_mode(self) -> str: @property def operation_list(self) -> List[str]: - return [OP_MODE_NORMAL, OP_MODE_SABBATH, self.turbo_mode] + 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: @@ -48,7 +61,7 @@ def unique_id(self) -> str: @property def name(self) -> Optional[str]: - return f"{self.serial_number} {self.heater_type.title()}" + return f"{self.serial_or_mac} {self.heater_type.title()}" @property def target_temps(self) -> FridgeSetPoints: @@ -63,11 +76,15 @@ def target_temperature(self) -> int: @property def current_temperature(self) -> int: """Return the current temperature.""" - 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 + 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) @@ -95,21 +112,32 @@ def setpoint_limits(self) -> FridgeSetPointLimits: @property def min_temp(self): - """Return the minimum temperature.""" - return getattr(self.setpoint_limits, f"{self.heater_type}_min") + """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 convert_temperature(self.temp_limits[f"{self.heater_type}_min"], TEMP_FAHRENHEIT, self.temperature_unit) @property def max_temp(self): - """Return the maximum temperature.""" - return getattr(self.setpoint_limits, f"{self.heater_type}_max") + """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 convert_temperature(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 - if self.appliance.get_erd_value(self.turbo_erd_code): - return self.turbo_mode + 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): diff --git a/custom_components/ge_home/entities/fridge/ge_dispenser.py b/custom_components/ge_home/entities/fridge/ge_dispenser.py index 759d3f6..a961df3 100644 --- a/custom_components/ge_home/entities/fridge/ge_dispenser.py +++ b/custom_components/ge_home/entities/fridge/ge_dispenser.py @@ -15,7 +15,7 @@ HotWaterStatus ) -from ..common import GeWaterHeater +from ..common import GeAbstractWaterHeater from .const import ( HEATER_TYPE_DISPENSER, OP_MODE_NORMAL, @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) -class GeDispenser(GeWaterHeater): +class GeDispenser(GeAbstractWaterHeater): """Entity for in-fridge dispensers""" # These values are from FridgeHotWaterFragment.smali in the android app (in imperial units) diff --git a/custom_components/ge_home/entities/fridge/ge_freezer.py b/custom_components/ge_home/entities/fridge/ge_freezer.py index 4b178fc..005dba9 100644 --- a/custom_components/ge_home/entities/fridge/ge_freezer.py +++ b/custom_components/ge_home/entities/fridge/ge_freezer.py @@ -26,7 +26,10 @@ class GeFreezer(GeAbstractFridge): @property def door_state_attrs(self) -> Optional[Dict[str, Any]]: - door_status = self.door_status.freezer - if door_status and door_status != ErdDoorStatus.NA: - return {ATTR_DOOR_STATUS: self._stringify(door_status)} + 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_home/entities/fridge/ge_fridge.py b/custom_components/ge_home/entities/fridge/ge_fridge.py index ef9e708..e24c3e0 100644 --- a/custom_components/ge_home/entities/fridge/ge_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_fridge.py @@ -36,23 +36,27 @@ def other_state_attrs(self) -> Dict[str, Any]: @property def door_state_attrs(self) -> Dict[str, Any]: """Get state attributes for the doors.""" - data = {} - door_status = self.door_status - if not door_status: + 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 {} - 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 diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index 54c0643..297b2c1 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -13,12 +13,12 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from ...const import DOMAIN from ...devices import ApplianceApi -from ..common import GeWaterHeater +from ..common import GeAbstractWaterHeater from .const import * _LOGGER = logging.getLogger(__name__) -class GeOven(GeWaterHeater): +class GeOven(GeAbstractWaterHeater): """GE Appliance Oven""" icon = "mdi:stove" @@ -41,7 +41,7 @@ def supported_features(self): @property def unique_id(self) -> str: - return f"{DOMAIN}_{self.serial_number}_{self.oven_select.lower()}" + return f"{DOMAIN}_{self.serial_or_mac}_{self.oven_select.lower()}" @property def name(self) -> Optional[str]: @@ -50,7 +50,7 @@ def name(self) -> Optional[str]: else: oven_title = "Oven" - return f"{self.serial_number} {oven_title}" + return f"{self.serial_or_mac} {oven_title}" @property def temperature_unit(self): diff --git a/custom_components/ge_home/entities/water_heater/__init__.py b/custom_components/ge_home/entities/water_heater/__init__.py new file mode 100644 index 0000000..c0fa79f --- /dev/null +++ b/custom_components/ge_home/entities/water_heater/__init__.py @@ -0,0 +1,2 @@ +from .heater_modes import WhHeaterModeConverter +from .ge_water_heater import GeWaterHeater \ No newline at end of file diff --git a/custom_components/ge_home/entities/water_heater/ge_water_heater.py b/custom_components/ge_home/entities/water_heater/ge_water_heater.py new file mode 100644 index 0000000..7954055 --- /dev/null +++ b/custom_components/ge_home/entities/water_heater/ge_water_heater.py @@ -0,0 +1,89 @@ +"""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_home/entities/water_heater/heater_modes.py b/custom_components/ge_home/entities/water_heater/heater_modes.py new file mode 100644 index 0000000..cd2e39a --- /dev/null +++ b/custom_components/ge_home/entities/water_heater/heater_modes.py @@ -0,0 +1,26 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdWaterHeaterMode +from ..common import OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class WhHeaterModeConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdWaterHeaterMode] + def from_option_string(self, value: str) -> Any: + enum_val = value.upper().replace(" ","_") + try: + return ErdWaterHeaterMode[enum_val] + except: + _LOGGER.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_home/light.py b/custom_components/ge_home/light.py index 310cabf..b652d02 100644 --- a/custom_components/ge_home/light.py +++ b/custom_components/ge_home/light.py @@ -1,13 +1,15 @@ """GE Home Select Entities""" -import async_timeout import logging from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +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__) @@ -19,20 +21,20 @@ async def async_setup_entry( """GE Home lights.""" _LOGGER.debug("Adding GE Home lights") coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug("Coordinator init future finished") + @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) - apis = list(coordinator.appliance_apis.values()) - _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 - ] - _LOGGER.debug(f"Found {len(entities):d} lights") - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 048a3cc..20a0297 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.4.25","magicattr==0.1.5","slixmpp==1.7.1"], + "requirements": ["gehomesdk==0.5.7","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], - "version": "0.6.1" + "version": "0.6.5" } diff --git a/custom_components/ge_home/number.py b/custom_components/ge_home/number.py index eed2189..2b4213c 100644 --- a/custom_components/ge_home/number.py +++ b/custom_components/ge_home/number.py @@ -1,12 +1,14 @@ """GE Home Number Entities""" -import async_timeout import logging from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +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 @@ -15,19 +17,21 @@ 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) - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future + @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) - apis = coordinator.appliance_apis.values() - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdNumber) - ] - _LOGGER.debug(f'Found {len(entities):d} numbers ') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/select.py b/custom_components/ge_home/select.py index bfa6b0b..613b03b 100644 --- a/custom_components/ge_home/select.py +++ b/custom_components/ge_home/select.py @@ -1,12 +1,14 @@ """GE Home Select Entities""" -import async_timeout import logging from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +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 @@ -19,20 +21,20 @@ async def async_setup_entry( """GE Home selects.""" _LOGGER.debug("Adding GE Home selects") coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug("Coordinator init future finished") + @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) - apis = list(coordinator.appliance_apis.values()) - _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 - ] - _LOGGER.debug(f"Found {len(entities):d} selectors") - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/sensor.py b/custom_components/ge_home/sensor.py index 2dea9a1..9732dbf 100644 --- a/custom_components/ge_home/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -1,13 +1,14 @@ """GE Home Sensor Entities""" -import async_timeout 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 +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers import entity_platform +from homeassistant.helpers import entity_registry as er from .const import ( DOMAIN, @@ -16,6 +17,7 @@ SERVICE_SET_INT_VALUE ) from .entities import GeErdSensor +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator ATTR_DURATION = "duration" @@ -27,25 +29,25 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn """GE Home sensors.""" _LOGGER.debug('Adding GE Home sensors') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - + registry = er.async_get(hass) + # Get the platform platform = entity_platform.async_get_current_platform() - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdSensor) and entity.erd_code in api.appliance._property_cache + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered sensors') + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdSensor) and entity.erd_code in api.appliance._property_cache - ] - _LOGGER.debug(f'Found {len(entities):d} sensors') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) # register set_timer entity service platform.async_register_entity_service( diff --git a/custom_components/ge_home/switch.py b/custom_components/ge_home/switch.py index 78cf896..3aa6f11 100644 --- a/custom_components/ge_home/switch.py +++ b/custom_components/ge_home/switch.py @@ -1,13 +1,15 @@ """GE Home Switch Entities""" -import async_timeout import logging from typing import Callable from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er from .entities import GeErdSwitch from .const import DOMAIN +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -16,19 +18,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn """GE Home sensors.""" _LOGGER.debug('Adding GE Home switches') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeErdSwitch) and entity.erd_code in api.appliance._property_cache + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered switches') + async_add_entities(entities) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdSwitch) and entity.erd_code in api.appliance._property_cache - ] - _LOGGER.debug(f'Found {len(entities):d} switches') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 8711d98..545a82c 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -21,6 +21,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -64,7 +65,6 @@ def _reset_initialization(self): self._got_roster = False self._init_done = False self._retry_count = 0 - self.initialization_future = asyncio.Future() def create_ge_client( self, event_loop: Optional[asyncio.AbstractEventLoop] @@ -95,6 +95,11 @@ def appliances(self) -> Iterable[GeAppliance]: def appliance_apis(self) -> Dict[str, ApplianceApi]: return self._appliance_apis + @property + def signal_ready(self) -> str: + """Event specific per entry to signal readiness""" + return f"{DOMAIN}-ready-{self._config_entry.entry_id}" + @property def online(self) -> bool: """ @@ -167,12 +172,6 @@ async def async_setup(self): except Exception: raise HaCannotConnect("Unknown connection failure") - try: - with async_timeout.timeout(ASYNC_TIMEOUT): - await self.initialization_future - except (asyncio.CancelledError, asyncio.TimeoutError): - raise HaCannotConnect("Initialization timed out") - return True async def async_start_client(self): @@ -322,16 +321,17 @@ async def on_connect(self, _): async def async_maybe_trigger_all_ready(self): """See if we're all ready to go, and if so, let the games begin.""" - if self._init_done or self.initialization_future.done(): + if self._init_done: # Been here, done this return if self._got_roster and self.all_appliances_updated: - _LOGGER.debug("Ready to go. Waiting 2 seconds and setting init future result.") - # The the flag and wait to prevent two different fun race conditions + _LOGGER.debug("Ready to go, sending ready signal") self._init_done = True - await asyncio.sleep(2) - self.initialization_future.set_result(True) await self.client.async_event(EVENT_ALL_APPLIANCES_READY, None) + async_dispatcher_send( + self.hass, + self.signal_ready, + list(self.appliance_apis.values())) def _get_retry_delay(self) -> int: delay = MIN_RETRY_DELAY * 2 ** (self._retry_count - 1) diff --git a/custom_components/ge_home/water_heater.py b/custom_components/ge_home/water_heater.py index ac0aa85..19e8b4d 100644 --- a/custom_components/ge_home/water_heater.py +++ b/custom_components/ge_home/water_heater.py @@ -5,10 +5,13 @@ from homeassistant.components.water_heater import WaterHeaterEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +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 GeWaterHeater +from .entities import GeAbstractWaterHeater from .const import DOMAIN +from .devices import ApplianceApi from .update_coordinator import GeHomeUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -17,19 +20,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn """GE Home Water Heaters.""" _LOGGER.debug('Adding GE "Water Heaters"') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) - # This should be a NOP, but let's be safe - with async_timeout.timeout(20): - await coordinator.initialization_future - _LOGGER.debug('Coordinator init future finished') + @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) - apis = list(coordinator.appliance_apis.values()) - _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeWaterHeater) - ] - _LOGGER.debug(f'Found {len(entities):d} "water heaters"') - async_add_entities(entities) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/info.md b/info.md index b07663e..33beba7 100644 --- a/info.md +++ b/info.md @@ -4,16 +4,18 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio - Fridge - Oven -- Dishwasher +- 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) - Range Hood - Advantium - Microwave - Opal Ice Maker -- Coffee Maker +- Coffee Maker / Espresso Maker +- Beverage Center **Forked from Andrew Mark's [repository](https://github.com/ajmarks/ha_components).** @@ -63,6 +65,17 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '0.6.5'.split('.') | map('int') %} +- Added beverage cooler support (@kksligh) +- Added dual dishwasher support (@jkili) +- Added initial espresso maker support (@datagen24) +- Added whole home water heater support (@seantibor) +{% endif %} + +{% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} +- Initial support for built-in air conditioners (@DaveZheng) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} - Initial support for Water Softeners (@npentell, @drjeff) - Initial support for Opal Ice Makers (@mbcomer, @knobunc) @@ -91,6 +104,15 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.3'.split('.') | map('int') %} +- Updated detection of invalid serial numbers (#89) +- Updated implementation of number entities to fix deprecation warnings (#85) +{% endif %} + +{% if version_installed.split('.') | map('int') < '0.6.2'.split('.') | map('int') %} +- Fixed issue with water heater naming when no serial is present +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.1'.split('.') | map('int') %} - Fixed issue with water filter life sensor (@rgabrielson11) {% endif %} From 3ccf9946f9ff8e60f35249448511888eb4781575 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 5 Dec 2022 21:36:49 -0500 Subject: [PATCH 226/338] - fixed region setting in update coordinator --- custom_components/ge_home/update_coordinator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 545a82c..041c1c9 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -19,7 +19,7 @@ from .exceptions import HaAuthError, HaCannotConnect from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -48,6 +48,7 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self._config_entry = config_entry self._username = config_entry.data[CONF_USERNAME] self._password = config_entry.data[CONF_PASSWORD] + self._region = config_entry.data[CONF_REGION] self._appliance_apis = {} # type: Dict[str, ApplianceApi] self._reset_initialization() @@ -78,6 +79,7 @@ def create_ge_client( client = GeWebsocketClient( self._username, self._password, + self._region, event_loop=event_loop, ) client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update) From 8a2ac1aa9af32a07cd33e6a02eb95cc78b6a4794 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 5 Dec 2022 21:41:24 -0500 Subject: [PATCH 227/338] - version bump - doc update - string fixes --- CHANGELOG.md | 4 ++++ custom_components/ge_home/manifest.json | 2 +- custom_components/ge_home/strings.json | 3 ++- custom_components/ge_home/translations/en.json | 6 ++++-- info.md | 4 ++++ 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d32298c..ffd97eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.6 + +- Fixed issue with region setting (EU accounts) [#130] + ## 0.6.5 - Added beverage cooler support (@kksligh) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 20a0297..12333e7 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==0.5.7","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], - "version": "0.6.5" + "version": "0.6.6" } diff --git a/custom_components/ge_home/strings.json b/custom_components/ge_home/strings.json index fe1a212..7cfb731 100644 --- a/custom_components/ge_home/strings.json +++ b/custom_components/ge_home/strings.json @@ -4,7 +4,8 @@ "user": { "data": { "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "region": "[%key:common::config_flow::data::region%]" } } }, diff --git a/custom_components/ge_home/translations/en.json b/custom_components/ge_home/translations/en.json index 1184d95..50680ea 100644 --- a/custom_components/ge_home/translations/en.json +++ b/custom_components/ge_home/translations/en.json @@ -5,13 +5,15 @@ "init": { "data": { "username": "Username", - "password": "Password" + "password": "Password", + "region": "Region" } }, "user": { "data": { "username": "Username", - "password": "Password" + "password": "Password", + "region": "Region" } } }, diff --git a/info.md b/info.md index 33beba7..7ff80d2 100644 --- a/info.md +++ b/info.md @@ -104,6 +104,10 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} +- Fixed region issues after setup +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.3'.split('.') | map('int') %} - Updated detection of invalid serial numbers (#89) - Updated implementation of number entities to fix deprecation warnings (#85) From 52a69a1ac6239f36be5ea1843b7be50106562e9b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 22 Jan 2023 20:05:56 -0500 Subject: [PATCH 228/338] - updated the temperature conversion to use non-deprecated HASS methods --- .../ge_home/entities/fridge/ge_abstract_fridge.py | 10 +++++----- .../ge_home/entities/fridge/ge_dispenser.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py index 167692f..312a3dd 100644 --- a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py @@ -1,13 +1,13 @@ """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_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.util.temperature import convert as convert_temperature - +from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.util.unit_conversion import TemperatureConverter from gehomesdk import ( ErdCode, ErdOnOff, @@ -117,7 +117,7 @@ def min_temp(self): return getattr(self.setpoint_limits, f"{self.heater_type}_min") except: _LOGGER.debug("No temperature setpoint limits available. Using hardcoded limits.") - return convert_temperature(self.temp_limits[f"{self.heater_type}_min"], TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self.temp_limits[f"{self.heater_type}_min"], TEMP_FAHRENHEIT, self.temperature_unit) @property def max_temp(self): @@ -126,7 +126,7 @@ def max_temp(self): return getattr(self.setpoint_limits, f"{self.heater_type}_max") except: _LOGGER.debug("No temperature setpoint limits available. Using hardcoded limits.") - return convert_temperature(self.temp_limits[f"{self.heater_type}_max"], TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self.temp_limits[f"{self.heater_type}_max"], TEMP_FAHRENHEIT, self.temperature_unit) @property def current_operation(self) -> str: diff --git a/custom_components/ge_home/entities/fridge/ge_dispenser.py b/custom_components/ge_home/entities/fridge/ge_dispenser.py index a961df3..04bc543 100644 --- a/custom_components/ge_home/entities/fridge/ge_dispenser.py +++ b/custom_components/ge_home/entities/fridge/ge_dispenser.py @@ -4,7 +4,7 @@ from typing import List, Optional, Dict, Any from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_conversion import TemperatureConverter from gehomesdk import ( ErdCode, @@ -102,12 +102,12 @@ def target_temperature(self) -> Optional[int]: @property def min_temp(self): """Return the minimum temperature.""" - return convert_temperature(self._min_temp, TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self._min_temp, TEMP_FAHRENHEIT, self.temperature_unit) @property def max_temp(self): """Return the maximum temperature.""" - return convert_temperature(self._max_temp, TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self._max_temp, TEMP_FAHRENHEIT, self.temperature_unit) @property def extra_state_attributes(self) -> Dict[str, Any]: From faeaa90f5dc16aeac1f281603ca1230319ea821a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 22 Jan 2023 20:28:39 -0500 Subject: [PATCH 229/338] - updated documentation (@gleepwurp) --- CHANGELOG.md | 1 + README.md | 18 +++++++++++++++++- custom_components/ge_home/manifest.json | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffd97eb..5226a6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## 0.6.6 - Fixed issue with region setting (EU accounts) [#130] +- Updated configuration documentation ## 0.6.5 diff --git a/README.md b/README.md index e21f84b..748e67c 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio ## Updates Unfortunately, I'm pretty much at the end of what I can do without assistance from others with these devices that can help provide logs. I'll do what I can to make updates if there's something broken, but I am not really able to add new functionality if I can't get a little help to do so. + ## Home Assistant UI Examples Entities card: @@ -56,9 +57,24 @@ A/C Controls: ## Installation (HACS) Please follow directions [here](https://hacs.xyz/docs/faq/custom_repositories/), and use https://github.com/simbaja/ha_gehome as the repository URL. + ## Configuration -Configuration is done via the HA user interface. +Configuration is done via the HA user interface. You need to have your device registered with the [SmartHQ](https://www.geappliances.com/connect) website. + +Once the HACS Integration of GE Home is completed: + +1. Navigate to Settings --> Devices & Services +2. Click **Add Integration** blue button on the bottom-right of the page +3. Locate the **GE Home (SmartHQ)** "Brand" (Integration) +4. Click on the integration, and you will be prompted to enter a Username, Password and Location (US or EU) +5. Enter the email address you used to register/connect your device as the Username +6. Same with the password +7. Select the region you registered your device in (US or EU). +8. Once you submit, the integration will log in and get all your connected devices. +9. You can define in which area you device is, then click **Finish** +10. Your sensors should appear as **sensor._** + ie: sensor.fs12345678_dishwasher_cycle_name ## Change Log diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 12333e7..8d2f7fd 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.7","magicattr==0.1.5","slixmpp==1.7.1"], + "requirements": ["gehomesdk==0.5.8","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], "version": "0.6.6" } From d65a41f62992a6333b22986f286a9331a83ac347 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 22 Jan 2023 20:31:59 -0500 Subject: [PATCH 230/338] - more documentation updates --- CHANGELOG.md | 1 + info.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5226a6a..4c7575d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## 0.6.6 - Fixed issue with region setting (EU accounts) [#130] +- Updated the temperature conversion (@partsdotpdf) - Updated configuration documentation ## 0.6.5 diff --git a/info.md b/info.md index 7ff80d2..39c53eb 100644 --- a/info.md +++ b/info.md @@ -105,7 +105,8 @@ A/C Controls: #### Bugfixes {% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} -- Fixed region issues after setup +- Fixed region issues after setup (#130) +- Updated the temperature conversion (#137) {% endif %} {% if version_installed.split('.') | map('int') < '0.6.3'.split('.') | map('int') %} From c5e385e067f30aaf57141b466340067b5b5ea231 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 22 Jan 2023 21:12:59 -0500 Subject: [PATCH 231/338] - updated dishwasher for new functionality - updated documentation --- CHANGELOG.md | 1 + custom_components/ge_home/devices/dishwasher.py | 13 +++++++++++-- info.md | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7575d..7cc3b0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Fixed issue with region setting (EU accounts) [#130] - Updated the temperature conversion (@partsdotpdf) - Updated configuration documentation +- Modified dishwasher to include new functionality (@NickWaterton) ## 0.6.5 diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index d51913e..2cd1da3 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -23,9 +23,13 @@ def get_all_entities(self) -> List[Entity]: 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"), - GeErdSensor(self, ErdCode.DISHWASHER_RINSE_AGENT, icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", icon_override="mdi:dishwasher-alert"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", icon_override="mdi:silverware-clean"), GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING), GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS), + GeErdBinarySensor(self, ErdCode.DISHWASHER_IS_CLEAN), + GeErdBinarySensor(self, ErdCode.DISHWASHER_REMOTE_START_ENABLE), #User Setttings GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound", icon_override="mdi:volume-high"), @@ -37,7 +41,12 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", icon_override="mdi:coolant-temperature"), 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") + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours", icon_override="mdi:clock-fast"), + + #Cycle Counts + GeErdPropertySensor(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "started", icon_override="mdi:counter"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "completed", icon_override="mdi:counter"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "reset", icon_override="mdi:counter") ] entities = base_entities + dishwasher_entities return entities diff --git a/info.md b/info.md index 39c53eb..86f0847 100644 --- a/info.md +++ b/info.md @@ -64,6 +64,9 @@ A/C Controls: {% endif %} #### Features +{% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} +- Modified dishwasher to include new functionality (@NickWaterton) +{% endif %} {% if version_installed.split('.') | map('int') < '0.6.5'.split('.') | map('int') %} - Added beverage cooler support (@kksligh) From a09d814dd2e625d397a153688d52b1091a0bcd11 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 29 Jan 2023 09:32:01 -0500 Subject: [PATCH 232/338] updated uom for liquid volume per HA specifications --- custom_components/ge_home/entities/common/ge_erd_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index ffb16f0..d161fee 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -118,7 +118,7 @@ def _get_uom(self): if self.erd_code_class == ErdCodeClass.LIQUID_VOLUME: #if self._measurement_system == ErdMeasurementUnits.METRIC: # return "l" - return "g" + return "gal" return None def _get_device_class(self) -> Optional[str]: From 56106b7b71a263cc4c3c38682fad3d62b224bcae Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 22 Apr 2023 22:01:51 -0400 Subject: [PATCH 233/338] - fixed typo in oven (#149) --- custom_components/ge_home/devices/oven.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index ff5e418..47792ff 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -64,7 +64,7 @@ def get_all_entities(self) -> List[Entity]: 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.UPPER_OVEN_DISPLAY_TEMPERATURE), + 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)), From 111951f136251d67ae67df797c5a2e88273c6584 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 23 Apr 2023 00:12:39 -0400 Subject: [PATCH 234/338] - updated change log - fixed oven light control (#144) --- CHANGELOG.md | 6 +++++- custom_components/ge_home/devices/oven.py | 7 +++++-- custom_components/ge_home/manifest.json | 2 +- hacs.json | 2 +- info.md | 7 +++++++ 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cc3b0e..ad2c6ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,14 @@ ## 0.6.6 -- Fixed issue with region setting (EU accounts) [#130] +- Bugfix: Fixed issue with region setting (EU accounts) [#130] - Updated the temperature conversion (@partsdotpdf) - Updated configuration documentation - Modified dishwasher to include new functionality (@NickWaterton) +- Bugfix: Fixed oven typo (@jdc0730) [#149] +- Bugfix: UoM updates (@morlince) [#138] +- Updated light control (@tcgoetz) [#144] +- Dependency version bumps ## 0.6.5 diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index 47792ff..f335f8d 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -10,6 +10,7 @@ OvenConfiguration, ErdCooktopConfig, CooktopStatus, + ErdOvenLightLevel, ErdOvenLightLevelAvailability ) @@ -43,7 +44,9 @@ def get_all_entities(self) -> List[Entity]: 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}") @@ -74,7 +77,7 @@ def get_all_entities(self) -> List[Entity]: 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: + 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([ @@ -88,7 +91,7 @@ def get_all_entities(self) -> List[Entity]: ]) 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: + 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))) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 8d2f7fd..934e468 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.8","magicattr==0.1.5","slixmpp==1.7.1"], + "requirements": ["gehomesdk==0.5.9","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.6" } diff --git a/hacs.json b/hacs.json index 7c919dc..fa75f45 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Home (SmartHQ)", - "homeassistant": "2021.12.0", + "homeassistant": "2022.12.0", "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select", "button", "climate", "light", "number"], "iot_class": "Cloud Polling" } diff --git a/info.md b/info.md index 86f0847..fb2e1d4 100644 --- a/info.md +++ b/info.md @@ -46,6 +46,10 @@ A/C Controls: #### Breaking Changes +{% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} +- Requires HA version 2022.12.0 or later +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} - Requires HA version 2021.12.0 or later - Enabled authentication to both US and EU regions (may require re-auth) @@ -110,6 +114,9 @@ A/C Controls: {% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} - Fixed region issues after setup (#130) - Updated the temperature conversion (#137) +- UoM updates (#138) +- Fixed oven typo (#149) +- Updated light control (#144) {% endif %} {% if version_installed.split('.') | map('int') < '0.6.3'.split('.') | map('int') %} From 1b7e8179b49d5fc1a83da4a465a746a6c83f8547 Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sun, 23 Apr 2023 01:05:15 -0400 Subject: [PATCH 235/338] v0.6.6 (#153) * - updated water heater naming * - initial support for built-in AC units * Add support for Built-In AC Unit Built-In AC unit operation and function is similar to a WAC. `gehomesdk` version bumped from 0.4.25 to 0.4.27 to include latest ErdApplianceType enum. * Update README.md Update README.md to include Built-In AC as supported device * - updated zero serial number detection (resolves #89) * - updated version - updated changelog * - hopefully fixed recursion bug with numbers * - added cooktop support * - fixed circular reference * Add support for fridges with reduced support for ERD codes (no turbo mode, no current temperature reporting, no temperature setpoint limit reporting, no door status reporting). This change has been tested on a Fisher & Paykel RF610AA. * - added dual dishwasher support - updated documentation - version bumps * - added water heater support * - added basic espresso maker device * - bugfixes * - rewrote initialization (resolves #99) * - added logic to prevent double registration of entities * - added correct min/max temps for water heaters * Fix CoffeeMaker after the NumberEntity refactoring * - added fix for CGY366P2TS1 (#116) to try to get the cooktop status, but fail more gracefully if it isn't supported * - fixed region setting in update coordinator * - version bump - doc update - string fixes * - updated the temperature conversion to use non-deprecated HASS methods * - updated documentation (@gleepwurp) * - more documentation updates * - updated dishwasher for new functionality - updated documentation * updated uom for liquid volume per HA specifications * - fixed typo in oven (#149) * - updated change log - fixed oven light control (#144) --------- Co-authored-by: Rob Schmidt Co-authored-by: Federico Sevilla Co-authored-by: alexanv1 <44785744+alexanv1@users.noreply.github.com> --- CHANGELOG.md | 11 +++++++++++ README.md | 18 +++++++++++++++++- .../ge_home/devices/dishwasher.py | 13 +++++++++++-- custom_components/ge_home/devices/oven.py | 9 ++++++--- .../ge_home/entities/common/ge_erd_sensor.py | 2 +- .../entities/fridge/ge_abstract_fridge.py | 10 +++++----- .../ge_home/entities/fridge/ge_dispenser.py | 6 +++--- custom_components/ge_home/manifest.json | 4 ++-- custom_components/ge_home/strings.json | 3 ++- custom_components/ge_home/translations/en.json | 6 ++++-- .../ge_home/update_coordinator.py | 4 +++- hacs.json | 2 +- info.md | 15 +++++++++++++++ 13 files changed, 81 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d32298c..ad2c6ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.6 + +- Bugfix: Fixed issue with region setting (EU accounts) [#130] +- Updated the temperature conversion (@partsdotpdf) +- Updated configuration documentation +- Modified dishwasher to include new functionality (@NickWaterton) +- Bugfix: Fixed oven typo (@jdc0730) [#149] +- Bugfix: UoM updates (@morlince) [#138] +- Updated light control (@tcgoetz) [#144] +- Dependency version bumps + ## 0.6.5 - Added beverage cooler support (@kksligh) diff --git a/README.md b/README.md index e21f84b..748e67c 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Integration for GE WiFi-enabled appliances into Home Assistant. This integratio ## Updates Unfortunately, I'm pretty much at the end of what I can do without assistance from others with these devices that can help provide logs. I'll do what I can to make updates if there's something broken, but I am not really able to add new functionality if I can't get a little help to do so. + ## Home Assistant UI Examples Entities card: @@ -56,9 +57,24 @@ A/C Controls: ## Installation (HACS) Please follow directions [here](https://hacs.xyz/docs/faq/custom_repositories/), and use https://github.com/simbaja/ha_gehome as the repository URL. + ## Configuration -Configuration is done via the HA user interface. +Configuration is done via the HA user interface. You need to have your device registered with the [SmartHQ](https://www.geappliances.com/connect) website. + +Once the HACS Integration of GE Home is completed: + +1. Navigate to Settings --> Devices & Services +2. Click **Add Integration** blue button on the bottom-right of the page +3. Locate the **GE Home (SmartHQ)** "Brand" (Integration) +4. Click on the integration, and you will be prompted to enter a Username, Password and Location (US or EU) +5. Enter the email address you used to register/connect your device as the Username +6. Same with the password +7. Select the region you registered your device in (US or EU). +8. Once you submit, the integration will log in and get all your connected devices. +9. You can define in which area you device is, then click **Finish** +10. Your sensors should appear as **sensor._** + ie: sensor.fs12345678_dishwasher_cycle_name ## Change Log diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index d51913e..2cd1da3 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -23,9 +23,13 @@ def get_all_entities(self) -> List[Entity]: 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"), - GeErdSensor(self, ErdCode.DISHWASHER_RINSE_AGENT, icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", icon_override="mdi:dishwasher-alert"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", icon_override="mdi:silverware-clean"), GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING), GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS), + GeErdBinarySensor(self, ErdCode.DISHWASHER_IS_CLEAN), + GeErdBinarySensor(self, ErdCode.DISHWASHER_REMOTE_START_ENABLE), #User Setttings GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound", icon_override="mdi:volume-high"), @@ -37,7 +41,12 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", icon_override="mdi:coolant-temperature"), 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") + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours", icon_override="mdi:clock-fast"), + + #Cycle Counts + GeErdPropertySensor(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "started", icon_override="mdi:counter"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "completed", icon_override="mdi:counter"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "reset", icon_override="mdi:counter") ] entities = base_entities + dishwasher_entities return entities diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index ff5e418..f335f8d 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -10,6 +10,7 @@ OvenConfiguration, ErdCooktopConfig, CooktopStatus, + ErdOvenLightLevel, ErdOvenLightLevelAvailability ) @@ -43,7 +44,9 @@ def get_all_entities(self) -> List[Entity]: 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}") @@ -64,7 +67,7 @@ def get_all_entities(self) -> List[Entity]: 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.UPPER_OVEN_DISPLAY_TEMPERATURE), + 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)), @@ -74,7 +77,7 @@ def get_all_entities(self) -> List[Entity]: 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: + 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([ @@ -88,7 +91,7 @@ def get_all_entities(self) -> List[Entity]: ]) 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: + 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))) diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index ffb16f0..d161fee 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -118,7 +118,7 @@ def _get_uom(self): if self.erd_code_class == ErdCodeClass.LIQUID_VOLUME: #if self._measurement_system == ErdMeasurementUnits.METRIC: # return "l" - return "g" + return "gal" return None def _get_device_class(self) -> Optional[str]: diff --git a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py index 167692f..312a3dd 100644 --- a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py @@ -1,13 +1,13 @@ """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_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.util.temperature import convert as convert_temperature - +from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.util.unit_conversion import TemperatureConverter from gehomesdk import ( ErdCode, ErdOnOff, @@ -117,7 +117,7 @@ def min_temp(self): return getattr(self.setpoint_limits, f"{self.heater_type}_min") except: _LOGGER.debug("No temperature setpoint limits available. Using hardcoded limits.") - return convert_temperature(self.temp_limits[f"{self.heater_type}_min"], TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self.temp_limits[f"{self.heater_type}_min"], TEMP_FAHRENHEIT, self.temperature_unit) @property def max_temp(self): @@ -126,7 +126,7 @@ def max_temp(self): return getattr(self.setpoint_limits, f"{self.heater_type}_max") except: _LOGGER.debug("No temperature setpoint limits available. Using hardcoded limits.") - return convert_temperature(self.temp_limits[f"{self.heater_type}_max"], TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self.temp_limits[f"{self.heater_type}_max"], TEMP_FAHRENHEIT, self.temperature_unit) @property def current_operation(self) -> str: diff --git a/custom_components/ge_home/entities/fridge/ge_dispenser.py b/custom_components/ge_home/entities/fridge/ge_dispenser.py index a961df3..04bc543 100644 --- a/custom_components/ge_home/entities/fridge/ge_dispenser.py +++ b/custom_components/ge_home/entities/fridge/ge_dispenser.py @@ -4,7 +4,7 @@ from typing import List, Optional, Dict, Any from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_conversion import TemperatureConverter from gehomesdk import ( ErdCode, @@ -102,12 +102,12 @@ def target_temperature(self) -> Optional[int]: @property def min_temp(self): """Return the minimum temperature.""" - return convert_temperature(self._min_temp, TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self._min_temp, TEMP_FAHRENHEIT, self.temperature_unit) @property def max_temp(self): """Return the maximum temperature.""" - return convert_temperature(self._max_temp, TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self._max_temp, TEMP_FAHRENHEIT, self.temperature_unit) @property def extra_state_attributes(self) -> Dict[str, Any]: diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 20a0297..934e468 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.7","magicattr==0.1.5","slixmpp==1.7.1"], + "requirements": ["gehomesdk==0.5.9","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.5" + "version": "0.6.6" } diff --git a/custom_components/ge_home/strings.json b/custom_components/ge_home/strings.json index fe1a212..7cfb731 100644 --- a/custom_components/ge_home/strings.json +++ b/custom_components/ge_home/strings.json @@ -4,7 +4,8 @@ "user": { "data": { "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "region": "[%key:common::config_flow::data::region%]" } } }, diff --git a/custom_components/ge_home/translations/en.json b/custom_components/ge_home/translations/en.json index 1184d95..50680ea 100644 --- a/custom_components/ge_home/translations/en.json +++ b/custom_components/ge_home/translations/en.json @@ -5,13 +5,15 @@ "init": { "data": { "username": "Username", - "password": "Password" + "password": "Password", + "region": "Region" } }, "user": { "data": { "username": "Username", - "password": "Password" + "password": "Password", + "region": "Region" } } }, diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 545a82c..041c1c9 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -19,7 +19,7 @@ from .exceptions import HaAuthError, HaCannotConnect from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -48,6 +48,7 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self._config_entry = config_entry self._username = config_entry.data[CONF_USERNAME] self._password = config_entry.data[CONF_PASSWORD] + self._region = config_entry.data[CONF_REGION] self._appliance_apis = {} # type: Dict[str, ApplianceApi] self._reset_initialization() @@ -78,6 +79,7 @@ def create_ge_client( client = GeWebsocketClient( self._username, self._password, + self._region, event_loop=event_loop, ) client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update) diff --git a/hacs.json b/hacs.json index 7c919dc..fa75f45 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Home (SmartHQ)", - "homeassistant": "2021.12.0", + "homeassistant": "2022.12.0", "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select", "button", "climate", "light", "number"], "iot_class": "Cloud Polling" } diff --git a/info.md b/info.md index 33beba7..fb2e1d4 100644 --- a/info.md +++ b/info.md @@ -46,6 +46,10 @@ A/C Controls: #### Breaking Changes +{% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} +- Requires HA version 2022.12.0 or later +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.0'.split('.') | map('int') %} - Requires HA version 2021.12.0 or later - Enabled authentication to both US and EU regions (may require re-auth) @@ -64,6 +68,9 @@ A/C Controls: {% endif %} #### Features +{% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} +- Modified dishwasher to include new functionality (@NickWaterton) +{% endif %} {% if version_installed.split('.') | map('int') < '0.6.5'.split('.') | map('int') %} - Added beverage cooler support (@kksligh) @@ -104,6 +111,14 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} +- Fixed region issues after setup (#130) +- Updated the temperature conversion (#137) +- UoM updates (#138) +- Fixed oven typo (#149) +- Updated light control (#144) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.3'.split('.') | map('int') %} - Updated detection of invalid serial numbers (#89) - Updated implementation of number entities to fix deprecation warnings (#85) From a6f0527b9d7fb5b51fe11ca9af40e321034e8e8b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 30 Apr 2023 10:24:06 -0400 Subject: [PATCH 236/338] - fixed issues with dishwasher (#155) - added oim descaling sensor (#154) - version bump --- custom_components/ge_home/devices/dishwasher.py | 3 ++- custom_components/ge_home/devices/oim.py | 1 + custom_components/ge_home/manifest.json | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index 2cd1da3..5943513 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -32,13 +32,14 @@ def get_all_entities(self) -> List[Entity]: GeErdBinarySensor(self, ErdCode.DISHWASHER_REMOTE_START_ENABLE), #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound", icon_override="mdi:volume-high"), + 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"), diff --git a/custom_components/ge_home/devices/oim.py b/custom_components/ge_home/devices/oim.py index 124ad3d..2eebd39 100644 --- a/custom_components/ge_home/devices/oim.py +++ b/custom_components/ge_home/devices/oim.py @@ -31,6 +31,7 @@ def get_all_entities(self) -> List[Entity]: 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"), ] diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 934e468..e437f04 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.9","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.10","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.6" + "version": "0.6.7" } From f260d26877b2bb8d3873e1dad6282ae14a4414aa Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 30 Apr 2023 10:28:40 -0400 Subject: [PATCH 237/338] - updated change log --- CHANGELOG.md | 5 +++++ info.md | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad2c6ad..03fc44a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.7 + +- Bugfix: fixed issues with dishwasher [#155] +- Added OIM descaling sensor [#154] + ## 0.6.6 - Bugfix: Fixed issue with region setting (EU accounts) [#130] diff --git a/info.md b/info.md index fb2e1d4..11a4b3b 100644 --- a/info.md +++ b/info.md @@ -68,6 +68,11 @@ A/C Controls: {% endif %} #### Features + +{% if version_installed.split('.') | map('int') < '0.6.7'.split('.') | map('int') %} +- Added OIM descaling sensor (#154) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} - Modified dishwasher to include new functionality (@NickWaterton) {% endif %} @@ -111,6 +116,10 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.7'.split('.') | map('int') %} +- Bugfix: fixed issues with dishwasher (#155) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} - Fixed region issues after setup (#130) - Updated the temperature conversion (#137) From 3ad1350ee0da62105280df680083fe339cce8fd7 Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sun, 30 Apr 2023 10:31:38 -0400 Subject: [PATCH 238/338] v.0.6.7 Develop -> Main (#156) * - updated water heater naming * - initial support for built-in AC units * Add support for Built-In AC Unit Built-In AC unit operation and function is similar to a WAC. `gehomesdk` version bumped from 0.4.25 to 0.4.27 to include latest ErdApplianceType enum. * Update README.md Update README.md to include Built-In AC as supported device * - updated zero serial number detection (resolves #89) * - updated version - updated changelog * - hopefully fixed recursion bug with numbers * - added cooktop support * - fixed circular reference * Add support for fridges with reduced support for ERD codes (no turbo mode, no current temperature reporting, no temperature setpoint limit reporting, no door status reporting). This change has been tested on a Fisher & Paykel RF610AA. * - added dual dishwasher support - updated documentation - version bumps * - added water heater support * - added basic espresso maker device * - bugfixes * - rewrote initialization (resolves #99) * - added logic to prevent double registration of entities * - added correct min/max temps for water heaters * Fix CoffeeMaker after the NumberEntity refactoring * - added fix for CGY366P2TS1 (#116) to try to get the cooktop status, but fail more gracefully if it isn't supported * - fixed region setting in update coordinator * - version bump - doc update - string fixes * - updated the temperature conversion to use non-deprecated HASS methods * - updated documentation (@gleepwurp) * - more documentation updates * - updated dishwasher for new functionality - updated documentation * updated uom for liquid volume per HA specifications * - fixed typo in oven (#149) * - updated change log - fixed oven light control (#144) * - fixed issues with dishwasher (#155) - added oim descaling sensor (#154) - version bump * - updated change log --------- Co-authored-by: Rob Schmidt Co-authored-by: Federico Sevilla Co-authored-by: alexanv1 <44785744+alexanv1@users.noreply.github.com> --- CHANGELOG.md | 5 +++++ custom_components/ge_home/devices/dishwasher.py | 3 ++- custom_components/ge_home/devices/oim.py | 1 + custom_components/ge_home/manifest.json | 4 ++-- info.md | 9 +++++++++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad2c6ad..03fc44a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.7 + +- Bugfix: fixed issues with dishwasher [#155] +- Added OIM descaling sensor [#154] + ## 0.6.6 - Bugfix: Fixed issue with region setting (EU accounts) [#130] diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index 2cd1da3..5943513 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -32,13 +32,14 @@ def get_all_entities(self) -> List[Entity]: GeErdBinarySensor(self, ErdCode.DISHWASHER_REMOTE_START_ENABLE), #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound", icon_override="mdi:volume-high"), + 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"), diff --git a/custom_components/ge_home/devices/oim.py b/custom_components/ge_home/devices/oim.py index 124ad3d..2eebd39 100644 --- a/custom_components/ge_home/devices/oim.py +++ b/custom_components/ge_home/devices/oim.py @@ -31,6 +31,7 @@ def get_all_entities(self) -> List[Entity]: 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"), ] diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 934e468..e437f04 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.9","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.10","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.6" + "version": "0.6.7" } diff --git a/info.md b/info.md index fb2e1d4..11a4b3b 100644 --- a/info.md +++ b/info.md @@ -68,6 +68,11 @@ A/C Controls: {% endif %} #### Features + +{% if version_installed.split('.') | map('int') < '0.6.7'.split('.') | map('int') %} +- Added OIM descaling sensor (#154) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} - Modified dishwasher to include new functionality (@NickWaterton) {% endif %} @@ -111,6 +116,10 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.7'.split('.') | map('int') %} +- Bugfix: fixed issues with dishwasher (#155) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} - Fixed region issues after setup (#130) - Updated the temperature conversion (#137) From 851f28d8ec7e67e2c4122f6132b5ccce21ec4e52 Mon Sep 17 00:00:00 2001 From: na4ma4 <25967676+na4ma4@users.noreply.github.com> Date: Fri, 12 May 2023 04:39:23 +1000 Subject: [PATCH 239/338] updated dual dishwasher for changes to gehomesdk (#161) Co-authored-by: na4ma4 --- .../ge_home/devices/dual_dishwasher.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/custom_components/ge_home/devices/dual_dishwasher.py b/custom_components/ge_home/devices/dual_dishwasher.py index 63c8b29..158da3c 100644 --- a/custom_components/ge_home/devices/dual_dishwasher.py +++ b/custom_components/ge_home/devices/dual_dishwasher.py @@ -19,18 +19,23 @@ def get_all_entities(self) -> List[Entity]: 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"), + #Reminders + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", erd_override="lower_reminder", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", erd_override="lower_reminder", icon_override="mdi:dishwasher-alert"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", erd_override="lower_reminder", icon_override="mdi:silverware-clean"), + #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound", erd_override="lower_setting", icon_override="mdi:volume-high"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "mute", erd_override="lower_setting", icon_override="mdi:volume-mute"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control", erd_override="lower_setting", icon_override="mdi:lock"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath", erd_override="lower_setting", icon_override="mdi:star-david"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode", erd_override="lower_setting", icon_override="mdi:state-machine"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak", erd_override="lower_setting", icon_override="mdi:water"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet", erd_override="lower_setting", icon_override="mdi:bottle-tonic-outline"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", erd_override="lower_setting", icon_override="mdi:coolant-temperature"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "rinse_aid", erd_override="lower_setting", icon_override="mdi:shimmer"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "dry_option", erd_override="lower_setting", icon_override="mdi:fan"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_zone", erd_override="lower_setting", icon_override="mdi:dock-top"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours", erd_override="lower_setting", icon_override="mdi:clock-fast") @@ -39,18 +44,23 @@ def get_all_entities(self) -> List[Entity]: 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"), + #Reminders + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "add_rinse_aid", erd_override="upper_reminder", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "clean_filter", erd_override="upper_reminder", icon_override="mdi:dishwasher-alert"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "sanitized", erd_override="upper_reminder", icon_override="mdi:silverware-clean"), + #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "sound", erd_override="upper_setting", icon_override="mdi:volume-high"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "mute", erd_override="upper_setting", icon_override="mdi:volume-mute"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "lock_control", erd_override="upper_setting", icon_override="mdi:lock"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "sabbath", erd_override="upper_setting", icon_override="mdi:star-david"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "cycle_mode", erd_override="upper_setting", icon_override="mdi:state-machine"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "presoak", erd_override="upper_setting", icon_override="mdi:water"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "bottle_jet", erd_override="upper_setting", icon_override="mdi:bottle-tonic-outline"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "wash_temp", erd_override="upper_setting", icon_override="mdi:coolant-temperature"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "rinse_aid", erd_override="upper_setting", icon_override="mdi:shimmer"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "dry_option", erd_override="upper_setting", icon_override="mdi:fan"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "wash_zone", erd_override="upper_setting", icon_override="mdi:dock-top"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "delay_hours", erd_override="upper_setting", icon_override="mdi:clock-fast") From edba2bb4f2b5572601872b754414d4b370ebc630 Mon Sep 17 00:00:00 2001 From: Alex Peters Date: Tue, 6 Jun 2023 07:34:40 +1000 Subject: [PATCH 240/338] await disconnect when unloading (#169) Fixes simbaja#164. --- custom_components/ge_home/update_coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 041c1c9..c0b525f 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -252,7 +252,7 @@ def shutdown(self, event) -> None: _LOGGER.info("ge_home shutting down") if self.client: self.client.clear_event_handlers() - self.client.disconnect() + self._hass.loop.create_task(self.client.disconnect()) async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]): """Let HA know there's new state.""" From 3d18d57a9aa975e27c97f7bc4ef6cbcbb8f24d09 Mon Sep 17 00:00:00 2001 From: Chris Petersen <154074+ex-nerd@users.noreply.github.com> Date: Sat, 17 Jun 2023 13:33:32 -0700 Subject: [PATCH 241/338] Check for upper oven light when there is a lower oven (#174) Resolves issue #121 --- custom_components/ge_home/devices/oven.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index f335f8d..142d19d 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -75,6 +75,8 @@ def get_all_entities(self) -> List[Entity]: ]) if has_upper_raw_temperature: oven_entities.append(GeErdSensor(self, 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)) 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: @@ -117,4 +119,4 @@ 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 + return "RAW_TEMPERATURE" if has_raw else "DISPLAY_TEMPERATURE" From b772321284129ed1a1298dc6f8e0b1d1d37a3417 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 24 Jun 2023 16:18:01 -0400 Subject: [PATCH 242/338] - added oven warming drawers - simplified oven entity logic --- custom_components/ge_home/devices/oven.py | 68 ++++++++++--------- .../ge_home/entities/oven/__init__.py | 1 + .../oven/ge_oven_warming_state_select.py | 56 +++++++++++++++ custom_components/ge_home/manifest.json | 2 +- 4 files changed, 95 insertions(+), 32 deletions(-) create mode 100644 custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index 142d19d..b768260 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -11,7 +11,8 @@ ErdCooktopConfig, CooktopStatus, ErdOvenLightLevel, - ErdOvenLightLevelAvailability + ErdOvenLightLevelAvailability, + ErdOvenWarmingState ) from .base import ApplianceApi @@ -23,6 +24,7 @@ GeErdPropertyBinarySensor, GeOven, GeOvenLightLevelSelect, + GeOvenWarmingStateSelect, UPPER_OVEN, LOWER_OVEN ) @@ -49,6 +51,10 @@ def get_all_entities(self) -> List[Entity]: lower_light : ErdOvenLightLevel = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT) lower_light_availability: ErdOvenLightLevelAvailability = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT_AVAILABILITY) + upper_warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE) + lower_warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE) + warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.WARMING_DRAWER_STATE) + _LOGGER.debug(f"Oven Config: {oven_config}") _LOGGER.debug(f"Cooktop Config: {cooktop_config}") oven_entities = [] @@ -56,13 +62,6 @@ def get_all_entities(self) -> List[Entity]: 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), @@ -70,32 +69,34 @@ def get_all_entities(self) -> List[Entity]: 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)), + GeOven(self, LOWER_OVEN, True, self._temperature_code(has_lower_raw_temperature)) ]) - if has_upper_raw_temperature: - oven_entities.append(GeErdSensor(self, 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)) 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 lower_warm_drawer is not None: + oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE)) + + oven_entities.extend([ + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, oven_config.has_lower_oven)), + GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER, oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, self._single_name(ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, oven_config.has_lower_oven)), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED, oven_config.has_lower_oven)), + + GeOven(self, UPPER_OVEN, False, self._temperature_code(has_upper_raw_temperature)) + ]) + if has_upper_raw_temperature: + oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_RAW_TEMPERATURE))) + 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 upper_warm_drawer is not None: + oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, self._single_name(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE))) + + if oven_config.has_warming_drawer and warm_drawer is not None: + oven_entities.append(GeErdSensor(self, ErdCode.WARMING_DRAWER_STATE)) if cooktop_config == ErdCooktopConfig.PRESENT: cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) @@ -112,8 +113,13 @@ def get_all_entities(self) -> List[Entity]: return base_entities + oven_entities + cooktop_entities - def _single_name(self, erd_code: ErdCode): - return erd_code.name.replace(UPPER_OVEN+"_","").replace("_", " ").title() + def _single_name(self, erd_code: ErdCode, make_single: bool): + name = erd_code.name + + if make_single: + name = name.replace(UPPER_OVEN+"_","") + + return name.replace("_", " ").title() def _camel_to_snake(self, s): return ''.join(['_'+c.lower() if c.isupper() else c for c in s]).lstrip('_') diff --git a/custom_components/ge_home/entities/oven/__init__.py b/custom_components/ge_home/entities/oven/__init__.py index e4166e8..6ef1066 100644 --- a/custom_components/ge_home/entities/oven/__init__.py +++ b/custom_components/ge_home/entities/oven/__init__.py @@ -1,3 +1,4 @@ from .ge_oven import GeOven from .ge_oven_light_level_select import GeOvenLightLevelSelect +from .ge_oven_warming_state_select import GeOvenWarmingStateSelect from .const import UPPER_OVEN, LOWER_OVEN \ No newline at end of file diff --git a/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py b/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py new file mode 100644 index 0000000..98dfcae --- /dev/null +++ b/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py @@ -0,0 +1,56 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCodeType, ErdOvenWarmingStateAvailability, ErdOvenWarmingState, ErdCode +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class OvenWarmingStateOptionsConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdOvenWarmingState] + def from_option_string(self, value: str) -> Any: + try: + return ErdOvenWarmingState[value.upper()] + except: + _LOGGER.warn(f"Could not set Oven warming state to {value.upper()}") + return ErdOvenWarmingState.OFF + def to_option_string(self, value: ErdOvenWarmingState) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdOvenWarmingState.OFF.stringify() + +class GeOvenWarmingStateSelect(GeErdSelect): + + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): + #check to see if we have a status + value: ErdOvenWarmingState = api.try_get_erd_value(erd_code) + self._has_status = value is not None and value != ErdOvenWarmingState.NOT_AVAILABLE + self._assumed_state = ErdOvenWarmingState.OFF + + super().__init__(api, erd_code, OvenWarmingStateOptionsConverter(self._availability), erd_override=erd_override) + + @property + def assumed_state(self) -> bool: + return not self._has_status + + @property + def current_option(self): + if self.assumed_state: + return self._assumed_state + + return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + _LOGGER.debug(f"Setting select from {self.current_option} to {option}") + + new_state = self._converter.from_option_string(option) + await self.appliance.async_set_erd_value(self.erd_code, new_state) + self._assumed_state = new_state + \ No newline at end of file diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index e437f04..330d5a3 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -3,7 +3,7 @@ "name": "GE Home (SmartHQ)", "config_flow": true, "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.10","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.13","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.7" } From 304a7bcefba9f5ed128cc881251e217b8d01a21f Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 25 Jun 2023 08:54:15 -0400 Subject: [PATCH 243/338] - fixed issues with the new oven initialization logic --- custom_components/ge_home/devices/oven.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index b768260..e391f84 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -79,21 +79,21 @@ def get_all_entities(self) -> List[Entity]: oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE)) oven_entities.extend([ - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, oven_config.has_lower_oven)), - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, oven_config.has_lower_oven)), - GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER, oven_config.has_lower_oven)), - GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, self._single_name(ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, oven_config.has_lower_oven)), - GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, oven_config.has_lower_oven)), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED, oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, ~oven_config.has_lower_oven)), + GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, self._single_name(ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, ~oven_config.has_lower_oven)), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED, ~oven_config.has_lower_oven)), GeOven(self, UPPER_OVEN, False, self._temperature_code(has_upper_raw_temperature)) ]) if has_upper_raw_temperature: - oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_RAW_TEMPERATURE))) + oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_RAW_TEMPERATURE, ~oven_config.has_lower_oven))) if upper_light_availability is None or upper_light_availability.is_available or upper_light is not None: - oven_entities.append(GeOvenLightLevelSelect(self, ErdCode.UPPER_OVEN_LIGHT, self._single_name(ErdCode.UPPER_OVEN_LIGHT))) + oven_entities.append(GeOvenLightLevelSelect(self, ErdCode.UPPER_OVEN_LIGHT, self._single_name(ErdCode.UPPER_OVEN_LIGHT, ~oven_config.has_lower_oven))) if upper_warm_drawer is not None: - oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, self._single_name(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE))) + oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, self._single_name(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, ~oven_config.has_lower_oven))) if oven_config.has_warming_drawer and warm_drawer is not None: oven_entities.append(GeErdSensor(self, ErdCode.WARMING_DRAWER_STATE)) From fc5e342fd8425a49a2f191d29124a46690e97fa0 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 25 Jun 2023 16:07:41 -0400 Subject: [PATCH 244/338] - fixed bad type import for warming drawer --- .../ge_home/entities/oven/ge_oven_warming_state_select.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py b/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py index 98dfcae..e10e2de 100644 --- a/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py +++ b/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py @@ -1,7 +1,7 @@ import logging from typing import List, Any, Optional -from gehomesdk import ErdCodeType, ErdOvenWarmingStateAvailability, ErdOvenWarmingState, ErdCode +from gehomesdk import ErdCodeType, ErdOvenWarmingState from ...devices import ApplianceApi from ..common import GeErdSelect, OptionsConverter From f2e2c25ac34ee1c5a1fcdb2ee7204fff05024f3a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 30 Jul 2023 15:15:47 -0400 Subject: [PATCH 245/338] -added iot class (#181) --- custom_components/ge_home/manifest.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 330d5a3..07c5cbc 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -2,6 +2,8 @@ "domain": "ge_home", "name": "GE Home (SmartHQ)", "config_flow": true, + "integration_type": "hub", + "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==0.5.13","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], From 8ef7da4bd526536a4ca3945ce83af573eadb37f0 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 30 Jul 2023 16:00:07 -0400 Subject: [PATCH 246/338] - updated the gehomesdk version requirement --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 07c5cbc..6706056 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.13","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.14","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.7" } From ed27905ad9379dafd0094a9e522efbc0aa477dbe Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 30 Jul 2023 16:55:40 -0400 Subject: [PATCH 247/338] - gehomesdk version bump --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 6706056..84df8fb 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.14","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.16","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.7" } From ba6dc60b36120131c901ac71b1b69dbdd6f3f95a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 30 Jul 2023 17:40:35 -0400 Subject: [PATCH 248/338] - added dehumidifier (#114) --- custom_components/ge_home/devices/__init__.py | 4 +- .../ge_home/devices/dehumidifier.py | 43 +++++++++++++++ .../ge_home/entities/__init__.py | 3 +- .../ge_home/entities/dehumidifier/__init__.py | 2 + .../dehumidifier/dehumidifier_fan_options.py | 26 +++++++++ .../dehumidifier_target_select.py | 55 +++++++++++++++++++ custom_components/ge_home/manifest.json | 2 +- 7 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 custom_components/ge_home/devices/dehumidifier.py create mode 100644 custom_components/ge_home/entities/dehumidifier/__init__.py create mode 100644 custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py create mode 100644 custom_components/ge_home/entities/dehumidifier/dehumidifier_target_select.py diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index b8a07ef..8badafd 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -25,7 +25,7 @@ from .coffee_maker import CcmApi from .dual_dishwasher import DualDishwasherApi from .espresso_maker import EspressoMakerApi - +from .dehumidifier import DehumidifierApi _LOGGER = logging.getLogger(__name__) @@ -77,6 +77,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return CcmApi if appliance_type == ErdApplianceType.ESPRESSO_MAKER: return EspressoMakerApi + if appliance_type == ErdApplianceType.DEHUMIDIFIER: + return DehumidifierApi # Fallback return ApplianceApi diff --git a/custom_components/ge_home/devices/dehumidifier.py b/custom_components/ge_home/devices/dehumidifier.py new file mode 100644 index 0000000..455a454 --- /dev/null +++ b/custom_components/ge_home/devices/dehumidifier.py @@ -0,0 +1,43 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdOnOff +) + +from .base import ApplianceApi +from ..entities import ( + GeErdSensor, + GeErdSelect, + GeErdPropertySensor, + GeErdSwitch, + ErdOnOffBoolConverter, + DehumidifierTargetHumiditySelect, + DehumidifierFanSettingOptionsConverter +) + +_LOGGER = logging.getLogger(__name__) + + +class DehumidifierApi(ApplianceApi): + """API class for Dehumidifier objects""" + APPLIANCE_TYPE = ErdApplianceType.WATER_HEATER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + dhum_entities = [ + GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + GeErdSelect(self, ErdCode.AC_FAN_SETTING, DehumidifierFanSettingOptionsConverter(), icon_override="mdi:fan"), + GeErdSensor(self, ErdCode.DHUM_CURRENT_HUMIDITY, uom_override="%", icon_override="mdi:water-percent"), + DehumidifierTargetHumiditySelect(self, ErdCode.DHUM_TARGET_HUMIDITY, icon_override="mdi:water-percent"), + GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "empty_bucket", device_class_override="problem"), + GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "clean_filter", device_class_override="problem") + ] + + entities = base_entities + dhum_entities + return entities + diff --git a/custom_components/ge_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index 2306cae..c063f8a 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -9,4 +9,5 @@ from .water_softener import * from .water_heater import * from .opal_ice_maker import * -from .ccm import * \ No newline at end of file +from .ccm import * +from .dehumidifier import * \ No newline at end of file diff --git a/custom_components/ge_home/entities/dehumidifier/__init__.py b/custom_components/ge_home/entities/dehumidifier/__init__.py new file mode 100644 index 0000000..282c7bd --- /dev/null +++ b/custom_components/ge_home/entities/dehumidifier/__init__.py @@ -0,0 +1,2 @@ +from .dehumidifier_target_select import DehumidifierTargetHumiditySelect +from .dehumidifier_fan_options import DehumidifierFanSettingOptionsConverter \ No newline at end of file diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py new file mode 100644 index 0000000..8cec48e --- /dev/null +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py @@ -0,0 +1,26 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdAcFanSetting +from ..common import OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class DehumidifierFanSettingOptionsConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [i.stringify() for i in [ErdAcFanSetting.DEFAULT, ErdAcFanSetting.LOW, ErdAcFanSetting.MED, ErdAcFanSetting.HIGH]] + + def from_option_string(self, value: str) -> Any: + try: + return ErdAcFanSetting[value.upper()] + except: + _LOGGER.warn(f"Could not set fan setting to {value.upper()}") + return ErdAcFanSetting.DEFAULT + def to_option_string(self, value: ErdAcFanSetting) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdAcFanSetting.DEFAULT.stringify() diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier_target_select.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier_target_select.py new file mode 100644 index 0000000..e0a4d8d --- /dev/null +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier_target_select.py @@ -0,0 +1,55 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCodeType, DehumidifierTargetRange +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_MIN_HUMIDITY = 35 +DEFAULT_MAX_HUMIDITY = 80 + +class DehumidifierTargetOptionsConverter(OptionsConverter): + def __init__(self, min = DEFAULT_MIN_HUMIDITY, max = DEFAULT_MAX_HUMIDITY) -> None: + super().__init__() + self._min = min + self._max = max + + @property + def options(self) -> List[str]: + return [str(i) for i in range(min,max) if i % 5 == 0] + + def from_option_string(self, value: str) -> Any: + return int(value) + def to_option_string(self, value: int) -> Optional[str]: + try: + if value is not None: + return str(value) + except: + return self._min + +class DehumidifierTargetHumiditySelect(GeErdSelect): + + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): + self._low = DEFAULT_MIN_HUMIDITY + self._high = DEFAULT_MAX_HUMIDITY + + #try to get the range + value: DehumidifierTargetRange = api.try_get_erd_value(erd_code) + if value is not None: + self._low = value.min_humidity + self._high = value.max_humidity + + super().__init__(api, erd_code, DehumidifierTargetOptionsConverter(self._low, self._high), erd_override=erd_override) + + @property + def current_option(self): + 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) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 84df8fb..ab98772 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.16","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.17","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.7" } From 40015294a3aaa81141c2740c5350ba659dc927c7 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 30 Jul 2023 18:10:32 -0400 Subject: [PATCH 249/338] - dehumidifier appliance type fix --- custom_components/ge_home/devices/dehumidifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/dehumidifier.py b/custom_components/ge_home/devices/dehumidifier.py index 455a454..1f1e0c0 100644 --- a/custom_components/ge_home/devices/dehumidifier.py +++ b/custom_components/ge_home/devices/dehumidifier.py @@ -24,7 +24,7 @@ class DehumidifierApi(ApplianceApi): """API class for Dehumidifier objects""" - APPLIANCE_TYPE = ErdApplianceType.WATER_HEATER + APPLIANCE_TYPE = ErdApplianceType.DEHUMIDIFIER def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() From 6ca4d56c5197d606ede1227fcfc47ca6c5116dc6 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 30 Jul 2023 18:19:08 -0400 Subject: [PATCH 250/338] - added oven state sensors (#175) --- custom_components/ge_home/devices/oven.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index e391f84..c83ce8d 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -63,6 +63,7 @@ def get_all_entities(self) -> List[Entity]: if oven_config.has_lower_oven: oven_entities.extend([ GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE), + GeErdSensor(self, ErdCode.LOWER_OVEN_CURRENT_STATE), GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING), GeErdTimerSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER), GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET), @@ -80,6 +81,7 @@ def get_all_entities(self) -> List[Entity]: oven_entities.extend([ GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_CURRENT_STATE, self._single_name(ErdCode.UPPER_OVEN_CURRENT_STATE, ~oven_config.has_lower_oven)), GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, ~oven_config.has_lower_oven)), GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER, ~oven_config.has_lower_oven)), GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, self._single_name(ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, ~oven_config.has_lower_oven)), From 0d9cb8f934d1fe7af2c91bc637c1354d0b28c8c7 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 30 Jul 2023 18:25:51 -0400 Subject: [PATCH 251/338] - updated change logs - updated sdk version requirement --- CHANGELOG.md | 11 +++++++++++ custom_components/ge_home/manifest.json | 2 +- info.md | 13 +++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fc44a..8a40d27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.8 + +- Added Dehumidifier [#114] +- Added oven drawer sensors +- Added oven current state sensors [#175] +- Added descriptors to manifest [#181] +- Bugfix: Fixed issue with oven lights [#174] +- Bugfix: Fixed issues with dual dishwasher [#161] +- Bugfix: Fixed disconnection issue [#169] + + ## 0.6.7 - Bugfix: fixed issues with dishwasher [#155] diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index ab98772..653ef7f 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==0.5.17","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.7" + "version": "0.6.8" } diff --git a/info.md b/info.md index 11a4b3b..aec528d 100644 --- a/info.md +++ b/info.md @@ -69,6 +69,13 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '0.6.8'.split('.') | map('int') %} +- Added Dehumidifier (#114) +- Added oven drawer sensors +- Added oven current state sensors (#175) +- Added descriptors to manifest (#181) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.7'.split('.') | map('int') %} - Added OIM descaling sensor (#154) {% endif %} @@ -116,6 +123,12 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.8'.split('.') | map('int') %} +- Bugfix: Fixed issue with oven lights (#174) +- Bugfix: Fixed issues with dual dishwasher (#161) +- Bugfix: Fixed disconnection issue (#169) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.7'.split('.') | map('int') %} - Bugfix: fixed issues with dishwasher (#155) {% endif %} From 296f81dfb2682417ed5d9cd0f55de190de4f2b95 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 31 Jul 2023 00:11:02 -0400 Subject: [PATCH 252/338] - removed target select - added dehumidifier entity - sdk version bump --- .../ge_home/devices/dehumidifier.py | 12 +-- .../ge_home/entities/common/__init__.py | 3 +- .../ge_home/entities/common/ge_erd_entity.py | 2 + .../ge_home/entities/common/ge_erd_sensor.py | 7 +- .../ge_home/entities/common/ge_humidifier.py | 98 +++++++++++++++++++ .../ge_home/entities/dehumidifier/__init__.py | 3 +- .../ge_home/entities/dehumidifier/const.py | 3 + .../entities/dehumidifier/dehumidifier.py | 74 ++++++++++++++ .../dehumidifier/dehumidifier_fan_options.py | 9 +- .../dehumidifier_target_select.py | 55 ----------- custom_components/ge_home/manifest.json | 2 +- 11 files changed, 199 insertions(+), 69 deletions(-) create mode 100644 custom_components/ge_home/entities/common/ge_humidifier.py create mode 100644 custom_components/ge_home/entities/dehumidifier/const.py create mode 100644 custom_components/ge_home/entities/dehumidifier/dehumidifier.py delete mode 100644 custom_components/ge_home/entities/dehumidifier/dehumidifier_target_select.py diff --git a/custom_components/ge_home/devices/dehumidifier.py b/custom_components/ge_home/devices/dehumidifier.py index 1f1e0c0..e91dc27 100644 --- a/custom_components/ge_home/devices/dehumidifier.py +++ b/custom_components/ge_home/devices/dehumidifier.py @@ -15,8 +15,7 @@ GeErdPropertySensor, GeErdSwitch, ErdOnOffBoolConverter, - DehumidifierTargetHumiditySelect, - DehumidifierFanSettingOptionsConverter + GeDehumidifier ) _LOGGER = logging.getLogger(__name__) @@ -31,11 +30,12 @@ def get_all_entities(self) -> List[Entity]: dhum_entities = [ GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), - GeErdSelect(self, ErdCode.AC_FAN_SETTING, DehumidifierFanSettingOptionsConverter(), icon_override="mdi:fan"), - GeErdSensor(self, ErdCode.DHUM_CURRENT_HUMIDITY, uom_override="%", icon_override="mdi:water-percent"), - DehumidifierTargetHumiditySelect(self, ErdCode.DHUM_TARGET_HUMIDITY, icon_override="mdi:water-percent"), + GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), + GeErdSensor(self, ErdCode.DHUM_CURRENT_HUMIDITY), + GeErdSensor(self, ErdCode.DHUM_TARGET_HUMIDITY), GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "empty_bucket", device_class_override="problem"), - GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "clean_filter", device_class_override="problem") + GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "clean_filter", device_class_override="problem"), + GeDehumidifier(self) ] entities = base_entities + dhum_entities diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 0b555ca..546ef73 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -13,4 +13,5 @@ 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 +from .ge_climate import GeClimate +from .ge_humidifier import GeHumidifier \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index 36b7883..25afa2a 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -145,5 +145,7 @@ def _get_icon(self): return "mdi:water" if self.erd_code_class == ErdCodeClass.CCM_SENSOR: return "mdi:coffee-maker" + if self.erd_code_class == ErdCodeClass.HUMIDITY: + return "mdi:water-percent" return None diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index d161fee..9dc917a 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -9,6 +9,7 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_HUMIDITY, TEMP_FAHRENHEIT, ) from gehomesdk import ErdCodeType, ErdCodeClass @@ -111,6 +112,8 @@ def _get_uom(self): return "%" if self.device_class == DEVICE_CLASS_POWER_FACTOR: return "%" + if self.erd_code_class == ErdCodeClass.HUMIDITY: + return "%" if self.erd_code_class == ErdCodeClass.FLOW_RATE: #if self._measurement_system == ErdMeasurementUnits.METRIC: # return "lpm" @@ -135,6 +138,8 @@ def _get_device_class(self) -> Optional[str]: return DEVICE_CLASS_POWER if self.erd_code_class == ErdCodeClass.ENERGY: return DEVICE_CLASS_ENERGY + if self.erd_code_class == ErdCodeClass.HUMIDITY: + return DEVICE_CLASS_HUMIDITY return None @@ -144,7 +149,7 @@ def _get_state_class(self) -> Optional[str]: 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]: + if self.erd_code_class in [ErdCodeClass.FLOW_RATE, ErdCodeClass.PERCENTAGE, ErdCodeClass.HUMIDITY]: return SensorStateClass.MEASUREMENT if self.erd_code_class in [ErdCodeClass.LIQUID_VOLUME]: return SensorStateClass.TOTAL_INCREASING diff --git a/custom_components/ge_home/entities/common/ge_humidifier.py b/custom_components/ge_home/entities/common/ge_humidifier.py new file mode 100644 index 0000000..2ddb469 --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_humidifier.py @@ -0,0 +1,98 @@ +import abc +import logging +from typing import Coroutine, Any, Optional + +from homeassistant.components.humidifier import HumidifierEntity, HumidifierDeviceClass +from homeassistant.components.humidifier.const import HumidifierEntityFeature + +from gehomesdk import ErdCodeType, ErdOnOff + +from ...const import DOMAIN +from ...devices import ApplianceApi +from .ge_entity import GeEntity +from .options_converter import OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class GeHumidifier(GeEntity, HumidifierEntity, metaclass=abc.ABCMeta): + """GE Humidifier Abstract Entity """ + + def __init__( + self, + api: ApplianceApi, + device_class: HumidifierDeviceClass, + power_status_erd_code: ErdCodeType, + target_humidity_erd_code: ErdCodeType, + current_humidity_erd_code: ErdCodeType, + range_min: int, + range_max: int + ): + super().__init__(api) + self._device_class = device_class + self._power_status_erd_code = power_status_erd_code + self._target_humidity_erd_code = target_humidity_erd_code + self._current_humidity_erd_code = current_humidity_erd_code + self._range_min = range_min + self._range_max = range_max + + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.serial_or_mac}_{self._device_class}" + + @property + def name(self) -> Optional[str]: + return f"{self.serial_or_mac} {self._device_class.title()}" + + @property + def target_humidity(self) -> int | None: + return int(self.appliance.get_erd_value(self._target_humidity_erd_code)) + + @property + def current_humidity(self) -> int | None: + return int(self.appliance.get_erd_value(self._current_humidity_erd_code)) + + @property + def min_humidity(self) -> int: + return self._range_min + + @property + def max_humidity(self) -> int: + return self._range_max + + @property + def supported_features(self) -> HumidifierEntityFeature: + return HumidifierEntityFeature(HumidifierEntityFeature.MODES) + + @property + def is_on(self) -> bool: + return self.appliance.get_erd_value(self.power_status_erd_code) == ErdOnOff.ON + + @property + def device_class(self): + return self.device_class + + async def async_set_humidity(self, humidity: int) -> Coroutine[Any, Any, None]: + if self.target_humidity == humidity: + return + + _LOGGER.debug(f"Setting Target Humidity from {self.target_humidity} to {humidity}") + + # make sure we're on + if not self.is_on: + await self.async_turn_on() + + # set the mode + await self.appliance.async_set_erd_value( + self.target_humidity_erd_code, + self.humidity, + ) + + async def async_turn_on(self): + await self.appliance.async_set_erd_value( + self.power_status_erd_code, ErdOnOff.ON + ) + + async def async_turn_off(self): + await self.appliance.async_set_erd_value( + self.power_status_erd_code, ErdOnOff.OFF + ) \ No newline at end of file diff --git a/custom_components/ge_home/entities/dehumidifier/__init__.py b/custom_components/ge_home/entities/dehumidifier/__init__.py index 282c7bd..f8b9506 100644 --- a/custom_components/ge_home/entities/dehumidifier/__init__.py +++ b/custom_components/ge_home/entities/dehumidifier/__init__.py @@ -1,2 +1 @@ -from .dehumidifier_target_select import DehumidifierTargetHumiditySelect -from .dehumidifier_fan_options import DehumidifierFanSettingOptionsConverter \ No newline at end of file +from .dehumidifier import GeDehumidifier \ No newline at end of file diff --git a/custom_components/ge_home/entities/dehumidifier/const.py b/custom_components/ge_home/entities/dehumidifier/const.py new file mode 100644 index 0000000..1e5a8b3 --- /dev/null +++ b/custom_components/ge_home/entities/dehumidifier/const.py @@ -0,0 +1,3 @@ +SMART_DRY = "Smart Dry" +DEFAULT_MIN_HUMIDITY = 35 +DEFAULT_MAX_HUMIDITY = 80 \ No newline at end of file diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier.py new file mode 100644 index 0000000..bb6ad0e --- /dev/null +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier.py @@ -0,0 +1,74 @@ +"""GE Home Dehumidifier""" +import logging + +from homeassistant.components.humidifier import HumidifierDeviceClass +from homeassistant.components.humidifier.const import HumidifierEntityFeature + +from gehomesdk import ErdCode, DehumidifierTargetRange + +from ...devices import ApplianceApi +from ..common import GeHumidifier +from .const import * +from .dehumidifier_fan_options import DehumidifierFanSettingOptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class GeDehumidifier(GeHumidifier): + """GE Dehumidifier""" + + icon = "mdi:air-humidifier" + + def __init__(self, api: ApplianceApi): + + #try to get the range + range: DehumidifierTargetRange = api.try_get_erd_value(ErdCode.DHUM_TARGET_HUMIDITY_RANGE) + low = DEFAULT_MIN_HUMIDITY if range is None else range.min_humidity + high = DEFAULT_MAX_HUMIDITY if range is None else range.max_humidity + + #try to get the fan mode and determine feature + mode = api.try_get_erd_value(ErdCode.AC_FAN_SETTING) + self._has_fan = mode is not None + self._mode_converter = DehumidifierFanSettingOptionsConverter() + + #initialize the dehumidifier + super().__init__(api, + HumidifierDeviceClass.DEHUMIDIFIER, + ErdCode.AC_POWER_STATUS, + ErdCode.DHUM_TARGET_HUMIDITY, + ErdCode.DHUM_CURRENT_HUMIDITY, + low, + high + ) + + @property + def supported_features(self) -> HumidifierEntityFeature: + if self._has_fan: + return HumidifierEntityFeature(HumidifierEntityFeature.MODES) + else: + return HumidifierEntityFeature(0) + + @property + def mode(self) -> str | None: + if not self._has_fan: + raise NotImplementedError() + + return self._mode_converter.to_option_string( + self.appliance.get_erd_value(ErdCode.AC_FAN_SETTING) + ) + + @property + def available_modes(self) -> list[str] | None: + if not self._has_fan: + raise NotImplementedError() + + return self._mode_converter.options + + async def async_set_mode(self, mode: str) -> None: + if not self._has_fan: + raise NotImplementedError() + + """Change the selected mode.""" + _LOGGER.debug(f"Setting mode from {self.mode} to {mode}") + + new_state = self._mode_converter.from_option_string(mode) + await self.appliance.async_set_erd_value(self.erd_code, new_state) diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py index 8cec48e..2d14832 100644 --- a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py @@ -3,16 +3,19 @@ from gehomesdk import ErdAcFanSetting from ..common import OptionsConverter +from .const import SMART_DRY _LOGGER = logging.getLogger(__name__) class DehumidifierFanSettingOptionsConverter(OptionsConverter): @property def options(self) -> List[str]: - return [i.stringify() for i in [ErdAcFanSetting.DEFAULT, ErdAcFanSetting.LOW, ErdAcFanSetting.MED, ErdAcFanSetting.HIGH]] + return [SMART_DRY] + [i.stringify() for i in [ErdAcFanSetting.LOW, ErdAcFanSetting.MED, ErdAcFanSetting.HIGH]] def from_option_string(self, value: str) -> Any: try: + if value == SMART_DRY: + return ErdAcFanSetting.DEFAULT return ErdAcFanSetting[value.upper()] except: _LOGGER.warn(f"Could not set fan setting to {value.upper()}") @@ -20,7 +23,7 @@ def from_option_string(self, value: str) -> Any: def to_option_string(self, value: ErdAcFanSetting) -> Optional[str]: try: if value is not None: - return value.stringify() + return SMART_DRY if value == ErdAcFanSetting.DEFAULT else value.stringify() except: pass - return ErdAcFanSetting.DEFAULT.stringify() + return SMART_DRY diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier_target_select.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier_target_select.py deleted file mode 100644 index e0a4d8d..0000000 --- a/custom_components/ge_home/entities/dehumidifier/dehumidifier_target_select.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging -from typing import List, Any, Optional - -from gehomesdk import ErdCodeType, DehumidifierTargetRange -from ...devices import ApplianceApi -from ..common import GeErdSelect, OptionsConverter - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_MIN_HUMIDITY = 35 -DEFAULT_MAX_HUMIDITY = 80 - -class DehumidifierTargetOptionsConverter(OptionsConverter): - def __init__(self, min = DEFAULT_MIN_HUMIDITY, max = DEFAULT_MAX_HUMIDITY) -> None: - super().__init__() - self._min = min - self._max = max - - @property - def options(self) -> List[str]: - return [str(i) for i in range(min,max) if i % 5 == 0] - - def from_option_string(self, value: str) -> Any: - return int(value) - def to_option_string(self, value: int) -> Optional[str]: - try: - if value is not None: - return str(value) - except: - return self._min - -class DehumidifierTargetHumiditySelect(GeErdSelect): - - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): - self._low = DEFAULT_MIN_HUMIDITY - self._high = DEFAULT_MAX_HUMIDITY - - #try to get the range - value: DehumidifierTargetRange = api.try_get_erd_value(erd_code) - if value is not None: - self._low = value.min_humidity - self._high = value.max_humidity - - super().__init__(api, erd_code, DehumidifierTargetOptionsConverter(self._low, self._high), erd_override=erd_override) - - @property - def current_option(self): - 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) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 653ef7f..cae7890 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.17","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.18","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.8" } From 2160b280850155ea1fbfb4de4518baebc8dc2232 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 31 Jul 2023 00:14:36 -0400 Subject: [PATCH 253/338] - updated dehumidifier icon --- custom_components/ge_home/entities/common/ge_erd_entity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index 25afa2a..6763253 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -147,5 +147,7 @@ def _get_icon(self): return "mdi:coffee-maker" if self.erd_code_class == ErdCodeClass.HUMIDITY: return "mdi:water-percent" + if self.erd_code_class == ErdCodeClass.DEHUMIDIFIER_SENSOR: + return "mdi:air-humidifier" return None From 294a1114799f7817f96287d916c2f68037a93c63 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 31 Jul 2023 21:54:34 -0400 Subject: [PATCH 254/338] - added humidifier platform --- custom_components/ge_home/humidifier.py | 36 +++++++++++++++++++ .../ge_home/update_coordinator.py | 13 ++++++- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 custom_components/ge_home/humidifier.py diff --git a/custom_components/ge_home/humidifier.py b/custom_components/ge_home/humidifier.py new file mode 100644 index 0000000..4d1ed11 --- /dev/null +++ b/custom_components/ge_home/humidifier.py @@ -0,0 +1,36 @@ +"""GE Home Humidifier Entities""" +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er + +from .entities import GeHumidifier +from .const import DOMAIN +from .devices import ApplianceApi +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): + """GE Home Water Heaters.""" + _LOGGER.debug('Adding GE "Humidifiers"') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeHumidifier) + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered humidifiers') + async_add_entities(entities) + + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index c0b525f..b846bb4 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -35,7 +35,18 @@ ) 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", + "water_heater", + "select", + "climate", + "light", + "button", + "number", + "humidifier" +] _LOGGER = logging.getLogger(__name__) From 102c7d98c9f6221d74e86389c16c4cb86be98975 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 1 Aug 2023 20:09:38 -0400 Subject: [PATCH 255/338] - fixed typos in humidifier class --- .../ge_home/entities/common/ge_humidifier.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_humidifier.py b/custom_components/ge_home/entities/common/ge_humidifier.py index 2ddb469..d4bfe97 100644 --- a/custom_components/ge_home/entities/common/ge_humidifier.py +++ b/custom_components/ge_home/entities/common/ge_humidifier.py @@ -65,11 +65,11 @@ def supported_features(self) -> HumidifierEntityFeature: @property def is_on(self) -> bool: - return self.appliance.get_erd_value(self.power_status_erd_code) == ErdOnOff.ON + return self.appliance.get_erd_value(self._power_status_erd_code) == ErdOnOff.ON @property def device_class(self): - return self.device_class + return self._device_class async def async_set_humidity(self, humidity: int) -> Coroutine[Any, Any, None]: if self.target_humidity == humidity: @@ -83,16 +83,16 @@ async def async_set_humidity(self, humidity: int) -> Coroutine[Any, Any, None]: # set the mode await self.appliance.async_set_erd_value( - self.target_humidity_erd_code, - self.humidity, + self._target_humidity_erd_code, + humidity, ) async def async_turn_on(self): await self.appliance.async_set_erd_value( - self.power_status_erd_code, ErdOnOff.ON + 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 + self._power_status_erd_code, ErdOnOff.OFF ) \ No newline at end of file From 7df803f4cdb5856791933a0d7884a3590af675df Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 1 Aug 2023 22:32:48 -0400 Subject: [PATCH 256/338] - fixed copy/paste error --- custom_components/ge_home/entities/dehumidifier/dehumidifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier.py index bb6ad0e..1ed2755 100644 --- a/custom_components/ge_home/entities/dehumidifier/dehumidifier.py +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier.py @@ -71,4 +71,4 @@ async def async_set_mode(self, mode: str) -> None: _LOGGER.debug(f"Setting mode from {self.mode} to {mode}") new_state = self._mode_converter.from_option_string(mode) - await self.appliance.async_set_erd_value(self.erd_code, new_state) + await self.appliance.async_set_erd_value(ErdCode.AC_FAN_SETTING, new_state) From 77aeba63693676f6e3f0c8c6e4db3af4e121b547 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 2 Aug 2023 08:58:16 -0400 Subject: [PATCH 257/338] - sdk version requirement bump --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index cae7890..ac8588c 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.18","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.19","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.8" } From 7feecd3e37f96872a6b62851d7d5ac6a226ceda8 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 2 Aug 2023 11:46:42 -0400 Subject: [PATCH 258/338] - sdk version bump --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index ac8588c..6794797 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.19","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.20","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.8" } From d3cae05e930555cd9a47d3e08a9d09a5ccd83bb3 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 2 Aug 2023 19:25:37 -0400 Subject: [PATCH 259/338] - updated dehumidifier to handle target precision - updated dehumidifier sensor value conversion --- .../ge_home/devices/dehumidifier.py | 3 ++- .../ge_home/entities/common/ge_humidifier.py | 19 +++++++++++++------ .../ge_home/entities/dehumidifier/__init__.py | 3 ++- .../dehumidifier_fan_speed_sensor.py | 8 ++++++++ 4 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py diff --git a/custom_components/ge_home/devices/dehumidifier.py b/custom_components/ge_home/devices/dehumidifier.py index e91dc27..fa0d8cc 100644 --- a/custom_components/ge_home/devices/dehumidifier.py +++ b/custom_components/ge_home/devices/dehumidifier.py @@ -15,6 +15,7 @@ GeErdPropertySensor, GeErdSwitch, ErdOnOffBoolConverter, + GeDehumidifierFanSpeedSensor, GeDehumidifier ) @@ -30,7 +31,7 @@ def get_all_entities(self) -> List[Entity]: dhum_entities = [ GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), - GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), + GeDehumidifierFanSpeedSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), GeErdSensor(self, ErdCode.DHUM_CURRENT_HUMIDITY), GeErdSensor(self, ErdCode.DHUM_TARGET_HUMIDITY), GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "empty_bucket", device_class_override="problem"), diff --git a/custom_components/ge_home/entities/common/ge_humidifier.py b/custom_components/ge_home/entities/common/ge_humidifier.py index d4bfe97..e102e3b 100644 --- a/custom_components/ge_home/entities/common/ge_humidifier.py +++ b/custom_components/ge_home/entities/common/ge_humidifier.py @@ -10,7 +10,8 @@ from ...const import DOMAIN from ...devices import ApplianceApi from .ge_entity import GeEntity -from .options_converter import OptionsConverter + +DEFAULT_TARGET_PRECISION = 5 _LOGGER = logging.getLogger(__name__) @@ -25,7 +26,8 @@ def __init__( target_humidity_erd_code: ErdCodeType, current_humidity_erd_code: ErdCodeType, range_min: int, - range_max: int + range_max: int, + target_precision = DEFAULT_TARGET_PRECISION ): super().__init__(api) self._device_class = device_class @@ -34,6 +36,7 @@ def __init__( self._current_humidity_erd_code = current_humidity_erd_code self._range_min = range_min self._range_max = range_max + self._target_precision = target_precision @property def unique_id(self) -> str: @@ -72,19 +75,23 @@ def device_class(self): return self._device_class async def async_set_humidity(self, humidity: int) -> Coroutine[Any, Any, None]: - if self.target_humidity == humidity: + # round to target precision + target = round(humidity / self._target_precision) * self._target_precision + + # if it's the same, just exit + if self.target_humidity == target: return - _LOGGER.debug(f"Setting Target Humidity from {self.target_humidity} to {humidity}") + _LOGGER.debug(f"Setting Target Humidity from {self.target_humidity} to {target}") # make sure we're on if not self.is_on: await self.async_turn_on() - # set the mode + # set the target humidity await self.appliance.async_set_erd_value( self._target_humidity_erd_code, - humidity, + target, ) async def async_turn_on(self): diff --git a/custom_components/ge_home/entities/dehumidifier/__init__.py b/custom_components/ge_home/entities/dehumidifier/__init__.py index f8b9506..f68fa50 100644 --- a/custom_components/ge_home/entities/dehumidifier/__init__.py +++ b/custom_components/ge_home/entities/dehumidifier/__init__.py @@ -1 +1,2 @@ -from .dehumidifier import GeDehumidifier \ No newline at end of file +from .dehumidifier import GeDehumidifier +from .dehumidifier_fan_speed_sensor import GeDehumidifierFanSpeedSensor \ No newline at end of file diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py new file mode 100644 index 0000000..124914a --- /dev/null +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py @@ -0,0 +1,8 @@ +from ..common import GeErdBinarySensor + +class GeCcmPotNotPresentBinarySensor(GeErdBinarySensor): + @property + def is_on(self) -> bool: + """Return True if entity is not pot present.""" + return not self._boolify(self.appliance.get_erd_value(self.erd_code)) + From e06b5bc6d59d1969b628c3a5ec9025e040daa42f Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 2 Aug 2023 19:27:34 -0400 Subject: [PATCH 260/338] - missed a commit --- .../dehumidifier_fan_speed_sensor.py | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py index 124914a..dd8424a 100644 --- a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py @@ -1,8 +1,40 @@ -from ..common import GeErdBinarySensor +from ...devices import ApplianceApi +from ..common import GeErdSensor +from .dehumidifier_fan_options import DehumidifierFanSettingOptionsConverter +from gehomesdk import ErdCodeType, ErdCodeClass, ErdDataType, ErdAcFanSetting + +class GeDehumidifierFanSpeedSensor(GeErdSensor): + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_override: str = None, + icon_override: str = None, + device_class_override: str = None, + state_class_override: str = None, + uom_override: str = None, + data_type_override: ErdDataType = None + ): + + super().__init__( + api, + erd_code, + erd_override, + icon_override, + device_class_override, + state_class_override, + uom_override, + data_type_override + ) + + self._converter = DehumidifierFanSettingOptionsConverter() -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)) + def native_value(self): + try: + value: ErdAcFanSetting = self.appliance.get_erd_value(self.erd_code) + return self._converter.to_option_string(value) + except KeyError: + return None + From 541d0bb76b9acef20d5485dca1406be107e79b56 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 10 Sep 2023 19:12:50 -0400 Subject: [PATCH 261/338] - SDK version bump --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 6794797..969c1f8 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.20","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.23","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.8" } From 3b6e657272a60a8fa76373e8b921c65b2a8da968 Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sun, 10 Sep 2023 19:30:38 -0400 Subject: [PATCH 262/338] v0.6.8 Develop -> Main (#194) * - updated water heater naming * - initial support for built-in AC units * Add support for Built-In AC Unit Built-In AC unit operation and function is similar to a WAC. `gehomesdk` version bumped from 0.4.25 to 0.4.27 to include latest ErdApplianceType enum. * Update README.md Update README.md to include Built-In AC as supported device * - updated zero serial number detection (resolves #89) * - updated version - updated changelog * - hopefully fixed recursion bug with numbers * - added cooktop support * - fixed circular reference * Add support for fridges with reduced support for ERD codes (no turbo mode, no current temperature reporting, no temperature setpoint limit reporting, no door status reporting). This change has been tested on a Fisher & Paykel RF610AA. * - added dual dishwasher support - updated documentation - version bumps * - added water heater support * - added basic espresso maker device * - bugfixes * - rewrote initialization (resolves #99) * - added logic to prevent double registration of entities * - added correct min/max temps for water heaters * Fix CoffeeMaker after the NumberEntity refactoring * - added fix for CGY366P2TS1 (#116) to try to get the cooktop status, but fail more gracefully if it isn't supported * - fixed region setting in update coordinator * - version bump - doc update - string fixes * - updated the temperature conversion to use non-deprecated HASS methods * - updated documentation (@gleepwurp) * - more documentation updates * - updated dishwasher for new functionality - updated documentation * updated uom for liquid volume per HA specifications * - fixed typo in oven (#149) * - updated change log - fixed oven light control (#144) * - fixed issues with dishwasher (#155) - added oim descaling sensor (#154) - version bump * - updated change log * updated dual dishwasher for changes to gehomesdk (#161) Co-authored-by: na4ma4 * await disconnect when unloading (#169) Fixes simbaja#164. * Check for upper oven light when there is a lower oven (#174) Resolves issue #121 * - added oven warming drawers - simplified oven entity logic * - fixed issues with the new oven initialization logic * - fixed bad type import for warming drawer * -added iot class (#181) * - updated the gehomesdk version requirement * - gehomesdk version bump * - added dehumidifier (#114) * - dehumidifier appliance type fix * - added oven state sensors (#175) * - updated change logs - updated sdk version requirement * - removed target select - added dehumidifier entity - sdk version bump * - updated dehumidifier icon * - added humidifier platform * - fixed typos in humidifier class * - fixed copy/paste error * - sdk version requirement bump * - sdk version bump * - updated dehumidifier to handle target precision - updated dehumidifier sensor value conversion * - missed a commit * - SDK version bump --------- Co-authored-by: Rob Schmidt Co-authored-by: Federico Sevilla Co-authored-by: alexanv1 <44785744+alexanv1@users.noreply.github.com> Co-authored-by: na4ma4 <25967676+na4ma4@users.noreply.github.com> Co-authored-by: na4ma4 Co-authored-by: Alex Peters Co-authored-by: Chris Petersen <154074+ex-nerd@users.noreply.github.com> --- CHANGELOG.md | 11 ++ custom_components/ge_home/devices/__init__.py | 4 +- .../ge_home/devices/dehumidifier.py | 44 ++++++++ .../ge_home/devices/dual_dishwasher.py | 18 ++- custom_components/ge_home/devices/oven.py | 70 +++++++----- .../ge_home/entities/__init__.py | 3 +- .../ge_home/entities/common/__init__.py | 3 +- .../ge_home/entities/common/ge_erd_entity.py | 4 + .../ge_home/entities/common/ge_erd_sensor.py | 7 +- .../ge_home/entities/common/ge_humidifier.py | 105 ++++++++++++++++++ .../ge_home/entities/dehumidifier/__init__.py | 2 + .../ge_home/entities/dehumidifier/const.py | 3 + .../entities/dehumidifier/dehumidifier.py | 74 ++++++++++++ .../dehumidifier/dehumidifier_fan_options.py | 29 +++++ .../dehumidifier_fan_speed_sensor.py | 40 +++++++ .../ge_home/entities/oven/__init__.py | 1 + .../oven/ge_oven_warming_state_select.py | 56 ++++++++++ custom_components/ge_home/humidifier.py | 36 ++++++ custom_components/ge_home/manifest.json | 6 +- .../ge_home/update_coordinator.py | 15 ++- info.md | 13 +++ 21 files changed, 502 insertions(+), 42 deletions(-) create mode 100644 custom_components/ge_home/devices/dehumidifier.py create mode 100644 custom_components/ge_home/entities/common/ge_humidifier.py create mode 100644 custom_components/ge_home/entities/dehumidifier/__init__.py create mode 100644 custom_components/ge_home/entities/dehumidifier/const.py create mode 100644 custom_components/ge_home/entities/dehumidifier/dehumidifier.py create mode 100644 custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py create mode 100644 custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py create mode 100644 custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py create mode 100644 custom_components/ge_home/humidifier.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fc44a..8a40d27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.8 + +- Added Dehumidifier [#114] +- Added oven drawer sensors +- Added oven current state sensors [#175] +- Added descriptors to manifest [#181] +- Bugfix: Fixed issue with oven lights [#174] +- Bugfix: Fixed issues with dual dishwasher [#161] +- Bugfix: Fixed disconnection issue [#169] + + ## 0.6.7 - Bugfix: fixed issues with dishwasher [#155] diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index b8a07ef..8badafd 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -25,7 +25,7 @@ from .coffee_maker import CcmApi from .dual_dishwasher import DualDishwasherApi from .espresso_maker import EspressoMakerApi - +from .dehumidifier import DehumidifierApi _LOGGER = logging.getLogger(__name__) @@ -77,6 +77,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return CcmApi if appliance_type == ErdApplianceType.ESPRESSO_MAKER: return EspressoMakerApi + if appliance_type == ErdApplianceType.DEHUMIDIFIER: + return DehumidifierApi # Fallback return ApplianceApi diff --git a/custom_components/ge_home/devices/dehumidifier.py b/custom_components/ge_home/devices/dehumidifier.py new file mode 100644 index 0000000..fa0d8cc --- /dev/null +++ b/custom_components/ge_home/devices/dehumidifier.py @@ -0,0 +1,44 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdOnOff +) + +from .base import ApplianceApi +from ..entities import ( + GeErdSensor, + GeErdSelect, + GeErdPropertySensor, + GeErdSwitch, + ErdOnOffBoolConverter, + GeDehumidifierFanSpeedSensor, + GeDehumidifier +) + +_LOGGER = logging.getLogger(__name__) + + +class DehumidifierApi(ApplianceApi): + """API class for Dehumidifier objects""" + APPLIANCE_TYPE = ErdApplianceType.DEHUMIDIFIER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + dhum_entities = [ + GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + GeDehumidifierFanSpeedSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), + GeErdSensor(self, ErdCode.DHUM_CURRENT_HUMIDITY), + GeErdSensor(self, ErdCode.DHUM_TARGET_HUMIDITY), + GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "empty_bucket", device_class_override="problem"), + GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "clean_filter", device_class_override="problem"), + GeDehumidifier(self) + ] + + entities = base_entities + dhum_entities + return entities + diff --git a/custom_components/ge_home/devices/dual_dishwasher.py b/custom_components/ge_home/devices/dual_dishwasher.py index 63c8b29..158da3c 100644 --- a/custom_components/ge_home/devices/dual_dishwasher.py +++ b/custom_components/ge_home/devices/dual_dishwasher.py @@ -19,18 +19,23 @@ def get_all_entities(self) -> List[Entity]: 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"), + #Reminders + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", erd_override="lower_reminder", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", erd_override="lower_reminder", icon_override="mdi:dishwasher-alert"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", erd_override="lower_reminder", icon_override="mdi:silverware-clean"), + #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sound", erd_override="lower_setting", icon_override="mdi:volume-high"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "mute", erd_override="lower_setting", icon_override="mdi:volume-mute"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control", erd_override="lower_setting", icon_override="mdi:lock"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath", erd_override="lower_setting", icon_override="mdi:star-david"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode", erd_override="lower_setting", icon_override="mdi:state-machine"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak", erd_override="lower_setting", icon_override="mdi:water"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet", erd_override="lower_setting", icon_override="mdi:bottle-tonic-outline"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", erd_override="lower_setting", icon_override="mdi:coolant-temperature"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "rinse_aid", erd_override="lower_setting", icon_override="mdi:shimmer"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "dry_option", erd_override="lower_setting", icon_override="mdi:fan"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_zone", erd_override="lower_setting", icon_override="mdi:dock-top"), GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours", erd_override="lower_setting", icon_override="mdi:clock-fast") @@ -39,18 +44,23 @@ def get_all_entities(self) -> List[Entity]: 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"), + #Reminders + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "add_rinse_aid", erd_override="upper_reminder", icon_override="mdi:shimmer"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "clean_filter", erd_override="upper_reminder", icon_override="mdi:dishwasher-alert"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "sanitized", erd_override="upper_reminder", icon_override="mdi:silverware-clean"), + #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "sound", erd_override="upper_setting", icon_override="mdi:volume-high"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "mute", erd_override="upper_setting", icon_override="mdi:volume-mute"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "lock_control", erd_override="upper_setting", icon_override="mdi:lock"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "sabbath", erd_override="upper_setting", icon_override="mdi:star-david"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "cycle_mode", erd_override="upper_setting", icon_override="mdi:state-machine"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "presoak", erd_override="upper_setting", icon_override="mdi:water"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "bottle_jet", erd_override="upper_setting", icon_override="mdi:bottle-tonic-outline"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "wash_temp", erd_override="upper_setting", icon_override="mdi:coolant-temperature"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "rinse_aid", erd_override="upper_setting", icon_override="mdi:shimmer"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "dry_option", erd_override="upper_setting", icon_override="mdi:fan"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "wash_zone", erd_override="upper_setting", icon_override="mdi:dock-top"), GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "delay_hours", erd_override="upper_setting", icon_override="mdi:clock-fast") diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index f335f8d..c83ce8d 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -11,7 +11,8 @@ ErdCooktopConfig, CooktopStatus, ErdOvenLightLevel, - ErdOvenLightLevelAvailability + ErdOvenLightLevelAvailability, + ErdOvenWarmingState ) from .base import ApplianceApi @@ -23,6 +24,7 @@ GeErdPropertyBinarySensor, GeOven, GeOvenLightLevelSelect, + GeOvenWarmingStateSelect, UPPER_OVEN, LOWER_OVEN ) @@ -49,6 +51,10 @@ def get_all_entities(self) -> List[Entity]: lower_light : ErdOvenLightLevel = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT) lower_light_availability: ErdOvenLightLevelAvailability = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT_AVAILABILITY) + upper_warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE) + lower_warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE) + warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.WARMING_DRAWER_STATE) + _LOGGER.debug(f"Oven Config: {oven_config}") _LOGGER.debug(f"Cooktop Config: {cooktop_config}") oven_entities = [] @@ -56,44 +62,43 @@ def get_all_entities(self) -> List[Entity]: 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_CURRENT_STATE), GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING), GeErdTimerSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER), GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET), GeErdSensor(self, ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE), GeErdBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED), - GeOven(self, LOWER_OVEN, True, self._temperature_code(has_lower_raw_temperature)), - GeOven(self, UPPER_OVEN, True, self._temperature_code(has_upper_raw_temperature)), + GeOven(self, LOWER_OVEN, True, self._temperature_code(has_lower_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 lower_warm_drawer is not None: + oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE)) + + oven_entities.extend([ + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_CURRENT_STATE, self._single_name(ErdCode.UPPER_OVEN_CURRENT_STATE, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, ~oven_config.has_lower_oven)), + GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, self._single_name(ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, ~oven_config.has_lower_oven)), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED, ~oven_config.has_lower_oven)), + + GeOven(self, UPPER_OVEN, False, self._temperature_code(has_upper_raw_temperature)) + ]) + if has_upper_raw_temperature: + oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_RAW_TEMPERATURE, ~oven_config.has_lower_oven))) + if upper_light_availability is None or upper_light_availability.is_available or upper_light is not None: + oven_entities.append(GeOvenLightLevelSelect(self, ErdCode.UPPER_OVEN_LIGHT, self._single_name(ErdCode.UPPER_OVEN_LIGHT, ~oven_config.has_lower_oven))) + if upper_warm_drawer is not None: + oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, self._single_name(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, ~oven_config.has_lower_oven))) + + if oven_config.has_warming_drawer and warm_drawer is not None: + oven_entities.append(GeErdSensor(self, ErdCode.WARMING_DRAWER_STATE)) if cooktop_config == ErdCooktopConfig.PRESENT: cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) @@ -110,11 +115,16 @@ def get_all_entities(self) -> List[Entity]: return base_entities + oven_entities + cooktop_entities - def _single_name(self, erd_code: ErdCode): - return erd_code.name.replace(UPPER_OVEN+"_","").replace("_", " ").title() + def _single_name(self, erd_code: ErdCode, make_single: bool): + name = erd_code.name + + if make_single: + name = name.replace(UPPER_OVEN+"_","") + + return name.replace("_", " ").title() def _camel_to_snake(self, s): return ''.join(['_'+c.lower() if c.isupper() else c for c in s]).lstrip('_') def _temperature_code(self, has_raw: bool): - return "RAW_TEMPERATURE" if has_raw else "DISPLAY_TEMPERATURE" \ No newline at end of file + return "RAW_TEMPERATURE" if has_raw else "DISPLAY_TEMPERATURE" diff --git a/custom_components/ge_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index 2306cae..c063f8a 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -9,4 +9,5 @@ from .water_softener import * from .water_heater import * from .opal_ice_maker import * -from .ccm import * \ No newline at end of file +from .ccm import * +from .dehumidifier import * \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 0b555ca..546ef73 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -13,4 +13,5 @@ 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 +from .ge_climate import GeClimate +from .ge_humidifier import GeHumidifier \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index 36b7883..6763253 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -145,5 +145,9 @@ def _get_icon(self): return "mdi:water" if self.erd_code_class == ErdCodeClass.CCM_SENSOR: return "mdi:coffee-maker" + if self.erd_code_class == ErdCodeClass.HUMIDITY: + return "mdi:water-percent" + if self.erd_code_class == ErdCodeClass.DEHUMIDIFIER_SENSOR: + return "mdi:air-humidifier" return None diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index d161fee..9dc917a 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -9,6 +9,7 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_HUMIDITY, TEMP_FAHRENHEIT, ) from gehomesdk import ErdCodeType, ErdCodeClass @@ -111,6 +112,8 @@ def _get_uom(self): return "%" if self.device_class == DEVICE_CLASS_POWER_FACTOR: return "%" + if self.erd_code_class == ErdCodeClass.HUMIDITY: + return "%" if self.erd_code_class == ErdCodeClass.FLOW_RATE: #if self._measurement_system == ErdMeasurementUnits.METRIC: # return "lpm" @@ -135,6 +138,8 @@ def _get_device_class(self) -> Optional[str]: return DEVICE_CLASS_POWER if self.erd_code_class == ErdCodeClass.ENERGY: return DEVICE_CLASS_ENERGY + if self.erd_code_class == ErdCodeClass.HUMIDITY: + return DEVICE_CLASS_HUMIDITY return None @@ -144,7 +149,7 @@ def _get_state_class(self) -> Optional[str]: 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]: + if self.erd_code_class in [ErdCodeClass.FLOW_RATE, ErdCodeClass.PERCENTAGE, ErdCodeClass.HUMIDITY]: return SensorStateClass.MEASUREMENT if self.erd_code_class in [ErdCodeClass.LIQUID_VOLUME]: return SensorStateClass.TOTAL_INCREASING diff --git a/custom_components/ge_home/entities/common/ge_humidifier.py b/custom_components/ge_home/entities/common/ge_humidifier.py new file mode 100644 index 0000000..e102e3b --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_humidifier.py @@ -0,0 +1,105 @@ +import abc +import logging +from typing import Coroutine, Any, Optional + +from homeassistant.components.humidifier import HumidifierEntity, HumidifierDeviceClass +from homeassistant.components.humidifier.const import HumidifierEntityFeature + +from gehomesdk import ErdCodeType, ErdOnOff + +from ...const import DOMAIN +from ...devices import ApplianceApi +from .ge_entity import GeEntity + +DEFAULT_TARGET_PRECISION = 5 + +_LOGGER = logging.getLogger(__name__) + +class GeHumidifier(GeEntity, HumidifierEntity, metaclass=abc.ABCMeta): + """GE Humidifier Abstract Entity """ + + def __init__( + self, + api: ApplianceApi, + device_class: HumidifierDeviceClass, + power_status_erd_code: ErdCodeType, + target_humidity_erd_code: ErdCodeType, + current_humidity_erd_code: ErdCodeType, + range_min: int, + range_max: int, + target_precision = DEFAULT_TARGET_PRECISION + ): + super().__init__(api) + self._device_class = device_class + self._power_status_erd_code = power_status_erd_code + self._target_humidity_erd_code = target_humidity_erd_code + self._current_humidity_erd_code = current_humidity_erd_code + self._range_min = range_min + self._range_max = range_max + self._target_precision = target_precision + + @property + def unique_id(self) -> str: + return f"{DOMAIN}_{self.serial_or_mac}_{self._device_class}" + + @property + def name(self) -> Optional[str]: + return f"{self.serial_or_mac} {self._device_class.title()}" + + @property + def target_humidity(self) -> int | None: + return int(self.appliance.get_erd_value(self._target_humidity_erd_code)) + + @property + def current_humidity(self) -> int | None: + return int(self.appliance.get_erd_value(self._current_humidity_erd_code)) + + @property + def min_humidity(self) -> int: + return self._range_min + + @property + def max_humidity(self) -> int: + return self._range_max + + @property + def supported_features(self) -> HumidifierEntityFeature: + return HumidifierEntityFeature(HumidifierEntityFeature.MODES) + + @property + def is_on(self) -> bool: + return self.appliance.get_erd_value(self._power_status_erd_code) == ErdOnOff.ON + + @property + def device_class(self): + return self._device_class + + async def async_set_humidity(self, humidity: int) -> Coroutine[Any, Any, None]: + # round to target precision + target = round(humidity / self._target_precision) * self._target_precision + + # if it's the same, just exit + if self.target_humidity == target: + return + + _LOGGER.debug(f"Setting Target Humidity from {self.target_humidity} to {target}") + + # make sure we're on + if not self.is_on: + await self.async_turn_on() + + # set the target humidity + await self.appliance.async_set_erd_value( + self._target_humidity_erd_code, + target, + ) + + async def async_turn_on(self): + await self.appliance.async_set_erd_value( + self._power_status_erd_code, ErdOnOff.ON + ) + + async def async_turn_off(self): + await self.appliance.async_set_erd_value( + self._power_status_erd_code, ErdOnOff.OFF + ) \ No newline at end of file diff --git a/custom_components/ge_home/entities/dehumidifier/__init__.py b/custom_components/ge_home/entities/dehumidifier/__init__.py new file mode 100644 index 0000000..f68fa50 --- /dev/null +++ b/custom_components/ge_home/entities/dehumidifier/__init__.py @@ -0,0 +1,2 @@ +from .dehumidifier import GeDehumidifier +from .dehumidifier_fan_speed_sensor import GeDehumidifierFanSpeedSensor \ No newline at end of file diff --git a/custom_components/ge_home/entities/dehumidifier/const.py b/custom_components/ge_home/entities/dehumidifier/const.py new file mode 100644 index 0000000..1e5a8b3 --- /dev/null +++ b/custom_components/ge_home/entities/dehumidifier/const.py @@ -0,0 +1,3 @@ +SMART_DRY = "Smart Dry" +DEFAULT_MIN_HUMIDITY = 35 +DEFAULT_MAX_HUMIDITY = 80 \ No newline at end of file diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier.py new file mode 100644 index 0000000..1ed2755 --- /dev/null +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier.py @@ -0,0 +1,74 @@ +"""GE Home Dehumidifier""" +import logging + +from homeassistant.components.humidifier import HumidifierDeviceClass +from homeassistant.components.humidifier.const import HumidifierEntityFeature + +from gehomesdk import ErdCode, DehumidifierTargetRange + +from ...devices import ApplianceApi +from ..common import GeHumidifier +from .const import * +from .dehumidifier_fan_options import DehumidifierFanSettingOptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class GeDehumidifier(GeHumidifier): + """GE Dehumidifier""" + + icon = "mdi:air-humidifier" + + def __init__(self, api: ApplianceApi): + + #try to get the range + range: DehumidifierTargetRange = api.try_get_erd_value(ErdCode.DHUM_TARGET_HUMIDITY_RANGE) + low = DEFAULT_MIN_HUMIDITY if range is None else range.min_humidity + high = DEFAULT_MAX_HUMIDITY if range is None else range.max_humidity + + #try to get the fan mode and determine feature + mode = api.try_get_erd_value(ErdCode.AC_FAN_SETTING) + self._has_fan = mode is not None + self._mode_converter = DehumidifierFanSettingOptionsConverter() + + #initialize the dehumidifier + super().__init__(api, + HumidifierDeviceClass.DEHUMIDIFIER, + ErdCode.AC_POWER_STATUS, + ErdCode.DHUM_TARGET_HUMIDITY, + ErdCode.DHUM_CURRENT_HUMIDITY, + low, + high + ) + + @property + def supported_features(self) -> HumidifierEntityFeature: + if self._has_fan: + return HumidifierEntityFeature(HumidifierEntityFeature.MODES) + else: + return HumidifierEntityFeature(0) + + @property + def mode(self) -> str | None: + if not self._has_fan: + raise NotImplementedError() + + return self._mode_converter.to_option_string( + self.appliance.get_erd_value(ErdCode.AC_FAN_SETTING) + ) + + @property + def available_modes(self) -> list[str] | None: + if not self._has_fan: + raise NotImplementedError() + + return self._mode_converter.options + + async def async_set_mode(self, mode: str) -> None: + if not self._has_fan: + raise NotImplementedError() + + """Change the selected mode.""" + _LOGGER.debug(f"Setting mode from {self.mode} to {mode}") + + new_state = self._mode_converter.from_option_string(mode) + await self.appliance.async_set_erd_value(ErdCode.AC_FAN_SETTING, new_state) diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py new file mode 100644 index 0000000..2d14832 --- /dev/null +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py @@ -0,0 +1,29 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdAcFanSetting +from ..common import OptionsConverter +from .const import SMART_DRY + +_LOGGER = logging.getLogger(__name__) + +class DehumidifierFanSettingOptionsConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [SMART_DRY] + [i.stringify() for i in [ErdAcFanSetting.LOW, ErdAcFanSetting.MED, ErdAcFanSetting.HIGH]] + + def from_option_string(self, value: str) -> Any: + try: + if value == SMART_DRY: + return ErdAcFanSetting.DEFAULT + return ErdAcFanSetting[value.upper()] + except: + _LOGGER.warn(f"Could not set fan setting to {value.upper()}") + return ErdAcFanSetting.DEFAULT + def to_option_string(self, value: ErdAcFanSetting) -> Optional[str]: + try: + if value is not None: + return SMART_DRY if value == ErdAcFanSetting.DEFAULT else value.stringify() + except: + pass + return SMART_DRY diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py new file mode 100644 index 0000000..dd8424a --- /dev/null +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py @@ -0,0 +1,40 @@ +from ...devices import ApplianceApi +from ..common import GeErdSensor +from .dehumidifier_fan_options import DehumidifierFanSettingOptionsConverter +from gehomesdk import ErdCodeType, ErdCodeClass, ErdDataType, ErdAcFanSetting + +class GeDehumidifierFanSpeedSensor(GeErdSensor): + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_override: str = None, + icon_override: str = None, + device_class_override: str = None, + state_class_override: str = None, + uom_override: str = None, + data_type_override: ErdDataType = None + ): + + super().__init__( + api, + erd_code, + erd_override, + icon_override, + device_class_override, + state_class_override, + uom_override, + data_type_override + ) + + self._converter = DehumidifierFanSettingOptionsConverter() + + @property + def native_value(self): + try: + value: ErdAcFanSetting = self.appliance.get_erd_value(self.erd_code) + return self._converter.to_option_string(value) + except KeyError: + return None + + diff --git a/custom_components/ge_home/entities/oven/__init__.py b/custom_components/ge_home/entities/oven/__init__.py index e4166e8..6ef1066 100644 --- a/custom_components/ge_home/entities/oven/__init__.py +++ b/custom_components/ge_home/entities/oven/__init__.py @@ -1,3 +1,4 @@ from .ge_oven import GeOven from .ge_oven_light_level_select import GeOvenLightLevelSelect +from .ge_oven_warming_state_select import GeOvenWarmingStateSelect from .const import UPPER_OVEN, LOWER_OVEN \ No newline at end of file diff --git a/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py b/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py new file mode 100644 index 0000000..e10e2de --- /dev/null +++ b/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py @@ -0,0 +1,56 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCodeType, ErdOvenWarmingState +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class OvenWarmingStateOptionsConverter(OptionsConverter): + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdOvenWarmingState] + def from_option_string(self, value: str) -> Any: + try: + return ErdOvenWarmingState[value.upper()] + except: + _LOGGER.warn(f"Could not set Oven warming state to {value.upper()}") + return ErdOvenWarmingState.OFF + def to_option_string(self, value: ErdOvenWarmingState) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdOvenWarmingState.OFF.stringify() + +class GeOvenWarmingStateSelect(GeErdSelect): + + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): + #check to see if we have a status + value: ErdOvenWarmingState = api.try_get_erd_value(erd_code) + self._has_status = value is not None and value != ErdOvenWarmingState.NOT_AVAILABLE + self._assumed_state = ErdOvenWarmingState.OFF + + super().__init__(api, erd_code, OvenWarmingStateOptionsConverter(self._availability), erd_override=erd_override) + + @property + def assumed_state(self) -> bool: + return not self._has_status + + @property + def current_option(self): + if self.assumed_state: + return self._assumed_state + + return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + _LOGGER.debug(f"Setting select from {self.current_option} to {option}") + + new_state = self._converter.from_option_string(option) + await self.appliance.async_set_erd_value(self.erd_code, new_state) + self._assumed_state = new_state + \ No newline at end of file diff --git a/custom_components/ge_home/humidifier.py b/custom_components/ge_home/humidifier.py new file mode 100644 index 0000000..4d1ed11 --- /dev/null +++ b/custom_components/ge_home/humidifier.py @@ -0,0 +1,36 @@ +"""GE Home Humidifier Entities""" +import logging +from typing import Callable + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers import entity_registry as er + +from .entities import GeHumidifier +from .const import DOMAIN +from .devices import ApplianceApi +from .update_coordinator import GeHomeUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): + """GE Home Water Heaters.""" + _LOGGER.debug('Adding GE "Humidifiers"') + coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + registry = er.async_get(hass) + + @callback + def async_devices_discovered(apis: list[ApplianceApi]): + _LOGGER.debug(f'Found {len(apis):d} appliance APIs') + entities = [ + entity + for api in apis + for entity in api.entities + if isinstance(entity, GeHumidifier) + if not registry.async_is_registered(entity.entity_id) + ] + _LOGGER.debug(f'Found {len(entities):d} unregistered humidifiers') + async_add_entities(entities) + + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index e437f04..969c1f8 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -2,8 +2,10 @@ "domain": "ge_home", "name": "GE Home (SmartHQ)", "config_flow": true, + "integration_type": "hub", + "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.10","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.23","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.7" + "version": "0.6.8" } diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 041c1c9..b846bb4 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -35,7 +35,18 @@ ) 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", + "water_heater", + "select", + "climate", + "light", + "button", + "number", + "humidifier" +] _LOGGER = logging.getLogger(__name__) @@ -252,7 +263,7 @@ def shutdown(self, event) -> None: _LOGGER.info("ge_home shutting down") if self.client: self.client.clear_event_handlers() - self.client.disconnect() + self._hass.loop.create_task(self.client.disconnect()) async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]): """Let HA know there's new state.""" diff --git a/info.md b/info.md index 11a4b3b..aec528d 100644 --- a/info.md +++ b/info.md @@ -69,6 +69,13 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '0.6.8'.split('.') | map('int') %} +- Added Dehumidifier (#114) +- Added oven drawer sensors +- Added oven current state sensors (#175) +- Added descriptors to manifest (#181) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.7'.split('.') | map('int') %} - Added OIM descaling sensor (#154) {% endif %} @@ -116,6 +123,12 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.8'.split('.') | map('int') %} +- Bugfix: Fixed issue with oven lights (#174) +- Bugfix: Fixed issues with dual dishwasher (#161) +- Bugfix: Fixed disconnection issue (#169) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.7'.split('.') | map('int') %} - Bugfix: fixed issues with dishwasher (#155) {% endif %} From 264c431ef9726ccf72b99db1155d138cff4dd558 Mon Sep 17 00:00:00 2001 From: Nathan Fiscus Date: Sun, 26 Nov 2023 13:01:10 -0700 Subject: [PATCH 263/338] Set device class on day flow for water softeners (#207) This should allow sensor to be added to the energy dashboard. --- custom_components/ge_home/devices/water_softener.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/water_softener.py b/custom_components/ge_home/devices/water_softener.py index a730471..a0afa83 100644 --- a/custom_components/ge_home/devices/water_softener.py +++ b/custom_components/ge_home/devices/water_softener.py @@ -27,7 +27,7 @@ def get_all_entities(self) -> List[Entity]: 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_FILTER_DAY_USAGE, device_class_override="water"), GeErdSensor(self, ErdCode.WH_SOFTENER_ERROR_CODE, icon_override="mdi:alert-circle"), GeErdBinarySensor(self, ErdCode.WH_SOFTENER_LOW_SALT, icon_on_override="mdi:alert", icon_off_override="mdi:grain"), GeErdSensor(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_STATE, icon_override="mdi:state-machine"), From 1d1ff7c9901d0cc9cfdb3790a2fb0e7fd0dfb8dd Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 26 Nov 2023 17:45:33 -0500 Subject: [PATCH 264/338] - removed references to _hass in update coordinator - removed hass attribute in ge_entity - version bump --- .../ge_home/entities/common/ge_entity.py | 1 - custom_components/ge_home/manifest.json | 2 +- custom_components/ge_home/update_coordinator.py | 14 ++++++-------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_entity.py b/custom_components/ge_home/entities/common/ge_entity.py index 34f4037..f20eb91 100644 --- a/custom_components/ge_home/entities/common/ge_entity.py +++ b/custom_components/ge_home/entities/common/ge_entity.py @@ -10,7 +10,6 @@ class GeEntity: def __init__(self, api: ApplianceApi): self._api = api - self.hass = None @property def unique_id(self) -> str: diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 969c1f8..f994893 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==0.5.23","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.8" + "version": "0.6.9" } diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index b846bb4..3b5ab5d 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -55,7 +55,8 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator): def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Set up the GeHomeUpdateCoordinator class.""" - self._hass = hass + super().__init__(hass, _LOGGER, name=DOMAIN) + self._config_entry = config_entry self._username = config_entry.data[CONF_USERNAME] self._password = config_entry.data[CONF_PASSWORD] @@ -64,8 +65,6 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self._reset_initialization() - super().__init__(hass, _LOGGER, name=DOMAIN) - def _reset_initialization(self): self.client = None # type: Optional[GeWebsocketClient] @@ -161,8 +160,7 @@ async def get_client(self) -> GeWebsocketClient: finally: self._reset_initialization() - loop = self._hass.loop - self.client = self.create_ge_client(event_loop=loop) + self.client = self.create_ge_client(event_loop=self.hass.loop) return self.client async def async_setup(self): @@ -201,9 +199,9 @@ async def async_start_client(self): async def async_begin_session(self): """Begins the ge_home session.""" _LOGGER.debug("Beginning session") - session = self._hass.helpers.aiohttp_client.async_get_clientsession() + session = self.hass.helpers.aiohttp_client.async_get_clientsession() await self.client.async_get_credentials(session) - fut = asyncio.ensure_future(self.client.async_run_client(), loop=self._hass.loop) + fut = asyncio.ensure_future(self.client.async_run_client(), loop=self.hass.loop) _LOGGER.debug("Client running") return fut @@ -263,7 +261,7 @@ def shutdown(self, event) -> None: _LOGGER.info("ge_home shutting down") if self.client: self.client.clear_event_handlers() - self._hass.loop.create_task(self.client.disconnect()) + self.hass.loop.create_task(self.client.disconnect()) async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]): """Let HA know there's new state.""" From ff983cc594058ee465bd37bc602bc6604f623350 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 26 Nov 2023 18:25:58 -0500 Subject: [PATCH 265/338] bumped gehomesdk version requirement --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index f994893..4e09966 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.23","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.24","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.9" } From 19c05fc83d1109e94a939f472c3454f54bead55b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 26 Nov 2023 20:23:49 -0500 Subject: [PATCH 266/338] - added fridge/freezer controls --- custom_components/ge_home/devices/fridge.py | 16 ++++++- .../ge_home/entities/common/ge_erd_switch.py | 2 +- .../ge_home/entities/fridge/__init__.py | 3 +- .../fridge/ge_fridge_ice_control_switch.py | 47 +++++++++++++++++++ custom_components/ge_home/manifest.json | 2 +- 5 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index 24f0657..8514894 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -31,7 +31,8 @@ GeDispenser, GeErdPropertySensor, GeErdPropertyBinarySensor, - ConvertableDrawerModeOptionsConverter + ConvertableDrawerModeOptionsConverter, + GeFridgeIceControlSwitch ) _LOGGER = logging.getLogger(__name__) @@ -61,7 +62,10 @@ def get_all_entities(self) -> List[Entity]: proximity_light: ErdOnOff = self.try_get_erd_value(ErdCode.PROXIMITY_LIGHT) display_mode: ErdOnOff = self.try_get_erd_value(ErdCode.DISPLAY_MODE) lockout_mode: ErdOnOff = self.try_get_erd_value(ErdCode.LOCKOUT_MODE) - + turbo_cool: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_COOL_STATUS) + turbo_freeze: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_FREEZE_STATUS) + ice_boost: ErdOnOff = self.try_get_erd_value(ErdCode.FRIDGE_ICE_BOOST) + units = self.hass.config.units # Common entities @@ -80,8 +84,11 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "fridge"), GeFridge(self), ]) + if turbo_cool is not None: + fridge_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST)) if(ice_maker_control and ice_maker_control.status_fridge != ErdOnOff.NA): fridge_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_fridge")) + fridge_entities.append(GeFridgeIceControlSwitch(self, "fridge")) if(water_filter and water_filter != ErdFilterStatus.NA): fridge_entities.append(GeErdSensor(self, ErdCode.WATER_FILTER_STATUS)) if(air_filter and air_filter != ErdFilterStatus.NA): @@ -105,8 +112,13 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "freezer"), GeFreezer(self), ]) + if turbo_freeze is not None: + freezer_entities.append(GeErdSwitch(self, ErdCode.TURBO_FREEZE_STATUS)) + if ice_boost is not None: + freezer_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST)) if(ice_maker_control and ice_maker_control.status_freezer != ErdOnOff.NA): freezer_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_freezer")) + freezer_entities.append(GeFridgeIceControlSwitch(self, "freezer")) if(ice_bucket_status and ice_bucket_status.is_present_freezer): freezer_entities.append(GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_freezer")) diff --git a/custom_components/ge_home/entities/common/ge_erd_switch.py b/custom_components/ge_home/entities/common/ge_erd_switch.py index cf39f9a..0fb3703 100644 --- a/custom_components/ge_home/entities/common/ge_erd_switch.py +++ b/custom_components/ge_home/entities/common/ge_erd_switch.py @@ -29,5 +29,5 @@ async def async_turn_on(self, **kwargs): async def async_turn_off(self, **kwargs): """Turn the switch off.""" - _LOGGER.debug(f"Turning on {self.unique_id}") + _LOGGER.debug(f"Turning off {self.unique_id}") await self.appliance.async_set_erd_value(self.erd_code, self._converter.false_value()) diff --git a/custom_components/ge_home/entities/fridge/__init__.py b/custom_components/ge_home/entities/fridge/__init__.py index b277fcf..2d14761 100644 --- a/custom_components/ge_home/entities/fridge/__init__.py +++ b/custom_components/ge_home/entities/fridge/__init__.py @@ -1,4 +1,5 @@ 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 +from .convertable_drawer_mode_options import ConvertableDrawerModeOptionsConverter +from .ge_fridge_ice_control_switch import GeFridgeIceControlSwitch \ No newline at end of file diff --git a/custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py b/custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py new file mode 100644 index 0000000..f86c59a --- /dev/null +++ b/custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py @@ -0,0 +1,47 @@ +import logging +from gehomesdk import ErdCode, IceMakerControlStatus, ErdOnOff + +from ...devices import ApplianceApi +from ..common import GeErdSwitch, BoolConverter + +_LOGGER = logging.getLogger(__name__) + +class GeFridgeIceControlSwitch(GeErdSwitch): + def __init__(self, api: ApplianceApi, control_type: str): + super().__init__(api, ErdCode.ICE_MAKER_CONTROL, BoolConverter()) + self._control_type = control_type + + @property + def control_status(self) -> IceMakerControlStatus: + return self.appliance.get_erd_value(ErdCode.ICE_MAKER_CONTROL) + + @property + def is_on(self) -> bool: + if self._control_type == "fridge": + return self.control_status.status_fridge == ErdOnOff.ON + else: + return self.control_status.status_freezer == ErdOnOff.ON + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + _LOGGER.debug(f"Turning on {self.unique_id}") + + old_status = self.control_status + if self._control_type == "fridge": + new_status = IceMakerControlStatus(ErdOnOff.ON, old_status.status_freezer) + else: + new_status = IceMakerControlStatus(old_status.status_fridge, ErdOnOff.ON) + + await self.appliance.async_set_erd_value(self.erd_code, new_status) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + _LOGGER.debug(f"Turning off {self.unique_id}") + + old_status = self.control_status + if self._control_type == "fridge": + new_status = IceMakerControlStatus(ErdOnOff.OFF, old_status.status_freezer) + else: + new_status = IceMakerControlStatus(old_status.status_fridge, ErdOnOff.OFF) + + await self.appliance.async_set_erd_value(self.erd_code, new_status) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 4e09966..9be6eb7 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.24","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.25","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.9" } From c665ac3fd98b9da969fd0a871eda4c7bf434d316 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 26 Nov 2023 20:52:18 -0500 Subject: [PATCH 267/338] - documentation updates --- CHANGELOG.md | 4 ++++ info.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a40d27..c6e99ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.9 + +- Added additional fridge controls [#200] + ## 0.6.8 - Added Dehumidifier [#114] diff --git a/info.md b/info.md index aec528d..10be149 100644 --- a/info.md +++ b/info.md @@ -69,6 +69,10 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '0.6.9'.split('.') | map('int') %} +- Added additional fridge controls (#200) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.8'.split('.') | map('int') %} - Added Dehumidifier (#114) - Added oven drawer sensors From d2329e6025b10efe38eeffd7e16e03e74dd250b3 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 27 Nov 2023 21:33:04 -0500 Subject: [PATCH 268/338] - fixed error when reloading integration --- custom_components/ge_home/binary_sensor.py | 4 +++- custom_components/ge_home/button.py | 4 +++- custom_components/ge_home/climate.py | 4 +++- custom_components/ge_home/humidifier.py | 4 +++- custom_components/ge_home/light.py | 4 +++- custom_components/ge_home/number.py | 4 +++- custom_components/ge_home/select.py | 4 +++- custom_components/ge_home/sensor.py | 6 ++++-- custom_components/ge_home/switch.py | 4 +++- custom_components/ge_home/update_coordinator.py | 12 +++++++++++- custom_components/ge_home/water_heater.py | 4 +++- 11 files changed, 42 insertions(+), 12 deletions(-) diff --git a/custom_components/ge_home/binary_sensor.py b/custom_components/ge_home/binary_sensor.py index fb808f8..9a5f690 100644 --- a/custom_components/ge_home/binary_sensor.py +++ b/custom_components/ge_home/binary_sensor.py @@ -36,4 +36,6 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered binary sensors') async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/button.py b/custom_components/ge_home/button.py index cfbb843..cff4818 100644 --- a/custom_components/ge_home/button.py +++ b/custom_components/ge_home/button.py @@ -34,4 +34,6 @@ def async_devices_discovered(apis: list[ApplianceApi]): _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 + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/climate.py b/custom_components/ge_home/climate.py index 8255fb8..d562cf2 100644 --- a/custom_components/ge_home/climate.py +++ b/custom_components/ge_home/climate.py @@ -36,4 +36,6 @@ def async_devices_discovered(apis: list[ApplianceApi]): _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 + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/humidifier.py b/custom_components/ge_home/humidifier.py index 4d1ed11..729a803 100644 --- a/custom_components/ge_home/humidifier.py +++ b/custom_components/ge_home/humidifier.py @@ -33,4 +33,6 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered humidifiers') async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/light.py b/custom_components/ge_home/light.py index b652d02..afe49d5 100644 --- a/custom_components/ge_home/light.py +++ b/custom_components/ge_home/light.py @@ -37,4 +37,6 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f"Found {len(entities):d} unregistered lights") async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/number.py b/custom_components/ge_home/number.py index 2b4213c..a44b820 100644 --- a/custom_components/ge_home/number.py +++ b/custom_components/ge_home/number.py @@ -34,4 +34,6 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregisterd numbers') async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/select.py b/custom_components/ge_home/select.py index 613b03b..842faef 100644 --- a/custom_components/ge_home/select.py +++ b/custom_components/ge_home/select.py @@ -37,4 +37,6 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f"Found {len(entities):d} unregistered selects") async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/sensor.py b/custom_components/ge_home/sensor.py index 9732dbf..d0092d5 100644 --- a/custom_components/ge_home/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -47,8 +47,10 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered sensors') async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) - + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + # register set_timer entity service platform.async_register_entity_service( SERVICE_SET_TIMER, diff --git a/custom_components/ge_home/switch.py b/custom_components/ge_home/switch.py index 3aa6f11..688f96b 100644 --- a/custom_components/ge_home/switch.py +++ b/custom_components/ge_home/switch.py @@ -33,4 +33,6 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered switches') async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 3b5ab5d..0a61eae 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -3,7 +3,7 @@ import asyncio import async_timeout import logging -from typing import Any, Dict, Iterable, Optional, Tuple +from typing import Any, Callable, Dict, Iterable, Optional, Tuple, List from gehomesdk import ( EVENT_APPLIANCE_INITIAL_UPDATE, @@ -62,6 +62,7 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self._password = config_entry.data[CONF_PASSWORD] self._region = config_entry.data[CONF_REGION] self._appliance_apis = {} # type: Dict[str, ApplianceApi] + self._signal_remove_callbacks = [] # type: List[Callable] self._reset_initialization() @@ -149,6 +150,9 @@ def maybe_add_appliance_api(self, appliance: GeAppliance): api = self.appliance_apis[mac_addr] api.appliance = appliance + def add_signal_remove_callback(self, cb: Callable): + self._signal_remove_callbacks.append(cb) + async def get_client(self) -> GeWebsocketClient: """Get a new GE Websocket client.""" if self.client: @@ -209,6 +213,12 @@ async def async_reset(self): """Resets the coordinator.""" _LOGGER.debug("resetting the coordinator") entry = self._config_entry + + # remove all the callbacks for this coordinator + for c in self._signal_remove_callbacks: + c() + self._signal_remove_callbacks.clear() + unload_ok = all( await asyncio.gather( *[ diff --git a/custom_components/ge_home/water_heater.py b/custom_components/ge_home/water_heater.py index 19e8b4d..1a855c1 100644 --- a/custom_components/ge_home/water_heater.py +++ b/custom_components/ge_home/water_heater.py @@ -35,4 +35,6 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered water heaters') async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) From fbb4db3067c09a0fdcad56a4dc59c38530226f52 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 28 Nov 2023 19:22:37 -0500 Subject: [PATCH 269/338] - added additional update error handling --- .../ge_home/entities/common/ge_entity.py | 13 +++++++++++ .../ge_home/update_coordinator.py | 23 ++++++++++++++----- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_entity.py b/custom_components/ge_home/entities/common/ge_entity.py index f20eb91..6104d20 100644 --- a/custom_components/ge_home/entities/common/ge_entity.py +++ b/custom_components/ge_home/entities/common/ge_entity.py @@ -10,6 +10,7 @@ class GeEntity: def __init__(self, api: ApplianceApi): self._api = api + self._added = False @property def unique_id(self) -> str: @@ -55,6 +56,18 @@ def icon(self) -> Optional[str]: def device_class(self) -> Optional[str]: return self._get_device_class() + @property + def added(self) -> bool: + return self._added + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self._added = True + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + self._added = False + def _stringify(self, value: any, **kwargs) -> Optional[str]: if isinstance(value, timedelta): return str(value)[:-3] if value else "" diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 0a61eae..6ad3d40 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -22,6 +22,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -280,24 +281,34 @@ async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any] try: api = self.appliance_apis[appliance.mac_addr] except KeyError: + _LOGGER.warn(f"Could not find appliance {appliance.mac_addr} in known device list.") return - - for entity in api.entities: - if entity.enabled: - _LOGGER.debug(f"Updating {entity} ({entity.unique_id}, {entity.entity_id})") - entity.async_write_ha_state() + + self._update_entity_state(api.entities) async def _refresh_ha_state(self): entities = [ entity for api in self.appliance_apis.values() for entity in api.entities ] + + self._update_entity_state(entities) + + def _update_entity_state(entities: List[Entity]): + from .entities import GeEntity for entity in entities: + # if this is a GeEntity, check if it's been added + #if not, don't try to refresh this entity + if isinstance(entity, GeEntity): + gee: GeEntity = entity + if not gee.added: + _LOGGER.debug(f"Entity {entity} ({entity.unique_id}, {entity.entity_id}) not yet added, skipping update...") + continue if entity.enabled: try: _LOGGER.debug(f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}") entity.async_write_ha_state() except: - _LOGGER.debug(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}") + _LOGGER.warn(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}", exc_info=1) @property def all_appliances_updated(self) -> bool: From 75ff9243fba9e9f9ddf3719d4c4c940b68347343 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 28 Nov 2023 20:11:14 -0500 Subject: [PATCH 270/338] - fixed coordinator bug --- custom_components/ge_home/update_coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 6ad3d40..5292b11 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -293,7 +293,7 @@ async def _refresh_ha_state(self): self._update_entity_state(entities) - def _update_entity_state(entities: List[Entity]): + def _update_entity_state(self, entities: List[Entity]): from .entities import GeEntity for entity in entities: # if this is a GeEntity, check if it's been added From c2025f97e752a18e7b34a6794cae19f5d02603c0 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 3 Dec 2023 19:08:09 -0500 Subject: [PATCH 271/338] - added additional logic to handle non-standard ready conditions --- custom_components/ge_home/binary_sensor.py | 14 ++++++++++---- custom_components/ge_home/button.py | 12 +++++++++--- custom_components/ge_home/climate.py | 12 +++++++++--- custom_components/ge_home/humidifier.py | 12 +++++++++--- custom_components/ge_home/light.py | 12 +++++++++--- custom_components/ge_home/number.py | 12 +++++++++--- custom_components/ge_home/select.py | 12 +++++++++--- custom_components/ge_home/sensor.py | 12 +++++++++--- custom_components/ge_home/switch.py | 12 +++++++++--- custom_components/ge_home/update_coordinator.py | 6 +++++- custom_components/ge_home/water_heater.py | 12 +++++++++--- 11 files changed, 96 insertions(+), 32 deletions(-) diff --git a/custom_components/ge_home/binary_sensor.py b/custom_components/ge_home/binary_sensor.py index 9a5f690..0a35ef5 100644 --- a/custom_components/ge_home/binary_sensor.py +++ b/custom_components/ge_home/binary_sensor.py @@ -35,7 +35,13 @@ def async_devices_discovered(apis: list[ApplianceApi]): ] _LOGGER.debug(f'Found {len(entities):d} unregistered binary sensors') async_add_entities(entities) - - # add the ready signal and register the remove callback - coordinator.add_signal_remove_callback( - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/button.py b/custom_components/ge_home/button.py index cff4818..748ee6b 100644 --- a/custom_components/ge_home/button.py +++ b/custom_components/ge_home/button.py @@ -34,6 +34,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered buttons ') async_add_entities(entities) - # add the ready signal and register the remove callback - coordinator.add_signal_remove_callback( - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/climate.py b/custom_components/ge_home/climate.py index d562cf2..4512b61 100644 --- a/custom_components/ge_home/climate.py +++ b/custom_components/ge_home/climate.py @@ -36,6 +36,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered climate entities') async_add_entities(entities) - # add the ready signal and register the remove callback - coordinator.add_signal_remove_callback( - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/humidifier.py b/custom_components/ge_home/humidifier.py index 729a803..68aa896 100644 --- a/custom_components/ge_home/humidifier.py +++ b/custom_components/ge_home/humidifier.py @@ -33,6 +33,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered humidifiers') async_add_entities(entities) - # add the ready signal and register the remove callback - coordinator.add_signal_remove_callback( - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/light.py b/custom_components/ge_home/light.py index afe49d5..ba2a69c 100644 --- a/custom_components/ge_home/light.py +++ b/custom_components/ge_home/light.py @@ -37,6 +37,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f"Found {len(entities):d} unregistered lights") async_add_entities(entities) - # add the ready signal and register the remove callback - coordinator.add_signal_remove_callback( - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/number.py b/custom_components/ge_home/number.py index a44b820..d864c09 100644 --- a/custom_components/ge_home/number.py +++ b/custom_components/ge_home/number.py @@ -34,6 +34,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregisterd numbers') async_add_entities(entities) - # add the ready signal and register the remove callback - coordinator.add_signal_remove_callback( - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/select.py b/custom_components/ge_home/select.py index 842faef..158af27 100644 --- a/custom_components/ge_home/select.py +++ b/custom_components/ge_home/select.py @@ -37,6 +37,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f"Found {len(entities):d} unregistered selects") async_add_entities(entities) - # add the ready signal and register the remove callback - coordinator.add_signal_remove_callback( - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/sensor.py b/custom_components/ge_home/sensor.py index d0092d5..0bf7ba6 100644 --- a/custom_components/ge_home/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -47,9 +47,15 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered sensors') async_add_entities(entities) - # add the ready signal and register the remove callback - coordinator.add_signal_remove_callback( - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) # register set_timer entity service platform.async_register_entity_service( diff --git a/custom_components/ge_home/switch.py b/custom_components/ge_home/switch.py index 688f96b..7c339ae 100644 --- a/custom_components/ge_home/switch.py +++ b/custom_components/ge_home/switch.py @@ -33,6 +33,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered switches') async_add_entities(entities) - # add the ready signal and register the remove callback - coordinator.add_signal_remove_callback( - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 5292b11..944c229 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -112,7 +112,11 @@ def appliance_apis(self) -> Dict[str, ApplianceApi]: @property def signal_ready(self) -> str: """Event specific per entry to signal readiness""" - return f"{DOMAIN}-ready-{self._config_entry.entry_id}" + return f"{DOMAIN}-ready-{self._config_entry.entry_id}" + + @property + def initialized(self) -> bool: + return self._init_done @property def online(self) -> bool: diff --git a/custom_components/ge_home/water_heater.py b/custom_components/ge_home/water_heater.py index 1a855c1..9bb0a8e 100644 --- a/custom_components/ge_home/water_heater.py +++ b/custom_components/ge_home/water_heater.py @@ -35,6 +35,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered water heaters') async_add_entities(entities) - # add the ready signal and register the remove callback - coordinator.add_signal_remove_callback( - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) From c9619ca6e41facbc2710a4b6bd4ad47d3156cc0a Mon Sep 17 00:00:00 2001 From: Jason Foley Date: Sat, 9 Dec 2023 08:26:50 -0500 Subject: [PATCH 272/338] Set device class on day flow for water filters (#209) --- custom_components/ge_home/devices/water_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/water_filter.py b/custom_components/ge_home/devices/water_filter.py index d466f67..7cebd6a 100644 --- a/custom_components/ge_home/devices/water_filter.py +++ b/custom_components/ge_home/devices/water_filter.py @@ -30,7 +30,7 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE, device_class_override="water"), GeErdPropertySensor(self, ErdCode.WH_FILTER_LIFE_REMAINING, "life_remaining"), GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"), ] From 4249141be11f3dbb2b7952c75f2567cff36fc1a4 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 9 Dec 2023 09:59:20 -0500 Subject: [PATCH 273/338] - bumped sdk version requirement --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 9be6eb7..33dd56f 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.25","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.26","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "0.6.9" } From 6e651bcc40770be689e47eb823c7fa5dd9cef3df Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 9 Dec 2023 10:45:12 -0500 Subject: [PATCH 274/338] added new style cooktop status support (#159) --- custom_components/ge_home/devices/oven.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index c83ce8d..2bd7bbd 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -100,18 +100,27 @@ def get_all_entities(self) -> List[Entity]: if oven_config.has_warming_drawer and warm_drawer is not None: oven_entities.append(GeErdSensor(self, ErdCode.WARMING_DRAWER_STATE)) - if cooktop_config == ErdCooktopConfig.PRESENT: + if cooktop_config == ErdCooktopConfig.PRESENT: + # attempt to get the cooktop status using legacy status + cooktop_status_erd = ErdCode.COOKTOP_STATUS cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) + + # if we didn't get it, try using the new version + if cooktop_status is None: + cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT + cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) + + # if we got a status through either mechanism, we can add the entities if cooktop_status is not None: - cooktop_entities.append(GeErdBinarySensor(self, ErdCode.COOKTOP_STATUS)) + cooktop_entities.append(GeErdBinarySensor(self, cooktop_status_erd)) for (k, v) in cooktop_status.burners.items(): if v.exists: prop = self._camel_to_snake(k) - cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, prop+".on")) - cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, prop+".synchronized")) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on")) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized")) if not v.on_off_only: - cooktop_entities.append(GeErdPropertySensor(self, ErdCode.COOKTOP_STATUS, prop+".power_pct", icon_override="mdi:fire", device_class_override=DEVICE_CLASS_POWER_FACTOR, data_type_override=ErdDataType.INT)) + cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, 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 From 70dfdb915ecb3e5ba114948e04d4f3eb8050e575 Mon Sep 17 00:00:00 2001 From: myztillx <33730898+myztillx@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:20:20 -0500 Subject: [PATCH 275/338] Update deprecated constants or remove unused ones (#219) --- custom_components/ge_home/devices/cooktop.py | 4 +- custom_components/ge_home/devices/fridge.py | 4 +- custom_components/ge_home/devices/oven.py | 4 +- .../ge_home/entities/ac/fan_mode_options.py | 5 --- .../ge_home/entities/ac/ge_biac_climate.py | 24 +++++------ .../ge_home/entities/ac/ge_pac_climate.py | 36 ++++++---------- .../ge_home/entities/ac/ge_sac_climate.py | 43 ++++++++----------- .../ge_home/entities/ac/ge_wac_climate.py | 24 +++++------ .../ge_home/entities/advantium/const.py | 9 ++-- .../entities/advantium/ge_advantium.py | 5 ++- .../entities/ccm/ge_ccm_brew_temperature.py | 1 - .../ge_home/entities/common/ge_climate.py | 33 ++++++-------- .../ge_home/entities/common/ge_erd_number.py | 4 +- .../ge_home/entities/common/ge_erd_sensor.py | 38 +++++++--------- .../entities/common/ge_water_heater.py | 9 ++-- .../ge_home/entities/fridge/const.py | 7 +-- .../fridge/convertable_drawer_mode_options.py | 4 +- .../entities/fridge/ge_abstract_fridge.py | 6 +-- .../ge_home/entities/fridge/ge_dispenser.py | 6 +-- .../ge_home/entities/oven/const.py | 7 +-- .../ge_home/entities/oven/ge_oven.py | 8 ++-- .../entities/water_heater/ge_water_heater.py | 11 ++--- 22 files changed, 116 insertions(+), 176 deletions(-) diff --git a/custom_components/ge_home/devices/cooktop.py b/custom_components/ge_home/devices/cooktop.py index 6fb3453..d681443 100644 --- a/custom_components/ge_home/devices/cooktop.py +++ b/custom_components/ge_home/devices/cooktop.py @@ -2,7 +2,7 @@ from typing import List from gehomesdk.erd.erd_data_type import ErdDataType -from homeassistant.const import DEVICE_CLASS_POWER_FACTOR +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.entity import Entity from gehomesdk import ( ErdCode, @@ -44,7 +44,7 @@ def get_all_entities(self) -> List[Entity]: 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)) + cooktop_entities.append(GeErdPropertySensor(self, ErdCode.COOKTOP_STATUS, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) return base_entities + cooktop_entities diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index 8514894..7c7aac8 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -1,5 +1,5 @@ from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM -from homeassistant.const import DEVICE_CLASS_TEMPERATURE +from homeassistant.components.sensor import SensorDeviceClass import logging from typing import List @@ -129,7 +129,7 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "current_temp", device_class_override=SensorDeviceClass.TEMPERATURE, data_type_override=ErdDataType.INT), GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=DEVICE_CLASS_PROBLEM), GeDispenser(self) ]) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index 2bd7bbd..9400991 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -2,7 +2,7 @@ from typing import List from gehomesdk.erd.erd_data_type import ErdDataType -from homeassistant.const import DEVICE_CLASS_POWER_FACTOR +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.entity import Entity from gehomesdk import ( ErdCode, @@ -120,7 +120,7 @@ def get_all_entities(self) -> List[Entity]: cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on")) cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized")) if not v.on_off_only: - cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=DEVICE_CLASS_POWER_FACTOR, data_type_override=ErdDataType.INT)) + cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) return base_entities + oven_entities + cooktop_entities diff --git a/custom_components/ge_home/entities/ac/fan_mode_options.py b/custom_components/ge_home/entities/ac/fan_mode_options.py index 0bf78cd..c24e72f 100644 --- a/custom_components/ge_home/entities/ac/fan_mode_options.py +++ b/custom_components/ge_home/entities/ac/fan_mode_options.py @@ -1,11 +1,6 @@ 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 diff --git a/custom_components/ge_home/entities/ac/ge_biac_climate.py b/custom_components/ge_home/entities/ac/ge_biac_climate.py index f3b7453..5e62428 100644 --- a/custom_components/ge_home/entities/ac/ge_biac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_biac_climate.py @@ -1,11 +1,7 @@ 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 homeassistant.components.climate import HVACMode from gehomesdk import ErdAcOperationMode from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter @@ -16,13 +12,13 @@ class BiacHvacModeOptionsConverter(OptionsConverter): @property def options(self) -> List[str]: - return [HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] + return [HVACMode.AUTO, HVACMode.COOL, HVACMode.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 + HVACMode.AUTO: ErdAcOperationMode.ENERGY_SAVER, + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") @@ -30,14 +26,14 @@ def from_option_string(self, value: str) -> Any: 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 + ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, + ErdAcOperationMode.AUTO: HVACMode.AUTO, + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_COOL + return HVACMode.COOL class GeBiacClimate(GeClimate): """Class for Built-In AC units""" diff --git a/custom_components/ge_home/entities/ac/ge_pac_climate.py b/custom_components/ge_home/entities/ac/ge_pac_climate.py index ba8eb75..558cdc8 100644 --- a/custom_components/ge_home/entities/ac/ge_pac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_pac_climate.py @@ -1,17 +1,7 @@ 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 homeassistant.components.climate import HVACMode from gehomesdk import ErdCode, ErdAcOperationMode, ErdSacAvailableModes, ErdSacTargetTemperatureRange from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter @@ -25,19 +15,19 @@ def __init__(self, available_modes: ErdSacAvailableModes): @property def options(self) -> List[str]: - modes = [HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] + modes = [HVACMode.COOL, HVACMode.FAN_ONLY] if self._available_modes and self._available_modes.has_heat: - modes.append(HVAC_MODE_HEAT) + modes.append(HVACMode.HEAT) if self._available_modes and self._available_modes.has_dry: - modes.append(HVAC_MODE_DRY) + modes.append(HVACMode.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 + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.HEAT: ErdAcOperationMode.HEAT, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY, + HVACMode.DRY: ErdAcOperationMode.DRY }.get(value) except: _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") @@ -45,14 +35,14 @@ def from_option_string(self, value: str) -> Any: 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 + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.HEAT: HVACMode.HEAT, + ErdAcOperationMode.DRY: HVACMode.DRY, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_COOL + return HVACMode.COOL class GePacClimate(GeClimate): """Class for Portable AC units""" diff --git a/custom_components/ge_home/entities/ac/ge_sac_climate.py b/custom_components/ge_home/entities/ac/ge_sac_climate.py index 7c7ed54..6198b01 100644 --- a/custom_components/ge_home/entities/ac/ge_sac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_sac_climate.py @@ -1,16 +1,7 @@ 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 homeassistant.components.climate import HVACMode from gehomesdk import ErdCode, ErdAcOperationMode, ErdSacAvailableModes, ErdSacTargetTemperatureRange from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter @@ -24,21 +15,21 @@ def __init__(self, available_modes: ErdSacAvailableModes): @property def options(self) -> List[str]: - modes = [HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] + modes = [HVACMode.COOL, HVACMode.FAN_ONLY] if self._available_modes and self._available_modes.has_heat: - modes.append(HVAC_MODE_HEAT) - modes.append(HVAC_MODE_AUTO) + modes.append(HVACMode.HEAT) + modes.append(HVACMode.AUTO) if self._available_modes and self._available_modes.has_dry: - modes.append(HVAC_MODE_DRY) + modes.append(HVACMode.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 + HVACMode.AUTO: ErdAcOperationMode.AUTO, + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.HEAT: ErdAcOperationMode.HEAT, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY, + HVACMode.DRY: ErdAcOperationMode.DRY }.get(value) except: _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") @@ -46,16 +37,16 @@ def from_option_string(self, value: str) -> Any: 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 + ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, + ErdAcOperationMode.AUTO: HVACMode.AUTO, + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.HEAT: HVACMode.HEAT, + ErdAcOperationMode.DRY: HVACMode.DRY, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_COOL + return HVACMode.COOL class GeSacClimate(GeClimate): """Class for Split AC units""" diff --git a/custom_components/ge_home/entities/ac/ge_wac_climate.py b/custom_components/ge_home/entities/ac/ge_wac_climate.py index a7b3980..4602af8 100644 --- a/custom_components/ge_home/entities/ac/ge_wac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_wac_climate.py @@ -1,11 +1,7 @@ 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 homeassistant.components.climate import HVACMode from gehomesdk import ErdAcOperationMode from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter @@ -16,13 +12,13 @@ class WacHvacModeOptionsConverter(OptionsConverter): @property def options(self) -> List[str]: - return [HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] + return [HVACMode.AUTO, HVACMode.COOL, HVACMode.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 + HVACMode.AUTO: ErdAcOperationMode.ENERGY_SAVER, + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") @@ -30,14 +26,14 @@ def from_option_string(self, value: str) -> Any: 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 + ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, + ErdAcOperationMode.AUTO: HVACMode.AUTO, + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_COOL + return HVACMode.COOL class GeWacClimate(GeClimate): """Class for Window AC units""" diff --git a/custom_components/ge_home/entities/advantium/const.py b/custom_components/ge_home/entities/advantium/const.py index 7c487be..674dcae 100644 --- a/custom_components/ge_home/entities/advantium/const.py +++ b/custom_components/ge_home/entities/advantium/const.py @@ -1,8 +1,5 @@ -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) +from homeassistant.components.water_heater import WaterHeaterEntityFeature SUPPORT_NONE = 0 -GE_ADVANTIUM_WITH_TEMPERATURE = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) -GE_ADVANTIUM = SUPPORT_OPERATION_MODE +GE_ADVANTIUM_WITH_TEMPERATURE = (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) +GE_ADVANTIUM = WaterHeaterEntityFeature.OPERATION_MODE diff --git a/custom_components/ge_home/entities/advantium/ge_advantium.py b/custom_components/ge_home/entities/advantium/ge_advantium.py index 2d1372b..a92b9ee 100644 --- a/custom_components/ge_home/entities/advantium/ge_advantium.py +++ b/custom_components/ge_home/entities/advantium/ge_advantium.py @@ -15,7 +15,8 @@ ) from gehomesdk.erd.values.advantium.advantium_enums import CookAction, CookMode -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ATTR_TEMPERATURE from ...const import DOMAIN from ...devices import ApplianceApi from ..common import GeAbstractWaterHeater @@ -272,7 +273,7 @@ async def _ensure_operation_mode(self): 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: + if self.temperature_unit == SensorDeviceClass.FAHRENHEIT: return float(target_temp_f) else: return (target_temp_f - 32.0) * (5/9) diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py index 86cac03..c7895d9 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py @@ -2,7 +2,6 @@ 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): diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 7f44edb..c3c4583 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -4,15 +4,10 @@ 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 + UnitOfTemperature, ) +from homeassistant.components.climate import ClimateEntityFeature, HVACMode +from homeassistant.components.water_heater import WaterHeaterEntityFeature from gehomesdk import ErdCode, ErdCodeType, ErdMeasurementUnits, ErdOnOff from ...const import DOMAIN from ...devices import ApplianceApi @@ -22,7 +17,7 @@ _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 +GE_CLIMATE_SUPPORT = WaterHeaterEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE class GeClimate(GeEntity, ClimateEntity): """GE Climate Base Entity (Window AC, Portable AC, etc)""" @@ -83,11 +78,11 @@ def fan_mode_erd_code(self): @property def temperature_unit(self): #appears to always be Fahrenheit internally, hardcode this - return TEMP_FAHRENHEIT + return UnitOfTemperature.FAHRENHEIT #measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) #if measurement_system == ErdMeasurementUnits.METRIC: - # return TEMP_CELSIUS - #return TEMP_FAHRENHEIT + # return UnitOfTemperature.CELSIUS + #return UnitOfTempterature.FAHRENHEIT @property def supported_features(self): @@ -116,30 +111,30 @@ def max_temp(self) -> float: @property def hvac_mode(self): if not self.is_on: - return HVAC_MODE_OFF + return HVACMode.OFF return self._hvac_mode_converter.to_option_string(self.appliance.get_erd_value(self.hvac_mode_erd_code)) @property def hvac_modes(self) -> List[str]: - return [HVAC_MODE_OFF] + self._hvac_mode_converter.options + return [HVACMode.OFF] + self._hvac_mode_converter.options @property def fan_mode(self): - if self.hvac_mode == HVAC_MODE_FAN_ONLY: + if self.hvac_mode == HVACMode.FAN_ONLY: return self._fan_only_fan_mode_converter.to_option_string(self.appliance.get_erd_value(self.fan_mode_erd_code)) return self._fan_mode_converter.to_option_string(self.appliance.get_erd_value(self.fan_mode_erd_code)) @property def fan_modes(self) -> List[str]: - if self.hvac_mode == HVAC_MODE_FAN_ONLY: + if self.hvac_mode == HVACMode.FAN_ONLY: return self._fan_only_fan_mode_converter.options return self._fan_mode_converter.options async def async_set_hvac_mode(self, hvac_mode: str) -> None: _LOGGER.debug(f"Setting HVAC mode from {self.hvac_mode} to {hvac_mode}") if hvac_mode != self.hvac_mode: - if hvac_mode == HVAC_MODE_OFF: + if hvac_mode == HVACMode.OFF: await self.appliance.async_set_erd_value(self.power_status_erd_code, ErdOnOff.OFF) else: #if it's not on, turn it on @@ -156,7 +151,7 @@ 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 + if self.hvac_mode == HVACMode.FAN_ONLY else self._fan_mode_converter ) @@ -185,7 +180,7 @@ 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: + if self.temperature_unit == UnitOfTemperature.FAHRENHEIT: return float(temperature_f) else: return (temperature_f - 32.0) * (5/9) diff --git a/custom_components/ge_home/entities/common/ge_erd_number.py b/custom_components/ge_home/entities/common/ge_erd_number.py index d773ad1..91026dd 100644 --- a/custom_components/ge_home/entities/common/ge_erd_number.py +++ b/custom_components/ge_home/entities/common/ge_erd_number.py @@ -5,7 +5,7 @@ NumberEntity, NumberDeviceClass, ) -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.const import UnitOfTemperature from gehomesdk import ErdCodeType, ErdCodeClass from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi @@ -91,7 +91,7 @@ def _get_uom(self): #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 UnitOfTemperature.FAHRENHEIT return None diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index 9dc917a..8466dd9 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -1,17 +1,9 @@ import logging from typing import Optional from gehomesdk.erd.erd_data_type import ErdDataType -from homeassistant.components.sensor import SensorEntity, SensorStateClass - -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_HUMIDITY, - TEMP_FAHRENHEIT, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity, SensorStateClass + +from homeassistant.const import UnitOfTemperature from gehomesdk import ErdCodeType, ErdCodeClass from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi @@ -76,8 +68,8 @@ def _temp_units(self) -> Optional[str]: return self.api.hass.config.units.temperature_unit #if self._measurement_system == ErdMeasurementUnits.METRIC: - # return TEMP_CELSIUS - #return TEMP_FAHRENHEIT + # return UnitOfTemperature.CELSIUS + #return UnitOfTemperature.FAHRENHEIT def _convert_numeric_value_from_device(self, value): """Convert to expected data type""" @@ -97,20 +89,20 @@ def _get_uom(self): if ( self.erd_code_class in [ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE] - or self.device_class == DEVICE_CLASS_TEMPERATURE + or self.device_class == SensorDeviceClass.TEMPERATURE ): #NOTE: it appears that the API only sets temperature in Fahrenheit, #so we'll hard code this UOM instead of using the device configured #settings - return TEMP_FAHRENHEIT + return UnitOfTemperature.FAHRENHEIT if ( self.erd_code_class == ErdCodeClass.BATTERY - or self.device_class == DEVICE_CLASS_BATTERY + or self.device_class == SensorDeviceClass.BATTERY ): return "%" if self.erd_code_class == ErdCodeClass.PERCENTAGE: return "%" - if self.device_class == DEVICE_CLASS_POWER_FACTOR: + if self.device_class == SensorDeviceClass.POWER_FACTOR: return "%" if self.erd_code_class == ErdCodeClass.HUMIDITY: return "%" @@ -131,15 +123,15 @@ def _get_device_class(self) -> Optional[str]: ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE, ]: - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE if self.erd_code_class == ErdCodeClass.BATTERY: - return DEVICE_CLASS_BATTERY + return SensorDeviceClass.BATTERY if self.erd_code_class == ErdCodeClass.POWER: - return DEVICE_CLASS_POWER + return SensorDeviceClass.POWER if self.erd_code_class == ErdCodeClass.ENERGY: - return DEVICE_CLASS_ENERGY + return SensorDeviceClass.ENERGY if self.erd_code_class == ErdCodeClass.HUMIDITY: - return DEVICE_CLASS_HUMIDITY + return SensorDeviceClass.HUMIDITY return None @@ -147,7 +139,7 @@ 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]: + if self.device_class in [SensorDeviceClass.TEMPERATURE, SensorDeviceClass.ENERGY]: return SensorStateClass.MEASUREMENT if self.erd_code_class in [ErdCodeClass.FLOW_RATE, ErdCodeClass.PERCENTAGE, ErdCodeClass.HUMIDITY]: return SensorStateClass.MEASUREMENT diff --git a/custom_components/ge_home/entities/common/ge_water_heater.py b/custom_components/ge_home/entities/common/ge_water_heater.py index 55ae4d9..88b376a 100644 --- a/custom_components/ge_home/entities/common/ge_water_heater.py +++ b/custom_components/ge_home/entities/common/ge_water_heater.py @@ -3,10 +3,7 @@ from typing import Any, Dict, List, Optional from homeassistant.components.water_heater import WaterHeaterEntity -from homeassistant.const import ( - TEMP_FAHRENHEIT, - TEMP_CELSIUS -) +from homeassistant.const import UnitOfTemperature from gehomesdk import ErdCode, ErdMeasurementUnits from ...const import DOMAIN from .ge_erd_entity import GeEntity @@ -37,8 +34,8 @@ 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 + # return UnitOfTemperature.CELSIUS + return UnitOfTemperature.FAHRENHEIT @property def supported_features(self): diff --git a/custom_components/ge_home/entities/fridge/const.py b/custom_components/ge_home/entities/fridge/const.py index f7ca729..ac71406 100644 --- a/custom_components/ge_home/entities/fridge/const.py +++ b/custom_components/ge_home/entities/fridge/const.py @@ -1,10 +1,7 @@ -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) +from homeassistant.components.water_heater import WaterHeaterEntityFeature ATTR_DOOR_STATUS = "door_status" -GE_FRIDGE_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) +GE_FRIDGE_SUPPORT = (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) HEATER_TYPE_FRIDGE = "fridge" HEATER_TYPE_FREEZER = "freezer" diff --git a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py index b9b933c..9705351 100644 --- a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py +++ b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py @@ -2,7 +2,7 @@ from typing import List, Any, Optional from gehomesdk import ErdConvertableDrawerMode -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.const import UnitOfTemperature from homeassistant.util.unit_system import UnitSystem from ..common import OptionsConverter @@ -43,7 +43,7 @@ def to_option_string(self, value: ErdConvertableDrawerMode) -> Optional[str]: t = _TEMP_MAP.get(value, None) if t and self._units.is_metric: - t = self._units.temperature(float(t), TEMP_FAHRENHEIT) + t = self._units.temperature(float(t), UnitOfTemperature.FAHRENHEIT) t = round(t,1) if t: diff --git a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py index 312a3dd..a024ca1 100644 --- a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py @@ -6,7 +6,7 @@ import logging from typing import Any, Dict, List, Optional -from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.util.unit_conversion import TemperatureConverter from gehomesdk import ( ErdCode, @@ -117,7 +117,7 @@ def min_temp(self): 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) + return TemperatureConverter.convert(self.temp_limits[f"{self.heater_type}_min"], UnitOfTemperature.FAHRENHEIT, self.temperature_unit) @property def max_temp(self): @@ -126,7 +126,7 @@ def max_temp(self): 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) + return TemperatureConverter.convert(self.temp_limits[f"{self.heater_type}_max"], UnitOfTemperature.FAHRENHEIT, self.temperature_unit) @property def current_operation(self) -> str: diff --git a/custom_components/ge_home/entities/fridge/ge_dispenser.py b/custom_components/ge_home/entities/fridge/ge_dispenser.py index 04bc543..394af9e 100644 --- a/custom_components/ge_home/entities/fridge/ge_dispenser.py +++ b/custom_components/ge_home/entities/fridge/ge_dispenser.py @@ -3,7 +3,7 @@ import logging from typing import List, Optional, Dict, Any -from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.util.unit_conversion import TemperatureConverter from gehomesdk import ( @@ -102,12 +102,12 @@ def target_temperature(self) -> Optional[int]: @property def min_temp(self): """Return the minimum temperature.""" - return TemperatureConverter.convert(self._min_temp, TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self._min_temp, UnitOfTemperature.FAHRENHEIT, self.temperature_unit) @property def max_temp(self): """Return the maximum temperature.""" - return TemperatureConverter.convert(self._max_temp, TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self._max_temp, UnitOfTemperature.FAHRENHEIT, self.temperature_unit) @property def extra_state_attributes(self) -> Dict[str, Any]: diff --git a/custom_components/ge_home/entities/oven/const.py b/custom_components/ge_home/entities/oven/const.py index af9d602..8195571 100644 --- a/custom_components/ge_home/entities/oven/const.py +++ b/custom_components/ge_home/entities/oven/const.py @@ -1,13 +1,10 @@ import bidict -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) +from homeassistant.components.water_heater import WaterHeaterEntityFeature from gehomesdk import ErdOvenCookMode SUPPORT_NONE = 0 -GE_OVEN_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) +GE_OVEN_SUPPORT = (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) OP_MODE_OFF = "Off" OP_MODE_BAKE = "Bake" diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index 297b2c1..710178f 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -10,7 +10,7 @@ OvenCookSetting ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from ...const import DOMAIN from ...devices import ApplianceApi from ..common import GeAbstractWaterHeater @@ -56,8 +56,8 @@ def name(self) -> Optional[str]: 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 + return UnitOfTemperature.CELSIUS + return UnitOfTemperature.FAHRENHEIT @property def oven_select(self) -> str: @@ -155,7 +155,7 @@ async def async_set_operation_mode(self, operation_mode: str): target_temp = 0 elif self.target_temperature: target_temp = self.target_temperature - elif self.temperature_unit == TEMP_FAHRENHEIT: + elif self.temperature_unit == UnitOfTemperature.FAHRENHEIT: target_temp = 350 else: target_temp = 177 diff --git a/custom_components/ge_home/entities/water_heater/ge_water_heater.py b/custom_components/ge_home/entities/water_heater/ge_water_heater.py index 7954055..e9958c4 100644 --- a/custom_components/ge_home/entities/water_heater/ge_water_heater.py +++ b/custom_components/ge_home/entities/water_heater/ge_water_heater.py @@ -7,12 +7,9 @@ ErdWaterHeaterMode ) -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) +from homeassistant.components.water_heater import WaterHeaterEntityFeature -from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from ...devices import ApplianceApi from ..common import GeAbstractWaterHeater from .heater_modes import WhHeaterModeConverter @@ -34,11 +31,11 @@ def heater_type(self) -> str: @property def supported_features(self): - return (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) + return (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) @property def temperature_unit(self): - return TEMP_FAHRENHEIT + return UnitOfTemperature.FAHRENHEIT @property def current_temperature(self) -> Optional[int]: From eac81c691e55b332ebcd59b3b6c5848e5fc417c7 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 28 Jan 2024 09:34:46 -0500 Subject: [PATCH 276/338] - updated documentation --- CHANGELOG.md | 2 ++ info.md | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6e99ce..e68ccb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## 0.6.9 - Added additional fridge controls [#200] +- Bugfix: Additional auth stability improvements [#215, #211] +- Bugfix: Removed deprecated constants [#218] ## 0.6.8 diff --git a/info.md b/info.md index 10be149..20477dc 100644 --- a/info.md +++ b/info.md @@ -127,6 +127,11 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.9'.split('.') | map('int') %} +- Bugfix: Additional auth stability improvements (#215, #211) +- Bugfix: Removed deprecated constants (#218) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.8'.split('.') | map('int') %} - Bugfix: Fixed issue with oven lights (#174) - Bugfix: Fixed issues with dual dishwasher (#161) From efe500fe9ad30ad01d59803dd96a516f1ad566f2 Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sun, 28 Jan 2024 10:07:52 -0500 Subject: [PATCH 277/338] Dev -> Main v0.6.9 (#226) * - updated water heater naming * - initial support for built-in AC units * Add support for Built-In AC Unit Built-In AC unit operation and function is similar to a WAC. `gehomesdk` version bumped from 0.4.25 to 0.4.27 to include latest ErdApplianceType enum. * Update README.md Update README.md to include Built-In AC as supported device * - updated zero serial number detection (resolves #89) * - updated version - updated changelog * - hopefully fixed recursion bug with numbers * - added cooktop support * - fixed circular reference * Add support for fridges with reduced support for ERD codes (no turbo mode, no current temperature reporting, no temperature setpoint limit reporting, no door status reporting). This change has been tested on a Fisher & Paykel RF610AA. * - added dual dishwasher support - updated documentation - version bumps * - added water heater support * - added basic espresso maker device * - bugfixes * - rewrote initialization (resolves #99) * - added logic to prevent double registration of entities * - added correct min/max temps for water heaters * Fix CoffeeMaker after the NumberEntity refactoring * - added fix for CGY366P2TS1 (#116) to try to get the cooktop status, but fail more gracefully if it isn't supported * - fixed region setting in update coordinator * - version bump - doc update - string fixes * - updated the temperature conversion to use non-deprecated HASS methods * - updated documentation (@gleepwurp) * - more documentation updates * - updated dishwasher for new functionality - updated documentation * updated uom for liquid volume per HA specifications * - fixed typo in oven (#149) * - updated change log - fixed oven light control (#144) * - fixed issues with dishwasher (#155) - added oim descaling sensor (#154) - version bump * - updated change log * updated dual dishwasher for changes to gehomesdk (#161) Co-authored-by: na4ma4 * await disconnect when unloading (#169) Fixes simbaja#164. * Check for upper oven light when there is a lower oven (#174) Resolves issue #121 * - added oven warming drawers - simplified oven entity logic * - fixed issues with the new oven initialization logic * - fixed bad type import for warming drawer * -added iot class (#181) * - updated the gehomesdk version requirement * - gehomesdk version bump * - added dehumidifier (#114) * - dehumidifier appliance type fix * - added oven state sensors (#175) * - updated change logs - updated sdk version requirement * - removed target select - added dehumidifier entity - sdk version bump * - updated dehumidifier icon * - added humidifier platform * - fixed typos in humidifier class * - fixed copy/paste error * - sdk version requirement bump * - sdk version bump * - updated dehumidifier to handle target precision - updated dehumidifier sensor value conversion * - missed a commit * - SDK version bump * Set device class on day flow for water softeners (#207) This should allow sensor to be added to the energy dashboard. * - removed references to _hass in update coordinator - removed hass attribute in ge_entity - version bump * bumped gehomesdk version requirement * - added fridge/freezer controls * - documentation updates * - fixed error when reloading integration * - added additional update error handling * - fixed coordinator bug * - added additional logic to handle non-standard ready conditions * Set device class on day flow for water filters (#209) * - bumped sdk version requirement * added new style cooktop status support (#159) * Update deprecated constants or remove unused ones (#219) * - updated documentation --------- Co-authored-by: Rob Schmidt Co-authored-by: Federico Sevilla Co-authored-by: alexanv1 <44785744+alexanv1@users.noreply.github.com> Co-authored-by: na4ma4 <25967676+na4ma4@users.noreply.github.com> Co-authored-by: na4ma4 Co-authored-by: Alex Peters Co-authored-by: Chris Petersen <154074+ex-nerd@users.noreply.github.com> Co-authored-by: Nathan Fiscus Co-authored-by: Jason Foley Co-authored-by: myztillx <33730898+myztillx@users.noreply.github.com> --- CHANGELOG.md | 6 ++ custom_components/ge_home/binary_sensor.py | 12 +++- custom_components/ge_home/button.py | 10 +++- custom_components/ge_home/climate.py | 10 +++- custom_components/ge_home/devices/cooktop.py | 4 +- custom_components/ge_home/devices/fridge.py | 20 +++++-- custom_components/ge_home/devices/oven.py | 21 +++++-- .../ge_home/devices/water_filter.py | 2 +- .../ge_home/devices/water_softener.py | 2 +- .../ge_home/entities/ac/fan_mode_options.py | 5 -- .../ge_home/entities/ac/ge_biac_climate.py | 24 ++++---- .../ge_home/entities/ac/ge_pac_climate.py | 36 +++++------- .../ge_home/entities/ac/ge_sac_climate.py | 43 ++++++--------- .../ge_home/entities/ac/ge_wac_climate.py | 24 ++++---- .../ge_home/entities/advantium/const.py | 9 +-- .../entities/advantium/ge_advantium.py | 5 +- .../entities/ccm/ge_ccm_brew_temperature.py | 1 - .../ge_home/entities/common/ge_climate.py | 33 +++++------ .../ge_home/entities/common/ge_entity.py | 14 ++++- .../ge_home/entities/common/ge_erd_number.py | 4 +- .../ge_home/entities/common/ge_erd_sensor.py | 38 +++++-------- .../ge_home/entities/common/ge_erd_switch.py | 2 +- .../entities/common/ge_water_heater.py | 9 +-- .../ge_home/entities/fridge/__init__.py | 3 +- .../ge_home/entities/fridge/const.py | 7 +-- .../fridge/convertable_drawer_mode_options.py | 4 +- .../entities/fridge/ge_abstract_fridge.py | 6 +- .../ge_home/entities/fridge/ge_dispenser.py | 6 +- .../fridge/ge_fridge_ice_control_switch.py | 47 ++++++++++++++++ .../ge_home/entities/oven/const.py | 7 +-- .../ge_home/entities/oven/ge_oven.py | 8 +-- .../entities/water_heater/ge_water_heater.py | 11 ++-- custom_components/ge_home/humidifier.py | 10 +++- custom_components/ge_home/light.py | 10 +++- custom_components/ge_home/manifest.json | 4 +- custom_components/ge_home/number.py | 10 +++- custom_components/ge_home/select.py | 10 +++- custom_components/ge_home/sensor.py | 12 +++- custom_components/ge_home/switch.py | 10 +++- .../ge_home/update_coordinator.py | 55 +++++++++++++------ custom_components/ge_home/water_heater.py | 10 +++- info.md | 9 +++ 42 files changed, 356 insertions(+), 217 deletions(-) create mode 100644 custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a40d27..e68ccb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.9 + +- Added additional fridge controls [#200] +- Bugfix: Additional auth stability improvements [#215, #211] +- Bugfix: Removed deprecated constants [#218] + ## 0.6.8 - Added Dehumidifier [#114] diff --git a/custom_components/ge_home/binary_sensor.py b/custom_components/ge_home/binary_sensor.py index fb808f8..0a35ef5 100644 --- a/custom_components/ge_home/binary_sensor.py +++ b/custom_components/ge_home/binary_sensor.py @@ -35,5 +35,13 @@ def async_devices_discovered(apis: list[ApplianceApi]): ] _LOGGER.debug(f'Found {len(entities):d} unregistered binary sensors') async_add_entities(entities) - - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/button.py b/custom_components/ge_home/button.py index cfbb843..748ee6b 100644 --- a/custom_components/ge_home/button.py +++ b/custom_components/ge_home/button.py @@ -34,4 +34,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _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 + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/climate.py b/custom_components/ge_home/climate.py index 8255fb8..4512b61 100644 --- a/custom_components/ge_home/climate.py +++ b/custom_components/ge_home/climate.py @@ -36,4 +36,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _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 + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/devices/cooktop.py b/custom_components/ge_home/devices/cooktop.py index 6fb3453..d681443 100644 --- a/custom_components/ge_home/devices/cooktop.py +++ b/custom_components/ge_home/devices/cooktop.py @@ -2,7 +2,7 @@ from typing import List from gehomesdk.erd.erd_data_type import ErdDataType -from homeassistant.const import DEVICE_CLASS_POWER_FACTOR +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.entity import Entity from gehomesdk import ( ErdCode, @@ -44,7 +44,7 @@ def get_all_entities(self) -> List[Entity]: 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)) + cooktop_entities.append(GeErdPropertySensor(self, ErdCode.COOKTOP_STATUS, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) return base_entities + cooktop_entities diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index 24f0657..7c7aac8 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -1,5 +1,5 @@ from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM -from homeassistant.const import DEVICE_CLASS_TEMPERATURE +from homeassistant.components.sensor import SensorDeviceClass import logging from typing import List @@ -31,7 +31,8 @@ GeDispenser, GeErdPropertySensor, GeErdPropertyBinarySensor, - ConvertableDrawerModeOptionsConverter + ConvertableDrawerModeOptionsConverter, + GeFridgeIceControlSwitch ) _LOGGER = logging.getLogger(__name__) @@ -61,7 +62,10 @@ def get_all_entities(self) -> List[Entity]: proximity_light: ErdOnOff = self.try_get_erd_value(ErdCode.PROXIMITY_LIGHT) display_mode: ErdOnOff = self.try_get_erd_value(ErdCode.DISPLAY_MODE) lockout_mode: ErdOnOff = self.try_get_erd_value(ErdCode.LOCKOUT_MODE) - + turbo_cool: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_COOL_STATUS) + turbo_freeze: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_FREEZE_STATUS) + ice_boost: ErdOnOff = self.try_get_erd_value(ErdCode.FRIDGE_ICE_BOOST) + units = self.hass.config.units # Common entities @@ -80,8 +84,11 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "fridge"), GeFridge(self), ]) + if turbo_cool is not None: + fridge_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST)) if(ice_maker_control and ice_maker_control.status_fridge != ErdOnOff.NA): fridge_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_fridge")) + fridge_entities.append(GeFridgeIceControlSwitch(self, "fridge")) if(water_filter and water_filter != ErdFilterStatus.NA): fridge_entities.append(GeErdSensor(self, ErdCode.WATER_FILTER_STATUS)) if(air_filter and air_filter != ErdFilterStatus.NA): @@ -105,8 +112,13 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "freezer"), GeFreezer(self), ]) + if turbo_freeze is not None: + freezer_entities.append(GeErdSwitch(self, ErdCode.TURBO_FREEZE_STATUS)) + if ice_boost is not None: + freezer_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST)) if(ice_maker_control and ice_maker_control.status_freezer != ErdOnOff.NA): freezer_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_freezer")) + freezer_entities.append(GeFridgeIceControlSwitch(self, "freezer")) if(ice_bucket_status and ice_bucket_status.is_present_freezer): freezer_entities.append(GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_freezer")) @@ -117,7 +129,7 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "current_temp", device_class_override=SensorDeviceClass.TEMPERATURE, data_type_override=ErdDataType.INT), GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=DEVICE_CLASS_PROBLEM), GeDispenser(self) ]) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index c83ce8d..9400991 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -2,7 +2,7 @@ from typing import List from gehomesdk.erd.erd_data_type import ErdDataType -from homeassistant.const import DEVICE_CLASS_POWER_FACTOR +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.entity import Entity from gehomesdk import ( ErdCode, @@ -100,18 +100,27 @@ def get_all_entities(self) -> List[Entity]: if oven_config.has_warming_drawer and warm_drawer is not None: oven_entities.append(GeErdSensor(self, ErdCode.WARMING_DRAWER_STATE)) - if cooktop_config == ErdCooktopConfig.PRESENT: + if cooktop_config == ErdCooktopConfig.PRESENT: + # attempt to get the cooktop status using legacy status + cooktop_status_erd = ErdCode.COOKTOP_STATUS cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) + + # if we didn't get it, try using the new version + if cooktop_status is None: + cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT + cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) + + # if we got a status through either mechanism, we can add the entities if cooktop_status is not None: - cooktop_entities.append(GeErdBinarySensor(self, ErdCode.COOKTOP_STATUS)) + cooktop_entities.append(GeErdBinarySensor(self, cooktop_status_erd)) for (k, v) in cooktop_status.burners.items(): if v.exists: prop = self._camel_to_snake(k) - cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, prop+".on")) - cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, prop+".synchronized")) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on")) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized")) if not v.on_off_only: - cooktop_entities.append(GeErdPropertySensor(self, ErdCode.COOKTOP_STATUS, prop+".power_pct", icon_override="mdi:fire", device_class_override=DEVICE_CLASS_POWER_FACTOR, data_type_override=ErdDataType.INT)) + cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) return base_entities + oven_entities + cooktop_entities diff --git a/custom_components/ge_home/devices/water_filter.py b/custom_components/ge_home/devices/water_filter.py index d466f67..7cebd6a 100644 --- a/custom_components/ge_home/devices/water_filter.py +++ b/custom_components/ge_home/devices/water_filter.py @@ -30,7 +30,7 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE, device_class_override="water"), GeErdPropertySensor(self, ErdCode.WH_FILTER_LIFE_REMAINING, "life_remaining"), GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"), ] diff --git a/custom_components/ge_home/devices/water_softener.py b/custom_components/ge_home/devices/water_softener.py index a730471..a0afa83 100644 --- a/custom_components/ge_home/devices/water_softener.py +++ b/custom_components/ge_home/devices/water_softener.py @@ -27,7 +27,7 @@ def get_all_entities(self) -> List[Entity]: 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_FILTER_DAY_USAGE, device_class_override="water"), GeErdSensor(self, ErdCode.WH_SOFTENER_ERROR_CODE, icon_override="mdi:alert-circle"), GeErdBinarySensor(self, ErdCode.WH_SOFTENER_LOW_SALT, icon_on_override="mdi:alert", icon_off_override="mdi:grain"), GeErdSensor(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_STATE, icon_override="mdi:state-machine"), diff --git a/custom_components/ge_home/entities/ac/fan_mode_options.py b/custom_components/ge_home/entities/ac/fan_mode_options.py index 0bf78cd..c24e72f 100644 --- a/custom_components/ge_home/entities/ac/fan_mode_options.py +++ b/custom_components/ge_home/entities/ac/fan_mode_options.py @@ -1,11 +1,6 @@ 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 diff --git a/custom_components/ge_home/entities/ac/ge_biac_climate.py b/custom_components/ge_home/entities/ac/ge_biac_climate.py index f3b7453..5e62428 100644 --- a/custom_components/ge_home/entities/ac/ge_biac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_biac_climate.py @@ -1,11 +1,7 @@ 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 homeassistant.components.climate import HVACMode from gehomesdk import ErdAcOperationMode from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter @@ -16,13 +12,13 @@ class BiacHvacModeOptionsConverter(OptionsConverter): @property def options(self) -> List[str]: - return [HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] + return [HVACMode.AUTO, HVACMode.COOL, HVACMode.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 + HVACMode.AUTO: ErdAcOperationMode.ENERGY_SAVER, + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") @@ -30,14 +26,14 @@ def from_option_string(self, value: str) -> Any: 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 + ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, + ErdAcOperationMode.AUTO: HVACMode.AUTO, + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_COOL + return HVACMode.COOL class GeBiacClimate(GeClimate): """Class for Built-In AC units""" diff --git a/custom_components/ge_home/entities/ac/ge_pac_climate.py b/custom_components/ge_home/entities/ac/ge_pac_climate.py index ba8eb75..558cdc8 100644 --- a/custom_components/ge_home/entities/ac/ge_pac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_pac_climate.py @@ -1,17 +1,7 @@ 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 homeassistant.components.climate import HVACMode from gehomesdk import ErdCode, ErdAcOperationMode, ErdSacAvailableModes, ErdSacTargetTemperatureRange from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter @@ -25,19 +15,19 @@ def __init__(self, available_modes: ErdSacAvailableModes): @property def options(self) -> List[str]: - modes = [HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] + modes = [HVACMode.COOL, HVACMode.FAN_ONLY] if self._available_modes and self._available_modes.has_heat: - modes.append(HVAC_MODE_HEAT) + modes.append(HVACMode.HEAT) if self._available_modes and self._available_modes.has_dry: - modes.append(HVAC_MODE_DRY) + modes.append(HVACMode.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 + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.HEAT: ErdAcOperationMode.HEAT, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY, + HVACMode.DRY: ErdAcOperationMode.DRY }.get(value) except: _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") @@ -45,14 +35,14 @@ def from_option_string(self, value: str) -> Any: 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 + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.HEAT: HVACMode.HEAT, + ErdAcOperationMode.DRY: HVACMode.DRY, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_COOL + return HVACMode.COOL class GePacClimate(GeClimate): """Class for Portable AC units""" diff --git a/custom_components/ge_home/entities/ac/ge_sac_climate.py b/custom_components/ge_home/entities/ac/ge_sac_climate.py index 7c7ed54..6198b01 100644 --- a/custom_components/ge_home/entities/ac/ge_sac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_sac_climate.py @@ -1,16 +1,7 @@ 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 homeassistant.components.climate import HVACMode from gehomesdk import ErdCode, ErdAcOperationMode, ErdSacAvailableModes, ErdSacTargetTemperatureRange from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter @@ -24,21 +15,21 @@ def __init__(self, available_modes: ErdSacAvailableModes): @property def options(self) -> List[str]: - modes = [HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] + modes = [HVACMode.COOL, HVACMode.FAN_ONLY] if self._available_modes and self._available_modes.has_heat: - modes.append(HVAC_MODE_HEAT) - modes.append(HVAC_MODE_AUTO) + modes.append(HVACMode.HEAT) + modes.append(HVACMode.AUTO) if self._available_modes and self._available_modes.has_dry: - modes.append(HVAC_MODE_DRY) + modes.append(HVACMode.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 + HVACMode.AUTO: ErdAcOperationMode.AUTO, + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.HEAT: ErdAcOperationMode.HEAT, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY, + HVACMode.DRY: ErdAcOperationMode.DRY }.get(value) except: _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") @@ -46,16 +37,16 @@ def from_option_string(self, value: str) -> Any: 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 + ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, + ErdAcOperationMode.AUTO: HVACMode.AUTO, + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.HEAT: HVACMode.HEAT, + ErdAcOperationMode.DRY: HVACMode.DRY, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_COOL + return HVACMode.COOL class GeSacClimate(GeClimate): """Class for Split AC units""" diff --git a/custom_components/ge_home/entities/ac/ge_wac_climate.py b/custom_components/ge_home/entities/ac/ge_wac_climate.py index a7b3980..4602af8 100644 --- a/custom_components/ge_home/entities/ac/ge_wac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_wac_climate.py @@ -1,11 +1,7 @@ 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 homeassistant.components.climate import HVACMode from gehomesdk import ErdAcOperationMode from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter @@ -16,13 +12,13 @@ class WacHvacModeOptionsConverter(OptionsConverter): @property def options(self) -> List[str]: - return [HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY] + return [HVACMode.AUTO, HVACMode.COOL, HVACMode.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 + HVACMode.AUTO: ErdAcOperationMode.ENERGY_SAVER, + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") @@ -30,14 +26,14 @@ def from_option_string(self, value: str) -> Any: 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 + ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, + ErdAcOperationMode.AUTO: HVACMode.AUTO, + ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: _LOGGER.warn(f"Could not determine operation mode mapping for {value}") - return HVAC_MODE_COOL + return HVACMode.COOL class GeWacClimate(GeClimate): """Class for Window AC units""" diff --git a/custom_components/ge_home/entities/advantium/const.py b/custom_components/ge_home/entities/advantium/const.py index 7c487be..674dcae 100644 --- a/custom_components/ge_home/entities/advantium/const.py +++ b/custom_components/ge_home/entities/advantium/const.py @@ -1,8 +1,5 @@ -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) +from homeassistant.components.water_heater import WaterHeaterEntityFeature SUPPORT_NONE = 0 -GE_ADVANTIUM_WITH_TEMPERATURE = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) -GE_ADVANTIUM = SUPPORT_OPERATION_MODE +GE_ADVANTIUM_WITH_TEMPERATURE = (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) +GE_ADVANTIUM = WaterHeaterEntityFeature.OPERATION_MODE diff --git a/custom_components/ge_home/entities/advantium/ge_advantium.py b/custom_components/ge_home/entities/advantium/ge_advantium.py index 2d1372b..a92b9ee 100644 --- a/custom_components/ge_home/entities/advantium/ge_advantium.py +++ b/custom_components/ge_home/entities/advantium/ge_advantium.py @@ -15,7 +15,8 @@ ) from gehomesdk.erd.values.advantium.advantium_enums import CookAction, CookMode -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ATTR_TEMPERATURE from ...const import DOMAIN from ...devices import ApplianceApi from ..common import GeAbstractWaterHeater @@ -272,7 +273,7 @@ async def _ensure_operation_mode(self): 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: + if self.temperature_unit == SensorDeviceClass.FAHRENHEIT: return float(target_temp_f) else: return (target_temp_f - 32.0) * (5/9) diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py index 86cac03..c7895d9 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py @@ -2,7 +2,6 @@ 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): diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 7f44edb..c3c4583 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -4,15 +4,10 @@ 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 + UnitOfTemperature, ) +from homeassistant.components.climate import ClimateEntityFeature, HVACMode +from homeassistant.components.water_heater import WaterHeaterEntityFeature from gehomesdk import ErdCode, ErdCodeType, ErdMeasurementUnits, ErdOnOff from ...const import DOMAIN from ...devices import ApplianceApi @@ -22,7 +17,7 @@ _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 +GE_CLIMATE_SUPPORT = WaterHeaterEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE class GeClimate(GeEntity, ClimateEntity): """GE Climate Base Entity (Window AC, Portable AC, etc)""" @@ -83,11 +78,11 @@ def fan_mode_erd_code(self): @property def temperature_unit(self): #appears to always be Fahrenheit internally, hardcode this - return TEMP_FAHRENHEIT + return UnitOfTemperature.FAHRENHEIT #measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) #if measurement_system == ErdMeasurementUnits.METRIC: - # return TEMP_CELSIUS - #return TEMP_FAHRENHEIT + # return UnitOfTemperature.CELSIUS + #return UnitOfTempterature.FAHRENHEIT @property def supported_features(self): @@ -116,30 +111,30 @@ def max_temp(self) -> float: @property def hvac_mode(self): if not self.is_on: - return HVAC_MODE_OFF + return HVACMode.OFF return self._hvac_mode_converter.to_option_string(self.appliance.get_erd_value(self.hvac_mode_erd_code)) @property def hvac_modes(self) -> List[str]: - return [HVAC_MODE_OFF] + self._hvac_mode_converter.options + return [HVACMode.OFF] + self._hvac_mode_converter.options @property def fan_mode(self): - if self.hvac_mode == HVAC_MODE_FAN_ONLY: + if self.hvac_mode == HVACMode.FAN_ONLY: return self._fan_only_fan_mode_converter.to_option_string(self.appliance.get_erd_value(self.fan_mode_erd_code)) return self._fan_mode_converter.to_option_string(self.appliance.get_erd_value(self.fan_mode_erd_code)) @property def fan_modes(self) -> List[str]: - if self.hvac_mode == HVAC_MODE_FAN_ONLY: + if self.hvac_mode == HVACMode.FAN_ONLY: return self._fan_only_fan_mode_converter.options return self._fan_mode_converter.options async def async_set_hvac_mode(self, hvac_mode: str) -> None: _LOGGER.debug(f"Setting HVAC mode from {self.hvac_mode} to {hvac_mode}") if hvac_mode != self.hvac_mode: - if hvac_mode == HVAC_MODE_OFF: + if hvac_mode == HVACMode.OFF: await self.appliance.async_set_erd_value(self.power_status_erd_code, ErdOnOff.OFF) else: #if it's not on, turn it on @@ -156,7 +151,7 @@ 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 + if self.hvac_mode == HVACMode.FAN_ONLY else self._fan_mode_converter ) @@ -185,7 +180,7 @@ 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: + if self.temperature_unit == UnitOfTemperature.FAHRENHEIT: return float(temperature_f) else: return (temperature_f - 32.0) * (5/9) diff --git a/custom_components/ge_home/entities/common/ge_entity.py b/custom_components/ge_home/entities/common/ge_entity.py index 34f4037..6104d20 100644 --- a/custom_components/ge_home/entities/common/ge_entity.py +++ b/custom_components/ge_home/entities/common/ge_entity.py @@ -10,7 +10,7 @@ class GeEntity: def __init__(self, api: ApplianceApi): self._api = api - self.hass = None + self._added = False @property def unique_id(self) -> str: @@ -56,6 +56,18 @@ def icon(self) -> Optional[str]: def device_class(self) -> Optional[str]: return self._get_device_class() + @property + def added(self) -> bool: + return self._added + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self._added = True + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + self._added = False + def _stringify(self, value: any, **kwargs) -> Optional[str]: if isinstance(value, timedelta): return str(value)[:-3] if value else "" diff --git a/custom_components/ge_home/entities/common/ge_erd_number.py b/custom_components/ge_home/entities/common/ge_erd_number.py index d773ad1..91026dd 100644 --- a/custom_components/ge_home/entities/common/ge_erd_number.py +++ b/custom_components/ge_home/entities/common/ge_erd_number.py @@ -5,7 +5,7 @@ NumberEntity, NumberDeviceClass, ) -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.const import UnitOfTemperature from gehomesdk import ErdCodeType, ErdCodeClass from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi @@ -91,7 +91,7 @@ def _get_uom(self): #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 UnitOfTemperature.FAHRENHEIT return None diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index 9dc917a..8466dd9 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -1,17 +1,9 @@ import logging from typing import Optional from gehomesdk.erd.erd_data_type import ErdDataType -from homeassistant.components.sensor import SensorEntity, SensorStateClass - -from homeassistant.const import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_POWER_FACTOR, - DEVICE_CLASS_HUMIDITY, - TEMP_FAHRENHEIT, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity, SensorStateClass + +from homeassistant.const import UnitOfTemperature from gehomesdk import ErdCodeType, ErdCodeClass from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi @@ -76,8 +68,8 @@ def _temp_units(self) -> Optional[str]: return self.api.hass.config.units.temperature_unit #if self._measurement_system == ErdMeasurementUnits.METRIC: - # return TEMP_CELSIUS - #return TEMP_FAHRENHEIT + # return UnitOfTemperature.CELSIUS + #return UnitOfTemperature.FAHRENHEIT def _convert_numeric_value_from_device(self, value): """Convert to expected data type""" @@ -97,20 +89,20 @@ def _get_uom(self): if ( self.erd_code_class in [ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE] - or self.device_class == DEVICE_CLASS_TEMPERATURE + or self.device_class == SensorDeviceClass.TEMPERATURE ): #NOTE: it appears that the API only sets temperature in Fahrenheit, #so we'll hard code this UOM instead of using the device configured #settings - return TEMP_FAHRENHEIT + return UnitOfTemperature.FAHRENHEIT if ( self.erd_code_class == ErdCodeClass.BATTERY - or self.device_class == DEVICE_CLASS_BATTERY + or self.device_class == SensorDeviceClass.BATTERY ): return "%" if self.erd_code_class == ErdCodeClass.PERCENTAGE: return "%" - if self.device_class == DEVICE_CLASS_POWER_FACTOR: + if self.device_class == SensorDeviceClass.POWER_FACTOR: return "%" if self.erd_code_class == ErdCodeClass.HUMIDITY: return "%" @@ -131,15 +123,15 @@ def _get_device_class(self) -> Optional[str]: ErdCodeClass.RAW_TEMPERATURE, ErdCodeClass.NON_ZERO_TEMPERATURE, ]: - return DEVICE_CLASS_TEMPERATURE + return SensorDeviceClass.TEMPERATURE if self.erd_code_class == ErdCodeClass.BATTERY: - return DEVICE_CLASS_BATTERY + return SensorDeviceClass.BATTERY if self.erd_code_class == ErdCodeClass.POWER: - return DEVICE_CLASS_POWER + return SensorDeviceClass.POWER if self.erd_code_class == ErdCodeClass.ENERGY: - return DEVICE_CLASS_ENERGY + return SensorDeviceClass.ENERGY if self.erd_code_class == ErdCodeClass.HUMIDITY: - return DEVICE_CLASS_HUMIDITY + return SensorDeviceClass.HUMIDITY return None @@ -147,7 +139,7 @@ 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]: + if self.device_class in [SensorDeviceClass.TEMPERATURE, SensorDeviceClass.ENERGY]: return SensorStateClass.MEASUREMENT if self.erd_code_class in [ErdCodeClass.FLOW_RATE, ErdCodeClass.PERCENTAGE, ErdCodeClass.HUMIDITY]: return SensorStateClass.MEASUREMENT diff --git a/custom_components/ge_home/entities/common/ge_erd_switch.py b/custom_components/ge_home/entities/common/ge_erd_switch.py index cf39f9a..0fb3703 100644 --- a/custom_components/ge_home/entities/common/ge_erd_switch.py +++ b/custom_components/ge_home/entities/common/ge_erd_switch.py @@ -29,5 +29,5 @@ async def async_turn_on(self, **kwargs): async def async_turn_off(self, **kwargs): """Turn the switch off.""" - _LOGGER.debug(f"Turning on {self.unique_id}") + _LOGGER.debug(f"Turning off {self.unique_id}") await self.appliance.async_set_erd_value(self.erd_code, self._converter.false_value()) diff --git a/custom_components/ge_home/entities/common/ge_water_heater.py b/custom_components/ge_home/entities/common/ge_water_heater.py index 55ae4d9..88b376a 100644 --- a/custom_components/ge_home/entities/common/ge_water_heater.py +++ b/custom_components/ge_home/entities/common/ge_water_heater.py @@ -3,10 +3,7 @@ from typing import Any, Dict, List, Optional from homeassistant.components.water_heater import WaterHeaterEntity -from homeassistant.const import ( - TEMP_FAHRENHEIT, - TEMP_CELSIUS -) +from homeassistant.const import UnitOfTemperature from gehomesdk import ErdCode, ErdMeasurementUnits from ...const import DOMAIN from .ge_erd_entity import GeEntity @@ -37,8 +34,8 @@ 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 + # return UnitOfTemperature.CELSIUS + return UnitOfTemperature.FAHRENHEIT @property def supported_features(self): diff --git a/custom_components/ge_home/entities/fridge/__init__.py b/custom_components/ge_home/entities/fridge/__init__.py index b277fcf..2d14761 100644 --- a/custom_components/ge_home/entities/fridge/__init__.py +++ b/custom_components/ge_home/entities/fridge/__init__.py @@ -1,4 +1,5 @@ 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 +from .convertable_drawer_mode_options import ConvertableDrawerModeOptionsConverter +from .ge_fridge_ice_control_switch import GeFridgeIceControlSwitch \ No newline at end of file diff --git a/custom_components/ge_home/entities/fridge/const.py b/custom_components/ge_home/entities/fridge/const.py index f7ca729..ac71406 100644 --- a/custom_components/ge_home/entities/fridge/const.py +++ b/custom_components/ge_home/entities/fridge/const.py @@ -1,10 +1,7 @@ -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) +from homeassistant.components.water_heater import WaterHeaterEntityFeature ATTR_DOOR_STATUS = "door_status" -GE_FRIDGE_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) +GE_FRIDGE_SUPPORT = (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) HEATER_TYPE_FRIDGE = "fridge" HEATER_TYPE_FREEZER = "freezer" diff --git a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py index b9b933c..9705351 100644 --- a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py +++ b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py @@ -2,7 +2,7 @@ from typing import List, Any, Optional from gehomesdk import ErdConvertableDrawerMode -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.const import UnitOfTemperature from homeassistant.util.unit_system import UnitSystem from ..common import OptionsConverter @@ -43,7 +43,7 @@ def to_option_string(self, value: ErdConvertableDrawerMode) -> Optional[str]: t = _TEMP_MAP.get(value, None) if t and self._units.is_metric: - t = self._units.temperature(float(t), TEMP_FAHRENHEIT) + t = self._units.temperature(float(t), UnitOfTemperature.FAHRENHEIT) t = round(t,1) if t: diff --git a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py index 312a3dd..a024ca1 100644 --- a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py @@ -6,7 +6,7 @@ import logging from typing import Any, Dict, List, Optional -from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.util.unit_conversion import TemperatureConverter from gehomesdk import ( ErdCode, @@ -117,7 +117,7 @@ def min_temp(self): 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) + return TemperatureConverter.convert(self.temp_limits[f"{self.heater_type}_min"], UnitOfTemperature.FAHRENHEIT, self.temperature_unit) @property def max_temp(self): @@ -126,7 +126,7 @@ def max_temp(self): 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) + return TemperatureConverter.convert(self.temp_limits[f"{self.heater_type}_max"], UnitOfTemperature.FAHRENHEIT, self.temperature_unit) @property def current_operation(self) -> str: diff --git a/custom_components/ge_home/entities/fridge/ge_dispenser.py b/custom_components/ge_home/entities/fridge/ge_dispenser.py index 04bc543..394af9e 100644 --- a/custom_components/ge_home/entities/fridge/ge_dispenser.py +++ b/custom_components/ge_home/entities/fridge/ge_dispenser.py @@ -3,7 +3,7 @@ import logging from typing import List, Optional, Dict, Any -from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.util.unit_conversion import TemperatureConverter from gehomesdk import ( @@ -102,12 +102,12 @@ def target_temperature(self) -> Optional[int]: @property def min_temp(self): """Return the minimum temperature.""" - return TemperatureConverter.convert(self._min_temp, TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self._min_temp, UnitOfTemperature.FAHRENHEIT, self.temperature_unit) @property def max_temp(self): """Return the maximum temperature.""" - return TemperatureConverter.convert(self._max_temp, TEMP_FAHRENHEIT, self.temperature_unit) + return TemperatureConverter.convert(self._max_temp, UnitOfTemperature.FAHRENHEIT, self.temperature_unit) @property def extra_state_attributes(self) -> Dict[str, Any]: diff --git a/custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py b/custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py new file mode 100644 index 0000000..f86c59a --- /dev/null +++ b/custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py @@ -0,0 +1,47 @@ +import logging +from gehomesdk import ErdCode, IceMakerControlStatus, ErdOnOff + +from ...devices import ApplianceApi +from ..common import GeErdSwitch, BoolConverter + +_LOGGER = logging.getLogger(__name__) + +class GeFridgeIceControlSwitch(GeErdSwitch): + def __init__(self, api: ApplianceApi, control_type: str): + super().__init__(api, ErdCode.ICE_MAKER_CONTROL, BoolConverter()) + self._control_type = control_type + + @property + def control_status(self) -> IceMakerControlStatus: + return self.appliance.get_erd_value(ErdCode.ICE_MAKER_CONTROL) + + @property + def is_on(self) -> bool: + if self._control_type == "fridge": + return self.control_status.status_fridge == ErdOnOff.ON + else: + return self.control_status.status_freezer == ErdOnOff.ON + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + _LOGGER.debug(f"Turning on {self.unique_id}") + + old_status = self.control_status + if self._control_type == "fridge": + new_status = IceMakerControlStatus(ErdOnOff.ON, old_status.status_freezer) + else: + new_status = IceMakerControlStatus(old_status.status_fridge, ErdOnOff.ON) + + await self.appliance.async_set_erd_value(self.erd_code, new_status) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + _LOGGER.debug(f"Turning off {self.unique_id}") + + old_status = self.control_status + if self._control_type == "fridge": + new_status = IceMakerControlStatus(ErdOnOff.OFF, old_status.status_freezer) + else: + new_status = IceMakerControlStatus(old_status.status_fridge, ErdOnOff.OFF) + + await self.appliance.async_set_erd_value(self.erd_code, new_status) diff --git a/custom_components/ge_home/entities/oven/const.py b/custom_components/ge_home/entities/oven/const.py index af9d602..8195571 100644 --- a/custom_components/ge_home/entities/oven/const.py +++ b/custom_components/ge_home/entities/oven/const.py @@ -1,13 +1,10 @@ import bidict -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) +from homeassistant.components.water_heater import WaterHeaterEntityFeature from gehomesdk import ErdOvenCookMode SUPPORT_NONE = 0 -GE_OVEN_SUPPORT = (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) +GE_OVEN_SUPPORT = (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) OP_MODE_OFF = "Off" OP_MODE_BAKE = "Bake" diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index 297b2c1..710178f 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -10,7 +10,7 @@ OvenCookSetting ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from ...const import DOMAIN from ...devices import ApplianceApi from ..common import GeAbstractWaterHeater @@ -56,8 +56,8 @@ def name(self) -> Optional[str]: 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 + return UnitOfTemperature.CELSIUS + return UnitOfTemperature.FAHRENHEIT @property def oven_select(self) -> str: @@ -155,7 +155,7 @@ async def async_set_operation_mode(self, operation_mode: str): target_temp = 0 elif self.target_temperature: target_temp = self.target_temperature - elif self.temperature_unit == TEMP_FAHRENHEIT: + elif self.temperature_unit == UnitOfTemperature.FAHRENHEIT: target_temp = 350 else: target_temp = 177 diff --git a/custom_components/ge_home/entities/water_heater/ge_water_heater.py b/custom_components/ge_home/entities/water_heater/ge_water_heater.py index 7954055..e9958c4 100644 --- a/custom_components/ge_home/entities/water_heater/ge_water_heater.py +++ b/custom_components/ge_home/entities/water_heater/ge_water_heater.py @@ -7,12 +7,9 @@ ErdWaterHeaterMode ) -from homeassistant.components.water_heater import ( - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE -) +from homeassistant.components.water_heater import WaterHeaterEntityFeature -from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from ...devices import ApplianceApi from ..common import GeAbstractWaterHeater from .heater_modes import WhHeaterModeConverter @@ -34,11 +31,11 @@ def heater_type(self) -> str: @property def supported_features(self): - return (SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE) + return (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) @property def temperature_unit(self): - return TEMP_FAHRENHEIT + return UnitOfTemperature.FAHRENHEIT @property def current_temperature(self) -> Optional[int]: diff --git a/custom_components/ge_home/humidifier.py b/custom_components/ge_home/humidifier.py index 4d1ed11..68aa896 100644 --- a/custom_components/ge_home/humidifier.py +++ b/custom_components/ge_home/humidifier.py @@ -33,4 +33,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered humidifiers') async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/light.py b/custom_components/ge_home/light.py index b652d02..ba2a69c 100644 --- a/custom_components/ge_home/light.py +++ b/custom_components/ge_home/light.py @@ -37,4 +37,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f"Found {len(entities):d} unregistered lights") async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 969c1f8..33dd56f 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.23","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.26","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.8" + "version": "0.6.9" } diff --git a/custom_components/ge_home/number.py b/custom_components/ge_home/number.py index 2b4213c..d864c09 100644 --- a/custom_components/ge_home/number.py +++ b/custom_components/ge_home/number.py @@ -34,4 +34,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregisterd numbers') async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/select.py b/custom_components/ge_home/select.py index 613b03b..158af27 100644 --- a/custom_components/ge_home/select.py +++ b/custom_components/ge_home/select.py @@ -37,4 +37,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f"Found {len(entities):d} unregistered selects") async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/sensor.py b/custom_components/ge_home/sensor.py index 9732dbf..0bf7ba6 100644 --- a/custom_components/ge_home/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -47,8 +47,16 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered sensors') async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) - + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + # register set_timer entity service platform.async_register_entity_service( SERVICE_SET_TIMER, diff --git a/custom_components/ge_home/switch.py b/custom_components/ge_home/switch.py index 3aa6f11..7c339ae 100644 --- a/custom_components/ge_home/switch.py +++ b/custom_components/ge_home/switch.py @@ -33,4 +33,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered switches') async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index b846bb4..944c229 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -3,7 +3,7 @@ import asyncio import async_timeout import logging -from typing import Any, Dict, Iterable, Optional, Tuple +from typing import Any, Callable, Dict, Iterable, Optional, Tuple, List from gehomesdk import ( EVENT_APPLIANCE_INITIAL_UPDATE, @@ -22,6 +22,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -55,17 +56,17 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator): def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Set up the GeHomeUpdateCoordinator class.""" - self._hass = hass + super().__init__(hass, _LOGGER, name=DOMAIN) + self._config_entry = config_entry self._username = config_entry.data[CONF_USERNAME] self._password = config_entry.data[CONF_PASSWORD] self._region = config_entry.data[CONF_REGION] self._appliance_apis = {} # type: Dict[str, ApplianceApi] + self._signal_remove_callbacks = [] # type: List[Callable] self._reset_initialization() - super().__init__(hass, _LOGGER, name=DOMAIN) - def _reset_initialization(self): self.client = None # type: Optional[GeWebsocketClient] @@ -111,7 +112,11 @@ def appliance_apis(self) -> Dict[str, ApplianceApi]: @property def signal_ready(self) -> str: """Event specific per entry to signal readiness""" - return f"{DOMAIN}-ready-{self._config_entry.entry_id}" + return f"{DOMAIN}-ready-{self._config_entry.entry_id}" + + @property + def initialized(self) -> bool: + return self._init_done @property def online(self) -> bool: @@ -150,6 +155,9 @@ def maybe_add_appliance_api(self, appliance: GeAppliance): api = self.appliance_apis[mac_addr] api.appliance = appliance + def add_signal_remove_callback(self, cb: Callable): + self._signal_remove_callbacks.append(cb) + async def get_client(self) -> GeWebsocketClient: """Get a new GE Websocket client.""" if self.client: @@ -161,8 +169,7 @@ async def get_client(self) -> GeWebsocketClient: finally: self._reset_initialization() - loop = self._hass.loop - self.client = self.create_ge_client(event_loop=loop) + self.client = self.create_ge_client(event_loop=self.hass.loop) return self.client async def async_setup(self): @@ -201,9 +208,9 @@ async def async_start_client(self): async def async_begin_session(self): """Begins the ge_home session.""" _LOGGER.debug("Beginning session") - session = self._hass.helpers.aiohttp_client.async_get_clientsession() + session = self.hass.helpers.aiohttp_client.async_get_clientsession() await self.client.async_get_credentials(session) - fut = asyncio.ensure_future(self.client.async_run_client(), loop=self._hass.loop) + fut = asyncio.ensure_future(self.client.async_run_client(), loop=self.hass.loop) _LOGGER.debug("Client running") return fut @@ -211,6 +218,12 @@ async def async_reset(self): """Resets the coordinator.""" _LOGGER.debug("resetting the coordinator") entry = self._config_entry + + # remove all the callbacks for this coordinator + for c in self._signal_remove_callbacks: + c() + self._signal_remove_callbacks.clear() + unload_ok = all( await asyncio.gather( *[ @@ -263,7 +276,7 @@ def shutdown(self, event) -> None: _LOGGER.info("ge_home shutting down") if self.client: self.client.clear_event_handlers() - self._hass.loop.create_task(self.client.disconnect()) + self.hass.loop.create_task(self.client.disconnect()) async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]): """Let HA know there's new state.""" @@ -272,24 +285,34 @@ async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any] try: api = self.appliance_apis[appliance.mac_addr] except KeyError: + _LOGGER.warn(f"Could not find appliance {appliance.mac_addr} in known device list.") return - - for entity in api.entities: - if entity.enabled: - _LOGGER.debug(f"Updating {entity} ({entity.unique_id}, {entity.entity_id})") - entity.async_write_ha_state() + + self._update_entity_state(api.entities) async def _refresh_ha_state(self): entities = [ entity for api in self.appliance_apis.values() for entity in api.entities ] + + self._update_entity_state(entities) + + def _update_entity_state(self, entities: List[Entity]): + from .entities import GeEntity for entity in entities: + # if this is a GeEntity, check if it's been added + #if not, don't try to refresh this entity + if isinstance(entity, GeEntity): + gee: GeEntity = entity + if not gee.added: + _LOGGER.debug(f"Entity {entity} ({entity.unique_id}, {entity.entity_id}) not yet added, skipping update...") + continue if entity.enabled: try: _LOGGER.debug(f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}") entity.async_write_ha_state() except: - _LOGGER.debug(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}") + _LOGGER.warn(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}", exc_info=1) @property def all_appliances_updated(self) -> bool: diff --git a/custom_components/ge_home/water_heater.py b/custom_components/ge_home/water_heater.py index 19e8b4d..9bb0a8e 100644 --- a/custom_components/ge_home/water_heater.py +++ b/custom_components/ge_home/water_heater.py @@ -35,4 +35,12 @@ def async_devices_discovered(apis: list[ApplianceApi]): _LOGGER.debug(f'Found {len(entities):d} unregistered water heaters') async_add_entities(entities) - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered) + #if we're already initialized at this point, call device + #discovery directly, otherwise add a callback based on the + #ready signal + if coordinator.initialized: + async_devices_discovered(coordinator.appliance_apis.values()) + else: + # add the ready signal and register the remove callback + coordinator.add_signal_remove_callback( + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) diff --git a/info.md b/info.md index aec528d..20477dc 100644 --- a/info.md +++ b/info.md @@ -69,6 +69,10 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '0.6.9'.split('.') | map('int') %} +- Added additional fridge controls (#200) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.8'.split('.') | map('int') %} - Added Dehumidifier (#114) - Added oven drawer sensors @@ -123,6 +127,11 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.9'.split('.') | map('int') %} +- Bugfix: Additional auth stability improvements (#215, #211) +- Bugfix: Removed deprecated constants (#218) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.8'.split('.') | map('int') %} - Bugfix: Fixed issue with oven lights (#174) - Bugfix: Fixed issues with dual dishwasher (#161) From 2ecb694706490bd611e3846cc08e4ab8d8d16807 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Mon, 29 Jan 2024 22:55:40 -0500 Subject: [PATCH 278/338] - fixed issues with new comments (#228, #229) --- custom_components/ge_home/devices/fridge.py | 4 ++-- custom_components/ge_home/entities/common/ge_climate.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index 7c7aac8..5f5dd8c 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -1,4 +1,4 @@ -from homeassistant.components.binary_sensor import DEVICE_CLASS_PROBLEM +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass import logging from typing import List @@ -130,7 +130,7 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "status", icon_override="mdi:information-outline"), GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_until_ready", icon_override="mdi:timer-outline"), GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "current_temp", device_class_override=SensorDeviceClass.TEMPERATURE, data_type_override=ErdDataType.INT), - GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=DEVICE_CLASS_PROBLEM), + GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=BinarySensorDeviceClass.PROBLEM), GeDispenser(self) ]) diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index c3c4583..e2f40dc 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) #by default, we'll support target temp and fan mode (derived classes can override) -GE_CLIMATE_SUPPORT = WaterHeaterEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE +GE_CLIMATE_SUPPORT = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE class GeClimate(GeEntity, ClimateEntity): """GE Climate Base Entity (Window AC, Portable AC, etc)""" From dddb3beff8ca55927f9fe3257c9bd9250944795f Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 1 Feb 2024 21:15:22 -0500 Subject: [PATCH 279/338] - version bump --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 33dd56f..88350da 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==0.5.26","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.9" + "version": "0.6.10" } From db011eada41b2aada5126f539132a761f62016b4 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Thu, 1 Feb 2024 21:24:01 -0500 Subject: [PATCH 280/338] - updated documentation --- CHANGELOG.md | 5 +++++ info.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e68ccb9..ebbf854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.10 + +- Bugfix: Removed additional deprecated constants [#229] +- Bugfix: Fixed issue with climate entities [#228] + ## 0.6.9 - Added additional fridge controls [#200] diff --git a/info.md b/info.md index 20477dc..93281fc 100644 --- a/info.md +++ b/info.md @@ -127,6 +127,11 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.10'.split('.') | map('int') %} +- Bugfix: Removed additional deprecated constants (#229) +- Bugfix: Fixed issue with climate entities (#228) +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.9'.split('.') | map('int') %} - Bugfix: Additional auth stability improvements (#215, #211) - Bugfix: Removed deprecated constants (#218) From 540bbe685715b67f4529d90cf81052a947646138 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 4 May 2024 17:12:21 -0400 Subject: [PATCH 281/338] - updated the climate support for new flags introduced in 2024.2.0 --- custom_components/ge_home/entities/common/ge_climate.py | 7 ++++++- hacs.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index e2f40dc..1487caf 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -17,7 +17,12 @@ _LOGGER = logging.getLogger(__name__) #by default, we'll support target temp and fan mode (derived classes can override) -GE_CLIMATE_SUPPORT = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE +GE_CLIMATE_SUPPORT = ( + ClimateEntityFeature.TARGET_TEMPERATURE | + ClimateEntityFeature.FAN_MODE | + ClimateEntityFeature.TURN_ON | + ClimateEntityFeature.TURN_OFF +) class GeClimate(GeEntity, ClimateEntity): """GE Climate Base Entity (Window AC, Portable AC, etc)""" diff --git a/hacs.json b/hacs.json index fa75f45..8ae196d 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Home (SmartHQ)", - "homeassistant": "2022.12.0", + "homeassistant": "2024.2.0", "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select", "button", "climate", "light", "number"], "iot_class": "Cloud Polling" } From 4f25f0206aed355607fddc7c67ff5da050c3f031 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 4 May 2024 20:17:38 -0400 Subject: [PATCH 282/338] - updated error strings --- custom_components/ge_home/strings.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/ge_home/strings.json b/custom_components/ge_home/strings.json index 7cfb731..8e3c913 100644 --- a/custom_components/ge_home/strings.json +++ b/custom_components/ge_home/strings.json @@ -3,19 +3,19 @@ "step": { "user": { "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]", - "region": "[%key:common::config_flow::data::region%]" + "username": "Username", + "password": "Password", + "region": "Region" } } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "cannot_connect": "Can't connect to SmartHQ", + "invalid_auth": "Invalid authentication provided, please check credentials", + "unknown": "Unknown error occurred" }, "abort": { - "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured_account": "Account already configured!" } } } From ad326bb05803bb5ef093342ecc9b9d217bad2cb4 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 7 May 2024 21:33:49 -0400 Subject: [PATCH 283/338] - updated client session to remove deprecation (resolves #253) --- custom_components/ge_home/update_coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 944c229..7e37600 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -24,7 +24,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( DOMAIN, EVENT_ALL_APPLIANCES_READY, @@ -208,7 +208,7 @@ async def async_start_client(self): async def async_begin_session(self): """Begins the ge_home session.""" _LOGGER.debug("Beginning session") - session = self.hass.helpers.aiohttp_client.async_get_clientsession() + session = async_get_clientsession(self.hass) await self.client.async_get_credentials(session) fut = asyncio.ensure_future(self.client.async_run_client(), loop=self.hass.loop) _LOGGER.debug("Client running") From 686ed638e7b1c9aeadf445c35129d2e17c8a2c99 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 7 May 2024 21:44:03 -0400 Subject: [PATCH 284/338] - updated app types to include electric cooktops (resolves #252) - updated cooktop to incorporate newer cooktop attributes --- custom_components/ge_home/devices/__init__.py | 2 ++ custom_components/ge_home/devices/cooktop.py | 30 ++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 8badafd..259090a 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -37,6 +37,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return OvenApi if appliance_type == ErdApplianceType.COOKTOP: return CooktopApi + if appliance_type == ErdApplianceType.ELECTRIC_COOKTOP: + return CooktopApi if appliance_type == ErdApplianceType.FRIDGE: return FridgeApi if appliance_type == ErdApplianceType.BEVERAGE_CENTER: diff --git a/custom_components/ge_home/devices/cooktop.py b/custom_components/ge_home/devices/cooktop.py index d681443..32fb597 100644 --- a/custom_components/ge_home/devices/cooktop.py +++ b/custom_components/ge_home/devices/cooktop.py @@ -34,17 +34,27 @@ def get_all_entities(self) -> List[Entity]: _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)) + if cooktop_config == ErdCooktopConfig.PRESENT: + # attempt to get the cooktop status using legacy status + cooktop_status_erd = ErdCode.COOKTOP_STATUS + cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) + + # if we didn't get it, try using the new version + if cooktop_status is None: + cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT + cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) + + # if we got a status through either mechanism, we can add the entities + if cooktop_status is not None: + cooktop_entities.append(GeErdBinarySensor(self, cooktop_status_erd)) - for (k, v) in cooktop_status.burners.items(): - if v.exists: - prop = self._camel_to_snake(k) - cooktop_entities.append(GeErdPropertyBinarySensor(self, 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=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) + for (k, v) in cooktop_status.burners.items(): + if v.exists: + prop = self._camel_to_snake(k) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on")) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized")) + if not v.on_off_only: + cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) return base_entities + cooktop_entities From 62692f66dbeebfd5774d55adca0af30a408f3f3b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 7 May 2024 22:49:45 -0400 Subject: [PATCH 285/338] - fixed convertable drawer issue (resolves #243) --- .../entities/fridge/convertable_drawer_mode_options.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py index 9705351..2a28d9c 100644 --- a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py +++ b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py @@ -3,7 +3,7 @@ from gehomesdk import ErdConvertableDrawerMode from homeassistant.const import UnitOfTemperature -from homeassistant.util.unit_system import UnitSystem +from homeassistant.util.unit_system import UnitSystem, UnitOfTemperature from ..common import OptionsConverter _LOGGER = logging.getLogger(__name__) @@ -34,7 +34,7 @@ def from_option_string(self, value: str) -> Any: v = value.split(" ")[0] return ErdConvertableDrawerMode[v.upper()] except: - _LOGGER.warn(f"Could not set hood light level to {value.upper()}") + _LOGGER.warn(f"Could not set drawer mode to {value.upper()}") return ErdConvertableDrawerMode.NA def to_option_string(self, value: ErdConvertableDrawerMode) -> Optional[str]: try: @@ -42,7 +42,7 @@ def to_option_string(self, value: ErdConvertableDrawerMode) -> Optional[str]: v = value.stringify() t = _TEMP_MAP.get(value, None) - if t and self._units.is_metric: + if t and self._units.temperature_unit == UnitOfTemperature.CELSIUS: t = self._units.temperature(float(t), UnitOfTemperature.FAHRENHEIT) t = round(t,1) From aa857f3587a0290b60478cf067683c3452e4037d Mon Sep 17 00:00:00 2001 From: Steve2Go Date: Wed, 8 May 2024 12:20:37 +1000 Subject: [PATCH 286/338] Update ge_climate.py --- .../ge_home/entities/common/ge_climate.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 1487caf..58cbbe1 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -99,10 +99,20 @@ def is_on(self) -> bool: @property def target_temperature(self) -> Optional[float]: + measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + if measurement_system == ErdMeasurementUnits.METRIC: + targ = float(self.appliance.get_erd_value(self.target_temperature_erd_code)) + targ = round( ((targ - 32.0) * (5/9)) / 2 ) * 2 + return (9 * targ) / 5 + 32 return float(self.appliance.get_erd_value(self.target_temperature_erd_code)) @property def current_temperature(self) -> Optional[float]: + measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + if measurement_system == ErdMeasurementUnits.METRIC: + current = float(self.appliance.get_erd_value(self.current_temperature_erd_code)) + current = round( (current - 32.0) * (5/9)) + return (9 * current) / 5 + 32 return float(self.appliance.get_erd_value(self.current_temperature_erd_code)) @property From 1e94af9d53d04469f6ba9fdeab7cffd3d8d140ca Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 23 Jun 2024 10:16:02 -0400 Subject: [PATCH 287/338] - updated documentation - version bump --- CHANGELOG.md | 8 ++++++++ custom_components/ge_home/manifest.json | 4 ++-- info.md | 10 ++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebbf854..5413855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.11 + +- Bugfix: Fixed convertable drawer issue [#243] +- Bugfix: Updated app types to include electric cooktops [#252] +- Bugfix: Updated clientsession to remove deprecation [#253] +- Bugfix: Fixed error strings +- Bugfix: Updated climate support for new flags introduced in 2024.2.0 + ## 0.6.10 - Bugfix: Removed additional deprecated constants [#229] diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 88350da..b254e87 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.26","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.28","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.10" + "version": "0.6.11" } diff --git a/info.md b/info.md index 93281fc..55d45ee 100644 --- a/info.md +++ b/info.md @@ -127,6 +127,16 @@ A/C Controls: #### Bugfixes + +{% if version_installed.split('.') | map('int') < '0.6.11'.split('.') | map('int') %} +- Bugfix: Fixed convertable drawer issue (#243) +- Bugfix: Updated app types to include electric cooktops (#252) +- Bugfix: Updated clientsession to remove deprecation (#253) +- Bugfix: Fixed error strings +- Bugfix: Updated climate support for new flags introduced in 2024.2.0 +{% endif %} + + {% if version_installed.split('.') | map('int') < '0.6.10'.split('.') | map('int') %} - Bugfix: Removed additional deprecated constants (#229) - Bugfix: Fixed issue with climate entities (#228) From 46f3d92ad017f71a9f6ab40981774f566fc7dfa5 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 4 May 2024 17:12:21 -0400 Subject: [PATCH 288/338] - updated the climate support for new flags introduced in 2024.2.0 --- custom_components/ge_home/entities/common/ge_climate.py | 7 ++++++- hacs.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index e2f40dc..1487caf 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -17,7 +17,12 @@ _LOGGER = logging.getLogger(__name__) #by default, we'll support target temp and fan mode (derived classes can override) -GE_CLIMATE_SUPPORT = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE +GE_CLIMATE_SUPPORT = ( + ClimateEntityFeature.TARGET_TEMPERATURE | + ClimateEntityFeature.FAN_MODE | + ClimateEntityFeature.TURN_ON | + ClimateEntityFeature.TURN_OFF +) class GeClimate(GeEntity, ClimateEntity): """GE Climate Base Entity (Window AC, Portable AC, etc)""" diff --git a/hacs.json b/hacs.json index fa75f45..8ae196d 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Home (SmartHQ)", - "homeassistant": "2022.12.0", + "homeassistant": "2024.2.0", "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select", "button", "climate", "light", "number"], "iot_class": "Cloud Polling" } From c8f29dd6790febd8d6fdd18d0dd1780707b15e90 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 4 May 2024 20:17:38 -0400 Subject: [PATCH 289/338] - updated error strings --- custom_components/ge_home/strings.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/ge_home/strings.json b/custom_components/ge_home/strings.json index 7cfb731..8e3c913 100644 --- a/custom_components/ge_home/strings.json +++ b/custom_components/ge_home/strings.json @@ -3,19 +3,19 @@ "step": { "user": { "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]", - "region": "[%key:common::config_flow::data::region%]" + "username": "Username", + "password": "Password", + "region": "Region" } } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "cannot_connect": "Can't connect to SmartHQ", + "invalid_auth": "Invalid authentication provided, please check credentials", + "unknown": "Unknown error occurred" }, "abort": { - "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured_account": "Account already configured!" } } } From e9fde572bf5f32c6db77981d78c2325fff5fa178 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 7 May 2024 21:33:49 -0400 Subject: [PATCH 290/338] - updated client session to remove deprecation (resolves #253) --- custom_components/ge_home/update_coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 944c229..7e37600 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -24,7 +24,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( DOMAIN, EVENT_ALL_APPLIANCES_READY, @@ -208,7 +208,7 @@ async def async_start_client(self): async def async_begin_session(self): """Begins the ge_home session.""" _LOGGER.debug("Beginning session") - session = self.hass.helpers.aiohttp_client.async_get_clientsession() + session = async_get_clientsession(self.hass) await self.client.async_get_credentials(session) fut = asyncio.ensure_future(self.client.async_run_client(), loop=self.hass.loop) _LOGGER.debug("Client running") From 8761ac32c800b425d54572f538b1e6dceafe1418 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 7 May 2024 21:44:03 -0400 Subject: [PATCH 291/338] - updated app types to include electric cooktops (resolves #252) - updated cooktop to incorporate newer cooktop attributes --- custom_components/ge_home/devices/__init__.py | 2 ++ custom_components/ge_home/devices/cooktop.py | 30 ++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 8badafd..259090a 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -37,6 +37,8 @@ def get_appliance_api_type(appliance_type: ErdApplianceType) -> Type: return OvenApi if appliance_type == ErdApplianceType.COOKTOP: return CooktopApi + if appliance_type == ErdApplianceType.ELECTRIC_COOKTOP: + return CooktopApi if appliance_type == ErdApplianceType.FRIDGE: return FridgeApi if appliance_type == ErdApplianceType.BEVERAGE_CENTER: diff --git a/custom_components/ge_home/devices/cooktop.py b/custom_components/ge_home/devices/cooktop.py index d681443..32fb597 100644 --- a/custom_components/ge_home/devices/cooktop.py +++ b/custom_components/ge_home/devices/cooktop.py @@ -34,17 +34,27 @@ def get_all_entities(self) -> List[Entity]: _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)) + if cooktop_config == ErdCooktopConfig.PRESENT: + # attempt to get the cooktop status using legacy status + cooktop_status_erd = ErdCode.COOKTOP_STATUS + cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) + + # if we didn't get it, try using the new version + if cooktop_status is None: + cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT + cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) + + # if we got a status through either mechanism, we can add the entities + if cooktop_status is not None: + cooktop_entities.append(GeErdBinarySensor(self, cooktop_status_erd)) - for (k, v) in cooktop_status.burners.items(): - if v.exists: - prop = self._camel_to_snake(k) - cooktop_entities.append(GeErdPropertyBinarySensor(self, 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=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) + for (k, v) in cooktop_status.burners.items(): + if v.exists: + prop = self._camel_to_snake(k) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on")) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized")) + if not v.on_off_only: + cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) return base_entities + cooktop_entities From 48ea3b3acfa026edb6989ff47f01055af42f8ec0 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 7 May 2024 22:49:45 -0400 Subject: [PATCH 292/338] - fixed convertable drawer issue (resolves #243) --- .../entities/fridge/convertable_drawer_mode_options.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py index 9705351..2a28d9c 100644 --- a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py +++ b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py @@ -3,7 +3,7 @@ from gehomesdk import ErdConvertableDrawerMode from homeassistant.const import UnitOfTemperature -from homeassistant.util.unit_system import UnitSystem +from homeassistant.util.unit_system import UnitSystem, UnitOfTemperature from ..common import OptionsConverter _LOGGER = logging.getLogger(__name__) @@ -34,7 +34,7 @@ def from_option_string(self, value: str) -> Any: v = value.split(" ")[0] return ErdConvertableDrawerMode[v.upper()] except: - _LOGGER.warn(f"Could not set hood light level to {value.upper()}") + _LOGGER.warn(f"Could not set drawer mode to {value.upper()}") return ErdConvertableDrawerMode.NA def to_option_string(self, value: ErdConvertableDrawerMode) -> Optional[str]: try: @@ -42,7 +42,7 @@ def to_option_string(self, value: ErdConvertableDrawerMode) -> Optional[str]: v = value.stringify() t = _TEMP_MAP.get(value, None) - if t and self._units.is_metric: + if t and self._units.temperature_unit == UnitOfTemperature.CELSIUS: t = self._units.temperature(float(t), UnitOfTemperature.FAHRENHEIT) t = round(t,1) From 6f8dab347c56023559ef1bb839d01a6cd9aceafe Mon Sep 17 00:00:00 2001 From: Steve2Go Date: Wed, 8 May 2024 12:20:37 +1000 Subject: [PATCH 293/338] Update ge_climate.py --- .../ge_home/entities/common/ge_climate.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 1487caf..58cbbe1 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -99,10 +99,20 @@ def is_on(self) -> bool: @property def target_temperature(self) -> Optional[float]: + measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + if measurement_system == ErdMeasurementUnits.METRIC: + targ = float(self.appliance.get_erd_value(self.target_temperature_erd_code)) + targ = round( ((targ - 32.0) * (5/9)) / 2 ) * 2 + return (9 * targ) / 5 + 32 return float(self.appliance.get_erd_value(self.target_temperature_erd_code)) @property def current_temperature(self) -> Optional[float]: + measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + if measurement_system == ErdMeasurementUnits.METRIC: + current = float(self.appliance.get_erd_value(self.current_temperature_erd_code)) + current = round( (current - 32.0) * (5/9)) + return (9 * current) / 5 + 32 return float(self.appliance.get_erd_value(self.current_temperature_erd_code)) @property From 8f543a88501ac718bfc66702bc4f0923e0467cf5 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 23 Jun 2024 10:16:02 -0400 Subject: [PATCH 294/338] - updated documentation - version bump --- CHANGELOG.md | 8 ++++++++ custom_components/ge_home/manifest.json | 4 ++-- info.md | 10 ++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebbf854..5413855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.11 + +- Bugfix: Fixed convertable drawer issue [#243] +- Bugfix: Updated app types to include electric cooktops [#252] +- Bugfix: Updated clientsession to remove deprecation [#253] +- Bugfix: Fixed error strings +- Bugfix: Updated climate support for new flags introduced in 2024.2.0 + ## 0.6.10 - Bugfix: Removed additional deprecated constants [#229] diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 88350da..b254e87 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.26","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.28","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.10" + "version": "0.6.11" } diff --git a/info.md b/info.md index 93281fc..55d45ee 100644 --- a/info.md +++ b/info.md @@ -127,6 +127,16 @@ A/C Controls: #### Bugfixes + +{% if version_installed.split('.') | map('int') < '0.6.11'.split('.') | map('int') %} +- Bugfix: Fixed convertable drawer issue (#243) +- Bugfix: Updated app types to include electric cooktops (#252) +- Bugfix: Updated clientsession to remove deprecation (#253) +- Bugfix: Fixed error strings +- Bugfix: Updated climate support for new flags introduced in 2024.2.0 +{% endif %} + + {% if version_installed.split('.') | map('int') < '0.6.10'.split('.') | map('int') %} - Bugfix: Removed additional deprecated constants (#229) - Bugfix: Fixed issue with climate entities (#228) From a0e7dede2e205f331748382b9974c8d10fb3195f Mon Sep 17 00:00:00 2001 From: disforw Date: Thu, 18 Jul 2024 19:54:29 -0400 Subject: [PATCH 295/338] Await async_forward_entry_setup As required by HA --- custom_components/ge_home/update_coordinator.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 7e37600..78a8acd 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -175,14 +175,12 @@ async def get_client(self) -> GeWebsocketClient: async def async_setup(self): """Setup a new coordinator""" _LOGGER.debug("Setting up coordinator") - + for component in PLATFORMS: - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self._config_entry, component - ) + await self.hass.config_entries.async_forward_entry_setup( + self._config_entry, component ) - + try: await self.async_start_client() except (GeNotAuthenticatedError, GeAuthFailedError): @@ -191,7 +189,7 @@ async def async_setup(self): raise HaCannotConnect("Cannot connect (server error)") except Exception: raise HaCannotConnect("Unknown connection failure") - + return True async def async_start_client(self): From 4ca590901e7cffc3726ed3788751dbb88eb1c92c Mon Sep 17 00:00:00 2001 From: disforw Date: Thu, 18 Jul 2024 19:56:49 -0400 Subject: [PATCH 296/338] Cleanup --- custom_components/ge_home/update_coordinator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 78a8acd..ea687e9 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -175,12 +175,12 @@ async def get_client(self) -> GeWebsocketClient: async def async_setup(self): """Setup a new coordinator""" _LOGGER.debug("Setting up coordinator") - + for component in PLATFORMS: await self.hass.config_entries.async_forward_entry_setup( self._config_entry, component ) - + try: await self.async_start_client() except (GeNotAuthenticatedError, GeAuthFailedError): @@ -189,7 +189,7 @@ async def async_setup(self): raise HaCannotConnect("Cannot connect (server error)") except Exception: raise HaCannotConnect("Unknown connection failure") - + return True async def async_start_client(self): From 6af402d921d3dab759730ba8cc685963f51dbc6a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 24 Sep 2024 17:40:35 -0400 Subject: [PATCH 297/338] - updated version and documentation --- CHANGELOG.md | 4 ++++ custom_components/ge_home/manifest.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5413855..3dcfbae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.12 + +- Bugfix: Deprecations [#271] + ## 0.6.11 - Bugfix: Fixed convertable drawer issue [#243] diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index b254e87..6241457 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==0.5.28","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.11" + "version": "0.6.12" } From 55ddc6148497ac4f791823e533dbded81c64aa4c Mon Sep 17 00:00:00 2001 From: disforw Date: Thu, 18 Jul 2024 19:54:29 -0400 Subject: [PATCH 298/338] Await async_forward_entry_setup As required by HA --- custom_components/ge_home/update_coordinator.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 7e37600..78a8acd 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -175,14 +175,12 @@ async def get_client(self) -> GeWebsocketClient: async def async_setup(self): """Setup a new coordinator""" _LOGGER.debug("Setting up coordinator") - + for component in PLATFORMS: - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self._config_entry, component - ) + await self.hass.config_entries.async_forward_entry_setup( + self._config_entry, component ) - + try: await self.async_start_client() except (GeNotAuthenticatedError, GeAuthFailedError): @@ -191,7 +189,7 @@ async def async_setup(self): raise HaCannotConnect("Cannot connect (server error)") except Exception: raise HaCannotConnect("Unknown connection failure") - + return True async def async_start_client(self): From 333f2f28a2939780a3f3fc4cdccbe9569b3ebb45 Mon Sep 17 00:00:00 2001 From: disforw Date: Thu, 18 Jul 2024 19:56:49 -0400 Subject: [PATCH 299/338] Cleanup --- custom_components/ge_home/update_coordinator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 78a8acd..ea687e9 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -175,12 +175,12 @@ async def get_client(self) -> GeWebsocketClient: async def async_setup(self): """Setup a new coordinator""" _LOGGER.debug("Setting up coordinator") - + for component in PLATFORMS: await self.hass.config_entries.async_forward_entry_setup( self._config_entry, component ) - + try: await self.async_start_client() except (GeNotAuthenticatedError, GeAuthFailedError): @@ -189,7 +189,7 @@ async def async_setup(self): raise HaCannotConnect("Cannot connect (server error)") except Exception: raise HaCannotConnect("Unknown connection failure") - + return True async def async_start_client(self): From ccd90eed3179b7fbb37e513497a4c2497de7f341 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 24 Sep 2024 17:40:35 -0400 Subject: [PATCH 300/338] - updated version and documentation --- CHANGELOG.md | 4 ++++ custom_components/ge_home/manifest.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5413855..3dcfbae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.12 + +- Bugfix: Deprecations [#271] + ## 0.6.11 - Bugfix: Fixed convertable drawer issue [#243] diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index b254e87..6241457 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==0.5.28","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.11" + "version": "0.6.12" } From 85f8922189f562d8f6d5cbcd73734e58f88ee736 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 3 Nov 2024 11:23:53 -0500 Subject: [PATCH 301/338] - updated ssl context - bumped gehomesdk version and integration version --- custom_components/ge_home/manifest.json | 4 ++-- custom_components/ge_home/update_coordinator.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 6241457..e8f834f 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.28","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.29","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.12" + "version": "0.6.13" } diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index ea687e9..e40bb11 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -25,6 +25,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util.ssl import get_default_context from .const import ( DOMAIN, EVENT_ALL_APPLIANCES_READY, @@ -93,6 +94,7 @@ def create_ge_client( self._password, self._region, event_loop=event_loop, + ssl_context=get_default_context() ) client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update) client.add_event_handler(EVENT_APPLIANCE_UPDATE_RECEIVED, self.on_device_update) From f17081cc5b83fe997c6374f39653eebd5673f274 Mon Sep 17 00:00:00 2001 From: chandlercord Date: Sun, 15 Sep 2024 18:06:05 -0700 Subject: [PATCH 302/338] Set "Could not find appliance % in known device list." to INFO Changed log level to INFO since known device list is deprecated. I'll try to find time later this week to read up on the 'device registry index' and see if it makes sense to add the appliances there. --- custom_components/ge_home/update_coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index ea687e9..45b4f9a 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -283,7 +283,7 @@ async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any] try: api = self.appliance_apis[appliance.mac_addr] except KeyError: - _LOGGER.warn(f"Could not find appliance {appliance.mac_addr} in known device list.") + _LOGGER.info(f"Could not find appliance {appliance.mac_addr} in known device list.") return self._update_entity_state(api.entities) From cc3fe5007b3f58cf5d4b90a615f5765deff02e78 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 3 Nov 2024 11:33:29 -0500 Subject: [PATCH 303/338] - fix for deprecation warnings --- .../ge_home/update_coordinator.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 2fc3828..7c90e0b 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -178,10 +178,9 @@ async def async_setup(self): """Setup a new coordinator""" _LOGGER.debug("Setting up coordinator") - for component in PLATFORMS: - await self.hass.config_entries.async_forward_entry_setup( - self._config_entry, component - ) + await self.hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) try: await self.async_start_client() @@ -224,15 +223,8 @@ async def async_reset(self): c() self._signal_remove_callbacks.clear() - unload_ok = all( - await asyncio.gather( - *[ - self.hass.config_entries.async_forward_entry_unload( - entry, component - ) - for component in PLATFORMS - ] - ) + unload_ok = await self.hass.config_entries.async_unload_platforms( + self._config_entry, PLATFORMS ) return unload_ok From dcecde71202979c85634cef62180eee7a7366bd0 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 3 Nov 2024 12:01:55 -0500 Subject: [PATCH 304/338] - documentation updates --- CHANGELOG.md | 4 ++++ info.md | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dcfbae..95b597b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.13 + +- Bugfix: Deprecations [#290] [#297] + ## 0.6.12 - Bugfix: Deprecations [#271] diff --git a/info.md b/info.md index 55d45ee..9f172ae 100644 --- a/info.md +++ b/info.md @@ -128,6 +128,16 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.13'.split('.') | map('int') %} +- Bugfix: Deprecations [#290] [#297] +{% endif %} + + +{% if version_installed.split('.') | map('int') < '0.6.12'.split('.') | map('int') %} +- Bugfix: Deprecations [#271] +{% endif %} + + {% if version_installed.split('.') | map('int') < '0.6.11'.split('.') | map('int') %} - Bugfix: Fixed convertable drawer issue (#243) - Bugfix: Updated app types to include electric cooktops (#252) From 4726509c08d487284a8be027b692047c41940169 Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sun, 3 Nov 2024 12:07:44 -0500 Subject: [PATCH 305/338] v0.6.13 (#299) --- CHANGELOG.md | 4 ++++ custom_components/ge_home/manifest.json | 4 ++-- .../ge_home/update_coordinator.py | 22 +++++++------------ info.md | 10 +++++++++ 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dcfbae..95b597b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.13 + +- Bugfix: Deprecations [#290] [#297] + ## 0.6.12 - Bugfix: Deprecations [#271] diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 6241457..e8f834f 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.28","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.29","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.12" + "version": "0.6.13" } diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index ea687e9..7c90e0b 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -25,6 +25,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util.ssl import get_default_context from .const import ( DOMAIN, EVENT_ALL_APPLIANCES_READY, @@ -93,6 +94,7 @@ def create_ge_client( self._password, self._region, event_loop=event_loop, + ssl_context=get_default_context() ) client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update) client.add_event_handler(EVENT_APPLIANCE_UPDATE_RECEIVED, self.on_device_update) @@ -176,10 +178,9 @@ async def async_setup(self): """Setup a new coordinator""" _LOGGER.debug("Setting up coordinator") - for component in PLATFORMS: - await self.hass.config_entries.async_forward_entry_setup( - self._config_entry, component - ) + await self.hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) try: await self.async_start_client() @@ -222,15 +223,8 @@ async def async_reset(self): c() self._signal_remove_callbacks.clear() - unload_ok = all( - await asyncio.gather( - *[ - self.hass.config_entries.async_forward_entry_unload( - entry, component - ) - for component in PLATFORMS - ] - ) + unload_ok = await self.hass.config_entries.async_unload_platforms( + self._config_entry, PLATFORMS ) return unload_ok @@ -283,7 +277,7 @@ async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any] try: api = self.appliance_apis[appliance.mac_addr] except KeyError: - _LOGGER.warn(f"Could not find appliance {appliance.mac_addr} in known device list.") + _LOGGER.info(f"Could not find appliance {appliance.mac_addr} in known device list.") return self._update_entity_state(api.entities) diff --git a/info.md b/info.md index 55d45ee..9f172ae 100644 --- a/info.md +++ b/info.md @@ -128,6 +128,16 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.13'.split('.') | map('int') %} +- Bugfix: Deprecations [#290] [#297] +{% endif %} + + +{% if version_installed.split('.') | map('int') < '0.6.12'.split('.') | map('int') %} +- Bugfix: Deprecations [#271] +{% endif %} + + {% if version_installed.split('.') | map('int') < '0.6.11'.split('.') | map('int') %} - Bugfix: Fixed convertable drawer issue (#243) - Bugfix: Updated app types to include electric cooktops (#252) From be961b16fcfe3ef7d2274e885657a6fe92a2660b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 30 Nov 2024 21:12:42 -0500 Subject: [PATCH 306/338] - added probe temperature (resolves #232) --- custom_components/ge_home/devices/oven.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index 9400991..fcefd7e 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -46,6 +46,9 @@ def get_all_entities(self) -> List[Entity]: has_upper_raw_temperature = self.has_erd_code(ErdCode.UPPER_OVEN_RAW_TEMPERATURE) has_lower_raw_temperature = self.has_erd_code(ErdCode.LOWER_OVEN_RAW_TEMPERATURE) + has_upper_probe_temperature = self.has_erd_code(ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP) + has_lower_probe_temperature = self.has_erd_code(ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP) + upper_light : ErdOvenLightLevel = self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT) upper_light_availability: ErdOvenLightLevelAvailability = self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT_AVAILABILITY) lower_light : ErdOvenLightLevel = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT) @@ -78,6 +81,8 @@ def get_all_entities(self) -> List[Entity]: oven_entities.append(GeOvenLightLevelSelect(self, ErdCode.LOWER_OVEN_LIGHT)) if lower_warm_drawer is not None: oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE)) + if has_lower_probe_temperature: + oven_entities.append(GeErdSensor(self, ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP, ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP)) oven_entities.extend([ GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, ~oven_config.has_lower_oven)), @@ -96,6 +101,8 @@ def get_all_entities(self) -> List[Entity]: oven_entities.append(GeOvenLightLevelSelect(self, ErdCode.UPPER_OVEN_LIGHT, self._single_name(ErdCode.UPPER_OVEN_LIGHT, ~oven_config.has_lower_oven))) if upper_warm_drawer is not None: oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, self._single_name(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, ~oven_config.has_lower_oven))) + if has_upper_probe_temperature: + oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, self._single_name(ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, ~oven_config.has_lower_oven))) if oven_config.has_warming_drawer and warm_drawer is not None: oven_entities.append(GeErdSensor(self, ErdCode.WARMING_DRAWER_STATE)) From 8d2acf365fa39335f15760541533f9452de86a2e Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 30 Nov 2024 21:57:48 -0500 Subject: [PATCH 307/338] - updated SDK version (resolves #304) --- custom_components/ge_home/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index e8f834f..c8f0726 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.29","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.30","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.13" + "version": "0.6.14" } From d96e5e88d7fd3f1782a22cb659a2971fc6ccfddf Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 30 Nov 2024 22:17:33 -0500 Subject: [PATCH 308/338] - added support for unloading the coordinator on setup if one already exists --- custom_components/ge_home/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/__init__.py b/custom_components/ge_home/__init__.py index f088fd7..f5337e8 100644 --- a/custom_components/ge_home/__init__.py +++ b/custom_components/ge_home/__init__.py @@ -37,13 +37,22 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up the ge_home component.""" + """Set up ge_home from a config entry.""" hass.data.setdefault(DOMAIN, {}) - """Set up ge_home from a config entry.""" + #try to get existing coordinator + existing: GeHomeUpdateCoordinator = dict.get(hass.data[DOMAIN],entry.entry_id) + coordinator = GeHomeUpdateCoordinator(hass, entry) hass.data[DOMAIN][entry.entry_id] = coordinator + # try to unload the existing coordinator + try: + if existing: + await coordinator.async_reset() + except: + _LOGGER.warning("Could not reset existing coordinator.") + try: if not await coordinator.async_setup(): return False From 48029fd3a888cef6a71d2b630d44e845995929e2 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 30 Nov 2024 22:21:22 -0500 Subject: [PATCH 309/338] - fixed issues with logging deprecations --- custom_components/ge_home/entities/ac/fan_mode_options.py | 2 +- custom_components/ge_home/entities/ac/ge_biac_climate.py | 4 ++-- custom_components/ge_home/entities/ac/ge_pac_climate.py | 4 ++-- custom_components/ge_home/entities/ac/ge_sac_climate.py | 4 ++-- custom_components/ge_home/entities/ac/ge_wac_climate.py | 4 ++-- .../ge_home/entities/ccm/ge_ccm_brew_strength.py | 2 +- .../ge_home/entities/common/ge_erd_timer_sensor.py | 4 ++-- .../entities/dehumidifier/dehumidifier_fan_options.py | 2 +- .../entities/fridge/convertable_drawer_mode_options.py | 2 +- .../ge_home/entities/hood/ge_hood_fan_speed.py | 2 +- .../ge_home/entities/hood/ge_hood_light_level.py | 2 +- .../entities/opal_ice_maker/oim_light_level_options.py | 2 +- .../ge_home/entities/oven/ge_oven_light_level_select.py | 2 +- .../ge_home/entities/oven/ge_oven_warming_state_select.py | 2 +- .../ge_home/entities/water_filter/filter_position.py | 2 +- .../ge_home/entities/water_heater/heater_modes.py | 2 +- .../ge_home/entities/water_softener/shutoff_position.py | 2 +- custom_components/ge_home/update_coordinator.py | 6 +++--- 18 files changed, 25 insertions(+), 25 deletions(-) diff --git a/custom_components/ge_home/entities/ac/fan_mode_options.py b/custom_components/ge_home/entities/ac/fan_mode_options.py index c24e72f..c8a50e6 100644 --- a/custom_components/ge_home/entities/ac/fan_mode_options.py +++ b/custom_components/ge_home/entities/ac/fan_mode_options.py @@ -18,7 +18,7 @@ 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}") + _LOGGER.warning(f"Could not set fan mode to {value}") return self._default def to_option_string(self, value: Any) -> Optional[str]: diff --git a/custom_components/ge_home/entities/ac/ge_biac_climate.py b/custom_components/ge_home/entities/ac/ge_biac_climate.py index 5e62428..db25033 100644 --- a/custom_components/ge_home/entities/ac/ge_biac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_biac_climate.py @@ -21,7 +21,7 @@ def from_option_string(self, value: str) -> Any: HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY }.get(value) except: - _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") + _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") return ErdAcOperationMode.COOL def to_option_string(self, value: Any) -> Optional[str]: try: @@ -32,7 +32,7 @@ def to_option_string(self, value: Any) -> Optional[str]: ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: - _LOGGER.warn(f"Could not determine operation mode mapping for {value}") + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") return HVACMode.COOL class GeBiacClimate(GeClimate): diff --git a/custom_components/ge_home/entities/ac/ge_pac_climate.py b/custom_components/ge_home/entities/ac/ge_pac_climate.py index 558cdc8..42a7a98 100644 --- a/custom_components/ge_home/entities/ac/ge_pac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_pac_climate.py @@ -30,7 +30,7 @@ def from_option_string(self, value: str) -> Any: HVACMode.DRY: ErdAcOperationMode.DRY }.get(value) except: - _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") + _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") return ErdAcOperationMode.COOL def to_option_string(self, value: Any) -> Optional[str]: try: @@ -41,7 +41,7 @@ def to_option_string(self, value: Any) -> Optional[str]: ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: - _LOGGER.warn(f"Could not determine operation mode mapping for {value}") + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") return HVACMode.COOL class GePacClimate(GeClimate): diff --git a/custom_components/ge_home/entities/ac/ge_sac_climate.py b/custom_components/ge_home/entities/ac/ge_sac_climate.py index 6198b01..5b239c7 100644 --- a/custom_components/ge_home/entities/ac/ge_sac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_sac_climate.py @@ -32,7 +32,7 @@ def from_option_string(self, value: str) -> Any: HVACMode.DRY: ErdAcOperationMode.DRY }.get(value) except: - _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") + _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") return ErdAcOperationMode.COOL def to_option_string(self, value: Any) -> Optional[str]: try: @@ -45,7 +45,7 @@ def to_option_string(self, value: Any) -> Optional[str]: ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: - _LOGGER.warn(f"Could not determine operation mode mapping for {value}") + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") return HVACMode.COOL class GeSacClimate(GeClimate): diff --git a/custom_components/ge_home/entities/ac/ge_wac_climate.py b/custom_components/ge_home/entities/ac/ge_wac_climate.py index 4602af8..2754b90 100644 --- a/custom_components/ge_home/entities/ac/ge_wac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_wac_climate.py @@ -21,7 +21,7 @@ def from_option_string(self, value: str) -> Any: HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY }.get(value) except: - _LOGGER.warn(f"Could not set HVAC mode to {value.upper()}") + _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") return ErdAcOperationMode.COOL def to_option_string(self, value: Any) -> Optional[str]: try: @@ -32,7 +32,7 @@ def to_option_string(self, value: Any) -> Optional[str]: ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: - _LOGGER.warn(f"Could not determine operation mode mapping for {value}") + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") return HVACMode.COOL class GeWacClimate(GeClimate): diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_strength.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_strength.py index b6faa93..a1d2395 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_strength.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_strength.py @@ -20,7 +20,7 @@ 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()}") + _LOGGER.warning(f"Could not set brew strength to {value.upper()}") return self._default def to_option_string(self, value: ErdCcmBrewStrength) -> Optional[str]: diff --git a/custom_components/ge_home/entities/common/ge_erd_timer_sensor.py b/custom_components/ge_home/entities/common/ge_erd_timer_sensor.py index 33cdfee..3f5e905 100644 --- a/custom_components/ge_home/entities/common/ge_erd_timer_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_timer_sensor.py @@ -19,7 +19,7 @@ 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) + _LOGGER.warning("Could not set timer value", exc_info=1) async def clear_timer(self): try: @@ -27,4 +27,4 @@ async def clear_timer(self): #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) + _LOGGER.warning("Could not clear timer value", exc_info=1) diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py index 2d14832..6ef918d 100644 --- a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_options.py @@ -18,7 +18,7 @@ def from_option_string(self, value: str) -> Any: return ErdAcFanSetting.DEFAULT return ErdAcFanSetting[value.upper()] except: - _LOGGER.warn(f"Could not set fan setting to {value.upper()}") + _LOGGER.warning(f"Could not set fan setting to {value.upper()}") return ErdAcFanSetting.DEFAULT def to_option_string(self, value: ErdAcFanSetting) -> Optional[str]: try: diff --git a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py index 2a28d9c..fbd5cba 100644 --- a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py +++ b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py @@ -34,7 +34,7 @@ def from_option_string(self, value: str) -> Any: v = value.split(" ")[0] return ErdConvertableDrawerMode[v.upper()] except: - _LOGGER.warn(f"Could not set drawer mode to {value.upper()}") + _LOGGER.warning(f"Could not set drawer mode to {value.upper()}") return ErdConvertableDrawerMode.NA def to_option_string(self, value: ErdConvertableDrawerMode) -> Optional[str]: try: diff --git a/custom_components/ge_home/entities/hood/ge_hood_fan_speed.py b/custom_components/ge_home/entities/hood/ge_hood_fan_speed.py index e38196c..0dcfe73 100644 --- a/custom_components/ge_home/entities/hood/ge_hood_fan_speed.py +++ b/custom_components/ge_home/entities/hood/ge_hood_fan_speed.py @@ -30,7 +30,7 @@ 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()}") + _LOGGER.warning(f"Could not set hood fan speed to {value.upper()}") return ErdHoodFanSpeed.OFF def to_option_string(self, value: ErdHoodFanSpeed) -> Optional[str]: try: diff --git a/custom_components/ge_home/entities/hood/ge_hood_light_level.py b/custom_components/ge_home/entities/hood/ge_hood_light_level.py index 52e2516..a44dccd 100644 --- a/custom_components/ge_home/entities/hood/ge_hood_light_level.py +++ b/custom_components/ge_home/entities/hood/ge_hood_light_level.py @@ -26,7 +26,7 @@ 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()}") + _LOGGER.warning(f"Could not set hood light level to {value.upper()}") return ErdHoodLightLevel.OFF def to_option_string(self, value: ErdHoodLightLevel) -> Optional[str]: try: diff --git a/custom_components/ge_home/entities/opal_ice_maker/oim_light_level_options.py b/custom_components/ge_home/entities/opal_ice_maker/oim_light_level_options.py index 019500d..6d0114d 100644 --- a/custom_components/ge_home/entities/opal_ice_maker/oim_light_level_options.py +++ b/custom_components/ge_home/entities/opal_ice_maker/oim_light_level_options.py @@ -15,7 +15,7 @@ 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()}") + _LOGGER.warning(f"Could not set hood light level to {value.upper()}") return ErdOimLightLevel.OFF def to_option_string(self, value: ErdOimLightLevel) -> Optional[str]: try: diff --git a/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py b/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py index 8c63973..ca7e0de 100644 --- a/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py +++ b/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py @@ -23,7 +23,7 @@ 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()}") + _LOGGER.warning(f"Could not set Oven light level to {value.upper()}") return ErdOvenLightLevel.OFF def to_option_string(self, value: ErdOvenLightLevel) -> Optional[str]: try: diff --git a/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py b/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py index e10e2de..86da410 100644 --- a/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py +++ b/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py @@ -15,7 +15,7 @@ def from_option_string(self, value: str) -> Any: try: return ErdOvenWarmingState[value.upper()] except: - _LOGGER.warn(f"Could not set Oven warming state to {value.upper()}") + _LOGGER.warning(f"Could not set Oven warming state to {value.upper()}") return ErdOvenWarmingState.OFF def to_option_string(self, value: ErdOvenWarmingState) -> Optional[str]: try: diff --git a/custom_components/ge_home/entities/water_filter/filter_position.py b/custom_components/ge_home/entities/water_filter/filter_position.py index a94bdc8..53038af 100644 --- a/custom_components/ge_home/entities/water_filter/filter_position.py +++ b/custom_components/ge_home/entities/water_filter/filter_position.py @@ -15,7 +15,7 @@ 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()}") + _LOGGER.warning(f"Could not set filter position to {value.upper()}") return ErdWaterFilterPosition.UNKNOWN def to_option_string(self, value: Any) -> Optional[str]: try: diff --git a/custom_components/ge_home/entities/water_heater/heater_modes.py b/custom_components/ge_home/entities/water_heater/heater_modes.py index cd2e39a..145bb20 100644 --- a/custom_components/ge_home/entities/water_heater/heater_modes.py +++ b/custom_components/ge_home/entities/water_heater/heater_modes.py @@ -15,7 +15,7 @@ def from_option_string(self, value: str) -> Any: try: return ErdWaterHeaterMode[enum_val] except: - _LOGGER.warn(f"Could not heater mode to {enum_val}") + _LOGGER.warning(f"Could not heater mode to {enum_val}") return ErdWaterHeaterMode.UNKNOWN def to_option_string(self, value: ErdWaterHeaterMode) -> Optional[str]: try: diff --git a/custom_components/ge_home/entities/water_softener/shutoff_position.py b/custom_components/ge_home/entities/water_softener/shutoff_position.py index 38b0929..53d08a4 100644 --- a/custom_components/ge_home/entities/water_softener/shutoff_position.py +++ b/custom_components/ge_home/entities/water_softener/shutoff_position.py @@ -17,7 +17,7 @@ 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()}") + _LOGGER.warning(f"Could not set filter position to {value.upper()}") return ErdWaterSoftenerShutoffValveState.UNKNOWN def to_option_string(self, value: Any) -> Optional[str]: try: diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 7c90e0b..9bd27f4 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -167,7 +167,7 @@ async def get_client(self) -> GeWebsocketClient: self.client.clear_event_handlers() await self.client.disconnect() except Exception as err: - _LOGGER.warn(f"exception while disconnecting client {err}") + _LOGGER.warning(f"exception while disconnecting client {err}") finally: self._reset_initialization() @@ -252,7 +252,7 @@ async def async_reconnect(self) -> None: with async_timeout.timeout(ASYNC_TIMEOUT): await self.async_start_client() except Exception as err: - _LOGGER.warn(f"could not reconnect: {err}, will retry in {self._get_retry_delay()} seconds") + _LOGGER.warning(f"could not reconnect: {err}, will retry in {self._get_retry_delay()} seconds") self.hass.loop.call_later(self._get_retry_delay(), self.reconnect) _LOGGER.debug("forcing a state refresh while disconnected") try: @@ -304,7 +304,7 @@ def _update_entity_state(self, entities: List[Entity]): _LOGGER.debug(f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}") entity.async_write_ha_state() except: - _LOGGER.warn(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}", exc_info=1) + _LOGGER.warning(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}", exc_info=1) @property def all_appliances_updated(self) -> bool: From 955c2f0816df2d4c4da870beee60f5986da06357 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 30 Nov 2024 22:32:09 -0500 Subject: [PATCH 310/338] - documentation updates --- CHANGELOG.md | 6 ++++++ info.md | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b597b..113abdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.14 + +- Bugfix: Error checking socket status [#304] +- Bugfix: Error with setup [#301] +- Bugfix: Logger deprecations + ## 0.6.13 - Bugfix: Deprecations [#290] [#297] diff --git a/info.md b/info.md index 9f172ae..107aee1 100644 --- a/info.md +++ b/info.md @@ -127,6 +127,13 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '0.6.14'.split('.') | map('int') %} + +- Bugfix: Error checking socket status [#304] +- Bugfix: Error with setup [#301] +- Bugfix: Logger deprecations +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.13'.split('.') | map('int') %} - Bugfix: Deprecations [#290] [#297] From d4c7cffc7ce1691daba6877d1d07b8507dfae6f1 Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sat, 30 Nov 2024 22:42:43 -0500 Subject: [PATCH 311/338] v0.6.14 --- info.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/info.md b/info.md index 107aee1..f808d7f 100644 --- a/info.md +++ b/info.md @@ -145,6 +145,16 @@ A/C Controls: {% endif %} +{% if version_installed.split('.') | map('int') < '0.6.13'.split('.') | map('int') %} +- Bugfix: Deprecations [#290] [#297] +{% endif %} + + +{% if version_installed.split('.') | map('int') < '0.6.12'.split('.') | map('int') %} +- Bugfix: Deprecations [#271] +{% endif %} + + {% if version_installed.split('.') | map('int') < '0.6.11'.split('.') | map('int') %} - Bugfix: Fixed convertable drawer issue (#243) - Bugfix: Updated app types to include electric cooktops (#252) From 1ef157acc415a9342dd5853063a027b644bab92c Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 30 Nov 2024 22:55:01 -0500 Subject: [PATCH 312/338] - updated SUPPORT_NONE constant to remove deprecation warning (may resolve #300) --- custom_components/ge_home/entities/advantium/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/entities/advantium/const.py b/custom_components/ge_home/entities/advantium/const.py index 674dcae..5495de9 100644 --- a/custom_components/ge_home/entities/advantium/const.py +++ b/custom_components/ge_home/entities/advantium/const.py @@ -1,5 +1,5 @@ from homeassistant.components.water_heater import WaterHeaterEntityFeature -SUPPORT_NONE = 0 +SUPPORT_NONE: WaterHeaterEntityFeature = 0 GE_ADVANTIUM_WITH_TEMPERATURE = (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) GE_ADVANTIUM = WaterHeaterEntityFeature.OPERATION_MODE From 558f7406e9d6629b919cf207bf26f9a1b008a56a Mon Sep 17 00:00:00 2001 From: tdfountain <174762217+tdfountain@users.noreply.github.com> Date: Wed, 25 Dec 2024 06:38:17 -0800 Subject: [PATCH 313/338] Update for gehome changes associated with GE Profile washer and dryer (#317) * v0.6.13 (#299) * Update for gehome library changes associated iwth GE Profile washer and dryer implementation --------- Co-authored-by: simbaja <59273948+simbaja@users.noreply.github.com> Co-authored-by: Jack Simbach --- custom_components/ge_home/devices/advantium.py | 2 +- custom_components/ge_home/devices/dryer.py | 9 +++++---- .../ge_home/entities/advantium/ge_advantium.py | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/custom_components/ge_home/devices/advantium.py b/custom_components/ge_home/devices/advantium.py index 23775ec..c3baf36 100644 --- a/custom_components/ge_home/devices/advantium.py +++ b/custom_components/ge_home/devices/advantium.py @@ -17,7 +17,7 @@ def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() advantium_entities = [ - GeErdSensor(self, ErdCode.UNIT_TYPE), + GeErdSensor(self, ErdCode.PERSONALITY), GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED)), GeErdBinarySensor(self, ErdCode.MICROWAVE_REMOTE_ENABLE), GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE)), diff --git a/custom_components/ge_home/devices/dryer.py b/custom_components/ge_home/devices/dryer.py index 9387f7e..1de4481 100644 --- a/custom_components/ge_home/devices/dryer.py +++ b/custom_components/ge_home/devices/dryer.py @@ -25,6 +25,7 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR), GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, icon_on_override="mdi:tumble-dryer", icon_off_override="mdi:tumble-dryer"), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_BLOCKED_VENT_FAULT, icon_on_override="mid:alert-circle", icon_off_override "mdi:alert-circle"), ] dryer_entities = self.get_dryer_entities() @@ -48,8 +49,8 @@ def get_dryer_entities(self): 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_EXTENDED_TUMBLE_OPTION_SELECTION): + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_EXTENDED_TUMBLE_OPTION_SELECTION)]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_WASHERLINK_STATUS): dryer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_WASHERLINK_STATUS)]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_LEVEL_SENSOR_DISABLED): @@ -58,8 +59,8 @@ def get_dryer_entities(self): 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)]) + if self.has_erd_code(ErdCode.LAUNDRY_DRYER_ECODRY_OPTION_SELECTION): + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_ECODRY_OPTION_SELECTION)]) return dryer_entities diff --git a/custom_components/ge_home/entities/advantium/ge_advantium.py b/custom_components/ge_home/entities/advantium/ge_advantium.py index a92b9ee..e76cd2d 100644 --- a/custom_components/ge_home/entities/advantium/ge_advantium.py +++ b/custom_components/ge_home/entities/advantium/ge_advantium.py @@ -5,7 +5,7 @@ from gehomesdk import ( ErdCode, - ErdUnitType, + ErdPersonality, ErdAdvantiumCookStatus, ErdAdvantiumCookSetting, AdvantiumOperationMode, @@ -48,9 +48,9 @@ def name(self) -> Optional[str]: return f"{self.serial_number} Advantium" @property - def unit_type(self) -> Optional[ErdUnitType]: + def personality(self) -> Optional[ErdPersonality]: try: - return self.appliance.get_erd_value(ErdCode.UNIT_TYPE) + return self.appliance.get_erd_value(ErdCode.PERSONALITY) except: return None @@ -272,7 +272,7 @@ async def _ensure_operation_mode(self): 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 + target_temp_f = temp_240v if unit_type in [ErdPersonality.PERSONALITY_240V_MONOGRAM, ErdPersonality.PERSONALITY_240V_CAFE] else temp_120v if self.temperature_unit == SensorDeviceClass.FAHRENHEIT: return float(target_temp_f) else: From aa9de9fc70adbe336f3c96fa9880f67c5d0d65be Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 25 Dec 2024 12:18:56 -0500 Subject: [PATCH 314/338] - resolved deprecation warnings (#320) --- custom_components/ge_home/entities/advantium/const.py | 2 +- custom_components/ge_home/entities/oven/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/entities/advantium/const.py b/custom_components/ge_home/entities/advantium/const.py index 5495de9..81e6490 100644 --- a/custom_components/ge_home/entities/advantium/const.py +++ b/custom_components/ge_home/entities/advantium/const.py @@ -1,5 +1,5 @@ from homeassistant.components.water_heater import WaterHeaterEntityFeature -SUPPORT_NONE: WaterHeaterEntityFeature = 0 +SUPPORT_NONE = WaterHeaterEntityFeature(0) GE_ADVANTIUM_WITH_TEMPERATURE = (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) GE_ADVANTIUM = WaterHeaterEntityFeature.OPERATION_MODE diff --git a/custom_components/ge_home/entities/oven/const.py b/custom_components/ge_home/entities/oven/const.py index 8195571..cb92f1c 100644 --- a/custom_components/ge_home/entities/oven/const.py +++ b/custom_components/ge_home/entities/oven/const.py @@ -3,7 +3,7 @@ from homeassistant.components.water_heater import WaterHeaterEntityFeature from gehomesdk import ErdOvenCookMode -SUPPORT_NONE = 0 +SUPPORT_NONE = WaterHeaterEntityFeature(0) GE_OVEN_SUPPORT = (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) OP_MODE_OFF = "Off" From 95da3e026d97ce3dbb1f9b00086fbb1c0be36ba0 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 25 Dec 2024 14:59:29 -0500 Subject: [PATCH 315/338] - fixed issue with oven probe display temp for lower ovens (resolves #314) --- custom_components/ge_home/devices/oven.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index fcefd7e..914b38e 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -82,7 +82,7 @@ def get_all_entities(self) -> List[Entity]: if lower_warm_drawer is not None: oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE)) if has_lower_probe_temperature: - oven_entities.append(GeErdSensor(self, ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP, ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP)) + oven_entities.append(GeErdSensor(self, ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP)) oven_entities.extend([ GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, ~oven_config.has_lower_oven)), From c36ac2e83da0d7ad1a7e588c6c80df4b8a66443a Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 25 Dec 2024 19:51:56 -0500 Subject: [PATCH 316/338] - documentation updates - version bump --- CHANGELOG.md | 6 ++++++ custom_components/ge_home/manifest.json | 4 ++-- hacs.json | 2 +- info.md | 14 +++++++++++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 113abdc..06cea60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.6.15 + +- Feature: Improved Support for Laundry +- Breaking: Some enums changed names/values and may need updates to client code +- Bugfix: More deprecation fixes + ## 0.6.14 - Bugfix: Error checking socket status [#304] diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index c8f0726..8e82ee4 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.30","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.41","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.14" + "version": "0.6.15" } diff --git a/hacs.json b/hacs.json index 8ae196d..1d7862f 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Home (SmartHQ)", - "homeassistant": "2024.2.0", + "homeassistant": "2024.9.0", "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select", "button", "climate", "light", "number"], "iot_class": "Cloud Polling" } diff --git a/info.md b/info.md index f808d7f..55488b9 100644 --- a/info.md +++ b/info.md @@ -46,6 +46,10 @@ A/C Controls: #### Breaking Changes +{% if version_installed.split('.') | map('int') < '0.6.15'.split('.') | map('int') %} +- Some enums changed names/values and may need updates to client code +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} - Requires HA version 2022.12.0 or later {% endif %} @@ -69,6 +73,10 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '0.6.15'.split('.') | map('int') %} +- Improved Support for Laundry +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.9'.split('.') | map('int') %} - Added additional fridge controls (#200) {% endif %} @@ -127,8 +135,12 @@ A/C Controls: #### Bugfixes -{% if version_installed.split('.') | map('int') < '0.6.14'.split('.') | map('int') %} +{% if version_installed.split('.') | map('int') < '0.6.15'.split('.') | map('int') %} +- Bugfix: More deprecation fixes +{% endif %} + +{% if version_installed.split('.') | map('int') < '0.6.14'.split('.') | map('int') %} - Bugfix: Error checking socket status [#304] - Bugfix: Error with setup [#301] - Bugfix: Logger deprecations From 605aca482ca0c0a86f4758d535806d7b481cb063 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Wed, 25 Dec 2024 20:23:13 -0500 Subject: [PATCH 317/338] - fixed typo in new laundry code --- custom_components/ge_home/devices/dryer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/dryer.py b/custom_components/ge_home/devices/dryer.py index 1de4481..cc0110d 100644 --- a/custom_components/ge_home/devices/dryer.py +++ b/custom_components/ge_home/devices/dryer.py @@ -25,7 +25,7 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR), GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, icon_on_override="mdi:tumble-dryer", icon_off_override="mdi:tumble-dryer"), - GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_BLOCKED_VENT_FAULT, icon_on_override="mid:alert-circle", icon_off_override "mdi:alert-circle"), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_BLOCKED_VENT_FAULT, icon_on_override="mid:alert-circle", icon_off_override="mdi:alert-circle"), ] dryer_entities = self.get_dryer_entities() From 6499306523862b31103eea5b609dd7738b1cc235 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Tue, 31 Dec 2024 11:17:03 -0500 Subject: [PATCH 318/338] - fixed deprecation warning for light entities --- .../ge_home/entities/common/ge_erd_light.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/custom_components/ge_home/entities/common/ge_erd_light.py b/custom_components/ge_home/entities/common/ge_erd_light.py index 1d9d714..35b0422 100644 --- a/custom_components/ge_home/entities/common/ge_erd_light.py +++ b/custom_components/ge_home/entities/common/ge_erd_light.py @@ -2,9 +2,8 @@ from gehomesdk import ErdCodeType from homeassistant.components.light import ( + ColorMode, ATTR_BRIGHTNESS, - COLOR_MODE_BRIGHTNESS, - SUPPORT_BRIGHTNESS, LightEntity ) @@ -25,19 +24,14 @@ def to_hass_level(level): 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): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None, color_mode = ColorMode.BRIGHTNESS): super().__init__(api, erd_code, erd_override) self._color_mode = color_mode - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS - @property def supported_color_modes(self): """Flag supported color modes.""" - return {COLOR_MODE_BRIGHTNESS} + return ColorMode.BRIGHTNESS @property def color_mode(self): From b71807840f4cbd9b294191c826689d42f9cf0fd7 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 1 Feb 2025 14:56:56 -0500 Subject: [PATCH 319/338] - initial add of under counter ice maker - restructured appliance type mapper --- custom_components/ge_home/devices/__init__.py | 80 +++++++------------ custom_components/ge_home/devices/ucim.py | 45 +++++++++++ 2 files changed, 75 insertions(+), 50 deletions(-) create mode 100644 custom_components/ge_home/devices/ucim.py diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 259090a..297da34 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -22,6 +22,7 @@ from .water_softener import WaterSoftenerApi from .water_heater import WaterHeaterApi from .oim import OimApi +from .ucim import UcimApi from .coffee_maker import CcmApi from .dual_dishwasher import DualDishwasherApi from .espresso_maker import EspressoMakerApi @@ -33,54 +34,33 @@ 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.ELECTRIC_COOKTOP: - return CooktopApi - if appliance_type == ErdApplianceType.FRIDGE: - return FridgeApi - if appliance_type == ErdApplianceType.BEVERAGE_CENTER: - return FridgeApi - if appliance_type == ErdApplianceType.DISH_WASHER: - 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 - if appliance_type == ErdApplianceType.DEHUMIDIFIER: - return DehumidifierApi + known_types = { + ErdApplianceType.OVEN: OvenApi, + ErdApplianceType.COOKTOP: CooktopApi, + ErdApplianceType.ELECTRIC_COOKTOP: CooktopApi, + ErdApplianceType.FRIDGE: FridgeApi, + ErdApplianceType.BEVERAGE_CENTER: FridgeApi, + ErdApplianceType.DISH_WASHER: DishwasherApi, + ErdApplianceType.DUAL_DISH_WASHER: DualDishwasherApi, + ErdApplianceType.WASHER: WasherApi, + ErdApplianceType.DRYER: DryerApi, + ErdApplianceType.COMBINATION_WASHER_DRYER: WasherDryerApi, + ErdApplianceType.POE_WATER_FILTER: WaterFilterApi, + ErdApplianceType.WATER_SOFTENER: WaterSoftenerApi, + ErdApplianceType.WATER_HEATER: WaterHeaterApi, + ErdApplianceType.ADVANTIUM: AdvantiumApi, + ErdApplianceType.AIR_CONDITIONER: WacApi, + ErdApplianceType.SPLIT_AIR_CONDITIONER: SacApi, + ErdApplianceType.PORTABLE_AIR_CONDITIONER: PacApi, + ErdApplianceType.BUILT_IN_AIR_CONDITIONER: BiacApi, + ErdApplianceType.HOOD: HoodApi, + ErdApplianceType.MICROWAVE: MicrowaveApi, + ErdApplianceType.OPAL_ICE_MAKER: OimApi, + ErdApplianceType.UNDER_COUNTER_ICE_MAKER: UcimApi, + ErdApplianceType.CAFE_COFFEE_MAKER: CcmApi, + ErdApplianceType.ESPRESSO_MAKER: EspressoMakerApi, + ErdApplianceType.DEHUMIDIFIER: DehumidifierApi + } - # Fallback - return ApplianceApi + # Get the appliance type + return known_types.get(appliance_type, ApplianceApi) diff --git a/custom_components/ge_home/devices/ucim.py b/custom_components/ge_home/devices/ucim.py new file mode 100644 index 0000000..e342a9e --- /dev/null +++ b/custom_components/ge_home/devices/ucim.py @@ -0,0 +1,45 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdOnOff +) + +from .base import ApplianceApi +from ..entities import ( + OimLightLevelOptionsConverter, + GeErdSensor, + GeErdBinarySensor, + GeErdSelect, + GeErdSwitch, + ErdOnOffBoolConverter +) + +_LOGGER = logging.getLogger(__name__) + + +class UcimApi(ApplianceApi): + """API class for Opal Ice Maker objects""" + APPLIANCE_TYPE = ErdApplianceType.OPAL_ICE_MAKER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + oim_entities = [ + GeErdSensor(self, ErdCode.OIM_STATUS), + GeErdBinarySensor(self, ErdCode.OIM_FILTER_STATUS, device_class_override="problem"), + GeErdBinarySensor(self, ErdCode.OIM_NEEDS_DESCALING, device_class_override="problem"), + GeErdSelect(self, ErdCode.OIM_LIGHT_LEVEL, OimLightLevelOptionsConverter()), + GeErdSwitch(self, ErdCode.OIM_POWER, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), + GeErdSensor(self, ErdCode.OIM_PRODUCTION), + GeErdSensor(self, ErdCode.UCIM_CLEAN_STATUS), + GeErdSensor(self, ErdCode.UCIM_FILTER_USED_PERCENTAGE), + GeErdBinarySensor(self, ErdCode.UCIM_BIN_FULL, device_class_override="problem"), + ] + + entities = base_entities + oim_entities + return entities + From d4a923c4056aa2dafa8176b679bed2122c1df623 Mon Sep 17 00:00:00 2001 From: Ken Roe Date: Sat, 1 Feb 2025 14:59:23 -0500 Subject: [PATCH 320/338] Fix issue #332 (AttributeError: 'GeAdvantium' object has no attribute 'unit_type') and add 240V Cafe standalone to enum in library (#333) * v0.6.13 (#299) * Fix Advantium 'unit_type' error. Fix "AttributeError: 'GeAdvantium' object has no attribute 'unit_type'" error, by using the personality to determine unit_type. * Add Advantium 240v Cafe standalone to 240v check. Requires update from gehome library (pull request submitted there to account for this model). --------- Co-authored-by: simbaja <59273948+simbaja@users.noreply.github.com> Co-authored-by: Jack Simbach --- .../ge_home/entities/advantium/ge_advantium.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/entities/advantium/ge_advantium.py b/custom_components/ge_home/entities/advantium/ge_advantium.py index e76cd2d..2088d36 100644 --- a/custom_components/ge_home/entities/advantium/ge_advantium.py +++ b/custom_components/ge_home/entities/advantium/ge_advantium.py @@ -150,7 +150,7 @@ def extra_state_attributes(self) -> Optional[Mapping[str, Any]]: 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) + data["unit_type"] = self._stringify(self.personality) if cook_time_remaining: data["cook_time_remaining"] = self._stringify(cook_time_remaining) if kitchen_timer: @@ -271,8 +271,8 @@ async def _ensure_operation_mode(self): 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 [ErdPersonality.PERSONALITY_240V_MONOGRAM, ErdPersonality.PERSONALITY_240V_CAFE] else temp_120v + unit_type = self.personality + target_temp_f = temp_240v if unit_type in [ErdPersonality.PERSONALITY_240V_MONOGRAM, ErdPersonality.PERSONALITY_240V_CAFE, ErdPersonality.PERSONALITY_240V_STANDALONE_CAFE] else temp_120v if self.temperature_unit == SensorDeviceClass.FAHRENHEIT: return float(target_temp_f) else: From fdaa8f161da2a0b153a89fa4ebf8abf2a83e3bfb Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 1 Feb 2025 15:36:51 -0500 Subject: [PATCH 321/338] - fixed typo in ucim sensor name --- custom_components/ge_home/devices/ucim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/ucim.py b/custom_components/ge_home/devices/ucim.py index e342a9e..f4b60e3 100644 --- a/custom_components/ge_home/devices/ucim.py +++ b/custom_components/ge_home/devices/ucim.py @@ -36,7 +36,7 @@ def get_all_entities(self) -> List[Entity]: GeErdSwitch(self, ErdCode.OIM_POWER, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), GeErdSensor(self, ErdCode.OIM_PRODUCTION), GeErdSensor(self, ErdCode.UCIM_CLEAN_STATUS), - GeErdSensor(self, ErdCode.UCIM_FILTER_USED_PERCENTAGE), + GeErdSensor(self, ErdCode.UCIM_FILTER_PERCENTAGE_USED), GeErdBinarySensor(self, ErdCode.UCIM_BIN_FULL, device_class_override="problem"), ] From f20f8f0d33f5888adc251e4bb7a4d23649b1a57b Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 1 Feb 2025 15:39:13 -0500 Subject: [PATCH 322/338] - updated sdk dependency - switched to date-based versioning --- custom_components/ge_home/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 8e82ee4..8d4d89b 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.41","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==0.5.42","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "0.6.15" + "version": "2025.2.0" } From ef35df1b4fe27bdf478b9a5b4e7add0fcb4a82c5 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sat, 1 Feb 2025 15:59:09 -0500 Subject: [PATCH 323/338] - updated sdk dependency version --- custom_components/ge_home/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 8d4d89b..2a61582 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==0.5.42","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==2025.2.0","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "2025.2.0" } From 35ba372f4841fd38f4bf3c28ea9bb5c91e5b62c1 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 2 Feb 2025 18:43:03 -0500 Subject: [PATCH 324/338] - added pods number to dishwasher --- custom_components/ge_home/devices/dishwasher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index 5943513..32fbafa 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -5,7 +5,7 @@ from gehomesdk.erd import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdNumber _LOGGER = logging.getLogger(__name__) @@ -23,6 +23,7 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_STATE, icon_override="mdi:state-machine"), GeErdSensor(self, ErdCode.DISHWASHER_OPERATING_MODE), GeErdSensor(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE, uom_override="pods"), + GeErdNumber(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE, uom_override="pods", min_value=0, max_value=255), GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", icon_override="mdi:shimmer"), GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", icon_override="mdi:dishwasher-alert"), GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", icon_override="mdi:silverware-clean"), From acd54065646a4ade06e35f1f8a48ff1a02fa49b2 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 2 Feb 2025 19:36:10 -0500 Subject: [PATCH 325/338] - updated dishwasher pods (removed sensor) --- custom_components/ge_home/devices/dishwasher.py | 2 +- custom_components/ge_home/number.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index 32fbafa..8c4e91c 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -22,7 +22,7 @@ def get_all_entities(self) -> List[Entity]: 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"), +# GeErdSensor(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE, uom_override="pods"), GeErdNumber(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE, uom_override="pods", min_value=0, max_value=255), GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", icon_override="mdi:shimmer"), GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", icon_override="mdi:dishwasher-alert"), diff --git a/custom_components/ge_home/number.py b/custom_components/ge_home/number.py index d864c09..e691988 100644 --- a/custom_components/ge_home/number.py +++ b/custom_components/ge_home/number.py @@ -31,7 +31,7 @@ def async_devices_discovered(apis: list[ApplianceApi]): if isinstance(entity, GeErdNumber) if not registry.async_is_registered(entity.entity_id) ] - _LOGGER.debug(f'Found {len(entities):d} unregisterd numbers') + _LOGGER.debug(f'Found {len(entities):d} unregistered numbers') async_add_entities(entities) #if we're already initialized at this point, call device From c5b48783548ffbd5ffccc11440d947353c9acab6 Mon Sep 17 00:00:00 2001 From: Jack Simbach Date: Sun, 2 Feb 2025 19:51:42 -0500 Subject: [PATCH 326/338] - updated documentation --- CHANGELOG.md | 8 ++++++++ info.md | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06cea60..38fb9a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # GE Home Appliances (SmartHQ) Changelog +## 2025.2.0 + +- Breaking: Changed dishwasher pods to number +- Breaking: Removed outdated laundry status sensor +- Feature: Added under counter ice maker controls and sensors +- Feature: Changed versioning scheme +- Bugfix: Updated SDK to fix broken types + ## 0.6.15 - Feature: Improved Support for Laundry diff --git a/info.md b/info.md index 55488b9..f5d30ea 100644 --- a/info.md +++ b/info.md @@ -46,6 +46,11 @@ A/C Controls: #### Breaking Changes +{% if version_installed.split('.') | map('int') < '2025.2.0'.split('.') | map('int') %} +- Changed dishwasher pods to number +- Removed outdated laundry status sensor +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.15'.split('.') | map('int') %} - Some enums changed names/values and may need updates to client code {% endif %} @@ -73,6 +78,11 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '2025.2.0'.split('.') | map('int') %} +- Added under counter ice maker controls and sensors +- Changed versioning scheme for integration +{% endif %} + {% if version_installed.split('.') | map('int') < '0.6.15'.split('.') | map('int') %} - Improved Support for Laundry {% endif %} @@ -135,8 +145,8 @@ A/C Controls: #### Bugfixes -{% if version_installed.split('.') | map('int') < '0.6.15'.split('.') | map('int') %} -- Bugfix: More deprecation fixes +{% if version_installed.split('.') | map('int') < '2025.2.0'.split('.') | map('int') %} +- Updated SDK to fix broken types {% endif %} From 745fa4576982b57c1a01e8c4ff255309c6c086f8 Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Mon, 3 Feb 2025 20:38:33 -0500 Subject: [PATCH 327/338] 2025.2.1 (#341) --- CHANGELOG.md | 4 ++++ custom_components/ge_home/manifest.json | 4 ++-- info.md | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38fb9a9..8be6308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # GE Home Appliances (SmartHQ) Changelog +## 2025.2.1 + +- Bugfix: Fixed #339 + ## 2025.2.0 - Breaking: Changed dishwasher pods to number diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 2a61582..21d1577 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==2025.2.0","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==2025.2.2","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "2025.2.0" + "version": "2025.2.1" } diff --git a/info.md b/info.md index f5d30ea..ad6135e 100644 --- a/info.md +++ b/info.md @@ -145,6 +145,11 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '2025.2.1'.split('.') | map('int') %} +- Fix for #339 +{% endif %} + + {% if version_installed.split('.') | map('int') < '2025.2.0'.split('.') | map('int') %} - Updated SDK to fix broken types {% endif %} From 13ab89177499842d0d3b2cd7664c801768fb593e Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sun, 11 May 2025 19:31:27 -0400 Subject: [PATCH 328/338] Dev -> Master 2025.5.0 (#367) * - cleanup * - additional exception handling (attempt to fix #356) * - added boost/active modes to water heater --- CHANGELOG.md | 6 ++++++ README.md | 15 ++++++++------- custom_components/ge_home/config_flow.py | 5 +++-- custom_components/ge_home/devices/water_heater.py | 11 +++++++++++ .../entities/ccm/ge_ccm_brew_temperature.py | 10 +++++++++- custom_components/ge_home/manifest.json | 4 ++-- info.md | 13 +++++++++++++ 7 files changed, 52 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be6308..3c28346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # GE Home Appliances (SmartHQ) Changelog +## 2025.5.0 + +- Bugfix: Fixed helper deprecations +- Feature: Added boost/active states for water heaters +- Change: Improved documentation around terms of acceptance + ## 2025.2.1 - Bugfix: Fixed #339 diff --git a/README.md b/README.md index 748e67c..9ee2bc6 100644 --- a/README.md +++ b/README.md @@ -67,13 +67,14 @@ Once the HACS Integration of GE Home is completed: 1. Navigate to Settings --> Devices & Services 2. Click **Add Integration** blue button on the bottom-right of the page 3. Locate the **GE Home (SmartHQ)** "Brand" (Integration) -4. Click on the integration, and you will be prompted to enter a Username, Password and Location (US or EU) -5. Enter the email address you used to register/connect your device as the Username -6. Same with the password -7. Select the region you registered your device in (US or EU). -8. Once you submit, the integration will log in and get all your connected devices. -9. You can define in which area you device is, then click **Finish** -10. Your sensors should appear as **sensor._** +4. Open a new browser tab and navigate to where you can verify your username/password (helpful) but more importantly Accept the TermsOfUseAgreement (required!) +5. Click on the integration, and you will be prompted to enter a Username, Password and Location (US or EU) +6. Enter the email address you used to register/connect your device as the Username +7. Same with the password +8. Select the region you registered your device in (US or EU). +9. Once you submit, the integration will log in and get all your connected devices. +10. You can define in which area you device is, then click **Finish** +11. Your sensors should appear as **sensor._** ie: sensor.fs12345678_dishwasher_cycle_name ## Change Log diff --git a/custom_components/ge_home/config_flow.py b/custom_components/ge_home/config_flow.py index 3116627..a8070a1 100644 --- a/custom_components/ge_home/config_flow.py +++ b/custom_components/ge_home/config_flow.py @@ -18,6 +18,7 @@ from homeassistant import config_entries, core from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN # pylint:disable=unused-import from .exceptions import HaAuthError, HaCannotConnect, HaAlreadyConfigured @@ -35,11 +36,11 @@ async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - session = hass.helpers.aiohttp_client.async_get_clientsession(hass) + session = async_get_clientsession(hass) # noinspection PyBroadException try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): _ = await async_get_oauth2_token(session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_REGION]) except (asyncio.TimeoutError, aiohttp.ClientError): raise HaCannotConnect('Connection failure') diff --git a/custom_components/ge_home/devices/water_heater.py b/custom_components/ge_home/devices/water_heater.py index aad2aa9..e571a75 100644 --- a/custom_components/ge_home/devices/water_heater.py +++ b/custom_components/ge_home/devices/water_heater.py @@ -29,6 +29,9 @@ class WaterHeaterApi(ApplianceApi): def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() + boost_mode: ErdOnOff = self.try_get_erd_value(ErdCode.WH_HEATER_BOOST_STATE) + active: ErdOnOff = self.try_get_erd_value(ErdCode.WH_HEATER_ACTIVE_STATE) + wh_entities = [ GeErdSensor(self, ErdCode.WH_HEATER_TARGET_TEMPERATURE), GeErdSensor(self, ErdCode.WH_HEATER_TEMPERATURE), @@ -38,6 +41,14 @@ def get_all_entities(self) -> List[Entity]: GeWaterHeater(self) ] + if(boost_mode and boost_mode != ErdOnOff.NA): + wh_entities.append(GeErdSensor(self, ErdCode.WH_HEATER_BOOST_STATE)) + wh_entities.append(GeErdSwitch(self, ErdCode.WH_HEATER_BOOST_CONTROL, ErdOnOffBoolConverter(), icon_on_override="mdi:rocket-launch", icon_off_override="mdi:rocket-launch-outline")) + + if(active and active != ErdOnOff.NA): + wh_entities.append(GeErdSensor(self, ErdCode.WH_HEATER_ACTIVE_STATE)) + wh_entities.append(GeErdSwitch(self, ErdCode.WH_HEATER_ACTIVE_CONTROL, ErdOnOffBoolConverter(), icon_on_override="mdi:power", icon_off_override="mdi:power-standby")) + entities = base_entities + wh_entities return entities diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py index c7895d9..81b13a4 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py @@ -3,9 +3,17 @@ from ..common import GeErdNumber from .ge_ccm_cached_value import GeCcmCachedValue +DEFAULT_MIN_TEMP = 100 +DEFAULT_MAX_TEMP = 225 + class GeCcmBrewTemperatureNumber(GeErdNumber, GeCcmCachedValue): def __init__(self, api: ApplianceApi): - min_temp, max_temp, _ = api.appliance.get_erd_value(ErdCode.CCM_BREW_TEMPERATURE_RANGE) + try: + min_temp, max_temp, _ = api.appliance.get_erd_value(ErdCode.CCM_BREW_TEMPERATURE_RANGE) + except: + min_temp = DEFAULT_MIN_TEMP + max_temp = DEFAULT_MAX_TEMP + GeErdNumber.__init__(self, api = api, erd_code = ErdCode.CCM_BREW_TEMPERATURE, min_value=min_temp, max_value=max_temp, mode="slider") GeCcmCachedValue.__init__(self) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 21d1577..3573241 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==2025.2.2","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==2025.5.0","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "2025.2.1" + "version": "2025.5.0" } diff --git a/info.md b/info.md index ad6135e..22b839d 100644 --- a/info.md +++ b/info.md @@ -72,12 +72,20 @@ A/C Controls: #### Changes +{% if version_installed.split('.') | map('int') < '2025.5.0'.split('.') | map('int') %} +- Improved documentation around terms of acceptance +{% endif %} + {% if version_installed.split('.') | map('int') < '0.5.0'.split('.') | map('int') %} - Added logic to prevent multiple configurations of the same GE account {% endif %} #### Features +{% if version_installed.split('.') | map('int') < '2025.5.0'.split('.') | map('int') %} +- Added boost/active states for water heaters +{% endif %} + {% if version_installed.split('.') | map('int') < '2025.2.0'.split('.') | map('int') %} - Added under counter ice maker controls and sensors - Changed versioning scheme for integration @@ -145,6 +153,11 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '2025.5.0'.split('.') | map('int') %} +- Fixed helper deprecations +{% endif %} + + {% if version_installed.split('.') | map('int') < '2025.2.1'.split('.') | map('int') %} - Fix for #339 {% endif %} From 0df0e4844c81201050d5d56f561166b1dd6774e5 Mon Sep 17 00:00:00 2001 From: derekcentrico <1930094+derekcentrico@users.noreply.github.com> Date: Sun, 27 Jul 2025 10:02:45 -0400 Subject: [PATCH 329/338] Native k-cup support, washer/dryer remote start support, and silence stringprep warning (#385) * Silence stringprep warning On systems without cython installed the underlying slixmpp library logs a frequent performance warning about stringprep. This warning is not actionable for most users and creates unnecessary noise in the logs. This change silences the warning by setting the specific logger's level to ERROR, preventing the message from appearing without affecting functionality. * Add native switch for refrigerator K-Cup hot water This adds a new switch entity to control the K-Cup/hot water brewing feature on compatible GE fridges. This change makes it a native switch that is automatically discovered, providing a user-friendly experience directly on the device card. The new switch is only created for devices that report having the hot water feature, so it shouldn't affect users with other appliance models. * washer toggle attempt The idea if it works is that this will create a switch that monitors the status of the machine. OFF is unit is off, -and- ON is when its in RPP delay status. When it's ON it can be selected to start the cycle. Very basic attempt here that needs tested to refine. * Update __init__.py Forgot to push the line. * Full implementation of RPP control switches This adds RPP control switches to washers and dryers with remote delay functionality. Device must be in the remote delay mode for the button to be available -and- for a press event to work. * dryer.py update Adding to have the button exactly as the washer. * Additional model support Some models do not report "Delay Run" as a status code. Changes made that permit support for more models. --- custom_components/ge_home/devices/dryer.py | 17 +++--- custom_components/ge_home/devices/fridge.py | 36 ++++++----- custom_components/ge_home/devices/washer.py | 19 ++++-- .../ge_home/entities/fridge/__init__.py | 3 +- .../ge_home/entities/fridge/ge_kcup_switch.py | 61 +++++++++++++++++++ .../ge_home/entities/laundry/__init__.py | 2 + .../entities/laundry/ge_dryer_cycle_button.py | 46 ++++++++++++++ .../laundry/ge_washer_cycle_button.py | 46 ++++++++++++++ custom_components/ge_home/switch.py | 47 ++++++++------ .../ge_home/update_coordinator.py | 2 + 10 files changed, 232 insertions(+), 47 deletions(-) create mode 100644 custom_components/ge_home/entities/fridge/ge_kcup_switch.py create mode 100644 custom_components/ge_home/entities/laundry/__init__.py create mode 100644 custom_components/ge_home/entities/laundry/ge_dryer_cycle_button.py create mode 100644 custom_components/ge_home/entities/laundry/ge_washer_cycle_button.py diff --git a/custom_components/ge_home/devices/dryer.py b/custom_components/ge_home/devices/dryer.py index cc0110d..6b59ac8 100644 --- a/custom_components/ge_home/devices/dryer.py +++ b/custom_components/ge_home/devices/dryer.py @@ -5,7 +5,9 @@ from gehomesdk import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdBinarySensor +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdButton +from ..entities.laundry.ge_dryer_cycle_button import GeDryerCycleButton + _LOGGER = logging.getLogger(__name__) @@ -25,19 +27,19 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR), GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, icon_on_override="mdi:tumble-dryer", icon_off_override="mdi:tumble-dryer"), - GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_BLOCKED_VENT_FAULT, icon_on_override="mid:alert-circle", icon_off_override="mdi:alert-circle"), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_BLOCKED_VENT_FAULT, icon_on_override="mdi:alert-circle", icon_off_override="mdi:alert-circle"), ] dryer_entities = self.get_dryer_entities() + + # Add the start cycle button + dryer_entities.append(GeDryerCycleButton(self)) 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 = [ - ] + dryer_entities = [] if self.has_erd_code(ErdCode.LAUNDRY_DRYER_DRYNESS_LEVEL): dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_DRYNESS_LEVEL)]) @@ -62,5 +64,4 @@ def get_dryer_entities(self): if self.has_erd_code(ErdCode.LAUNDRY_DRYER_ECODRY_OPTION_SELECTION): dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_ECODRY_OPTION_SELECTION)]) - return dryer_entities - + return dryer_entities \ No newline at end of file diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index 5f5dd8c..bde5813 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -5,7 +5,7 @@ from homeassistant.helpers.entity import Entity from gehomesdk import ( - ErdCode, + ErdCode, ErdApplianceType, ErdOnOff, ErdHotWaterStatus, @@ -19,22 +19,27 @@ ) from .base import ApplianceApi +# This block is now split to import from the correct sub-folders from ..entities import ( ErdOnOffBoolConverter, GeErdSensor, GeErdBinarySensor, - GeErdSwitch, + GeErdSwitch, GeErdSelect, GeErdLight, - GeFridge, - GeFreezer, - GeDispenser, GeErdPropertySensor, - GeErdPropertyBinarySensor, + GeErdPropertyBinarySensor +) +from ..entities.fridge import ( + GeFridge, + GeFreezer, + GeDispenser, ConvertableDrawerModeOptionsConverter, - GeFridgeIceControlSwitch + GeFridgeIceControlSwitch, + GeKCupSwitch ) + _LOGGER = logging.getLogger(__name__) class FridgeApi(ApplianceApi): @@ -43,7 +48,7 @@ class FridgeApi(ApplianceApi): def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() - + fridge_entities = [] freezer_entities = [] dispenser_entities = [] @@ -65,7 +70,7 @@ def get_all_entities(self) -> List[Entity]: turbo_cool: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_COOL_STATUS) turbo_freeze: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_FREEZE_STATUS) ice_boost: ErdOnOff = self.try_get_erd_value(ErdCode.FRIDGE_ICE_BOOST) - + units = self.hass.config.units # Common entities @@ -92,7 +97,7 @@ def get_all_entities(self) -> List[Entity]: 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)) + 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): @@ -100,17 +105,17 @@ def get_all_entities(self) -> List[Entity]: 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))) + 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), + GeFreezer(self), ]) if turbo_freeze is not None: freezer_entities.append(GeErdSwitch(self, ErdCode.TURBO_FREEZE_STATUS)) @@ -131,8 +136,9 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_until_ready", icon_override="mdi:timer-outline"), GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "current_temp", device_class_override=SensorDeviceClass.TEMPERATURE, data_type_override=ErdDataType.INT), GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=BinarySensorDeviceClass.PROBLEM), - GeDispenser(self) + GeDispenser(self), + GeKCupSwitch(self) ]) entities = base_entities + common_entities + fridge_entities + freezer_entities + dispenser_entities - return entities + return entities \ No newline at end of file diff --git a/custom_components/ge_home/devices/washer.py b/custom_components/ge_home/devices/washer.py index 9cc0372..119e4ed 100644 --- a/custom_components/ge_home/devices/washer.py +++ b/custom_components/ge_home/devices/washer.py @@ -5,7 +5,9 @@ from gehomesdk import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdButton +from ..entities.laundry.ge_washer_cycle_button import GeWasherCycleButton + _LOGGER = logging.getLogger(__name__) @@ -28,13 +30,15 @@ def get_all_entities(self) -> List[Entity]: GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS), ] - washer_entities = self.get_washer_entities() + washer_entities = self.get_washer_entities() + + washer_entities.append(GeWasherCycleButton(self)) entities = base_entities + common_entities + washer_entities return entities - + def get_washer_entities(self) -> List[Entity]: - washer_entities = [ + 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"), @@ -44,20 +48,23 @@ def get_washer_entities(self) -> List[Entity]: 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)]) + 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 + return washer_entities \ No newline at end of file diff --git a/custom_components/ge_home/entities/fridge/__init__.py b/custom_components/ge_home/entities/fridge/__init__.py index 2d14761..0703f82 100644 --- a/custom_components/ge_home/entities/fridge/__init__.py +++ b/custom_components/ge_home/entities/fridge/__init__.py @@ -2,4 +2,5 @@ from .ge_freezer import GeFreezer from .ge_dispenser import GeDispenser from .convertable_drawer_mode_options import ConvertableDrawerModeOptionsConverter -from .ge_fridge_ice_control_switch import GeFridgeIceControlSwitch \ No newline at end of file +from .ge_fridge_ice_control_switch import GeFridgeIceControlSwitch +from .ge_kcup_switch import GeKCupSwitch \ No newline at end of file diff --git a/custom_components/ge_home/entities/fridge/ge_kcup_switch.py b/custom_components/ge_home/entities/fridge/ge_kcup_switch.py new file mode 100644 index 0000000..e50fd03 --- /dev/null +++ b/custom_components/ge_home/entities/fridge/ge_kcup_switch.py @@ -0,0 +1,61 @@ +import logging +from typing import Optional + +from homeassistant.components.switch import SwitchEntity +from gehomesdk import ErdCode + +from ...devices import ApplianceApi +from ..common import GeEntity + +_LOGGER = logging.getLogger(__name__) + +# Define the ON and OFF temperature values as constants +K_CUP_ON_TEMP = 190 +K_CUP_OFF_TEMP = 0 + +class GeKCupSwitch(GeEntity, SwitchEntity): + """A switch to control the K-Cup hot water feature.""" + + def __init__(self, api: ApplianceApi): + # Pass the api instance to the base class + super().__init__(api) + + @property + def unique_id(self) -> str: + # Create a unique ID for this entity + return f"{self.api.serial_or_mac}_kcup_hot_water" + + @property + def name(self) -> Optional[str]: + # Set the friendly name to match other switches using the device's unique ID + return f"{self.api.serial_or_mac} K-Cup Hot Water" + + @property + def icon(self) -> Optional[str]: + # Set the icon based on the switch's state + return "mdi:coffee-maker" if self.is_on else "mdi:coffee-maker-off-outline" + + @property + def is_on(self) -> bool: + """Return true if the hot water is set to a non-zero temperature.""" + try: + # The switch is "on" if the target temperature is not the "off" value + current_set_temp = self.api.try_get_erd_value(ErdCode.HOT_WATER_SET_TEMP) + return current_set_temp != K_CUP_OFF_TEMP + except Exception as e: + _LOGGER.warning(f"Could not get K-Cup status for {self.unique_id}: {e}") + return False + + async def async_turn_on(self, **kwargs): + """Turn the K-Cup heater on by setting the target temperature.""" + _LOGGER.debug(f"Turning on K-Cup heater for {self.unique_id}") + await self.api.appliance.async_set_erd_value( + ErdCode.HOT_WATER_SET_TEMP, K_CUP_ON_TEMP + ) + + async def async_turn_off(self, **kwargs): + """Turn the K-Cup heater off by setting the target temperature to zero.""" + _LOGGER.debug(f"Turning off K-Cup heater for {self.unique_id}") + await self.api.appliance.async_set_erd_value( + ErdCode.HOT_WATER_SET_TEMP, K_CUP_OFF_TEMP + ) \ No newline at end of file diff --git a/custom_components/ge_home/entities/laundry/__init__.py b/custom_components/ge_home/entities/laundry/__init__.py new file mode 100644 index 0000000..9e91857 --- /dev/null +++ b/custom_components/ge_home/entities/laundry/__init__.py @@ -0,0 +1,2 @@ +from .ge_washer_cycle_button import GeWasherCycleButton +from .ge_dryer_cycle_button import GeDryerCycleButton \ No newline at end of file diff --git a/custom_components/ge_home/entities/laundry/ge_dryer_cycle_button.py b/custom_components/ge_home/entities/laundry/ge_dryer_cycle_button.py new file mode 100644 index 0000000..7d44184 --- /dev/null +++ b/custom_components/ge_home/entities/laundry/ge_dryer_cycle_button.py @@ -0,0 +1,46 @@ +import logging +from typing import Any +from datetime import timedelta + +from gehomesdk import ErdCode, ErdMachineState +from homeassistant.components.button import ButtonEntity +from ..common import GeErdButton + +_LOGGER = logging.getLogger(__name__) + +class GeDryerCycleButton(GeErdButton): + """A button to start a dryer cycle.""" + + def __init__(self, api): + super().__init__(api, ErdCode.LAUNDRY_MACHINE_STATE) + + @property + def unique_id(self) -> str: + """Return a unique ID for the button.""" + return f"{self.serial_or_mac}_start_cycle_button" + + @property + def name(self) -> str: + """Return the name of the button.""" + return f"{self.serial_or_mac} Start Cycle" + + @property + def icon(self): + """Return the icon.""" + return "mdi:play-circle" + + @property + def available(self) -> bool: + """The button is only available if remote start is enabled on the appliance.""" + try: + return self.appliance.get_erd_value(ErdCode.LAUNDRY_REMOTE_STATUS) + except: + return False + + async def async_press(self) -> None: + """Send the start command by setting the delay time to zero.""" + _LOGGER.debug(f"Sending START command to {self.unique_id}") + await self.appliance.async_set_erd_value( + ErdCode.LAUNDRY_REMOTE_DELAY_CONTROL, + timedelta(seconds=0) + ) \ No newline at end of file diff --git a/custom_components/ge_home/entities/laundry/ge_washer_cycle_button.py b/custom_components/ge_home/entities/laundry/ge_washer_cycle_button.py new file mode 100644 index 0000000..2a52a44 --- /dev/null +++ b/custom_components/ge_home/entities/laundry/ge_washer_cycle_button.py @@ -0,0 +1,46 @@ +import logging +from typing import Any +from datetime import timedelta + +from gehomesdk import ErdCode, ErdMachineState +from homeassistant.components.button import ButtonEntity +from ..common import GeErdButton + +_LOGGER = logging.getLogger(__name__) + +class GeWasherCycleButton(GeErdButton): + """A button to start a washer cycle.""" + + def __init__(self, api): + super().__init__(api, ErdCode.LAUNDRY_MACHINE_STATE) + + @property + def unique_id(self) -> str: + """Return a unique ID for the button.""" + return f"{self.serial_or_mac}_start_cycle_button" + + @property + def name(self) -> str: + """Return the name of the button.""" + return f"{self.serial_or_mac} Start Cycle" + + @property + def icon(self): + """Return the icon.""" + return "mdi:play-circle" + + @property + def available(self) -> bool: + """The button is only available if remote start is enabled on the appliance.""" + try: + return self.appliance.get_erd_value(ErdCode.LAUNDRY_REMOTE_STATUS) + except: + return False + + async def async_press(self) -> None: + """Send the start command by setting the delay time to zero.""" + _LOGGER.debug(f"Sending START command to {self.unique_id}") + await self.appliance.async_set_erd_value( + ErdCode.LAUNDRY_REMOTE_DELAY_CONTROL, + timedelta(seconds=0) + ) \ No newline at end of file diff --git a/custom_components/ge_home/switch.py b/custom_components/ge_home/switch.py index 7c339ae..a2b4823 100644 --- a/custom_components/ge_home/switch.py +++ b/custom_components/ge_home/switch.py @@ -6,6 +6,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers import entity_registry as er +from homeassistant.components.switch import SwitchEntity from .entities import GeErdSwitch from .const import DOMAIN @@ -15,30 +16,42 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home sensors.""" + """GE Home switches.""" _LOGGER.debug('Adding GE Home switches') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] registry = er.async_get(hass) @callback def async_devices_discovered(apis: list[ApplianceApi]): + """Add new switch entities from the device API.""" _LOGGER.debug(f'Found {len(apis):d} appliance APIs') - entities = [ - entity - for api in apis - for entity in api.entities - if isinstance(entity, GeErdSwitch) and entity.erd_code in api.appliance._property_cache - if not registry.async_is_registered(entity.entity_id) - ] - _LOGGER.debug(f'Found {len(entities):d} unregistered switches') - async_add_entities(entities) - - #if we're already initialized at this point, call device - #discovery directly, otherwise add a callback based on the - #ready signal + + new_entities = [] + for api in apis: + for entity in api.entities: + # Skip if the entity is already registered + if registry.async_is_registered(entity.entity_id): + continue + + # Check if it's a switch entity we should add + if isinstance(entity, SwitchEntity): + # Special handling for GeErdSwitch: it requires the erd_code to be in the property cache + if isinstance(entity, GeErdSwitch): + if entity.erd_code in api.appliance._property_cache: + new_entities.append(entity) + else: + # For other switche types add them directly + new_entities.append(entity) + + _LOGGER.debug(f'Found {len(new_entities):d} unregistered switches') + async_add_entities(new_entities) + + # If we're already initialized at this point, call device + # discovery directly, otherwise add a callback based on the + # ready signal if coordinator.initialized: async_devices_discovered(coordinator.appliance_apis.values()) - else: - # add the ready signal and register the remove callback + else: + # Add the ready signal and register the remove callback coordinator.add_signal_remove_callback( - async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) + async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)) \ No newline at end of file diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 9bd27f4..8ab3eae 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -5,6 +5,8 @@ import logging from typing import Any, Callable, Dict, Iterable, Optional, Tuple, List +logging.getLogger('slixmpp.stringprep').setLevel(logging.ERROR) + from gehomesdk import ( EVENT_APPLIANCE_INITIAL_UPDATE, EVENT_APPLIANCE_UPDATE_RECEIVED, From 6528af6d9432b7880e00cb4f17d88b1393bcad2a Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sun, 27 Jul 2025 11:26:59 -0400 Subject: [PATCH 330/338] Documentation/Version Updates - updated documentation of changes - updated version --- CHANGELOG.md | 6 ++++++ custom_components/ge_home/manifest.json | 2 +- hacs.json | 2 +- info.md | 9 +++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c28346..2f64974 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # GE Home Appliances (SmartHQ) Changelog +## 2025.7.0 + +- Change: Silenced string prep warning [#386] (@derekcentrico) +- Feature: Enabled Washer/Dryer remote start [#369] (@derekcentrico) +- Feature: Enabled K-cup refrigerator functionality [#101] (@derekcentrico) + ## 2025.5.0 - Bugfix: Fixed helper deprecations diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 3573241..a2d8d83 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==2025.5.0","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], - "version": "2025.5.0" + "version": "2025.7.0" } diff --git a/hacs.json b/hacs.json index 1d7862f..6bfcd26 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Home (SmartHQ)", - "homeassistant": "2024.9.0", + "homeassistant": "2025.1.0", "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select", "button", "climate", "light", "number"], "iot_class": "Cloud Polling" } diff --git a/info.md b/info.md index 22b839d..aa54b32 100644 --- a/info.md +++ b/info.md @@ -72,6 +72,10 @@ A/C Controls: #### Changes +{% if version_installed.split('.') | map('int') < '2025.7.0'.split('.') | map('int') %} +- Silenced string prep warning [#386] (@derekcentrico) +{% endif %} + {% if version_installed.split('.') | map('int') < '2025.5.0'.split('.') | map('int') %} - Improved documentation around terms of acceptance {% endif %} @@ -82,6 +86,11 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '2025.7.0'.split('.') | map('int') %} +- Enabled Washer/Dryer remote start [#369] (@derekcentrico) +- Enabled K-cup refrigerator functionality [#101] (@derekcentrico) +{% endif %} + {% if version_installed.split('.') | map('int') < '2025.5.0'.split('.') | map('int') %} - Added boost/active states for water heaters {% endif %} From 870c64ef7c6a1ea7ca37cea1b6ebe62e0a94d4af Mon Sep 17 00:00:00 2001 From: Mitch Just Date: Mon, 13 Oct 2025 00:19:40 +1100 Subject: [PATCH 331/338] Support Controls with separate state/control ERDs (#404) * Handle Dual ERD controls for water heaters * Handle Dual ERD controls for water heaters --------- Co-authored-by: mitchjust --- .../ge_home/devices/water_heater.py | 10 ++--- .../ge_home/entities/common/__init__.py | 1 + .../common/ge_erd_binary_sensor_switch.py | 37 +++++++++++++++++++ 3 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 custom_components/ge_home/entities/common/ge_erd_binary_sensor_switch.py diff --git a/custom_components/ge_home/devices/water_heater.py b/custom_components/ge_home/devices/water_heater.py index e571a75..19ca226 100644 --- a/custom_components/ge_home/devices/water_heater.py +++ b/custom_components/ge_home/devices/water_heater.py @@ -13,9 +13,7 @@ from .base import ApplianceApi from ..entities import ( GeErdSensor, - GeErdBinarySensor, - GeErdSelect, - GeErdSwitch, + GeErdBinarySensorSwitch, ErdOnOffBoolConverter ) @@ -42,12 +40,10 @@ def get_all_entities(self) -> List[Entity]: ] if(boost_mode and boost_mode != ErdOnOff.NA): - wh_entities.append(GeErdSensor(self, ErdCode.WH_HEATER_BOOST_STATE)) - wh_entities.append(GeErdSwitch(self, ErdCode.WH_HEATER_BOOST_CONTROL, ErdOnOffBoolConverter(), icon_on_override="mdi:rocket-launch", icon_off_override="mdi:rocket-launch-outline")) + wh_entities.append(GeErdBinarySensorSwitch(self, ErdCode.WH_HEATER_BOOST_STATE, ErdCode.WH_HEATER_BOOST_CONTROL, ErdOnOffBoolConverter(), icon_on_override="mdi:rocket-launch", icon_off_override="mdi:rocket-launch-outline")) if(active and active != ErdOnOff.NA): - wh_entities.append(GeErdSensor(self, ErdCode.WH_HEATER_ACTIVE_STATE)) - wh_entities.append(GeErdSwitch(self, ErdCode.WH_HEATER_ACTIVE_CONTROL, ErdOnOffBoolConverter(), icon_on_override="mdi:power", icon_off_override="mdi:power-standby")) + wh_entities.append(GeErdBinarySensorSwitch(self, ErdCode.WH_HEATER_ACTIVE_STATE, ErdCode.WH_HEATER_ACTIVE_CONTROL, ErdOnOffBoolConverter(), icon_on_override="mdi:power", icon_off_override="mdi:power-standby")) entities = base_entities + wh_entities return entities diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 546ef73..91b2bfa 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -3,6 +3,7 @@ from .ge_entity import GeEntity from .ge_erd_entity import GeErdEntity from .ge_erd_binary_sensor import GeErdBinarySensor +from .ge_erd_binary_sensor_switch import GeErdBinarySensorSwitch from .ge_erd_property_binary_sensor import GeErdPropertyBinarySensor from .ge_erd_sensor import GeErdSensor from .ge_erd_light import GeErdLight diff --git a/custom_components/ge_home/entities/common/ge_erd_binary_sensor_switch.py b/custom_components/ge_home/entities/common/ge_erd_binary_sensor_switch.py new file mode 100644 index 0000000..b7596aa --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_erd_binary_sensor_switch.py @@ -0,0 +1,37 @@ +import logging + +from homeassistant.components.switch import SwitchEntity + +from gehomesdk import ErdCodeType +from ...devices import ApplianceApi +from .ge_erd_binary_sensor import GeErdBinarySensor +from .bool_converter import BoolConverter + +_LOGGER = logging.getLogger(__name__) + +class GeErdBinarySensorSwitch(GeErdBinarySensor, SwitchEntity): + """Switch that uses separate ERD codes for reading state and writing control.""" + device_class = "switch" + + def __init__(self, api: ApplianceApi, state_erd_code: ErdCodeType, control_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): + # Use the state ERD code for the base initialization (for entity ID, etc.) + super().__init__(api, state_erd_code, erd_override, icon_on_override, icon_off_override, device_class_override) + self._state_erd_code = state_erd_code + self._control_erd_code = control_erd_code + self._converter = bool_converter + + @property + def is_on(self) -> bool: + """Return True if switch is on based on state ERD code.""" + return self._converter.boolify(self.appliance.get_erd_value(self._state_erd_code)) + + + async def async_turn_on(self, **kwargs): + """Turn the switch on using control ERD code.""" + _LOGGER.debug(f"Turning on {self.unique_id}") + await self.appliance.async_set_erd_value(self._control_erd_code, self._converter.true_value()) + + async def async_turn_off(self, **kwargs): + """Turn the switch off using control ERD code.""" + _LOGGER.debug(f"Turning off {self.unique_id}") + await self.appliance.async_set_erd_value(self._control_erd_code, self._converter.false_value()) From 0bba0c80c30d3b21acf547dc089ae9e08d3ed324 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 12 Oct 2025 09:20:46 -0400 Subject: [PATCH 332/338] Add heat mode for Window ACs that support it (#396) * Add heat mode for Window ACs that support it * bug fixes * redundant --- .../ge_home/entities/ac/ge_wac_climate.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/custom_components/ge_home/entities/ac/ge_wac_climate.py b/custom_components/ge_home/entities/ac/ge_wac_climate.py index 2754b90..56b7edb 100644 --- a/custom_components/ge_home/entities/ac/ge_wac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_wac_climate.py @@ -2,7 +2,7 @@ from typing import Any, List, Optional from homeassistant.components.climate import HVACMode -from gehomesdk import ErdAcOperationMode +from gehomesdk import ErdAcOperationMode, ErdCode, ErdSacAvailableModes from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter from .fan_mode_options import AcFanModeOptionsConverter, AcFanOnlyFanModeOptionsConverter @@ -10,14 +10,22 @@ _LOGGER = logging.getLogger(__name__) class WacHvacModeOptionsConverter(OptionsConverter): + def __init__(self, available_modes: ErdSacAvailableModes): + self._available_modes = available_modes + @property def options(self) -> List[str]: - return [HVACMode.AUTO, HVACMode.COOL, HVACMode.FAN_ONLY] + modes = [HVACMode.AUTO, HVACMode.COOL, HVACMode.FAN_ONLY] + if self._available_modes and self._available_modes.has_heat: + modes.append(HVACMode.HEAT) + return modes + def from_option_string(self, value: str) -> Any: try: return { HVACMode.AUTO: ErdAcOperationMode.ENERGY_SAVER, HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.HEAT: ErdAcOperationMode.HEAT, HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY }.get(value) except: @@ -29,6 +37,7 @@ def to_option_string(self, value: Any) -> Optional[str]: ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, ErdAcOperationMode.AUTO: HVACMode.AUTO, ErdAcOperationMode.COOL: HVACMode.COOL, + ErdAcOperationMode.HEAT: HVACMode.HEAT, ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) except: @@ -38,4 +47,5 @@ def to_option_string(self, value: Any) -> Optional[str]: class GeWacClimate(GeClimate): """Class for Window AC units""" def __init__(self, api: ApplianceApi): - super().__init__(api, WacHvacModeOptionsConverter(), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) + available_modes = api.try_get_erd_value(ErdCode.SAC_AVAILABLE_MODES) + super().__init__(api, WacHvacModeOptionsConverter(available_modes), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) From 7c87b764a3bfe061d49fbab391052b04957f7477 Mon Sep 17 00:00:00 2001 From: nu <2722344+ncd7@users.noreply.github.com> Date: Sun, 16 Nov 2025 21:21:06 -0500 Subject: [PATCH 333/338] Resolves issue where a single invalid/unsupported devices blocks the initialization of all other valid/supported devices (#422) * Resolves issue where a single invalid/unsupported blocks the initialization of all valid/supported devices. * Fix return type of _is_appliance_valid method --- .../ge_home/update_coordinator.py | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 8ab3eae..4389a0d 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -107,7 +107,10 @@ def create_ge_client( @property def appliances(self) -> Iterable[GeAppliance]: - return self.client.appliances.values() + return ( + appliance for appliance in self.client.appliances.values() + if self._is_appliance_valid(appliance) + ) @property def appliance_apis(self) -> Dict[str, ApplianceApi]: @@ -125,7 +128,7 @@ def initialized(self) -> bool: @property def online(self) -> bool: """ - Indicates whether the services is online. If it's retried several times, it's assumed + Indicates whether the services is online. If it's retried several times, it's assumed that it's offline for some reason """ return self.connected or self._retry_count <= RETRY_OFFLINE_COUNT @@ -138,16 +141,17 @@ def connected(self) -> bool: return self.client and self.client.connected def _get_appliance_api(self, appliance: GeAppliance) -> ApplianceApi: + self._dump_appliance(appliance) api_type = get_appliance_api_type(appliance.appliance_type) return api_type(self, appliance) def regenerate_appliance_apis(self): """Regenerate the appliance_apis dictionary, adding elements as necessary.""" for jid, appliance in self.client.appliances.keys(): - if jid not in self._appliance_apis: + if jid not in self._appliance_apis and self._is_appliance_valid(appliance): self._appliance_apis[jid] = self._get_appliance_api(appliance) - def maybe_add_appliance_api(self, appliance: GeAppliance): + def _maybe_add_appliance_api(self, appliance: GeAppliance): mac_addr = appliance.mac_addr if mac_addr not in self.appliance_apis: _LOGGER.debug(f"Adding appliance api for appliance {mac_addr} ({appliance.appliance_type})") @@ -276,6 +280,13 @@ async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any] """Let HA know there's new state.""" self.last_update_success = True appliance, _ = data + + self._dump_appliance(appliance) + + if not self._is_appliance_valid(appliance): + _LOGGER.debug(f"on_device_update: skipping invalid appliance {appliance.mac_addr}") + return + try: api = self.appliance_apis[appliance.mac_addr] except KeyError: @@ -325,10 +336,16 @@ async def on_appliance_list(self, _): await self.async_maybe_trigger_all_ready() async def on_device_initial_update(self, appliance: GeAppliance): + self._dump_appliance(appliance) + + if not self._is_appliance_valid(appliance): + _LOGGER.debug(f"on_device_initial_update: skipping invalid appliance {appliance.mac_addr}") + return + """When an appliance first becomes ready, let the system know and schedule periodic updates.""" _LOGGER.debug(f"Got initial update for {appliance.mac_addr}") self.last_update_success = True - self.maybe_add_appliance_api(appliance) + self._maybe_add_appliance_api(appliance) await self.async_maybe_trigger_all_ready() _LOGGER.debug(f"Requesting updates for {appliance.mac_addr}") while self.connected: @@ -366,3 +383,33 @@ async def async_maybe_trigger_all_ready(self): def _get_retry_delay(self) -> int: delay = MIN_RETRY_DELAY * 2 ** (self._retry_count - 1) return min(delay, MAX_RETRY_DELAY) + + def _is_appliance_valid(self, appliance: GeAppliance) -> bool: + return appliance.appliance_type and appliance.available + + def _dump_appliance(self, appliance: GeAppliance) -> None: + if not _LOGGER.isEnabledFor(logging.DEBUG): + return + + import pprint + try: + _LOGGER.debug(f"--- COMPREHENSIVE DUMP FOR APPLIANCE: {appliance.mac_addr} ---") + appliance_data = {} + # dir() gets all attrs, including properties and methods + for attr_name in dir(appliance): + # skip "magic" methods and "private" attributes to reduce noise + if attr_name.startswith('_'): + continue + try: + value = getattr(appliance, attr_name) + # for now skip methods - we only want data + if callable(value): + continue + appliance_data[attr_name] = value + except Exception: + # some props might fail if called out of context + appliance_data[attr_name] = "Error: Could not read attribute" + _LOGGER.debug(pprint.pformat(appliance_data)) + _LOGGER.debug("--- END OF COMPREHENSIVE DUMP ---") + except Exception as e: + _LOGGER.error(f"Could not dump appliance {appliance}: {e}") From 982d881c1b29aead4fa103adc6ac14cf7d402180 Mon Sep 17 00:00:00 2001 From: Dave Date: Sun, 16 Nov 2025 21:21:35 -0500 Subject: [PATCH 334/338] Heat uses a separate temperature code. (#416) Also, sync up with heat-related updates to gehomesdk - REQUIRES gehomesdk 2025.6.0. --- custom_components/ge_home/entities/ac/ge_pac_climate.py | 6 +++--- custom_components/ge_home/entities/ac/ge_sac_climate.py | 6 +++--- custom_components/ge_home/entities/ac/ge_wac_climate.py | 6 +++--- custom_components/ge_home/entities/common/ge_climate.py | 6 +++++- custom_components/ge_home/manifest.json | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/custom_components/ge_home/entities/ac/ge_pac_climate.py b/custom_components/ge_home/entities/ac/ge_pac_climate.py index 42a7a98..488b585 100644 --- a/custom_components/ge_home/entities/ac/ge_pac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_pac_climate.py @@ -2,7 +2,7 @@ from typing import Any, List, Optional from homeassistant.components.climate import HVACMode -from gehomesdk import ErdCode, ErdAcOperationMode, ErdSacAvailableModes, ErdSacTargetTemperatureRange +from gehomesdk import ErdCode, ErdAcOperationMode, ErdAcAvailableModes, ErdSacTargetTemperatureRange from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter from .fan_mode_options import AcFanOnlyFanModeOptionsConverter @@ -10,7 +10,7 @@ _LOGGER = logging.getLogger(__name__) class PacHvacModeOptionsConverter(OptionsConverter): - def __init__(self, available_modes: ErdSacAvailableModes): + def __init__(self, available_modes: ErdAcAvailableModes): self._available_modes = available_modes @property @@ -51,7 +51,7 @@ def __init__(self, api: ApplianceApi): 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._modes: ErdAcAvailableModes = self.api.try_get_erd_value(ErdCode.AC_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) diff --git a/custom_components/ge_home/entities/ac/ge_sac_climate.py b/custom_components/ge_home/entities/ac/ge_sac_climate.py index 5b239c7..fb28105 100644 --- a/custom_components/ge_home/entities/ac/ge_sac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_sac_climate.py @@ -2,7 +2,7 @@ from typing import Any, List, Optional from homeassistant.components.climate import HVACMode -from gehomesdk import ErdCode, ErdAcOperationMode, ErdSacAvailableModes, ErdSacTargetTemperatureRange +from gehomesdk import ErdCode, ErdAcOperationMode, ErdAcAvailableModes, ErdSacTargetTemperatureRange from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter from .fan_mode_options import AcFanOnlyFanModeOptionsConverter, AcFanModeOptionsConverter @@ -10,7 +10,7 @@ _LOGGER = logging.getLogger(__name__) class SacHvacModeOptionsConverter(OptionsConverter): - def __init__(self, available_modes: ErdSacAvailableModes): + def __init__(self, available_modes: ErdAcAvailableModes): self._available_modes = available_modes @property @@ -55,7 +55,7 @@ def __init__(self, api: ApplianceApi): 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._modes: ErdAcAvailableModes = self.api.try_get_erd_value(ErdCode.AC_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) diff --git a/custom_components/ge_home/entities/ac/ge_wac_climate.py b/custom_components/ge_home/entities/ac/ge_wac_climate.py index 56b7edb..e8c6979 100644 --- a/custom_components/ge_home/entities/ac/ge_wac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_wac_climate.py @@ -2,7 +2,7 @@ from typing import Any, List, Optional from homeassistant.components.climate import HVACMode -from gehomesdk import ErdAcOperationMode, ErdCode, ErdSacAvailableModes +from gehomesdk import ErdAcOperationMode, ErdCode, ErdAcAvailableModes from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter from .fan_mode_options import AcFanModeOptionsConverter, AcFanOnlyFanModeOptionsConverter @@ -10,7 +10,7 @@ _LOGGER = logging.getLogger(__name__) class WacHvacModeOptionsConverter(OptionsConverter): - def __init__(self, available_modes: ErdSacAvailableModes): + def __init__(self, available_modes: ErdAcAvailableModes): self._available_modes = available_modes @property @@ -47,5 +47,5 @@ def to_option_string(self, value: Any) -> Optional[str]: class GeWacClimate(GeClimate): """Class for Window AC units""" def __init__(self, api: ApplianceApi): - available_modes = api.try_get_erd_value(ErdCode.SAC_AVAILABLE_MODES) + available_modes = api.try_get_erd_value(ErdCode.AC_AVAILABLE_MODES) super().__init__(api, WacHvacModeOptionsConverter(available_modes), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 58cbbe1..1aeff42 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -36,7 +36,8 @@ def __init__( 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 + fan_mode_erd_code: ErdCodeType = ErdCode.AC_FAN_SETTING, + target_heating_temperature_erd_code: ErdCodeType = ErdCode.AC_TARGET_HEATING_TEMPERATURE, ): super().__init__(api) @@ -51,6 +52,7 @@ def __init__( 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) + self._target_heating_temperature_erd_code = api.appliance.translate_erd_code(target_heating_temperature_erd_code) @property def unique_id(self) -> str: @@ -66,6 +68,8 @@ def power_status_erd_code(self): @property def target_temperature_erd_code(self): + if self.hvac_mode == HVACMode.HEAT: + return self._target_heating_temperature_erd_code return self._target_temperature_erd_code @property diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index a2d8d83..c995486 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==2025.5.0","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==2025.6.0","magicattr==0.1.6","slixmpp==1.8.3"], "codeowners": ["@simbaja"], "version": "2025.7.0" } From bcfb8566e586ec170854276690243b11cce73d4b Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Sun, 16 Nov 2025 19:21:48 -0700 Subject: [PATCH 335/338] Updated coding for Advantium Oven and Speed Oven (#414) * Updates for Monogram Advantium Speed Oven * Removed some debug not required anymore. * Implement sw_version property in AdvantiumApi Add a property to retrieve software version information. * Refactor temperature handling and cook settings --- .../ge_home/devices/advantium.py | 9 ++ .../entities/advantium/ge_advantium.py | 84 +++++++++++-------- 2 files changed, 57 insertions(+), 36 deletions(-) diff --git a/custom_components/ge_home/devices/advantium.py b/custom_components/ge_home/devices/advantium.py index c3baf36..4726234 100644 --- a/custom_components/ge_home/devices/advantium.py +++ b/custom_components/ge_home/devices/advantium.py @@ -13,6 +13,15 @@ class AdvantiumApi(ApplianceApi): """API class for Advantium objects""" APPLIANCE_TYPE = ErdApplianceType.ADVANTIUM + @property + def sw_version(self) -> str: + appVer = self.try_get_erd_value(ErdCode.APPLIANCE_SW_VERSION) + if appVer == "0.0.0.0": + appVer = self.try_get_erd_value(ErdCode.LCD_SW_VERSION) + wifiVer = self.try_get_erd_value(ErdCode.WIFI_MODULE_SW_VERSION) + + return 'Appliance=' + str(appVer or 'Unknown') + '/Wifi=' + str(wifiVer or 'Unknown') + def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() diff --git a/custom_components/ge_home/entities/advantium/ge_advantium.py b/custom_components/ge_home/entities/advantium/ge_advantium.py index 2088d36..2dee761 100644 --- a/custom_components/ge_home/entities/advantium/ge_advantium.py +++ b/custom_components/ge_home/entities/advantium/ge_advantium.py @@ -31,6 +31,7 @@ class GeAdvantium(GeAbstractWaterHeater): def __init__(self, api: ApplianceApi): super().__init__(api) + self._current_operation_mode = None @property def supported_features(self): @@ -101,6 +102,7 @@ def current_cook_status(self) -> ErdAdvantiumCookStatus: @property def current_operation_mode(self) -> AdvantiumOperationMode: """Gets the current operation mode""" + self._ensure_operation_mode() return self._current_operation_mode @property @@ -110,7 +112,7 @@ def current_operation_setting(self) -> Optional[AdvantiumCookSetting]: 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}") + _LOGGER.warning(f"Unable to determine operation setting, mode = {self.current_operation_mode}") return None @property @@ -126,7 +128,11 @@ 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: + if ( + cook_mode.cook_mode != CookMode.NO_MODE and + cook_mode.target_temperature and + cook_mode.target_temperature > 0 + ): return cook_mode.target_temperature except: pass @@ -135,13 +141,13 @@ def target_temperature(self) -> Optional[int]: @property def min_temp(self) -> int: """Return the minimum temperature.""" - min_temp, _ = self.appliance.get_erd_value(ErdCode.OVEN_MODE_MIN_MAX_TEMP) + min_temp, max_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) + min_temp, max_temp = self.appliance.get_erd_value(ErdCode.OVEN_MODE_MIN_MAX_TEMP) return max_temp @property @@ -176,8 +182,11 @@ async def async_set_operation_mode(self, operation_mode: str): 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 + if ( + setting.allow_temperature_set and + self.current_cook_status.cook_mode == setting.cook_mode and + self.target_temperature): + target_temp = max(self.min_temp, min(self.max_temp, int(self.target_temperature))) #by default we will start an operation, but handle other actions too action = CookAction.START @@ -189,13 +198,15 @@ async def async_set_operation_mode(self, operation_mode: str): 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 + new_cook_mode = ErdAdvantiumCookSetting( + d=randrange(255), + cook_action=action, + cook_mode=setting.cook_mode, + target_temperature=target_temp or 0, + power_level=setting.target_power_level or 0, + warm_status=setting.warm_status or 0, + ) + _LOGGER.debug("New ErdAdvantiumCookSetting: %s", new_cook_mode) await self.appliance.async_set_erd_value(ErdCode.ADVANTIUM_COOK_SETTING, new_cook_mode) @@ -222,16 +233,18 @@ async def async_set_temperature(self, **kwargs): 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 + current_cook_mode = self.current_cook_setting + new_cook_mode = current_cook_mode._replace( + d = randrange(255), + target_temperature = target_temp, + 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 + def _ensure_operation_mode(self): + cook_status = self.current_cook_status + cook_mode = cook_status.cook_mode #if we have a current mode if(self._current_operation_mode is not None): @@ -239,17 +252,16 @@ async def _ensure_operation_mode(self): #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 + 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: + if cook_status.power_level == 3: self._current_operation_mode = AdvantiumOperationMode.MICROWAVE_PL3 - elif cook_setting.power_level == 5: + elif cook_status.power_level == 5: self._current_operation_mode = AdvantiumOperationMode.MICROWAVE_PL5 - elif cook_setting.power_level == 7: + elif cook_status.power_level == 7: self._current_operation_mode = AdvantiumOperationMode.MICROWAVE_PL7 else: self._current_operation_mode = AdvantiumOperationMode.MICROWAVE_PL10 @@ -257,27 +269,27 @@ async def _ensure_operation_mode(self): 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( + cook_status.warm_status == value.warm_status and + cook_status.temperature == self._convert_target_temperature( value.target_temperature_120v_f, value.target_temperature_240v_f)): self._current_operation_mode = key - return + break #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 + break + + _LOGGER.debug("Operation mode is set to %s", self._current_operation_mode) + return - async def _convert_target_temperature(self, temp_120v: int, temp_240v: int): - unit_type = self.personality + def _convert_target_temperature(self, temp_120v: int, temp_240v: int): + unit_type = self.personality target_temp_f = temp_240v if unit_type in [ErdPersonality.PERSONALITY_240V_MONOGRAM, ErdPersonality.PERSONALITY_240V_CAFE, ErdPersonality.PERSONALITY_240V_STANDALONE_CAFE] else temp_120v - if self.temperature_unit == SensorDeviceClass.FAHRENHEIT: - return float(target_temp_f) - else: - return (target_temp_f - 32.0) * (5/9) + return target_temp_f async def async_device_update(self, warning: bool) -> None: await super().async_device_update(warning=warning) - await self._ensure_operation_mode() + self._ensure_operation_mode() From 46efc749c34254353d44825187f6a5f307e5ac4a Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sun, 30 Nov 2025 19:29:35 -0500 Subject: [PATCH 336/338] v2025.11.0 (#432) - removed slixmpp dependency - pylance cleanup of issues - updated advantium for new enum names - fixed ac climate initialization - added support for new-style hood fan/light (e.g. Haier) - improved debugging for coordinator - updated library dependency and HA version - code refactoring to improve reliability - added stale device/entity removal - changed device identifier to mac address - added brand inference for devices - cached certain device properties that shouldn't change - forced oven temperature_unit for ovens to be fahrenheit - added dishwasher remote start command button - added entity categorization --- CHANGELOG.md | 14 + custom_components/ge_home/__init__.py | 94 ++- custom_components/ge_home/binary_sensor.py | 7 +- custom_components/ge_home/button.py | 7 +- custom_components/ge_home/climate.py | 6 +- custom_components/ge_home/config_flow.py | 137 ++-- custom_components/ge_home/const.py | 13 +- .../ge_home/devices/advantium.py | 29 +- custom_components/ge_home/devices/base.py | 90 ++- custom_components/ge_home/devices/biac.py | 15 +- .../ge_home/devices/coffee_maker.py | 15 +- custom_components/ge_home/devices/const.py | 22 + custom_components/ge_home/devices/cooktop.py | 11 +- .../ge_home/devices/dehumidifier.py | 12 +- .../ge_home/devices/dishwasher.py | 58 +- custom_components/ge_home/devices/dryer.py | 31 +- .../ge_home/devices/dual_dishwasher.py | 88 ++- .../ge_home/devices/espresso_maker.py | 9 +- custom_components/ge_home/devices/fridge.py | 86 +-- custom_components/ge_home/devices/hood.py | 34 +- .../ge_home/devices/microwave.py | 20 +- custom_components/ge_home/devices/oim.py | 12 +- custom_components/ge_home/devices/oven.py | 61 +- custom_components/ge_home/devices/pac.py | 11 +- custom_components/ge_home/devices/sac.py | 15 +- custom_components/ge_home/devices/ucim.py | 25 +- custom_components/ge_home/devices/wac.py | 17 +- custom_components/ge_home/devices/washer.py | 35 +- .../ge_home/devices/washer_dryer.py | 5 +- .../ge_home/devices/water_filter.py | 17 +- .../ge_home/devices/water_heater.py | 21 +- .../ge_home/devices/water_softener.py | 17 +- .../ge_home/entities/ac/fan_mode_options.py | 12 +- .../ge_home/entities/ac/ge_biac_climate.py | 29 +- .../ge_home/entities/ac/ge_pac_climate.py | 47 +- .../ge_home/entities/ac/ge_sac_climate.py | 43 +- .../ge_home/entities/ac/ge_wac_climate.py | 32 +- .../entities/advantium/ge_advantium.py | 82 ++- .../ge_home/entities/ccm/ge_ccm_brew_cups.py | 9 +- .../entities/ccm/ge_ccm_brew_settings.py | 5 +- .../entities/ccm/ge_ccm_brew_strength.py | 15 +- .../entities/ccm/ge_ccm_brew_temperature.py | 9 +- .../entities/ccm/ge_ccm_cached_value.py | 4 +- .../ge_ccm_pot_not_present_binary_sensor.py | 2 +- .../ge_home/entities/common/__init__.py | 1 - .../ge_home/entities/common/bool_converter.py | 2 +- .../ge_home/entities/common/ge_climate.py | 52 +- .../ge_home/entities/common/ge_entity.py | 23 +- .../entities/common/ge_erd_binary_sensor.py | 45 +- .../common/ge_erd_binary_sensor_switch.py | 37 - .../ge_home/entities/common/ge_erd_button.py | 32 +- .../ge_home/entities/common/ge_erd_entity.py | 24 +- .../ge_home/entities/common/ge_erd_light.py | 39 +- .../ge_home/entities/common/ge_erd_number.py | 85 ++- .../common/ge_erd_property_binary_sensor.py | 38 +- .../entities/common/ge_erd_property_sensor.py | 37 +- .../ge_home/entities/common/ge_erd_select.py | 38 +- .../ge_home/entities/common/ge_erd_sensor.py | 61 +- .../ge_home/entities/common/ge_erd_switch.py | 80 +- .../entities/common/ge_erd_timer_sensor.py | 11 +- .../ge_home/entities/common/ge_humidifier.py | 37 +- .../entities/common/ge_water_heater.py | 17 +- .../entities/dehumidifier/dehumidifier.py | 15 +- .../dehumidifier_fan_speed_sensor.py | 24 +- .../ge_home/entities/dishwasher/__init__.py | 3 +- .../ge_dishwasher_command_button.py | 33 + .../ge_dishwasher_control_locked_switch.py | 2 +- .../fridge/convertable_drawer_mode_options.py | 14 +- .../entities/fridge/ge_abstract_fridge.py | 52 +- .../ge_home/entities/fridge/ge_dispenser.py | 42 +- .../ge_home/entities/fridge/ge_freezer.py | 24 +- .../ge_home/entities/fridge/ge_fridge.py | 22 +- .../fridge/ge_fridge_ice_control_switch.py | 3 +- .../ge_home/entities/fridge/ge_kcup_switch.py | 22 +- .../entities/hood/ge_hood_fan_speed.py | 19 +- .../entities/hood/ge_hood_light_level.py | 53 +- .../entities/laundry/ge_dryer_cycle_button.py | 9 +- .../laundry/ge_washer_cycle_button.py | 9 +- .../ge_home/entities/oven/ge_oven.py | 51 +- .../oven/ge_oven_light_level_select.py | 22 +- .../oven/ge_oven_warming_state_select.py | 13 +- .../entities/water_filter/filter_position.py | 9 +- .../entities/water_heater/ge_water_heater.py | 25 +- .../entities/water_heater/heater_modes.py | 10 +- .../water_softener/shutoff_position.py | 7 +- custom_components/ge_home/exceptions.py | 4 +- custom_components/ge_home/humidifier.py | 7 +- custom_components/ge_home/light.py | 5 +- custom_components/ge_home/manifest.json | 4 +- custom_components/ge_home/number.py | 7 +- custom_components/ge_home/select.py | 7 +- custom_components/ge_home/sensor.py | 9 +- custom_components/ge_home/strings.json | 21 - custom_components/ge_home/switch.py | 7 +- .../ge_home/translations/en.json | 17 +- .../ge_home/update_coordinator.py | 686 ++++++++++++------ custom_components/ge_home/water_heater.py | 7 +- hacs.json | 2 +- info.md | 72 +- 99 files changed, 2120 insertions(+), 1200 deletions(-) create mode 100644 custom_components/ge_home/devices/const.py delete mode 100644 custom_components/ge_home/entities/common/ge_erd_binary_sensor_switch.py create mode 100644 custom_components/ge_home/entities/dishwasher/ge_dishwasher_command_button.py delete mode 100644 custom_components/ge_home/strings.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f64974..d07d508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # GE Home Appliances (SmartHQ) Changelog +## 2025.11.0 + +- Breaking: changed name of some SAC/WAC entities to have a AC prefix +- Feature: Added heat mode for Window ACs +- Feature: Added support for Advantium +- Feature: Brand inference and stale device cleanup +- Feature: Added support for new hoods that require state/control ERDs +- Feature: Added entity categorization +- Feature: Added dishwasher remote commands +- Change: Refactored code internally to improve reliability +- Change: Cleaned up initialization and config flow +- Bugfix: Fixed temperature unit for ovens [#248, #328, #344] +- Bugfix: Water heater mode setting [#107] + ## 2025.7.0 - Change: Silenced string prep warning [#386] (@derekcentrico) diff --git a/custom_components/ge_home/__init__.py b/custom_components/ge_home/__init__.py index f5337e8..f200bd0 100644 --- a/custom_components/ge_home/__init__.py +++ b/custom_components/ge_home/__init__.py @@ -1,12 +1,12 @@ """The ge_home integration.""" import logging -from homeassistant.const import EVENT_HOMEASSISTANT_STOP import voluptuous as vol +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.const import CONF_REGION +from homeassistant.const import CONF_USERNAME, CONF_REGION, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN from .exceptions import HaAuthError, HaCannotConnect @@ -15,44 +15,82 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): - """Migrate old entry.""" - _LOGGER.debug("Migrating from version %s", config_entry.version) +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old config entry to the latest schema.""" + + old_version: int = config_entry.version + data: dict[str, Any] = dict(config_entry.data) + + # --- Migrate from version 1 to 2 + if old_version == 1: + _LOGGER.debug(f"GE Home: Migrating entry {config_entry.entry_id} from v1 to v2") + + # Apply default US region if missing + data[CONF_REGION] = "US" - if config_entry.version == 1: + hass.config_entries.async_update_entry( + config_entry, + data=data, + version=2, + ) - new = {**config_entry.data} - new[CONF_REGION] = "US" + _LOGGER.info(f"GE Home: Migration of entry {config_entry.entry_id} to v2 successful") + old_version = 2 - config_entry.version = 2 - hass.config_entries.async_update_entry(config_entry, data=new) + # --- Migrate any version 2 to 3 + if old_version == 2: + _LOGGER.debug(f"GE Home: Migrating entry {config_entry.entry_id} from v{old_version} to v3") - _LOGGER.info("Migration to version %s successful", config_entry.version) + # Normalize username + username: str = data[CONF_USERNAME].strip().lower() + data[CONF_USERNAME] = username - return True + # Normalize region + region: str = data[CONF_REGION].strip().upper() + data[CONF_REGION] = region + # Determine unique_id + unique_id: str = (config_entry.unique_id or username).strip().lower() + + # Update entry: data, version, and unique_id in one call + hass.config_entries.async_update_entry( + config_entry, + data=data, + version=3, + unique_id=unique_id, + ) + + _LOGGER.info ( + f"GE Home: Migration of entry {config_entry.entry_id} to v3 successful " + f"(unique_id='{unique_id}')" + ) + + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up ge_home from a config entry.""" - hass.data.setdefault(DOMAIN, {}) + + coordinators: dict[str, GeHomeUpdateCoordinator] = hass.data.setdefault(DOMAIN, {}) #try to get existing coordinator - existing: GeHomeUpdateCoordinator = dict.get(hass.data[DOMAIN],entry.entry_id) + existing: GeHomeUpdateCoordinator | None = coordinators.get(entry.entry_id) + + # try to unload the existing coordinator + if existing: + try: + _LOGGER.debug("Found existing coordinator, resetting before setup.") + await existing.async_reset() + except Exception: + _LOGGER.warning("Could not reset existing coordinator.", exc_info=True) + finally: + coordinators.pop(entry.entry_id, None) coordinator = GeHomeUpdateCoordinator(hass, entry) - hass.data[DOMAIN][entry.entry_id] = coordinator + coordinators[entry.entry_id] = coordinator - # try to unload the existing coordinator - try: - if existing: - await coordinator.async_reset() - except: - _LOGGER.warning("Could not reset existing coordinator.") - try: if not await coordinator.async_setup(): return False @@ -60,9 +98,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise ConfigEntryNotReady("Could not connect to SmartHQ") except HaAuthError: raise ConfigEntryAuthFailed("Could not authenticate to SmartHQ") - + except Exception as exc: + _LOGGER.exception("Unexpected error during coordinator setup", exc_info=True) + raise ConfigEntryNotReady from exc + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown) + _LOGGER.debug("Coordinator setup complete") return True @@ -76,6 +118,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return ok -async def async_update_options(hass, config_entry): +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry): """Update options.""" - await hass.config_entries.async_reload(config_entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) diff --git a/custom_components/ge_home/binary_sensor.py b/custom_components/ge_home/binary_sensor.py index 0a35ef5..fb30f5d 100644 --- a/custom_components/ge_home/binary_sensor.py +++ b/custom_components/ge_home/binary_sensor.py @@ -1,5 +1,6 @@ """GE Home Sensor Entities""" import logging +from collections.abc import Collection from typing import Callable from homeassistant.components.switch import SwitchEntity @@ -16,14 +17,14 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home binary sensors.""" + """GE Home Binary Sensors.""" _LOGGER.debug('Adding GE Binary Sensor Entities') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] registry = er.async_get(hass) @callback - def async_devices_discovered(apis: list[ApplianceApi]): + def async_devices_discovered(apis: Collection[ApplianceApi]): _LOGGER.debug(f'Found {len(apis):d} appliance APIs') entities = [ @@ -33,7 +34,7 @@ def async_devices_discovered(apis: list[ApplianceApi]): if isinstance(entity, GeErdBinarySensor) and not isinstance(entity, SwitchEntity) if not registry.async_is_registered(entity.entity_id) ] - _LOGGER.debug(f'Found {len(entities):d} unregistered binary sensors') + _LOGGER.debug(f'Found {len(entities):d} unregistered binary sensors to register') async_add_entities(entities) #if we're already initialized at this point, call device diff --git a/custom_components/ge_home/button.py b/custom_components/ge_home/button.py index 748ee6b..c6cf76b 100644 --- a/custom_components/ge_home/button.py +++ b/custom_components/ge_home/button.py @@ -1,5 +1,6 @@ """GE Home Button Entities""" import logging +from collections.abc import Collection from typing import Callable from homeassistant.config_entries import ConfigEntry @@ -15,14 +16,14 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home buttons.""" + """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]): + def async_devices_discovered(apis: Collection[ApplianceApi]): _LOGGER.debug(f'Found {len(apis):d} appliance APIs') entities = [ entity @@ -31,7 +32,7 @@ def async_devices_discovered(apis: list[ApplianceApi]): if isinstance(entity, GeErdButton) if not registry.async_is_registered(entity.entity_id) ] - _LOGGER.debug(f'Found {len(entities):d} unregistered buttons ') + _LOGGER.debug(f'Found {len(entities):d} unregistered buttons to register') async_add_entities(entities) #if we're already initialized at this point, call device diff --git a/custom_components/ge_home/climate.py b/custom_components/ge_home/climate.py index 4512b61..2869110 100644 --- a/custom_components/ge_home/climate.py +++ b/custom_components/ge_home/climate.py @@ -1,8 +1,8 @@ """GE Home Climate Entities""" import logging +from collections.abc import Collection 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 @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn registry = er.async_get(hass) @callback - def async_devices_discovered(apis: list[ApplianceApi]): + def async_devices_discovered(apis: Collection[ApplianceApi]): _LOGGER.debug(f'Found {len(apis):d} appliance APIs') entities = [ @@ -33,7 +33,7 @@ def async_devices_discovered(apis: list[ApplianceApi]): if isinstance(entity, GeClimate) if not registry.async_is_registered(entity.entity_id) ] - _LOGGER.debug(f'Found {len(entities):d} unregistered climate entities') + _LOGGER.debug(f'Found {len(entities):d} unregistered climate entities to register') async_add_entities(entities) #if we're already initialized at this point, call device diff --git a/custom_components/ge_home/config_flow.py b/custom_components/ge_home/config_flow.py index a8070a1..d393085 100644 --- a/custom_components/ge_home/config_flow.py +++ b/custom_components/ge_home/config_flow.py @@ -20,8 +20,8 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN # pylint:disable=unused-import -from .exceptions import HaAuthError, HaCannotConnect, HaAlreadyConfigured +from .const import DOMAIN, VALIDATE_DATA_TIMEOUT, CONFIG_FLOW_VERSION +from .exceptions import HaAuthError, HaCannotConnect _LOGGER = logging.getLogger(__name__) @@ -33,97 +33,128 @@ } ) -async def validate_input(hass: core.HomeAssistant, data): - """Validate the user input allows us to connect.""" +def _normalize_username(username: Optional[str]) -> str: + """Trim whitespace and lowercase the username.""" + if username is None or username.strip() == "": + raise HaAuthError("Username is required") + return username.strip().lower() + +def _normalize_password(password: Optional[str]) -> str: + """Trim whitespace from password.""" + if password is None or password.strip() == "": + raise HaAuthError("Password is required") + return password.strip() + +def _normalize_region(region: Optional[str]) -> str: + """Ensure valid region.""" + if region is None or not region.upper() in LOGIN_REGIONS.keys(): + raise HaAuthError("Invalid region") + return region.upper() + +def _get_user_schema(user_input: Optional[Dict] = None) -> vol.Schema: + """Return the user step schema, prefilled with previous input if available.""" + user_input = user_input or {} + return vol.Schema( + { + vol.Required(CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")): str, + vol.Required(CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")): str, + vol.Required(CONF_REGION, default=user_input.get(CONF_REGION, "")): vol.In(LOGIN_REGIONS.keys()) + } + ) +async def validate_input(hass: core.HomeAssistant, data: dict): + """Validate the user input allows us to connect.""" session = async_get_clientsession(hass) + username = _normalize_username(data.get(CONF_USERNAME)) + password = _normalize_password(data.get(CONF_PASSWORD)) + region = _normalize_region(data.get(CONF_REGION)) - # noinspection PyBroadException try: - async with async_timeout.timeout(10): - _ = await async_get_oauth2_token(session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_REGION]) - except (asyncio.TimeoutError, aiohttp.ClientError): - raise HaCannotConnect('Connection failure') + async with async_timeout.timeout(VALIDATE_DATA_TIMEOUT): + await async_get_oauth2_token(session, username, password, region) + except (asyncio.TimeoutError, aiohttp.ClientError) as err: + _LOGGER.warning(f"Connection failure for user {username} in region {region}: {err}") + raise HaCannotConnect("Connection failure") except (GeAuthFailedError, GeNotAuthenticatedError): - raise HaAuthError('Authentication failure') + _LOGGER.warning(f"Authentication failure for user {username} in region {region}") + raise HaAuthError("Authentication failure") except GeGeneralServerError: - raise HaCannotConnect('Cannot connect (server error)') - except Exception as exc: - _LOGGER.exception("Unknown connection failure", exc_info=exc) - raise HaCannotConnect('Unknown connection failure') + _LOGGER.warning(f"Server error for user {username} in region {region}") + raise HaCannotConnect("Cannot connect (server error)") + except Exception as err: + _LOGGER.exception(f"Unknown connection failure for user {username} in region {region}") + raise HaCannotConnect("Unknown connection failure") from err - # Return info that you want to store in the config entry. - return {"title": f"{data[CONF_USERNAME]:s}"} + return {"title": username.lower()} class GeHomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for GE Home.""" - VERSION = 2 + VERSION = CONFIG_FLOW_VERSION CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH - async def _async_validate_input(self, user_input): - """Validate form input.""" - errors = {} - info = None - - if user_input is not None: - # noinspection PyBroadException - try: - info = await validate_input(self.hass, user_input) - except HaCannotConnect: - errors["base"] = "cannot_connect" - except HaAuthError: - errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - return info, errors - - def _ensure_not_configured(self, username: str): - """Ensure that we haven't configured this account""" - existing_accounts = { - entry.data[CONF_USERNAME] for entry in self._async_current_entries() - } - _LOGGER.debug(f"Existing accounts: {existing_accounts}") - if username in existing_accounts: - raise HaAlreadyConfigured + async def _async_validate_input(self, user_input: dict): + """Map validation to HA-friendly error codes.""" + try: + info = await validate_input(self.hass, user_input) + return info, {} + except HaCannotConnect: + return None, {"base": "cannot_connect"} + except HaAuthError: + return None, {"base": "invalid_auth"} + except Exception: + return None, {"base": "unknown"} async def async_step_user(self, user_input: Optional[Dict] = None): """Handle the initial step.""" errors = {} - if user_input is not None: + + if user_input: + username = _normalize_username(user_input.get(CONF_USERNAME)) + + # test uniqueness and abort if not unique + await self.async_set_unique_id(username) + self._abort_if_unique_id_configured() + try: - self._ensure_not_configured(user_input[CONF_USERNAME]) info, errors = await self._async_validate_input(user_input) if info: return self.async_create_entry(title=info["title"], data=user_input) - except HaAlreadyConfigured: - return self.async_abort(reason="already_configured_account") + except Exception as err: + _LOGGER.exception(f"Unexpected error in user step: {err}") + errors["base"] = "unknown" return self.async_show_form( - step_id="user", data_schema=GEHOME_SCHEMA, errors=errors + step_id="user", + data_schema=_get_user_schema(user_input), + errors=errors ) async def async_step_reauth(self, user_input: Optional[dict] = None): """Handle re-auth if login is invalid.""" errors = {} - if user_input is not None: + if user_input: + username = _normalize_username(user_input.get(CONF_USERNAME)) _, errors = await self._async_validate_input(user_input) if not errors: for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: + if entry.unique_id == username: self.hass.config_entries.async_update_entry( entry, data=user_input ) await self.hass.config_entries.async_reload(entry.entry_id) return self.async_abort(reason="reauth_successful") - if errors["base"] != "invalid_auth": - return self.async_abort(reason=errors["base"]) + if errors.get("base") != "invalid_auth": + return self.async_abort(reason=errors.get("base") or "unknown") return self.async_show_form( - step_id="reauth", data_schema=GEHOME_SCHEMA, errors=errors, + step_id="reauth", + data_schema=_get_user_schema(user_input), + errors=errors ) + + diff --git a/custom_components/ge_home/const.py b/custom_components/ge_home/const.py index 76cef76..47dfda2 100644 --- a/custom_components/ge_home/const.py +++ b/custom_components/ge_home/const.py @@ -3,12 +3,21 @@ DOMAIN = "ge_home" EVENT_ALL_APPLIANCES_READY = 'all_appliances_ready' +CONNECTION_NOTIFICATION_ID = "ge_home_connection" +CONFIG_FLOW_VERSION = 3 + +HA_REFRESH_INTERVAL = 60 +STATE_UPDATE_INTERVAL = 30 +CLIENT_START_TIMEOUT = 30 +INITIAL_UPDATE_TIMEOUT = 10 +VALIDATE_DATA_TIMEOUT = 10 -UPDATE_INTERVAL = 30 -ASYNC_TIMEOUT = 30 MIN_RETRY_DELAY = 15 MAX_RETRY_DELAY = 1800 +RECONNECT_JITTER = 0.2 +PERSISTENT_RETRY_LOG_INTERVAL = 300 RETRY_OFFLINE_COUNT = 5 +NOTIFY_AFTER_RETRIES = 5 SERVICE_SET_TIMER = "set_timer" SERVICE_CLEAR_TIMER = "clear_timer" diff --git a/custom_components/ge_home/devices/advantium.py b/custom_components/ge_home/devices/advantium.py index 4726234..abdc837 100644 --- a/custom_components/ge_home/devices/advantium.py +++ b/custom_components/ge_home/devices/advantium.py @@ -1,6 +1,7 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk.erd import ErdCode, ErdApplianceType, ErdDataType @@ -26,25 +27,25 @@ def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() advantium_entities = [ - GeErdSensor(self, ErdCode.PERSONALITY), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED)), - GeErdBinarySensor(self, ErdCode.MICROWAVE_REMOTE_ENABLE), - GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE)), + GeErdSensor(self, ErdCode.PERSONALITY, entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED), entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.MICROWAVE_REMOTE_ENABLE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE), entity_category=EntityCategory.DIAGNOSTIC), 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"), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "cook_mode", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "termination_reason", icon_override="mdi:information-outline", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "preheat_status", icon_override="mdi:fire", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "temperature", icon_override="mdi:thermometer", data_type_override=ErdDataType.INT, entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "power_level", icon_override="mdi:gauge", data_type_override=ErdDataType.INT, entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "warm_status", icon_override="mdi:radiator", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "door_status", device_class_override="door", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "sensing_active", icon_on_override="mdi:flash-auto", icon_off_override="mdi:flash-off", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "cooling_fan_status", icon_on_override="mdi:fan", icon_off_override="mdi:fan-off", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertyBinarySensor(self, ErdCode.ADVANTIUM_COOK_STATUS, "oven_light_status", icon_on_override="mdi:lightbulb-on", icon_off_override="mdi:lightbulb-off", entity_category=EntityCategory.DIAGNOSTIC), ] entities = base_entities + advantium_entities return entities diff --git a/custom_components/ge_home/devices/base.py b/custom_components/ge_home/devices/base.py index 179e3e9..00703b9 100644 --- a/custom_components/ge_home/devices/base.py +++ b/custom_components/ge_home/devices/base.py @@ -1,14 +1,23 @@ import asyncio import logging +from propcache.api import cached_property from typing import Dict, List, Optional -from gehomesdk import GeAppliance -from gehomesdk.erd import ErdCode, ErdCodeType, ErdApplianceType - +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - +from homeassistant.helpers.device_registry import DeviceInfo +from gehomesdk import ( + GeAppliance, + ErdCode, + ErdCodeType, + ErdApplianceType, + ERD_BRAND_NAME_MAP, + ErdBrand +) + +from .const import BRAND_FIRST_LETTER_MAP, BRAND_SPECIAL_PREFIXES from ..const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -29,7 +38,7 @@ def __init__(self, coordinator: DataUpdateCoordinator, appliance: GeAppliance): self._hass = coordinator.hass self.coordinator = coordinator self.initial_update = False - self._entities = {} # type: Optional[Dict[str, Entity]] + self._entities: Dict[str, Entity] = {} @property def hass(self) -> HomeAssistant: @@ -54,17 +63,17 @@ def available(self) -> bool: #Note - online will be there since we're using the GE coordinator #Didn't want to deal with the circular references to get the type hints #working. - return self.appliance.available and self.coordinator.online + return self.appliance.available and self.coordinator.online # type: ignore - @property + @cached_property def serial_number(self) -> str: return self.appliance.get_erd_value(ErdCode.SERIAL_NUMBER) - @property + @cached_property def mac_addr(self) -> str: return self.appliance.mac_addr - @property + @cached_property def serial_or_mac(self) -> str: def is_zero(val: str) -> bool: try: @@ -79,7 +88,17 @@ def is_zero(val: str) -> bool: return self.serial_number return self.mac_addr - @property + @cached_property + def brand(self) -> str: + b: ErdBrand | None = self.try_get_erd_value(ErdCode.BRAND) + + if b in (None, ErdBrand.UNKNOWN, ErdBrand.NOT_DEFINED): + inferred = self._infer_brand_from_model(self.model_number) + b = inferred or ErdBrand.GE + + return ERD_BRAND_NAME_MAP.get(b, 'GE') + + @cached_property def model_number(self) -> str: return self.appliance.get_erd_value(ErdCode.MODEL_NUMBER) @@ -90,29 +109,30 @@ def sw_version(self) -> str: return 'Appliance=' + str(appVer or 'Unknown') + '/Wifi=' + str(wifiVer or 'Unknown') - @property + @cached_property def name(self) -> str: appliance_type = self.appliance.appliance_type if appliance_type is None or appliance_type == ErdApplianceType.UNKNOWN: appliance_type = "Appliance" else: appliance_type = appliance_type.name.replace("_", " ").title() - return f"GE {appliance_type} {self.serial_or_mac}" + return f"{self.brand} {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)}, + "identifiers": {(DOMAIN, self.mac_addr)}, + "serial_number": self.serial_number, "name": self.name, - "manufacturer": "GE", + "manufacturer": self.brand, "model": self.model_number, "sw_version": self.sw_version } @property - def entities(self) -> List[Entity]: + def entities(self) -> List[Entity]: return list(self._entities.values()) def get_all_entities(self) -> List[Entity]: @@ -123,7 +143,7 @@ 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), + GeErdSensor(self, ErdCode.CLOCK_TIME, entity_category=EntityCategory.DIAGNOSTIC), GeErdSwitch(self, ErdCode.SABBATH_MODE), ] return entities @@ -137,7 +157,7 @@ def build_entities_list(self) -> None: ] for entity in entities: - if entity.unique_id not in self._entities: + if entity.unique_id is not None and entity.unique_id not in self._entities: self._entities[entity.unique_id] = entity def try_get_erd_value(self, code: ErdCodeType): @@ -152,3 +172,37 @@ def has_erd_code(self, code: ErdCodeType): return True except: return False + + def _infer_brand_from_model(self, model: str) -> Optional[ErdBrand]: + """ + Infer the appliance brand from model number using first-letter mapping + and special prefix handling. + """ + if not model: + _LOGGER.debug("Model number is empty, cannot infer brand.") + return None + + m = model.strip().upper() + + # Try special prefixes + for prefix, idx in BRAND_SPECIAL_PREFIXES.items(): + if m.startswith(prefix): + if len(m) > idx: + brand_letter = m[idx] + brand = BRAND_FIRST_LETTER_MAP.get(brand_letter) + if brand: + _LOGGER.debug(f"Model '{m}': inferred brand '{brand.name}' from prefix '{prefix}' at position {idx + 1}") + return brand + _LOGGER.debug(f"Model '{m}': prefix '{prefix}' found but brand letter at position {idx + 1} not recognized") + return None + + # Try general + first_letter = m[0] + brand = BRAND_FIRST_LETTER_MAP.get(first_letter) + if brand: + _LOGGER.debug(f"Model '{m}': inferred brand '{brand.name}' from first letter '{first_letter}'") + return brand + + # Log and return + _LOGGER.debug(f"Model '{m}': could not infer brand (first letter '{first_letter}' not in mapping)") + return None \ No newline at end of file diff --git a/custom_components/ge_home/devices/biac.py b/custom_components/ge_home/devices/biac.py index 916b6cb..05716b4 100644 --- a/custom_components/ge_home/devices/biac.py +++ b/custom_components/ge_home/devices/biac.py @@ -1,6 +1,7 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk.erd import ErdCode, ErdApplianceType @@ -20,14 +21,14 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdSensor(self, ErdCode.AC_TARGET_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_OPERATION_MODE, entity_category=EntityCategory.DIAGNOSTIC), 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"), + GeErdBinarySensor(self, ErdCode.AC_FILTER_STATUS, device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WAC_DEMAND_RESPONSE_STATE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WAC_DEMAND_RESPONSE_POWER, uom_override="kW", entity_category=EntityCategory.DIAGNOSTIC), ] entities = base_entities + sac_entities diff --git a/custom_components/ge_home/devices/coffee_maker.py b/custom_components/ge_home/devices/coffee_maker.py index d3f39c9..2dc5db3 100644 --- a/custom_components/ge_home/devices/coffee_maker.py +++ b/custom_components/ge_home/devices/coffee_maker.py @@ -1,6 +1,7 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ( GeAppliance, @@ -40,18 +41,18 @@ 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), + GeErdBinarySensor(self, ErdCode.CCM_IS_BREWING, entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.CCM_IS_DESCALING, entity_category=EntityCategory.DIAGNOSTIC), GeCcmBrewSettingsButton(self), - GeErdButton(self, ErdCode.CCM_CANCEL_DESCALING), - GeErdButton(self, ErdCode.CCM_START_DESCALING), + GeErdButton(self, ErdCode.CCM_CANCEL_DESCALING, entity_category=EntityCategory.CONFIG), + GeErdButton(self, ErdCode.CCM_START_DESCALING, entity_category=EntityCategory.CONFIG), 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") + GeErdSensor(self, ErdCode.CCM_CURRENT_WATER_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.CCM_OUT_OF_WATER, device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC), + GeCcmPotNotPresentBinarySensor(self, ErdCode.CCM_POT_PRESENT, device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC) ] entities = base_entities + ccm_entities diff --git a/custom_components/ge_home/devices/const.py b/custom_components/ge_home/devices/const.py new file mode 100644 index 0000000..2be4c3a --- /dev/null +++ b/custom_components/ge_home/devices/const.py @@ -0,0 +1,22 @@ +from gehomesdk import ErdBrand + +BRAND_FIRST_LETTER_MAP: dict[str, ErdBrand] = { + "C": ErdBrand.GE_CAFE, + "G": ErdBrand.GE, + "J": ErdBrand.GE, + "N": ErdBrand.GE, + "A": ErdBrand.GE, + "F": ErdBrand.GE, + "H": ErdBrand.HOTPOINT, + "P": ErdBrand.GE_PROFILE, + "Q": ErdBrand.HEIER, + "Z": ErdBrand.GE_MONOGRAM, + "R": ErdBrand.HOTPOINT, + "M": ErdBrand.HEIER, + "U": ErdBrand.UNKNOWN, # also might be universal +} + +BRAND_SPECIAL_PREFIXES: dict[str, int] = { + "OPAL01": 6, # Opal ice maker: brand letter at 7th position + "XP": 1, # XP Opal variant: brand letter at 2nd position +} diff --git a/custom_components/ge_home/devices/cooktop.py b/custom_components/ge_home/devices/cooktop.py index 32fb597..f4b08d2 100644 --- a/custom_components/ge_home/devices/cooktop.py +++ b/custom_components/ge_home/devices/cooktop.py @@ -1,11 +1,12 @@ import logging from typing import List -from gehomesdk.erd.erd_data_type import ErdDataType +from homeassistant.const import EntityCategory from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.entity import Entity from gehomesdk import ( - ErdCode, + ErdCode, + ErdDataType, ErdApplianceType, ErdCooktopConfig, CooktopStatus @@ -37,12 +38,12 @@ def get_all_entities(self) -> List[Entity]: if cooktop_config == ErdCooktopConfig.PRESENT: # attempt to get the cooktop status using legacy status cooktop_status_erd = ErdCode.COOKTOP_STATUS - cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) + cooktop_status: CooktopStatus | None = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) # if we didn't get it, try using the new version if cooktop_status is None: cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT - cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) + cooktop_status = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) # if we got a status through either mechanism, we can add the entities if cooktop_status is not None: @@ -52,7 +53,7 @@ def get_all_entities(self) -> List[Entity]: if v.exists: prop = self._camel_to_snake(k) cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on")) - cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized")) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized", entity_category=EntityCategory.DIAGNOSTIC)) if not v.on_off_only: cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) diff --git a/custom_components/ge_home/devices/dehumidifier.py b/custom_components/ge_home/devices/dehumidifier.py index fa0d8cc..0cc7f5d 100644 --- a/custom_components/ge_home/devices/dehumidifier.py +++ b/custom_components/ge_home/devices/dehumidifier.py @@ -1,11 +1,11 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ( ErdCode, - ErdApplianceType, - ErdOnOff + ErdApplianceType ) from .base import ApplianceApi @@ -32,10 +32,10 @@ def get_all_entities(self) -> List[Entity]: dhum_entities = [ GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), GeDehumidifierFanSpeedSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan"), - GeErdSensor(self, ErdCode.DHUM_CURRENT_HUMIDITY), - GeErdSensor(self, ErdCode.DHUM_TARGET_HUMIDITY), - GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "empty_bucket", device_class_override="problem"), - GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "clean_filter", device_class_override="problem"), + GeErdSensor(self, ErdCode.DHUM_CURRENT_HUMIDITY, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.DHUM_TARGET_HUMIDITY, entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "empty_bucket", device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DHUM_MAINTENANCE, "clean_filter", device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC), GeDehumidifier(self) ] diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index 8c4e91c..07a8a81 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -1,11 +1,12 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity -from gehomesdk.erd import ErdCode, ErdApplianceType +from gehomesdk import ErdCode, ErdApplianceType, ErdRemoteCommand from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdNumber +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdNumber, GeDishwasherCommandButton _LOGGER = logging.getLogger(__name__) @@ -18,38 +19,47 @@ def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() dishwasher_entities = [ - #GeDishwasherControlLockedSwitch(self, ErdCode.USER_INTERFACE_LOCKED), GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_NAME), GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_STATE, icon_override="mdi:state-machine"), GeErdSensor(self, ErdCode.DISHWASHER_OPERATING_MODE), -# GeErdSensor(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE, uom_override="pods"), - GeErdNumber(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE, uom_override="pods", min_value=0, max_value=255), - GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", icon_override="mdi:shimmer"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", icon_override="mdi:dishwasher-alert"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", icon_override="mdi:silverware-clean"), + GeErdNumber(self, ErdCode.DISHWASHER_PODS_REMAINING_VALUE, uom_override="pods", min_value=0, max_value=255, entity_category=EntityCategory.CONFIG), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", icon_override="mdi:shimmer", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", icon_override="mdi:dishwasher-alert", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", icon_override="mdi:silverware-clean", entity_category=EntityCategory.DIAGNOSTIC), GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING), - GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS), + GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS, entity_category=EntityCategory.DIAGNOSTIC), GeErdBinarySensor(self, ErdCode.DISHWASHER_IS_CLEAN), - GeErdBinarySensor(self, ErdCode.DISHWASHER_REMOTE_START_ENABLE), #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "mute", icon_override="mdi:volume-mute"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control", icon_override="mdi:lock"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath", icon_override="mdi:star-david"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode", icon_override="mdi:state-machine"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak", icon_override="mdi:water"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet", icon_override="mdi:bottle-tonic-outline"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", icon_override="mdi:coolant-temperature"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "rinse_aid", icon_override="mdi:shimmer"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "dry_option", icon_override="mdi:fan"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_zone", icon_override="mdi:dock-top"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours", icon_override="mdi:clock-fast"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "mute", icon_override="mdi:volume-mute", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control", icon_override="mdi:lock", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath", icon_override="mdi:star-david", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode", icon_override="mdi:state-machine", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak", icon_override="mdi:water", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet", icon_override="mdi:bottle-tonic-outline", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", icon_override="mdi:coolant-temperature", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "rinse_aid", icon_override="mdi:shimmer", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "dry_option", icon_override="mdi:fan", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_zone", icon_override="mdi:dock-top", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours", icon_override="mdi:clock-fast", entity_category=EntityCategory.DIAGNOSTIC), #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(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "started", icon_override="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "completed", icon_override="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_CYCLE_COUNTS, "reset", icon_override="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC) ] + + # check for remote command availability and add if present + if self.has_erd_code(ErdCode.DISHWASHER_REMOTE_START_ENABLE): + dishwasher_entities.extend( + [ + GeErdBinarySensor(self, ErdCode.DISHWASHER_REMOTE_START_ENABLE, entity_category=EntityCategory.DIAGNOSTIC), + GeDishwasherCommandButton(self, ErdCode.DISHWASHER_REMOTE_START_COMMAND, ErdRemoteCommand.START_RESUME), + GeDishwasherCommandButton(self, ErdCode.DISHWASHER_REMOTE_START_COMMAND, ErdRemoteCommand.PAUSE), + GeDishwasherCommandButton(self, ErdCode.DISHWASHER_REMOTE_START_COMMAND, ErdRemoteCommand.CANCEL) + ] + ) + entities = base_entities + dishwasher_entities return entities diff --git a/custom_components/ge_home/devices/dryer.py b/custom_components/ge_home/devices/dryer.py index 6b59ac8..5709299 100644 --- a/custom_components/ge_home/devices/dryer.py +++ b/custom_components/ge_home/devices/dryer.py @@ -1,6 +1,7 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ErdCode, ErdApplianceType @@ -19,15 +20,15 @@ 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_MACHINE_STATE, icon_override="mdi:tumble-dryer", entity_category=EntityCategory.DIAGNOSTIC), GeErdSensor(self, ErdCode.LAUNDRY_CYCLE, icon_override="mdi:state-machine"), GeErdSensor(self, ErdCode.LAUNDRY_SUB_CYCLE, icon_override="mdi:state-machine"), GeErdBinarySensor(self, ErdCode.LAUNDRY_END_OF_CYCLE, icon_on_override="mdi:tumble-dryer", icon_off_override="mdi:tumble-dryer"), GeErdSensor(self, ErdCode.LAUNDRY_TIME_REMAINING), GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING), - GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR), - GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, icon_on_override="mdi:tumble-dryer", icon_off_override="mdi:tumble-dryer"), - GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_BLOCKED_VENT_FAULT, icon_on_override="mdi:alert-circle", icon_off_override="mdi:alert-circle"), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR, entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, icon_on_override="mdi:tumble-dryer", icon_off_override="mdi:tumble-dryer", entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_BLOCKED_VENT_FAULT, icon_on_override="mdi:alert-circle", icon_off_override="mdi:alert-circle", entity_category=EntityCategory.DIAGNOSTIC), ] dryer_entities = self.get_dryer_entities() @@ -42,26 +43,26 @@ def get_dryer_entities(self): dryer_entities = [] if self.has_erd_code(ErdCode.LAUNDRY_DRYER_DRYNESS_LEVEL): - dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_DRYNESS_LEVEL)]) + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_DRYNESS_LEVEL, entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_DRYNESSNEW_LEVEL): - dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_DRYNESSNEW_LEVEL)]) + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_DRYNESSNEW_LEVEL, entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_TEMPERATURE_OPTION): - dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_TEMPERATURE_OPTION)]) + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_TEMPERATURE_OPTION, entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_TEMPERATURENEW_OPTION): - dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_TEMPERATURENEW_OPTION)]) + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_TEMPERATURENEW_OPTION, entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_TUMBLE_STATUS): - dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_TUMBLE_STATUS)]) + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_TUMBLE_STATUS, entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_EXTENDED_TUMBLE_OPTION_SELECTION): - dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_EXTENDED_TUMBLE_OPTION_SELECTION)]) + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_EXTENDED_TUMBLE_OPTION_SELECTION, entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_WASHERLINK_STATUS): - dryer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_WASHERLINK_STATUS)]) + dryer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_WASHERLINK_STATUS, entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_LEVEL_SENSOR_DISABLED): - dryer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_LEVEL_SENSOR_DISABLED)]) + dryer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_LEVEL_SENSOR_DISABLED, entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_SHEET_USAGE_CONFIGURATION): - dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_SHEET_USAGE_CONFIGURATION)]) + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_SHEET_USAGE_CONFIGURATION, entity_category=EntityCategory.DIAGNOSTIC)]) 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")]) + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_SHEET_INVENTORY, icon_override="mdi:tray-full", uom_override="sheets", entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_DRYER_ECODRY_OPTION_SELECTION): - dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_ECODRY_OPTION_SELECTION)]) + dryer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_DRYER_ECODRY_OPTION_SELECTION, entity_category=EntityCategory.DIAGNOSTIC)]) return dryer_entities \ No newline at end of file diff --git a/custom_components/ge_home/devices/dual_dishwasher.py b/custom_components/ge_home/devices/dual_dishwasher.py index 158da3c..8fa0604 100644 --- a/custom_components/ge_home/devices/dual_dishwasher.py +++ b/custom_components/ge_home/devices/dual_dishwasher.py @@ -1,11 +1,12 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity -from gehomesdk.erd import ErdCode, ErdApplianceType +from gehomesdk import ErdCode, ErdApplianceType, ErdRemoteCommand from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeDishwasherCommandButton _LOGGER = logging.getLogger(__name__) @@ -20,52 +21,73 @@ def get_all_entities(self) -> List[Entity]: lower_entities = [ GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_STATE, erd_override="lower_cycle_state", icon_override="mdi:state-machine"), GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING, erd_override="lower_time_remaining"), - GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS, erd_override="lower_door_status"), + GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS, erd_override="lower_door_status", entity_category=EntityCategory.DIAGNOSTIC), #Reminders - GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", erd_override="lower_reminder", icon_override="mdi:shimmer"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", erd_override="lower_reminder", icon_override="mdi:dishwasher-alert"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", erd_override="lower_reminder", icon_override="mdi:silverware-clean"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", erd_override="lower_reminder", icon_override="mdi:shimmer", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", erd_override="lower_reminder", icon_override="mdi:dishwasher-alert", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", erd_override="lower_reminder", icon_override="mdi:silverware-clean", entity_category=EntityCategory.DIAGNOSTIC), #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "mute", erd_override="lower_setting", icon_override="mdi:volume-mute"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control", erd_override="lower_setting", icon_override="mdi:lock"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath", erd_override="lower_setting", icon_override="mdi:star-david"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode", erd_override="lower_setting", icon_override="mdi:state-machine"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak", erd_override="lower_setting", icon_override="mdi:water"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet", erd_override="lower_setting", icon_override="mdi:bottle-tonic-outline"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", erd_override="lower_setting", icon_override="mdi:coolant-temperature"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "rinse_aid", erd_override="lower_setting", icon_override="mdi:shimmer"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "dry_option", erd_override="lower_setting", icon_override="mdi:fan"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_zone", erd_override="lower_setting", icon_override="mdi:dock-top"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours", erd_override="lower_setting", icon_override="mdi:clock-fast") + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "mute", erd_override="lower_setting", icon_override="mdi:volume-mute", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "lock_control", erd_override="lower_setting", icon_override="mdi:lock", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "sabbath", erd_override="lower_setting", icon_override="mdi:star-david", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "cycle_mode", erd_override="lower_setting", icon_override="mdi:state-machine", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "presoak", erd_override="lower_setting", icon_override="mdi:water", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "bottle_jet", erd_override="lower_setting", icon_override="mdi:bottle-tonic-outline", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_temp", erd_override="lower_setting", icon_override="mdi:coolant-temperature", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "rinse_aid", erd_override="lower_setting", icon_override="mdi:shimmer", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "dry_option", erd_override="lower_setting", icon_override="mdi:fan", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wash_zone", erd_override="lower_setting", icon_override="mdi:dock-top", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_USER_SETTING, "delay_hours", erd_override="lower_setting", icon_override="mdi:clock-fast", entity_category=EntityCategory.DIAGNOSTIC) ] upper_entities = [ - #GeDishwasherControlLockedSwitch(self, ErdCode.USER_INTERFACE_LOCKED), GeErdSensor(self, ErdCode.DISHWASHER_UPPER_CYCLE_STATE, erd_override="upper_cycle_state", icon_override="mdi:state-machine"), GeErdSensor(self, ErdCode.DISHWASHER_UPPER_TIME_REMAINING, erd_override="upper_time_remaining"), - GeErdBinarySensor(self, ErdCode.DISHWASHER_UPPER_DOOR_STATUS, erd_override="upper_door_status"), + GeErdBinarySensor(self, ErdCode.DISHWASHER_UPPER_DOOR_STATUS, erd_override="upper_door_status", entity_category=EntityCategory.DIAGNOSTIC), #Reminders - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "add_rinse_aid", erd_override="upper_reminder", icon_override="mdi:shimmer"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "clean_filter", erd_override="upper_reminder", icon_override="mdi:dishwasher-alert"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "sanitized", erd_override="upper_reminder", icon_override="mdi:silverware-clean"), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "add_rinse_aid", erd_override="upper_reminder", icon_override="mdi:shimmer", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "clean_filter", erd_override="upper_reminder", icon_override="mdi:dishwasher-alert", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_REMINDERS, "sanitized", erd_override="upper_reminder", icon_override="mdi:silverware-clean", entity_category=EntityCategory.DIAGNOSTIC), #User Setttings - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "mute", erd_override="upper_setting", icon_override="mdi:volume-mute"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "lock_control", erd_override="upper_setting", icon_override="mdi:lock"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "sabbath", erd_override="upper_setting", icon_override="mdi:star-david"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "cycle_mode", erd_override="upper_setting", icon_override="mdi:state-machine"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "presoak", erd_override="upper_setting", icon_override="mdi:water"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "bottle_jet", erd_override="upper_setting", icon_override="mdi:bottle-tonic-outline"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "wash_temp", erd_override="upper_setting", icon_override="mdi:coolant-temperature"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "rinse_aid", erd_override="upper_setting", icon_override="mdi:shimmer"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "dry_option", erd_override="upper_setting", icon_override="mdi:fan"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "wash_zone", erd_override="upper_setting", icon_override="mdi:dock-top"), - GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "delay_hours", erd_override="upper_setting", icon_override="mdi:clock-fast") + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "mute", erd_override="upper_setting", icon_override="mdi:volume-mute", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "lock_control", erd_override="upper_setting", icon_override="mdi:lock", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "sabbath", erd_override="upper_setting", icon_override="mdi:star-david", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "cycle_mode", erd_override="upper_setting", icon_override="mdi:state-machine", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "presoak", erd_override="upper_setting", icon_override="mdi:water", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "bottle_jet", erd_override="upper_setting", icon_override="mdi:bottle-tonic-outline", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "wash_temp", erd_override="upper_setting", icon_override="mdi:coolant-temperature", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "rinse_aid", erd_override="upper_setting", icon_override="mdi:shimmer", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "dry_option", erd_override="upper_setting", icon_override="mdi:fan", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "wash_zone", erd_override="upper_setting", icon_override="mdi:dock-top", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "delay_hours", erd_override="upper_setting", icon_override="mdi:clock-fast", entity_category=EntityCategory.DIAGNOSTIC) ] + # check for remote command availability and add if present (lower) + if self.has_erd_code(ErdCode.DISHWASHER_REMOTE_START_ENABLE): + lower_entities.extend( + [ + GeErdBinarySensor(self, ErdCode.DISHWASHER_REMOTE_START_ENABLE, erd_override="lower_remote_command_enable", entity_category=EntityCategory.DIAGNOSTIC), + GeDishwasherCommandButton(self, ErdCode.DISHWASHER_REMOTE_START_COMMAND, ErdRemoteCommand.START_RESUME, erd_override="lower_remote_command"), + GeDishwasherCommandButton(self, ErdCode.DISHWASHER_REMOTE_START_COMMAND, ErdRemoteCommand.PAUSE, erd_override="lower_remote_command"), + GeDishwasherCommandButton(self, ErdCode.DISHWASHER_REMOTE_START_COMMAND, ErdRemoteCommand.CANCEL, erd_override="lower_remote_command") + ] + ) + + # check for remote command availability and add if present (upper) + if self.has_erd_code(ErdCode.DISHWASHER_UPPER_REMOTE_START_ENABLE): + upper_entities.extend( + [ + GeErdBinarySensor(self, ErdCode.DISHWASHER_UPPER_REMOTE_START_ENABLE, erd_override="upper_remote_command_enable", entity_category=EntityCategory.DIAGNOSTIC), + GeDishwasherCommandButton(self, ErdCode.DISHWASHER_UPPER_REMOTE_START_COMMAND, ErdRemoteCommand.START_RESUME, erd_override="upper_remote_command"), + GeDishwasherCommandButton(self, ErdCode.DISHWASHER_UPPER_REMOTE_START_COMMAND, ErdRemoteCommand.PAUSE, erd_override="upper_remote_command"), + GeDishwasherCommandButton(self, ErdCode.DISHWASHER_UPPER_REMOTE_START_COMMAND, ErdRemoteCommand.CANCEL, erd_override="upper_remote_command") + ] + ) + entities = base_entities + lower_entities + upper_entities return entities diff --git a/custom_components/ge_home/devices/espresso_maker.py b/custom_components/ge_home/devices/espresso_maker.py index efb184e..8cb2456 100644 --- a/custom_components/ge_home/devices/espresso_maker.py +++ b/custom_components/ge_home/devices/espresso_maker.py @@ -1,6 +1,7 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ( ErdCode, @@ -24,10 +25,10 @@ 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"), + GeErdBinarySensor(self, ErdCode.CCM_IS_DESCALING, entity_category=EntityCategory.DIAGNOSTIC), + GeErdButton(self, ErdCode.CCM_CANCEL_DESCALING, entity_category=EntityCategory.CONFIG), + GeErdButton(self, ErdCode.CCM_START_DESCALING, entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.CCM_OUT_OF_WATER, device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC), ] entities = base_entities + em_entities diff --git a/custom_components/ge_home/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index bde5813..100728e 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -3,6 +3,7 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ( ErdCode, @@ -49,39 +50,38 @@ class FridgeApi(ApplianceApi): def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() - fridge_entities = [] - freezer_entities = [] - dispenser_entities = [] + fridge_entities: List[Entity] = [] + freezer_entities: List[Entity] = [] + dispenser_entities: List[Entity] = [] # Get the statuses used to determine presence - ice_maker_control: IceMakerControlStatus = self.try_get_erd_value(ErdCode.ICE_MAKER_CONTROL) - ice_bucket_status: FridgeIceBucketStatus = self.try_get_erd_value(ErdCode.ICE_MAKER_BUCKET_STATUS) - water_filter: ErdFilterStatus = self.try_get_erd_value(ErdCode.WATER_FILTER_STATUS) - air_filter: ErdFilterStatus = self.try_get_erd_value(ErdCode.AIR_FILTER_STATUS) - hot_water_status: HotWaterStatus = self.try_get_erd_value(ErdCode.HOT_WATER_STATUS) - fridge_model_info: FridgeModelInfo = self.try_get_erd_value(ErdCode.FRIDGE_MODEL_INFO) - convertable_drawer: ErdConvertableDrawerMode = self.try_get_erd_value(ErdCode.CONVERTABLE_DRAWER_MODE) - - interior_light: int = self.try_get_erd_value(ErdCode.INTERIOR_LIGHT) - proximity_light: ErdOnOff = self.try_get_erd_value(ErdCode.PROXIMITY_LIGHT) - display_mode: ErdOnOff = self.try_get_erd_value(ErdCode.DISPLAY_MODE) - lockout_mode: ErdOnOff = self.try_get_erd_value(ErdCode.LOCKOUT_MODE) - turbo_cool: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_COOL_STATUS) - turbo_freeze: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_FREEZE_STATUS) - ice_boost: ErdOnOff = self.try_get_erd_value(ErdCode.FRIDGE_ICE_BOOST) + ice_maker_control: IceMakerControlStatus | None = self.try_get_erd_value(ErdCode.ICE_MAKER_CONTROL) + ice_bucket_status: FridgeIceBucketStatus | None = self.try_get_erd_value(ErdCode.ICE_MAKER_BUCKET_STATUS) + water_filter: ErdFilterStatus | None = self.try_get_erd_value(ErdCode.WATER_FILTER_STATUS) + air_filter: ErdFilterStatus | None = self.try_get_erd_value(ErdCode.AIR_FILTER_STATUS) + hot_water_status: HotWaterStatus | None = self.try_get_erd_value(ErdCode.HOT_WATER_STATUS) + fridge_model_info: FridgeModelInfo | None = self.try_get_erd_value(ErdCode.FRIDGE_MODEL_INFO) + convertable_drawer: ErdConvertableDrawerMode | None = self.try_get_erd_value(ErdCode.CONVERTABLE_DRAWER_MODE) + + interior_light: int | None = self.try_get_erd_value(ErdCode.INTERIOR_LIGHT) + proximity_light: ErdOnOff | None = self.try_get_erd_value(ErdCode.PROXIMITY_LIGHT) + display_mode: ErdOnOff | None = self.try_get_erd_value(ErdCode.DISPLAY_MODE) + lockout_mode: ErdOnOff | None = self.try_get_erd_value(ErdCode.LOCKOUT_MODE) + turbo_cool: ErdOnOff | None = self.try_get_erd_value(ErdCode.TURBO_COOL_STATUS) + turbo_freeze: ErdOnOff | None = self.try_get_erd_value(ErdCode.TURBO_FREEZE_STATUS) + ice_boost: ErdOnOff | None = self.try_get_erd_value(ErdCode.FRIDGE_ICE_BOOST) units = self.hass.config.units # Common entities common_entities = [ - GeErdSensor(self, ErdCode.FRIDGE_MODEL_INFO), - GeErdSwitch(self, ErdCode.SABBATH_MODE), - GeErdSensor(self, ErdCode.DOOR_STATUS), - GeErdPropertyBinarySensor(self, ErdCode.DOOR_STATUS, "any_open") + GeErdSensor(self, ErdCode.FRIDGE_MODEL_INFO, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.DOOR_STATUS, entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertyBinarySensor(self, ErdCode.DOOR_STATUS, "any_open", entity_category=EntityCategory.DIAGNOSTIC) ] 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)) + common_entities.append(GeErdSensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, entity_category=EntityCategory.DIAGNOSTIC)) # Fridge entities if fridge_model_info is None or fridge_model_info.has_fridge: @@ -90,26 +90,26 @@ def get_all_entities(self) -> List[Entity]: GeFridge(self), ]) if turbo_cool is not None: - fridge_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST)) + fridge_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST, entity_category=EntityCategory.CONFIG)) if(ice_maker_control and ice_maker_control.status_fridge != ErdOnOff.NA): - fridge_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_fridge")) + fridge_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_fridge", entity_category=EntityCategory.DIAGNOSTIC)) fridge_entities.append(GeFridgeIceControlSwitch(self, "fridge")) if(water_filter and water_filter != ErdFilterStatus.NA): - fridge_entities.append(GeErdSensor(self, ErdCode.WATER_FILTER_STATUS)) + fridge_entities.append(GeErdSensor(self, ErdCode.WATER_FILTER_STATUS, entity_category=EntityCategory.DIAGNOSTIC)) if(air_filter and air_filter != ErdFilterStatus.NA): - fridge_entities.append(GeErdSensor(self, ErdCode.AIR_FILTER_STATUS)) + fridge_entities.append(GeErdSensor(self, ErdCode.AIR_FILTER_STATUS, entity_category=EntityCategory.DIAGNOSTIC)) if(ice_bucket_status and ice_bucket_status.is_present_fridge): - fridge_entities.append(GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_fridge")) + fridge_entities.append(GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_fridge", entity_category=EntityCategory.DIAGNOSTIC)) if(interior_light and interior_light != 255): - fridge_entities.append(GeErdLight(self, ErdCode.INTERIOR_LIGHT)) + fridge_entities.append(GeErdLight(self, ErdCode.INTERIOR_LIGHT, entity_category=EntityCategory.CONFIG)) 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")) + fridge_entities.append(GeErdSwitch(self, ErdCode.PROXIMITY_LIGHT, ErdOnOffBoolConverter(), icon_on_override="mdi:lightbulb-on", icon_off_override="mdi:lightbulb", entity_category=EntityCategory.CONFIG)) if(convertable_drawer and convertable_drawer != ErdConvertableDrawerMode.NA): - fridge_entities.append(GeErdSelect(self, ErdCode.CONVERTABLE_DRAWER_MODE, ConvertableDrawerModeOptionsConverter(units))) + fridge_entities.append(GeErdSelect(self, ErdCode.CONVERTABLE_DRAWER_MODE, ConvertableDrawerModeOptionsConverter(units), entity_category=EntityCategory.CONFIG)) 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")) + fridge_entities.append(GeErdSwitch(self, ErdCode.DISPLAY_MODE, ErdOnOffBoolConverter(), icon_on_override="mdi:lightbulb-on", icon_off_override="mdi:lightbulb", entity_category=EntityCategory.CONFIG)) 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")) + fridge_entities.append(GeErdSwitch(self, ErdCode.LOCKOUT_MODE, ErdOnOffBoolConverter(), icon_on_override="mdi:lock", icon_off_override="mdi:lock-open", entity_category=EntityCategory.CONFIG)) # Freezer entities if fridge_model_info is None or fridge_model_info.has_freezer: @@ -118,24 +118,24 @@ def get_all_entities(self) -> List[Entity]: GeFreezer(self), ]) if turbo_freeze is not None: - freezer_entities.append(GeErdSwitch(self, ErdCode.TURBO_FREEZE_STATUS)) + freezer_entities.append(GeErdSwitch(self, ErdCode.TURBO_FREEZE_STATUS, entity_category=EntityCategory.CONFIG)) if ice_boost is not None: - freezer_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST)) + freezer_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST, entity_category=EntityCategory.CONFIG)) if(ice_maker_control and ice_maker_control.status_freezer != ErdOnOff.NA): - freezer_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_freezer")) + freezer_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_freezer", entity_category=EntityCategory.DIAGNOSTIC)) freezer_entities.append(GeFridgeIceControlSwitch(self, "freezer")) if(ice_bucket_status and ice_bucket_status.is_present_freezer): - freezer_entities.append(GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_freezer")) + freezer_entities.append(GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_freezer", entity_category=EntityCategory.DIAGNOSTIC)) # Dispenser entities if(hot_water_status and hot_water_status.status != ErdHotWaterStatus.NA): dispenser_entities.extend([ - GeErdBinarySensor(self, ErdCode.HOT_WATER_IN_USE), - GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), - GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "status", icon_override="mdi:information-outline"), - GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_until_ready", icon_override="mdi:timer-outline"), - GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "current_temp", device_class_override=SensorDeviceClass.TEMPERATURE, data_type_override=ErdDataType.INT), - GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=BinarySensorDeviceClass.PROBLEM), + GeErdBinarySensor(self, ErdCode.HOT_WATER_IN_USE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP, entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "status", icon_override="mdi:information-outline", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "time_until_ready", icon_override="mdi:timer-outline", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.HOT_WATER_STATUS, "current_temp", device_class_override=SensorDeviceClass.TEMPERATURE, data_type_override=ErdDataType.INT, entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertyBinarySensor(self, ErdCode.HOT_WATER_STATUS, "faulted", device_class_override=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC), GeDispenser(self), GeKCupSwitch(self) ]) diff --git a/custom_components/ge_home/devices/hood.py b/custom_components/ge_home/devices/hood.py index 439c775..ed7daac 100644 --- a/custom_components/ge_home/devices/hood.py +++ b/custom_components/ge_home/devices/hood.py @@ -7,6 +7,8 @@ ErdApplianceType, ErdHoodFanSpeedAvailability, ErdHoodLightLevelAvailability, + ErdHoodFanSpeed, + ErdHoodLightLevel, ErdOnOff ) @@ -29,22 +31,36 @@ class HoodApi(ApplianceApi): 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) + #old-style entities + fan_availability: ErdHoodFanSpeedAvailability | None = self.try_get_erd_value(ErdCode.HOOD_FAN_SPEED_AVAILABILITY) + light_availability: ErdHoodLightLevelAvailability | None = self.try_get_erd_value(ErdCode.HOOD_LIGHT_LEVEL_AVAILABILITY) + timer_availability: ErdOnOff | None = self.try_get_erd_value(ErdCode.HOOD_TIMER_AVAILABILITY) - hood_entities = [ + #new-style entities + available_fan_speeds: int | None = self.try_get_erd_value(ErdCode.HOOD_AVAILABLE_FAN_SPEEDS) + available_light_levels: int | None = self.try_get_erd_value(ErdCode.HOOD_AVAILABLE_LIGHT_LEVELS) + actual_fan_speed: ErdHoodFanSpeed | None = self.try_get_erd_value(ErdCode.HOOD_ACTUAL_FAN_SPEED) + actual_light_level: ErdHoodLightLevel | None = self.try_get_erd_value(ErdCode.HOOD_ACTUAL_LIGHT_LEVEL) + + hood_entities: List[Entity] = [ #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: + #old-style + if fan_availability is not None 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: + if light_availability is not None and light_availability.is_available: hood_entities.append(GeHoodLightLevelSelect(self, ErdCode.HOOD_LIGHT_LEVEL)) - if timer_availability == ErdOnOff.ON: + + #new-style + if available_fan_speeds is not None and available_fan_speeds > 0 and actual_fan_speed is not None: + hood_entities.append(GeHoodFanSpeedSelect(self, ErdCode.HOOD_ACTUAL_FAN_SPEED, ErdCode.HOOD_REQUESTED_FAN_SPEED)) + if available_light_levels is not None and available_light_levels > 0 and actual_light_level is not None: + hood_entities.append(GeHoodLightLevelSelect(self, ErdCode.HOOD_ACTUAL_LIGHT_LEVEL, ErdCode.HOOD_REQUESTED_LIGHT_LEVEL)) + + #timer + if timer_availability is not None: hood_entities.append(GeErdTimerSensor(self, ErdCode.HOOD_TIMER)) entities = base_entities + hood_entities diff --git a/custom_components/ge_home/devices/microwave.py b/custom_components/ge_home/devices/microwave.py index ec943fa..43471ff 100644 --- a/custom_components/ge_home/devices/microwave.py +++ b/custom_components/ge_home/devices/microwave.py @@ -1,13 +1,13 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ( ErdCode, ErdApplianceType, ErdHoodFanSpeedAvailability, - ErdHoodLightLevelAvailability, - ErdOnOff + ErdHoodLightLevelAvailability ) from .base import ApplianceApi @@ -31,16 +31,16 @@ 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) + fan_availability: ErdHoodFanSpeedAvailability | None = self.try_get_erd_value(ErdCode.HOOD_FAN_SPEED_AVAILABILITY) + light_availability: ErdHoodLightLevelAvailability | None = 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"), + GeErdBinarySensor(self, ErdCode.MICROWAVE_REMOTE_ENABLE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "status", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertyBinarySensor(self, ErdCode.MICROWAVE_STATE, "door_status", device_class_override="door", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "cook_mode", icon_override="mdi:food-turkey", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "power_level", icon_override="mdi:gauge", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "temperature", icon_override="mdi:thermometer", entity_category=EntityCategory.DIAGNOSTIC), GeErdTimerSensor(self, ErdCode.MICROWAVE_COOK_TIMER), GeErdTimerSensor(self, ErdCode.MICROWAVE_KITCHEN_TIMER) ] diff --git a/custom_components/ge_home/devices/oim.py b/custom_components/ge_home/devices/oim.py index 2eebd39..3be1de6 100644 --- a/custom_components/ge_home/devices/oim.py +++ b/custom_components/ge_home/devices/oim.py @@ -1,11 +1,11 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ( ErdCode, - ErdApplianceType, - ErdOnOff + ErdApplianceType ) from .base import ApplianceApi @@ -29,10 +29,10 @@ 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()), + GeErdSensor(self, ErdCode.OIM_STATUS, entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.OIM_FILTER_STATUS, device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.OIM_NEEDS_DESCALING, device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSelect(self, ErdCode.OIM_LIGHT_LEVEL, OimLightLevelOptionsConverter(), entity_category=EntityCategory.CONFIG), GeErdSwitch(self, ErdCode.OIM_POWER, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), ] diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index 914b38e..81188bb 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -1,11 +1,12 @@ import logging from typing import List -from gehomesdk.erd.erd_data_type import ErdDataType +from homeassistant.const import EntityCategory from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.entity import Entity from gehomesdk import ( ErdCode, + ErdDataType, ErdApplianceType, OvenConfiguration, ErdCooktopConfig, @@ -49,14 +50,14 @@ def get_all_entities(self) -> List[Entity]: has_upper_probe_temperature = self.has_erd_code(ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP) has_lower_probe_temperature = self.has_erd_code(ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP) - upper_light : ErdOvenLightLevel = self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT) - upper_light_availability: ErdOvenLightLevelAvailability = self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT_AVAILABILITY) - lower_light : ErdOvenLightLevel = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT) - lower_light_availability: ErdOvenLightLevelAvailability = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT_AVAILABILITY) + upper_light: ErdOvenLightLevel | None = self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT) + upper_light_availability: ErdOvenLightLevelAvailability | None = self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT_AVAILABILITY) + lower_light: ErdOvenLightLevel | None = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT) + lower_light_availability: ErdOvenLightLevelAvailability | None = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT_AVAILABILITY) - upper_warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE) - lower_warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE) - warm_drawer : ErdOvenWarmingState = self.try_get_erd_value(ErdCode.WARMING_DRAWER_STATE) + upper_warm_drawer : ErdOvenWarmingState | None = self.try_get_erd_value(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE) + lower_warm_drawer : ErdOvenWarmingState | None = self.try_get_erd_value(ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE) + warm_drawer : ErdOvenWarmingState | None = self.try_get_erd_value(ErdCode.WARMING_DRAWER_STATE) _LOGGER.debug(f"Oven Config: {oven_config}") _LOGGER.debug(f"Cooktop Config: {cooktop_config}") @@ -65,57 +66,57 @@ def get_all_entities(self) -> List[Entity]: if oven_config.has_lower_oven: oven_entities.extend([ - GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE), - GeErdSensor(self, ErdCode.LOWER_OVEN_CURRENT_STATE), + GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.LOWER_OVEN_CURRENT_STATE, entity_category=EntityCategory.DIAGNOSTIC), 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), + GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED, entity_category=EntityCategory.DIAGNOSTIC), GeOven(self, LOWER_OVEN, True, self._temperature_code(has_lower_raw_temperature)) ]) if has_lower_raw_temperature: - oven_entities.append(GeErdSensor(self, ErdCode.LOWER_OVEN_RAW_TEMPERATURE)) + oven_entities.append(GeErdSensor(self, ErdCode.LOWER_OVEN_RAW_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC)) if lower_light_availability is None or lower_light_availability.is_available or lower_light is not None: oven_entities.append(GeOvenLightLevelSelect(self, ErdCode.LOWER_OVEN_LIGHT)) if lower_warm_drawer is not None: oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE)) if has_lower_probe_temperature: - oven_entities.append(GeErdSensor(self, ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP)) + oven_entities.append(GeErdSensor(self, ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP, entity_category=EntityCategory.DIAGNOSTIC)) oven_entities.extend([ - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, ~oven_config.has_lower_oven)), - GeErdSensor(self, ErdCode.UPPER_OVEN_CURRENT_STATE, self._single_name(ErdCode.UPPER_OVEN_CURRENT_STATE, ~oven_config.has_lower_oven)), - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, ~oven_config.has_lower_oven)), - GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER, ~oven_config.has_lower_oven)), - GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, self._single_name(ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, ~oven_config.has_lower_oven)), - GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, ~oven_config.has_lower_oven)), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED, ~oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.UPPER_OVEN_CURRENT_STATE, self._single_name(ErdCode.UPPER_OVEN_CURRENT_STATE, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, not oven_config.has_lower_oven)), + GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER, not oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, self._single_name(ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), GeOven(self, UPPER_OVEN, False, self._temperature_code(has_upper_raw_temperature)) ]) if has_upper_raw_temperature: - oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_RAW_TEMPERATURE, ~oven_config.has_lower_oven))) + oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_RAW_TEMPERATURE, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC)) if upper_light_availability is None or upper_light_availability.is_available or upper_light is not None: - oven_entities.append(GeOvenLightLevelSelect(self, ErdCode.UPPER_OVEN_LIGHT, self._single_name(ErdCode.UPPER_OVEN_LIGHT, ~oven_config.has_lower_oven))) + oven_entities.append(GeOvenLightLevelSelect(self, ErdCode.UPPER_OVEN_LIGHT, self._single_name(ErdCode.UPPER_OVEN_LIGHT, not oven_config.has_lower_oven))) if upper_warm_drawer is not None: - oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, self._single_name(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, ~oven_config.has_lower_oven))) + oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, self._single_name(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, not oven_config.has_lower_oven))) if has_upper_probe_temperature: - oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, self._single_name(ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, ~oven_config.has_lower_oven))) + oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, self._single_name(ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC)) if oven_config.has_warming_drawer and warm_drawer is not None: - oven_entities.append(GeErdSensor(self, ErdCode.WARMING_DRAWER_STATE)) + oven_entities.append(GeErdSensor(self, ErdCode.WARMING_DRAWER_STATE, entity_category=EntityCategory.DIAGNOSTIC)) if cooktop_config == ErdCooktopConfig.PRESENT: # attempt to get the cooktop status using legacy status cooktop_status_erd = ErdCode.COOKTOP_STATUS - cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) + cooktop_status: CooktopStatus | None = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) # if we didn't get it, try using the new version if cooktop_status is None: cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT - cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) + cooktop_status = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) # if we got a status through either mechanism, we can add the entities if cooktop_status is not None: @@ -125,7 +126,7 @@ def get_all_entities(self) -> List[Entity]: if v.exists: prop = self._camel_to_snake(k) cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on")) - cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized")) + cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized", entity_category=EntityCategory.DIAGNOSTIC)) if not v.on_off_only: cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) diff --git a/custom_components/ge_home/devices/pac.py b/custom_components/ge_home/devices/pac.py index fa2da9d..066e0a3 100644 --- a/custom_components/ge_home/devices/pac.py +++ b/custom_components/ge_home/devices/pac.py @@ -1,8 +1,9 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity -from gehomesdk.erd import ErdCode, ErdApplianceType +from gehomesdk import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import GePacClimate, GeErdSensor, GeErdSwitch, ErdOnOffBoolConverter @@ -19,10 +20,10 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdSensor(self, ErdCode.AC_TARGET_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_OPERATION_MODE, entity_category=EntityCategory.DIAGNOSTIC), GeErdSwitch(self, ErdCode.AC_POWER_STATUS, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), ] diff --git a/custom_components/ge_home/devices/sac.py b/custom_components/ge_home/devices/sac.py index a1dfad5..acfce4e 100644 --- a/custom_components/ge_home/devices/sac.py +++ b/custom_components/ge_home/devices/sac.py @@ -1,8 +1,9 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity -from gehomesdk.erd import ErdCode, ErdApplianceType +from gehomesdk import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import GeSacClimate, GeErdSensor, GeErdSwitch, ErdOnOffBoolConverter @@ -19,17 +20,17 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdSensor(self, ErdCode.AC_TARGET_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_OPERATION_MODE, entity_category=EntityCategory.DIAGNOSTIC), 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")) + sac_entities.append(GeErdSwitch(self, ErdCode.SAC_SLEEP_MODE, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:sleep", icon_off_override="mdi:sleep-off", entity_category=EntityCategory.CONFIG)) 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")) + 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", entity_category=EntityCategory.CONFIG)) entities = base_entities + sac_entities diff --git a/custom_components/ge_home/devices/ucim.py b/custom_components/ge_home/devices/ucim.py index f4b60e3..01e7780 100644 --- a/custom_components/ge_home/devices/ucim.py +++ b/custom_components/ge_home/devices/ucim.py @@ -1,12 +1,9 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity -from gehomesdk import ( - ErdCode, - ErdApplianceType, - ErdOnOff -) +from gehomesdk import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import ( @@ -22,22 +19,22 @@ class UcimApi(ApplianceApi): - """API class for Opal Ice Maker objects""" + """API class for Under Counter 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()), + GeErdSensor(self, ErdCode.OIM_STATUS, entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.OIM_FILTER_STATUS, device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.OIM_NEEDS_DESCALING, device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSelect(self, ErdCode.OIM_LIGHT_LEVEL, OimLightLevelOptionsConverter(), entity_category=EntityCategory.CONFIG), GeErdSwitch(self, ErdCode.OIM_POWER, bool_converter=ErdOnOffBoolConverter(), icon_on_override="mdi:power-on", icon_off_override="mdi:power-off"), - GeErdSensor(self, ErdCode.OIM_PRODUCTION), - GeErdSensor(self, ErdCode.UCIM_CLEAN_STATUS), - GeErdSensor(self, ErdCode.UCIM_FILTER_PERCENTAGE_USED), - GeErdBinarySensor(self, ErdCode.UCIM_BIN_FULL, device_class_override="problem"), + GeErdSensor(self, ErdCode.OIM_PRODUCTION, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.UCIM_CLEAN_STATUS, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.UCIM_FILTER_PERCENTAGE_USED, entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.UCIM_BIN_FULL, device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC), ] entities = base_entities + oim_entities diff --git a/custom_components/ge_home/devices/wac.py b/custom_components/ge_home/devices/wac.py index 6208a82..04c4021 100644 --- a/custom_components/ge_home/devices/wac.py +++ b/custom_components/ge_home/devices/wac.py @@ -1,8 +1,9 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity -from gehomesdk.erd import ErdCode, ErdApplianceType +from gehomesdk import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import GeWacClimate, GeErdSensor, GeErdBinarySensor, GeErdSwitch, ErdOnOffBoolConverter @@ -19,14 +20,14 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdSensor(self, ErdCode.AC_TARGET_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_AMBIENT_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_FAN_SETTING, icon_override="mdi:fan", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.AC_OPERATION_MODE, entity_category=EntityCategory.DIAGNOSTIC), 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"), + GeErdBinarySensor(self, ErdCode.AC_FILTER_STATUS, device_class_override="problem", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WAC_DEMAND_RESPONSE_STATE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WAC_DEMAND_RESPONSE_POWER, uom_override="kW", entity_category=EntityCategory.DIAGNOSTIC), ] entities = base_entities + wac_entities return entities diff --git a/custom_components/ge_home/devices/washer.py b/custom_components/ge_home/devices/washer.py index 119e4ed..6d25595 100644 --- a/custom_components/ge_home/devices/washer.py +++ b/custom_components/ge_home/devices/washer.py @@ -1,11 +1,12 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ErdCode, ErdApplianceType from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdButton +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor from ..entities.laundry.ge_washer_cycle_button import GeWasherCycleButton @@ -26,8 +27,8 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR, entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, entity_category=EntityCategory.DIAGNOSTIC), ] washer_entities = self.get_washer_entities() @@ -38,33 +39,33 @@ def get_all_entities(self) -> List[Entity]: 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"), + washer_entities: List[Entity] = [ + GeErdSensor(self, ErdCode.LAUNDRY_WASHER_SOIL_LEVEL, icon_override="mdi:emoticon-poop", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.LAUNDRY_WASHER_WASHTEMP_LEVEL, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.LAUNDRY_WASHER_SPINTIME_LEVEL, icon_override="mdi:speedometer", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.LAUNDRY_WASHER_RINSE_OPTION, icon_override="mdi:shimmer", entity_category=EntityCategory.DIAGNOSTIC), ] if self.has_erd_code(ErdCode.LAUNDRY_WASHER_DOOR_LOCK): - washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_DOOR_LOCK)]) + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_DOOR_LOCK, entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_WASHER_TANK_STATUS): - washer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_WASHER_TANK_STATUS)]) + washer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_WASHER_TANK_STATUS, entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_WASHER_TANK_SELECTED): - washer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_WASHER_TANK_SELECTED)]) + washer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_WASHER_TANK_SELECTED, entity_category=EntityCategory.DIAGNOSTIC)]) 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")]) + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_TIMESAVER, icon_on_override="mdi:sort-clock-ascending", icon_off_override="mdi:sort-clock-ascending-outline", entity_category=EntityCategory.DIAGNOSTIC)]) 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")]) + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_POWERSTEAM, icon_on_override="mdi:kettle-steam", icon_off_override="mdi:kettle-steam-outline", entity_category=EntityCategory.DIAGNOSTIC)]) 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")]) + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_PREWASH, icon_on_override="mdi:water-plus", icon_off_override="mdi:water-remove-outline", entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_WASHER_TUMBLECARE): - washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_TUMBLECARE)]) + washer_entities.extend([GeErdBinarySensor(self, ErdCode.LAUNDRY_WASHER_TUMBLECARE, entity_category=EntityCategory.DIAGNOSTIC)]) 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")]) + washer_entities.extend([GeErdPropertySensor(self, ErdCode.LAUNDRY_WASHER_SMART_DISPENSE, "loads_left", uom_override="loads", entity_category=EntityCategory.DIAGNOSTIC)]) if self.has_erd_code(ErdCode.LAUNDRY_WASHER_SMART_DISPENSE_TANK_STATUS): - washer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_WASHER_SMART_DISPENSE_TANK_STATUS)]) + washer_entities.extend([GeErdSensor(self, ErdCode.LAUNDRY_WASHER_SMART_DISPENSE_TANK_STATUS, entity_category=EntityCategory.DIAGNOSTIC)]) return washer_entities \ No newline at end of file diff --git a/custom_components/ge_home/devices/washer_dryer.py b/custom_components/ge_home/devices/washer_dryer.py index f701ff2..73187ec 100644 --- a/custom_components/ge_home/devices/washer_dryer.py +++ b/custom_components/ge_home/devices/washer_dryer.py @@ -1,6 +1,7 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ErdCode, ErdApplianceType @@ -24,8 +25,8 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR, entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, entity_category=EntityCategory.DIAGNOSTIC), ] washer_entities = self.get_washer_entities() diff --git a/custom_components/ge_home/devices/water_filter.py b/custom_components/ge_home/devices/water_filter.py index 7cebd6a..63e9277 100644 --- a/custom_components/ge_home/devices/water_filter.py +++ b/custom_components/ge_home/devices/water_filter.py @@ -1,6 +1,7 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ErdCode, ErdApplianceType @@ -24,15 +25,15 @@ 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"), + GeErdSensor(self, ErdCode.WH_FILTER_MODE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WH_FILTER_VALVE_STATE, icon_override="mdi:state-machine", entity_category=EntityCategory.DIAGNOSTIC), GeErdFilterPositionSelect(self, ErdCode.WH_FILTER_POSITION), - GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE, icon_on_override="mdi:human", icon_off_override="mdi:robot"), - GeErdBinarySensor(self, ErdCode.WH_FILTER_LEAK_VALIDITY, device_class_override="moisture"), - GeErdPropertySensor(self, ErdCode.WH_FILTER_FLOW_RATE, "flow_rate"), - GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE, device_class_override="water"), - GeErdPropertySensor(self, ErdCode.WH_FILTER_LIFE_REMAINING, "life_remaining"), - GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"), + GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE, icon_on_override="mdi:human", icon_off_override="mdi:robot", entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.WH_FILTER_LEAK_VALIDITY, device_class_override="moisture", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.WH_FILTER_FLOW_RATE, "flow_rate", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE, device_class_override="water", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.WH_FILTER_LIFE_REMAINING, "life_remaining", entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture", entity_category=EntityCategory.DIAGNOSTIC), ] entities = base_entities + wf_entities return entities diff --git a/custom_components/ge_home/devices/water_heater.py b/custom_components/ge_home/devices/water_heater.py index 19ca226..3fc0c05 100644 --- a/custom_components/ge_home/devices/water_heater.py +++ b/custom_components/ge_home/devices/water_heater.py @@ -1,6 +1,7 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ( ErdCode, @@ -13,7 +14,7 @@ from .base import ApplianceApi from ..entities import ( GeErdSensor, - GeErdBinarySensorSwitch, + GeErdSwitch, ErdOnOffBoolConverter ) @@ -27,23 +28,23 @@ class WaterHeaterApi(ApplianceApi): def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() - boost_mode: ErdOnOff = self.try_get_erd_value(ErdCode.WH_HEATER_BOOST_STATE) - active: ErdOnOff = self.try_get_erd_value(ErdCode.WH_HEATER_ACTIVE_STATE) + boost_mode: ErdOnOff | None = self.try_get_erd_value(ErdCode.WH_HEATER_BOOST_STATE) + active: ErdOnOff | None = self.try_get_erd_value(ErdCode.WH_HEATER_ACTIVE_STATE) wh_entities = [ - GeErdSensor(self, ErdCode.WH_HEATER_TARGET_TEMPERATURE), - GeErdSensor(self, ErdCode.WH_HEATER_TEMPERATURE), - GeErdSensor(self, ErdCode.WH_HEATER_MODE_HOURS_REMAINING), - GeErdSensor(self, ErdCode.WH_HEATER_ELECTRIC_MODE_MAX_TIME), - GeErdSensor(self, ErdCode.WH_HEATER_VACATION_MODE_MAX_TIME), + GeErdSensor(self, ErdCode.WH_HEATER_TARGET_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WH_HEATER_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WH_HEATER_MODE_HOURS_REMAINING, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WH_HEATER_ELECTRIC_MODE_MAX_TIME, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WH_HEATER_VACATION_MODE_MAX_TIME, entity_category=EntityCategory.DIAGNOSTIC), GeWaterHeater(self) ] if(boost_mode and boost_mode != ErdOnOff.NA): - wh_entities.append(GeErdBinarySensorSwitch(self, ErdCode.WH_HEATER_BOOST_STATE, ErdCode.WH_HEATER_BOOST_CONTROL, ErdOnOffBoolConverter(), icon_on_override="mdi:rocket-launch", icon_off_override="mdi:rocket-launch-outline")) + wh_entities.append(GeErdSwitch(self, ErdCode.WH_HEATER_BOOST_STATE, ErdOnOffBoolConverter(), icon_on_override="mdi:rocket-launch", icon_off_override="mdi:rocket-launch-outline", control_erd_code=ErdCode.WH_HEATER_BOOST_CONTROL, entity_category=EntityCategory.CONFIG)) if(active and active != ErdOnOff.NA): - wh_entities.append(GeErdBinarySensorSwitch(self, ErdCode.WH_HEATER_ACTIVE_STATE, ErdCode.WH_HEATER_ACTIVE_CONTROL, ErdOnOffBoolConverter(), icon_on_override="mdi:power", icon_off_override="mdi:power-standby")) + wh_entities.append(GeErdSwitch(self, ErdCode.WH_HEATER_ACTIVE_STATE, ErdOnOffBoolConverter(), icon_on_override="mdi:power", icon_off_override="mdi:power-standby", control_erd_code=ErdCode.WH_HEATER_ACTIVE_CONTROL, entity_category=EntityCategory.CONFIG)) entities = base_entities + wh_entities return entities diff --git a/custom_components/ge_home/devices/water_softener.py b/custom_components/ge_home/devices/water_softener.py index a0afa83..cdd3621 100644 --- a/custom_components/ge_home/devices/water_softener.py +++ b/custom_components/ge_home/devices/water_softener.py @@ -1,6 +1,7 @@ import logging from typing import List +from homeassistant.const import EntityCategory from homeassistant.helpers.entity import Entity from gehomesdk import ErdCode, ErdApplianceType @@ -24,14 +25,14 @@ def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() ws_entities = [ - GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE, icon_on_override="mdi:human", icon_off_override="mdi:robot"), - GeErdPropertySensor(self, ErdCode.WH_FILTER_FLOW_RATE, "flow_rate"), - GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"), - GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE, device_class_override="water"), - GeErdSensor(self, ErdCode.WH_SOFTENER_ERROR_CODE, icon_override="mdi:alert-circle"), - GeErdBinarySensor(self, ErdCode.WH_SOFTENER_LOW_SALT, icon_on_override="mdi:alert", icon_off_override="mdi:grain"), - GeErdSensor(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_STATE, icon_override="mdi:state-machine"), - GeErdSensor(self, ErdCode.WH_SOFTENER_SALT_LIFE_REMAINING, icon_override="mdi:calendar-clock"), + GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE, icon_on_override="mdi:human", icon_off_override="mdi:robot", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertySensor(self, ErdCode.WH_FILTER_FLOW_RATE, "flow_rate", entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE, device_class_override="water", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WH_SOFTENER_ERROR_CODE, icon_override="mdi:alert-circle", entity_category=EntityCategory.DIAGNOSTIC), + GeErdBinarySensor(self, ErdCode.WH_SOFTENER_LOW_SALT, icon_on_override="mdi:alert", icon_off_override="mdi:grain", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_STATE, icon_override="mdi:state-machine", entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WH_SOFTENER_SALT_LIFE_REMAINING, icon_override="mdi:calendar-clock", entity_category=EntityCategory.DIAGNOSTIC), GeErdShutoffPositionSelect(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_CONTROL), ] entities = base_entities + ws_entities diff --git a/custom_components/ge_home/entities/ac/fan_mode_options.py b/custom_components/ge_home/entities/ac/fan_mode_options.py index c8a50e6..8653a21 100644 --- a/custom_components/ge_home/entities/ac/fan_mode_options.py +++ b/custom_components/ge_home/entities/ac/fan_mode_options.py @@ -22,8 +22,7 @@ def from_option_string(self, value: str) -> Any: return self._default def to_option_string(self, value: Any) -> Optional[str]: - try: - return { + mapped = { ErdAcFanSetting.AUTO: ErdAcFanSetting.AUTO, ErdAcFanSetting.LOW: ErdAcFanSetting.LOW, ErdAcFanSetting.LOW_AUTO: ErdAcFanSetting.AUTO, @@ -31,9 +30,12 @@ def to_option_string(self, value: Any) -> Optional[str]: ErdAcFanSetting.MED_AUTO: ErdAcFanSetting.AUTO, ErdAcFanSetting.HIGH: ErdAcFanSetting.HIGH, ErdAcFanSetting.HIGH_AUTO: ErdAcFanSetting.HIGH - }.get(value).stringify() - except: - pass + }.get(value) + + if(isinstance(mapped, ErdAcFanSetting)): + return mapped.stringify() + + _LOGGER.warning(f"Could not determine fan mode mapping for {value}") return self._default.stringify() class AcFanOnlyFanModeOptionsConverter(AcFanModeOptionsConverter): diff --git a/custom_components/ge_home/entities/ac/ge_biac_climate.py b/custom_components/ge_home/entities/ac/ge_biac_climate.py index db25033..184248e 100644 --- a/custom_components/ge_home/entities/ac/ge_biac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_biac_climate.py @@ -1,8 +1,9 @@ import logging from typing import Any, List, Optional -from homeassistant.components.climate import HVACMode +from homeassistant.components.climate.const import HVACMode from gehomesdk import ErdAcOperationMode + from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter from .fan_mode_options import AcFanModeOptionsConverter, AcFanOnlyFanModeOptionsConverter @@ -13,27 +14,33 @@ class BiacHvacModeOptionsConverter(OptionsConverter): @property def options(self) -> List[str]: return [HVACMode.AUTO, HVACMode.COOL, HVACMode.FAN_ONLY] + def from_option_string(self, value: str) -> Any: try: + hvac = HVACMode(value.lower()) return { - HVACMode.AUTO: ErdAcOperationMode.ENERGY_SAVER, - HVACMode.COOL: ErdAcOperationMode.COOL, - HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY - }.get(value) - except: + HVACMode.AUTO: ErdAcOperationMode.ENERGY_SAVER, + HVACMode.COOL: ErdAcOperationMode.COOL, + HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY + }.get(hvac) + + except ValueError: _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") return ErdAcOperationMode.COOL + def to_option_string(self, value: Any) -> Optional[str]: - try: - return { + mapped = { ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, ErdAcOperationMode.AUTO: HVACMode.AUTO, ErdAcOperationMode.COOL: HVACMode.COOL, ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) - except: - _LOGGER.warning(f"Could not determine operation mode mapping for {value}") - return HVACMode.COOL + + if(isinstance(mapped, HVACMode)): + return mapped + + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") + return HVACMode.COOL class GeBiacClimate(GeClimate): """Class for Built-In AC units""" diff --git a/custom_components/ge_home/entities/ac/ge_pac_climate.py b/custom_components/ge_home/entities/ac/ge_pac_climate.py index 488b585..d42aad7 100644 --- a/custom_components/ge_home/entities/ac/ge_pac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_pac_climate.py @@ -1,8 +1,10 @@ import logging +from propcache.api import cached_property from typing import Any, List, Optional -from homeassistant.components.climate import HVACMode +from homeassistant.components.climate.const import HVACMode from gehomesdk import ErdCode, ErdAcOperationMode, ErdAcAvailableModes, ErdSacTargetTemperatureRange + from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter from .fan_mode_options import AcFanOnlyFanModeOptionsConverter @@ -10,7 +12,7 @@ _LOGGER = logging.getLogger(__name__) class PacHvacModeOptionsConverter(OptionsConverter): - def __init__(self, available_modes: ErdAcAvailableModes): + def __init__(self, available_modes: Optional[ErdAcAvailableModes] = None): self._available_modes = available_modes @property @@ -20,52 +22,57 @@ def options(self) -> List[str]: modes.append(HVACMode.HEAT) if self._available_modes and self._available_modes.has_dry: modes.append(HVACMode.DRY) - return modes + + return [i.value for i in modes] + def from_option_string(self, value: str) -> Any: try: + hvac = HVACMode(value.lower()) return { HVACMode.COOL: ErdAcOperationMode.COOL, HVACMode.HEAT: ErdAcOperationMode.HEAT, HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY, HVACMode.DRY: ErdAcOperationMode.DRY - }.get(value) - except: + }.get(hvac) + except ValueError: _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") return ErdAcOperationMode.COOL + def to_option_string(self, value: Any) -> Optional[str]: - try: - return { + mapped = { ErdAcOperationMode.COOL: HVACMode.COOL, ErdAcOperationMode.HEAT: HVACMode.HEAT, ErdAcOperationMode.DRY: HVACMode.DRY, ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) - except: - _LOGGER.warning(f"Could not determine operation mode mapping for {value}") - return HVACMode.COOL + + if(isinstance(mapped, HVACMode)): + return mapped + + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") + return HVACMode.COOL class GePacClimate(GeClimate): """Class for Portable AC units""" def __init__(self, api: ApplianceApi): - #initialize the climate control - super().__init__(api, None, AcFanOnlyFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) - #get a couple ERDs that shouldn't change if available - self._modes: ErdAcAvailableModes = self.api.try_get_erd_value(ErdCode.AC_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) + self._modes: ErdAcAvailableModes | None = api.try_get_erd_value(ErdCode.AC_AVAILABLE_MODES) + self._temp_range: ErdSacTargetTemperatureRange | None = api.try_get_erd_value(ErdCode.SAC_TARGET_TEMPERATURE_RANGE) - @property + #initialize the climate control with defaults + super().__init__(api, PacHvacModeOptionsConverter(self._modes), AcFanOnlyFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) + + @cached_property def min_temp(self) -> float: temp = 64 if self._temp_range: temp = self._temp_range.min return self._convert_temp(temp) - @property + @cached_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 + return self._convert_temp(temp) + \ No newline at end of file diff --git a/custom_components/ge_home/entities/ac/ge_sac_climate.py b/custom_components/ge_home/entities/ac/ge_sac_climate.py index fb28105..dbbe943 100644 --- a/custom_components/ge_home/entities/ac/ge_sac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_sac_climate.py @@ -1,8 +1,10 @@ import logging +from propcache.api import cached_property from typing import Any, List, Optional -from homeassistant.components.climate import HVACMode +from homeassistant.components.climate.const import HVACMode from gehomesdk import ErdCode, ErdAcOperationMode, ErdAcAvailableModes, ErdSacTargetTemperatureRange + from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter from .fan_mode_options import AcFanOnlyFanModeOptionsConverter, AcFanModeOptionsConverter @@ -10,7 +12,7 @@ _LOGGER = logging.getLogger(__name__) class SacHvacModeOptionsConverter(OptionsConverter): - def __init__(self, available_modes: ErdAcAvailableModes): + def __init__(self, available_modes: Optional[ErdAcAvailableModes] = None): self._available_modes = available_modes @property @@ -21,22 +23,24 @@ def options(self) -> List[str]: modes.append(HVACMode.AUTO) if self._available_modes and self._available_modes.has_dry: modes.append(HVACMode.DRY) - return modes + return [i.value for i in modes] + def from_option_string(self, value: str) -> Any: try: + hvac = HVACMode(value.lower()) return { HVACMode.AUTO: ErdAcOperationMode.AUTO, HVACMode.COOL: ErdAcOperationMode.COOL, HVACMode.HEAT: ErdAcOperationMode.HEAT, HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY, HVACMode.DRY: ErdAcOperationMode.DRY - }.get(value) - except: + }.get(hvac) + except ValueError: _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") return ErdAcOperationMode.COOL + def to_option_string(self, value: Any) -> Optional[str]: - try: - return { + mapped = { ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, ErdAcOperationMode.AUTO: HVACMode.AUTO, ErdAcOperationMode.COOL: HVACMode.COOL, @@ -44,30 +48,31 @@ def to_option_string(self, value: Any) -> Optional[str]: ErdAcOperationMode.DRY: HVACMode.DRY, ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) - except: - _LOGGER.warning(f"Could not determine operation mode mapping for {value}") - return HVACMode.COOL + + if(isinstance(mapped, HVACMode)): + return mapped + + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") + return HVACMode.COOL class GeSacClimate(GeClimate): """Class for Split AC units""" def __init__(self, api: ApplianceApi): - #initialize the climate control - super().__init__(api, None, AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) - #get a couple ERDs that shouldn't change if available - self._modes: ErdAcAvailableModes = self.api.try_get_erd_value(ErdCode.AC_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) + self._modes: ErdAcAvailableModes | None = api.try_get_erd_value(ErdCode.AC_AVAILABLE_MODES) + self._temp_range: ErdSacTargetTemperatureRange | None = api.try_get_erd_value(ErdCode.SAC_TARGET_TEMPERATURE_RANGE) - @property + #initialize the climate control + super().__init__(api, SacHvacModeOptionsConverter(self._modes), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) + + @cached_property def min_temp(self) -> float: temp = 60 if self._temp_range: temp = self._temp_range.min return self._convert_temp(temp) - @property + @cached_property def max_temp(self) -> float: temp = 86 if self._temp_range: diff --git a/custom_components/ge_home/entities/ac/ge_wac_climate.py b/custom_components/ge_home/entities/ac/ge_wac_climate.py index e8c6979..e240842 100644 --- a/custom_components/ge_home/entities/ac/ge_wac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_wac_climate.py @@ -1,8 +1,9 @@ import logging from typing import Any, List, Optional -from homeassistant.components.climate import HVACMode +from homeassistant.components.climate.const import HVACMode from gehomesdk import ErdAcOperationMode, ErdCode, ErdAcAvailableModes + from ...devices import ApplianceApi from ..common import GeClimate, OptionsConverter from .fan_mode_options import AcFanModeOptionsConverter, AcFanOnlyFanModeOptionsConverter @@ -10,7 +11,7 @@ _LOGGER = logging.getLogger(__name__) class WacHvacModeOptionsConverter(OptionsConverter): - def __init__(self, available_modes: ErdAcAvailableModes): + def __init__(self, available_modes: Optional[ErdAcAvailableModes] = None): self._available_modes = available_modes @property @@ -18,34 +19,41 @@ def options(self) -> List[str]: modes = [HVACMode.AUTO, HVACMode.COOL, HVACMode.FAN_ONLY] if self._available_modes and self._available_modes.has_heat: modes.append(HVACMode.HEAT) - return modes + return [i.value for i in modes] def from_option_string(self, value: str) -> Any: try: + hvac = HVACMode(value.lower()) return { HVACMode.AUTO: ErdAcOperationMode.ENERGY_SAVER, HVACMode.COOL: ErdAcOperationMode.COOL, HVACMode.HEAT: ErdAcOperationMode.HEAT, HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY - }.get(value) - except: + }.get(hvac) + except ValueError: _LOGGER.warning(f"Could not set HVAC mode to {value.upper()}") return ErdAcOperationMode.COOL + def to_option_string(self, value: Any) -> Optional[str]: - try: - return { + mapped = { ErdAcOperationMode.ENERGY_SAVER: HVACMode.AUTO, ErdAcOperationMode.AUTO: HVACMode.AUTO, ErdAcOperationMode.COOL: HVACMode.COOL, ErdAcOperationMode.HEAT: HVACMode.HEAT, ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) - except: - _LOGGER.warning(f"Could not determine operation mode mapping for {value}") - return HVACMode.COOL + + if(isinstance(mapped, HVACMode)): + return mapped + + _LOGGER.warning(f"Could not determine operation mode mapping for {value}") + return HVACMode.COOL class GeWacClimate(GeClimate): """Class for Window AC units""" def __init__(self, api: ApplianceApi): - available_modes = api.try_get_erd_value(ErdCode.AC_AVAILABLE_MODES) - super().__init__(api, WacHvacModeOptionsConverter(available_modes), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) + #get the available modes + self._modes: ErdAcAvailableModes | None = api.try_get_erd_value(ErdCode.AC_AVAILABLE_MODES) + + super().__init__(api, WacHvacModeOptionsConverter(self._modes), AcFanModeOptionsConverter(), AcFanOnlyFanModeOptionsConverter()) + diff --git a/custom_components/ge_home/entities/advantium/ge_advantium.py b/custom_components/ge_home/entities/advantium/ge_advantium.py index 2dee761..8721b3c 100644 --- a/custom_components/ge_home/entities/advantium/ge_advantium.py +++ b/custom_components/ge_home/entities/advantium/ge_advantium.py @@ -1,8 +1,10 @@ """GE Home Sensor Entities - Advantium""" import logging -from typing import Any, Dict, List, Mapping, Optional, Set +from propcache.api import cached_property +from typing import Any, List, Mapping, Optional, cast from random import randrange +from homeassistant.const import ATTR_TEMPERATURE from gehomesdk import ( ErdCode, ErdPersonality, @@ -10,13 +12,13 @@ ErdAdvantiumCookSetting, AdvantiumOperationMode, AdvantiumCookSetting, + AdvantiumCookAction, + AdvantiumCookMode, + AdvantiumWarmStatus, ErdAdvantiumRemoteCookModeConfig, ADVANTIUM_OPERATION_MODE_COOK_SETTING_MAPPING ) -from gehomesdk.erd.values.advantium.advantium_enums import CookAction, CookMode -from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import ATTR_TEMPERATURE from ...const import DOMAIN from ...devices import ApplianceApi from ..common import GeAbstractWaterHeater @@ -27,12 +29,14 @@ class GeAdvantium(GeAbstractWaterHeater): """GE Appliance Advantium""" - icon = "mdi:microwave" - def __init__(self, api: ApplianceApi): super().__init__(api) self._current_operation_mode = None + @property + def icon(self) -> Optional[str]: + return "mdi:microwave" + @property def supported_features(self): if self.remote_enabled: @@ -40,11 +44,11 @@ def supported_features(self): else: return SUPPORT_NONE - @property + @cached_property def unique_id(self) -> str: return f"{DOMAIN}_{self.serial_number}" - @property + @cached_property def name(self) -> Optional[str]: return f"{self.serial_number} Advantium" @@ -62,27 +66,30 @@ def remote_enabled(self) -> bool: return value == True @property - def current_temperature(self) -> Optional[int]: + def current_temperature(self) -> int | None: # type: ignore return self.appliance.get_erd_value(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE) @property - def current_operation(self) -> Optional[str]: + def current_operation(self) -> Optional[str]: # type: ignore + if self.current_operation_mode is None: + return None + try: return self.current_operation_mode.stringify() except: return None - @property + @cached_property def operation_list(self) -> List[str]: invalid = [] if not self._remote_config.broil_enable: - invalid.append(CookMode.BROIL) + invalid.append(AdvantiumCookMode.BROIL) if not self._remote_config.convection_bake_enable: - invalid.append(CookMode.CONVECTION_BAKE) + invalid.append(AdvantiumCookMode.CONVECTION_BAKE) if not self._remote_config.proof_enable: - invalid.append(CookMode.PROOF) + invalid.append(AdvantiumCookMode.PROOF) if not self._remote_config.warm_enable: - invalid.append(CookMode.WARM) + invalid.append(AdvantiumCookMode.WARM) return [ k.stringify() @@ -92,15 +99,15 @@ def operation_list(self) -> List[str]: @property def current_cook_setting(self) -> ErdAdvantiumCookSetting: """Get the current cook setting.""" - return self.appliance.get_erd_value(ErdCode.ADVANTIUM_COOK_SETTING) + return cast(ErdAdvantiumCookSetting, 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) + return cast(ErdAdvantiumCookStatus, self.appliance.get_erd_value(ErdCode.ADVANTIUM_COOK_STATUS)) @property - def current_operation_mode(self) -> AdvantiumOperationMode: + def current_operation_mode(self) -> AdvantiumOperationMode | None: """Gets the current operation mode""" self._ensure_operation_mode() return self._current_operation_mode @@ -118,18 +125,21 @@ def current_operation_setting(self) -> Optional[AdvantiumCookSetting]: @property def can_set_temperature(self) -> bool: """Indicates whether we can set the temperature based on the current mode""" - try: + + if self.current_operation_setting is None: + return False + try: return self.current_operation_setting.allow_temperature_set except: return False @property - def target_temperature(self) -> Optional[int]: + def target_temperature(self) -> int | None: # type: ignore """Return the temperature we try to reach.""" try: cook_mode = self.current_cook_setting if ( - cook_mode.cook_mode != CookMode.NO_MODE and + cook_mode.cook_mode != AdvantiumCookMode.NO_MODE and cook_mode.target_temperature and cook_mode.target_temperature > 0 ): @@ -141,17 +151,17 @@ def target_temperature(self) -> Optional[int]: @property def min_temp(self) -> int: """Return the minimum temperature.""" - min_temp, max_temp = self.appliance.get_erd_value(ErdCode.OVEN_MODE_MIN_MAX_TEMP) + 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.""" - min_temp, max_temp = self.appliance.get_erd_value(ErdCode.OVEN_MODE_MIN_MAX_TEMP) + _, 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]]: + def extra_state_attributes(self) -> Mapping[str, Any] | None: # type: ignore data = {} cook_time_remaining = self.appliance.get_erd_value(ErdCode.ADVANTIUM_COOK_TIME_REMAINING) @@ -189,13 +199,13 @@ async def async_set_operation_mode(self, operation_mode: str): target_temp = max(self.min_temp, min(self.max_temp, int(self.target_temperature))) #by default we will start an operation, but handle other actions too - action = CookAction.START + action = AdvantiumCookAction.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 + action = AdvantiumCookAction.STOP + elif self.current_cook_setting.cook_action == AdvantiumCookAction.PAUSE: + action = AdvantiumCookAction.RESUME + elif self.current_cook_setting.cook_action in [AdvantiumCookAction.START, AdvantiumCookAction.RESUME]: + action = AdvantiumCookAction.UPDATED #construct the new mode based on the existing mode new_cook_mode = ErdAdvantiumCookSetting( @@ -204,7 +214,7 @@ async def async_set_operation_mode(self, operation_mode: str): cook_mode=setting.cook_mode, target_temperature=target_temp or 0, power_level=setting.target_power_level or 0, - warm_status=setting.warm_status or 0, + warm_status=setting.warm_status or AdvantiumWarmStatus.OFF, ) _LOGGER.debug("New ErdAdvantiumCookSetting: %s", new_cook_mode) @@ -230,7 +240,7 @@ async def async_set_temperature(self, **kwargs): return #should only need to update - action = CookAction.UPDATED + action = AdvantiumCookAction.UPDATED #construct the new mode based on the existing mode current_cook_mode = self.current_cook_setting @@ -255,7 +265,7 @@ def _ensure_operation_mode(self): self._current_operation_mode = None #synchronize the operation mode with the device state - if cook_mode == CookMode.MICROWAVE: + if cook_mode == AdvantiumCookMode.MICROWAVE: #microwave matches on cook mode and power level if cook_status.power_level == 3: self._current_operation_mode = AdvantiumOperationMode.MICROWAVE_PL3 @@ -265,7 +275,7 @@ def _ensure_operation_mode(self): self._current_operation_mode = AdvantiumOperationMode.MICROWAVE_PL7 else: self._current_operation_mode = AdvantiumOperationMode.MICROWAVE_PL10 - elif cook_mode == CookMode.WARM: + elif cook_mode == AdvantiumCookMode.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 @@ -285,11 +295,11 @@ def _ensure_operation_mode(self): _LOGGER.debug("Operation mode is set to %s", self._current_operation_mode) return - def _convert_target_temperature(self, temp_120v: int, temp_240v: int): + def _convert_target_temperature(self, temp_120v: Optional[int], temp_240v: Optional[int]): unit_type = self.personality target_temp_f = temp_240v if unit_type in [ErdPersonality.PERSONALITY_240V_MONOGRAM, ErdPersonality.PERSONALITY_240V_CAFE, ErdPersonality.PERSONALITY_240V_STANDALONE_CAFE] else temp_120v return target_temp_f - async def async_device_update(self, warning: bool) -> None: + async def async_device_update(self, warning: bool = True) -> None: await super().async_device_update(warning=warning) self._ensure_operation_mode() diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py index 5792f12..693d0eb 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_cups.py @@ -1,11 +1,14 @@ +from homeassistant.const import EntityCategory +from homeassistant.components.number import NumberMode 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") + GeErdNumber.__init__(self, api = api, erd_code = ErdCode.CCM_BREW_CUPS, min_value=1, max_value=10, mode=NumberMode.BOX, entity_category=EntityCategory.DIAGNOSTIC) GeCcmCachedValue.__init__(self) self._set_value = None @@ -15,5 +18,5 @@ async def async_set_native_value(self, value): self.schedule_update_ha_state() @property - def native_value(self): - return self.get_value(device_value = super().native_value) + def native_value(self) -> int: # type: ignore + return int(self.get_value(device_value = super().native_value) or 0.0) diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_settings.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_settings.py index 121392b..bb6a5d5 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_settings.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_settings.py @@ -9,5 +9,8 @@ def __init__(self, api: ApplianceApi): async def async_press(self) -> None: """Handle the button press.""" + from ...devices import CcmApi + # Forward the call up to the Coffee Maker device to handle - await self.api.start_brewing() \ No newline at end of file + if isinstance(self.api, CcmApi): + await self.api.start_brewing() \ No newline at end of file diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_strength.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_strength.py index a1d2395..34da572 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_strength.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_strength.py @@ -1,6 +1,7 @@ import logging from typing import List, Any, Optional +from homeassistant.const import EntityCategory from gehomesdk import ErdCode, ErdCcmBrewStrength from ...devices import ApplianceApi from ..common import GeErdSelect, OptionsConverter @@ -8,9 +9,11 @@ _LOGGER = logging.getLogger(__name__) +DEFAULT_BREW_STRENGTH = ErdCcmBrewStrength.MEDIUM + class GeCcmBrewStrengthOptionsConverter(OptionsConverter): def __init__(self): - self._default = ErdCcmBrewStrength.MEDIUM + self._default = DEFAULT_BREW_STRENGTH @property def options(self) -> List[str]: @@ -31,17 +34,17 @@ def to_option_string(self, value: ErdCcmBrewStrength) -> Optional[str]: class GeCcmBrewStrengthSelect(GeErdSelect, GeCcmCachedValue): def __init__(self, api: ApplianceApi): - GeErdSelect.__init__(self, api = api, erd_code = ErdCode.CCM_BREW_STRENGTH, converter = GeCcmBrewStrengthOptionsConverter()) + GeErdSelect.__init__(self, api = api, erd_code = ErdCode.CCM_BREW_STRENGTH, converter = GeCcmBrewStrengthOptionsConverter(), entity_category=EntityCategory.CONFIG) GeCcmCachedValue.__init__(self) @property def brew_strength(self) -> ErdCcmBrewStrength: - return self._converter.from_option_string(self.current_option) + return self._converter.from_option_string(self.current_option or DEFAULT_BREW_STRENGTH.name) - async def async_select_option(self, value): - GeCcmCachedValue.set_value(self, value) + async def async_select_option(self, option): + GeCcmCachedValue.set_value(self, option) self.schedule_update_ha_state() @property - def current_option(self): + def current_option(self) -> str | None: # type: ignore return self.get_value(device_value = super().current_option) \ No newline at end of file diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py index 81b13a4..da8eae7 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py @@ -1,4 +1,7 @@ +from homeassistant.const import EntityCategory +from homeassistant.components.number import NumberMode from gehomesdk import ErdCode + from ...devices import ApplianceApi from ..common import GeErdNumber from .ge_ccm_cached_value import GeCcmCachedValue @@ -14,7 +17,7 @@ def __init__(self, api: ApplianceApi): min_temp = DEFAULT_MIN_TEMP max_temp = DEFAULT_MAX_TEMP - GeErdNumber.__init__(self, api = api, erd_code = ErdCode.CCM_BREW_TEMPERATURE, min_value=min_temp, max_value=max_temp, mode="slider") + GeErdNumber.__init__(self, api = api, erd_code = ErdCode.CCM_BREW_TEMPERATURE, min_value=min_temp, max_value=max_temp, mode=NumberMode.SLIDER, entity_category=EntityCategory.DIAGNOSTIC) GeCcmCachedValue.__init__(self) async def async_set_native_value(self, value): @@ -22,5 +25,5 @@ async def async_set_native_value(self, value): self.schedule_update_ha_state() @property - def native_value(self): - return int(self.get_value(device_value = super().native_value)) + def native_value(self) -> int: # type: ignore + return int(self.get_value(device_value = super().native_value) or self.min_value) diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_cached_value.py b/custom_components/ge_home/entities/ccm/ge_ccm_cached_value.py index 95c2b94..32bd68c 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_cached_value.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_cached_value.py @@ -16,5 +16,5 @@ def get_value(self, device_value): return device_value - def set_value(self, set_value): - self._set_value = set_value \ No newline at end of file + def set_value(self, value): + self._set_value = value \ No newline at end of file diff --git a/custom_components/ge_home/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py b/custom_components/ge_home/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py index 124914a..5ee1355 100644 --- a/custom_components/ge_home/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py +++ b/custom_components/ge_home/entities/ccm/ge_ccm_pot_not_present_binary_sensor.py @@ -2,7 +2,7 @@ class GeCcmPotNotPresentBinarySensor(GeErdBinarySensor): @property - def is_on(self) -> bool: + def is_on(self) -> bool: # type: ignore """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_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 91b2bfa..546ef73 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -3,7 +3,6 @@ from .ge_entity import GeEntity from .ge_erd_entity import GeErdEntity from .ge_erd_binary_sensor import GeErdBinarySensor -from .ge_erd_binary_sensor_switch import GeErdBinarySensorSwitch from .ge_erd_property_binary_sensor import GeErdPropertyBinarySensor from .ge_erd_sensor import GeErdSensor from .ge_erd_light import GeErdLight diff --git a/custom_components/ge_home/entities/common/bool_converter.py b/custom_components/ge_home/entities/common/bool_converter.py index b8dcd14..d1dc9c4 100644 --- a/custom_components/ge_home/entities/common/bool_converter.py +++ b/custom_components/ge_home/entities/common/bool_converter.py @@ -12,7 +12,7 @@ def false_value(self) -> Any: class ErdOnOffBoolConverter(BoolConverter): def boolify(self, value: ErdOnOff) -> bool: - return value.boolify() + return value.boolify() or False def true_value(self) -> Any: return ErdOnOff.ON def false_value(self) -> Any: diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 1aeff42..990738a 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -1,14 +1,15 @@ import logging +from propcache.api import cached_property from typing import List, Optional from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode from homeassistant.const import ( ATTR_TEMPERATURE, UnitOfTemperature, ) -from homeassistant.components.climate import ClimateEntityFeature, HVACMode -from homeassistant.components.water_heater import WaterHeaterEntityFeature from gehomesdk import ErdCode, ErdCodeType, ErdMeasurementUnits, ErdOnOff + from ...const import DOMAIN from ...devices import ApplianceApi from .ge_erd_entity import GeEntity @@ -31,7 +32,7 @@ def __init__( api: ApplianceApi, hvac_mode_converter: OptionsConverter, fan_mode_converter: OptionsConverter, - fan_only_fan_mode_converter: OptionsConverter = None, + fan_only_fan_mode_converter: Optional[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, @@ -54,14 +55,22 @@ def __init__( self._fan_mode_erd_code = api.appliance.translate_erd_code(fan_mode_erd_code) self._target_heating_temperature_erd_code = api.appliance.translate_erd_code(target_heating_temperature_erd_code) - @property + @cached_property def unique_id(self) -> str: return f"{DOMAIN}_{self.serial_or_mac}_climate" - @property + @cached_property def name(self) -> Optional[str]: return f"{self.serial_or_mac} Climate" + @property + def icon(self) ->str | None: # type: ignore + return self._get_icon() + + @property + def available(self) -> bool: # type: ignore + return self.api.available + @property def power_status_erd_code(self): return self._power_status_erd_code @@ -84,7 +93,7 @@ def hvac_mode_erd_code(self): def fan_mode_erd_code(self): return self._fan_mode_erd_code - @property + @cached_property def temperature_unit(self): #appears to always be Fahrenheit internally, hardcode this return UnitOfTemperature.FAHRENHEIT @@ -93,7 +102,7 @@ def temperature_unit(self): # return UnitOfTemperature.CELSIUS #return UnitOfTempterature.FAHRENHEIT - @property + @cached_property def supported_features(self): return GE_CLIMATE_SUPPORT @@ -102,7 +111,7 @@ 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]: + def target_temperature(self) -> float | None: # type: ignore measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) if measurement_system == ErdMeasurementUnits.METRIC: targ = float(self.appliance.get_erd_value(self.target_temperature_erd_code)) @@ -111,7 +120,7 @@ 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]: + def current_temperature(self) -> float | None: # type: ignore measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) if measurement_system == ErdMeasurementUnits.METRIC: current = float(self.appliance.get_erd_value(self.current_temperature_erd_code)) @@ -119,32 +128,35 @@ def current_temperature(self) -> Optional[float]: return (9 * current) / 5 + 32 return float(self.appliance.get_erd_value(self.current_temperature_erd_code)) - @property + @cached_property def min_temp(self) -> float: return self._convert_temp(64) - @property + @cached_property def max_temp(self) -> float: return self._convert_temp(86) @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode | None: # type: ignore if not self.is_on: - return HVACMode.OFF + return HVACMode.OFF + try: + hm = self._hvac_mode_converter.to_option_string(self.appliance.get_erd_value(self.hvac_mode_erd_code)) + return HVACMode(hm) + except: + return None - return self._hvac_mode_converter.to_option_string(self.appliance.get_erd_value(self.hvac_mode_erd_code)) + @cached_property + def hvac_modes(self) -> List[HVACMode]: + return [HVACMode.OFF] + [HVACMode(m) for m in self._hvac_mode_converter.options] @property - def hvac_modes(self) -> List[str]: - return [HVACMode.OFF] + self._hvac_mode_converter.options - - @property - def fan_mode(self): + def fan_mode(self) -> str | None: # type: ignore if self.hvac_mode == HVACMode.FAN_ONLY: return self._fan_only_fan_mode_converter.to_option_string(self.appliance.get_erd_value(self.fan_mode_erd_code)) return self._fan_mode_converter.to_option_string(self.appliance.get_erd_value(self.fan_mode_erd_code)) - @property + @cached_property def fan_modes(self) -> List[str]: if self.hvac_mode == HVACMode.FAN_ONLY: return self._fan_only_fan_mode_converter.options diff --git a/custom_components/ge_home/entities/common/ge_entity.py b/custom_components/ge_home/entities/common/ge_entity.py index 6104d20..1835bff 100644 --- a/custom_components/ge_home/entities/common/ge_entity.py +++ b/custom_components/ge_home/entities/common/ge_entity.py @@ -1,5 +1,8 @@ from datetime import timedelta -from typing import Optional, Dict, Any +from propcache.api import cached_property +from typing import Optional, Any + +from homeassistant.helpers.device_registry import DeviceInfo from gehomesdk import GeAppliance from ...devices import ApplianceApi @@ -12,16 +15,16 @@ def __init__(self, api: ApplianceApi): self._api = api self._added = False - @property - def unique_id(self) -> str: + @cached_property + def unique_id(self) -> str | None: raise NotImplementedError @property def api(self) -> ApplianceApi: return self._api - @property - def device_info(self) -> Optional[Dict[str, Any]]: + @cached_property + def device_info(self) -> DeviceInfo | None: return self.api.device_info @property @@ -44,15 +47,15 @@ def mac_addr(self) -> str: def serial_or_mac(self) -> str: return self.api.serial_or_mac - @property + @cached_property def name(self) -> Optional[str]: raise NotImplementedError @property - def icon(self) -> Optional[str]: + def icon(self) ->str | None: # type: ignore return self._get_icon() - @property + @cached_property def device_class(self) -> Optional[str]: return self._get_device_class() @@ -68,14 +71,14 @@ async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" self._added = False - def _stringify(self, value: any, **kwargs) -> Optional[str]: + def _stringify(self, value: Any, **kwargs) -> Optional[str]: if isinstance(value, timedelta): return str(value)[:-3] if value else "" if value is None: return None return self.appliance.stringify_erd_value(value, **kwargs) - def _boolify(self, value: any) -> Optional[bool]: + def _boolify(self, value: Any) -> Optional[bool]: return self.appliance.boolify_erd_value(value) def _get_icon(self) -> Optional[str]: diff --git a/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py b/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py index 55afc01..cc21fae 100644 --- a/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_binary_sensor.py @@ -1,23 +1,56 @@ +from propcache.api import cached_property from typing import Optional -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.const import EntityCategory +from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorDeviceClass +from gehomesdk import ErdCodeType, ErdCodeClass -from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass from ...devices import ApplianceApi from .ge_erd_entity import GeErdEntity class GeErdBinarySensor(GeErdEntity, BinarySensorEntity): - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None, icon_on_override: str = None, icon_off_override: str = None, device_class_override: str = None): - super().__init__(api, erd_code, erd_override=erd_override, icon_override=icon_on_override, device_class_override=device_class_override) + """GE Entity for binary sensors""" + + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_override: Optional[str] = None, + icon_on_override: Optional[str] = None, + icon_off_override: Optional[str] = None, + device_class_override: Optional[str] = None, + entity_category: Optional[EntityCategory] = None + ): + super().__init__(api, erd_code, erd_override, icon_on_override, device_class_override, entity_category) self._icon_on_override = icon_on_override self._icon_off_override = icon_off_override - """GE Entity for binary sensors""" @property - def is_on(self) -> bool: + def icon(self) ->str | None: # type: ignore + return super().icon + + @property + def available(self) -> bool: # type: ignore + return super().available + + @property + def is_on(self) -> bool | None: # type: ignore """Return True if entity is on.""" return self._boolify(self.appliance.get_erd_value(self.erd_code)) + + @cached_property + def device_class(self) -> BinarySensorDeviceClass | None: + # Use GeEntity’s logic, but adapt to HA’s BinarySensorDeviceClass expectations + dc = super(GeErdEntity, self).device_class # call GeEntity version + + if isinstance(dc, str): + try: + return BinarySensorDeviceClass(dc) + except ValueError: + return None + + return dc def _get_icon(self): if self._icon_on_override and self.is_on: diff --git a/custom_components/ge_home/entities/common/ge_erd_binary_sensor_switch.py b/custom_components/ge_home/entities/common/ge_erd_binary_sensor_switch.py deleted file mode 100644 index b7596aa..0000000 --- a/custom_components/ge_home/entities/common/ge_erd_binary_sensor_switch.py +++ /dev/null @@ -1,37 +0,0 @@ -import logging - -from homeassistant.components.switch import SwitchEntity - -from gehomesdk import ErdCodeType -from ...devices import ApplianceApi -from .ge_erd_binary_sensor import GeErdBinarySensor -from .bool_converter import BoolConverter - -_LOGGER = logging.getLogger(__name__) - -class GeErdBinarySensorSwitch(GeErdBinarySensor, SwitchEntity): - """Switch that uses separate ERD codes for reading state and writing control.""" - device_class = "switch" - - def __init__(self, api: ApplianceApi, state_erd_code: ErdCodeType, control_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): - # Use the state ERD code for the base initialization (for entity ID, etc.) - super().__init__(api, state_erd_code, erd_override, icon_on_override, icon_off_override, device_class_override) - self._state_erd_code = state_erd_code - self._control_erd_code = control_erd_code - self._converter = bool_converter - - @property - def is_on(self) -> bool: - """Return True if switch is on based on state ERD code.""" - return self._converter.boolify(self.appliance.get_erd_value(self._state_erd_code)) - - - async def async_turn_on(self, **kwargs): - """Turn the switch on using control ERD code.""" - _LOGGER.debug(f"Turning on {self.unique_id}") - await self.appliance.async_set_erd_value(self._control_erd_code, self._converter.true_value()) - - async def async_turn_off(self, **kwargs): - """Turn the switch off using control ERD code.""" - _LOGGER.debug(f"Turning off {self.unique_id}") - await self.appliance.async_set_erd_value(self._control_erd_code, self._converter.false_value()) diff --git a/custom_components/ge_home/entities/common/ge_erd_button.py b/custom_components/ge_home/entities/common/ge_erd_button.py index ef28295..775aa6c 100644 --- a/custom_components/ge_home/entities/common/ge_erd_button.py +++ b/custom_components/ge_home/entities/common/ge_erd_button.py @@ -1,6 +1,8 @@ +from propcache.api import cached_property from typing import Optional -from homeassistant.components.button import ButtonEntity +from homeassistant.const import EntityCategory +from homeassistant.components.button import ButtonEntity, ButtonDeviceClass from gehomesdk import ErdCodeType from ...devices import ApplianceApi @@ -8,10 +10,32 @@ 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""" + + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: Optional[str] = None, entity_category: Optional[EntityCategory] = None): + super().__init__(api, erd_code, erd_override=erd_override, entity_category=entity_category) + + @property + def icon(self) ->str | None: # type: ignore + return super().icon + + @property + def available(self) -> bool: # type: ignore + return super().available + + @cached_property + def device_class(self) -> ButtonDeviceClass | None: + # Use GeEntity’s logic, but adapt to HA’s ButtonDeviceClass expectations + dc = super(GeErdEntity, self).device_class # call GeEntity version + + if isinstance(dc, str): + try: + return ButtonDeviceClass(dc) + except ValueError: + return None + + return dc + 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_home/entities/common/ge_erd_entity.py b/custom_components/ge_home/entities/common/ge_erd_entity.py index 6763253..34df9e6 100644 --- a/custom_components/ge_home/entities/common/ge_erd_entity.py +++ b/custom_components/ge_home/entities/common/ge_erd_entity.py @@ -1,6 +1,8 @@ from datetime import timedelta -from typing import Optional +from propcache.api import cached_property +from typing import Optional, Any +from homeassistant.const import EntityCategory from gehomesdk import ErdCode, ErdCodeType, ErdCodeClass, ErdMeasurementUnits from ...const import DOMAIN @@ -15,9 +17,10 @@ def __init__( self, api: ApplianceApi, erd_code: ErdCodeType, - erd_override: str = None, - icon_override: str = None, - device_class_override: str = None, + erd_override: Optional[str] = None, + icon_override: Optional[str] = None, + device_class_override: Optional[str] = None, + entity_category: Optional[EntityCategory] = None ): super().__init__(api) self._erd_code = api.appliance.translate_erd_code(erd_code) @@ -25,6 +28,7 @@ def __init__( self._erd_override = erd_override self._icon_override = icon_override self._device_class_override = device_class_override + self._attr_entity_category = entity_category if not self._erd_code_class: self._erd_code_class = ErdCodeClass.GENERAL @@ -40,11 +44,11 @@ def erd_code_class(self) -> ErdCodeClass: @property def erd_string(self) -> str: erd_code = self.erd_code - if isinstance(self.erd_code, ErdCode): + if isinstance(erd_code, ErdCode): return erd_code.name - return erd_code + return str(erd_code) - @property + @cached_property def name(self) -> Optional[str]: erd_string = self.erd_string @@ -55,11 +59,11 @@ def name(self) -> Optional[str]: erd_title = " ".join(erd_string.split("_")).title() return f"{self.serial_or_mac} {erd_title}" - @property + @cached_property def unique_id(self) -> Optional[str]: return f"{DOMAIN}_{self.serial_or_mac}_{self.erd_string.lower()}" - def _stringify(self, value: any, **kwargs) -> Optional[str]: + def _stringify(self, value: Any, **kwargs) -> Optional[str]: """Stringify a value""" # perform special processing before passing over to the default method if self.erd_code == ErdCode.CLOCK_TIME: @@ -83,7 +87,7 @@ def _measurement_system(self) -> Optional[ErdMeasurementUnits]: try: value = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) except KeyError: - return ErdMeasurementUnits.Imperial + return ErdMeasurementUnits.IMPERIAL return value def _get_icon(self): diff --git a/custom_components/ge_home/entities/common/ge_erd_light.py b/custom_components/ge_home/entities/common/ge_erd_light.py index 35b0422..cad9380 100644 --- a/custom_components/ge_home/entities/common/ge_erd_light.py +++ b/custom_components/ge_home/entities/common/ge_erd_light.py @@ -1,18 +1,22 @@ import logging +from propcache.api import cached_property +from typing import Optional -from gehomesdk import ErdCodeType from homeassistant.components.light import ( - ColorMode, ATTR_BRIGHTNESS, LightEntity ) +from homeassistant.components.light.const import ( + ColorMode +) +from homeassistant.const import EntityCategory +from gehomesdk import ErdCodeType 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)) @@ -24,30 +28,35 @@ def to_hass_level(level): class GeErdLight(GeErdEntity, LightEntity): """Lights for ERD codes.""" - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None, color_mode = ColorMode.BRIGHTNESS): - super().__init__(api, erd_code, erd_override) + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: Optional[str] = None, color_mode: ColorMode = ColorMode.BRIGHTNESS, entity_category: Optional[EntityCategory] = None): + super().__init__(api, erd_code, erd_override, entity_category=entity_category) self._color_mode = color_mode @property - def supported_color_modes(self): + def icon(self) ->str | None: # type: ignore + return super().icon + + @property + def available(self) -> bool: # type: ignore + return super().available + + @cached_property + def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" - return ColorMode.BRIGHTNESS + return set([ColorMode.BRIGHTNESS]) @property - def color_mode(self): + def color_mode(self) -> ColorMode: # type: ignore """Return the color mode of the light.""" return self._color_mode @property - def brightness(self): + def brightness(self): # type: ignore """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: + def is_on(self) -> bool: # type: ignore """Return True if light is on.""" return self.appliance.get_erd_value(self.erd_code) > 0 @@ -62,3 +71,7 @@ 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) + + async def _set_brightness(self, brightness, **kwargs): + await self.appliance.async_set_erd_value(self.erd_code, to_ge_level(brightness)) + diff --git a/custom_components/ge_home/entities/common/ge_erd_number.py b/custom_components/ge_home/entities/common/ge_erd_number.py index 91026dd..76d4103 100644 --- a/custom_components/ge_home/entities/common/ge_erd_number.py +++ b/custom_components/ge_home/entities/common/ge_erd_number.py @@ -1,12 +1,15 @@ import logging +from propcache.api import cached_property from typing import Optional -from gehomesdk.erd.erd_data_type import ErdDataType + from homeassistant.components.number import ( NumberEntity, + NumberMode, NumberDeviceClass, ) -from homeassistant.const import UnitOfTemperature -from gehomesdk import ErdCodeType, ErdCodeClass +from homeassistant.const import UnitOfTemperature, EntityCategory +from gehomesdk import ErdCodeType, ErdCodeClass, ErdDataType + from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi @@ -19,17 +22,18 @@ 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, + erd_override: Optional[str] = None, + icon_override: Optional[str] = None, + device_class_override: Optional[str] = None, + uom_override: Optional[str] = None, + data_type_override: Optional[ErdDataType] = None, min_value: float = 1, max_value: float = 100, step_value: float = 1, - mode: str = "auto" + mode: NumberMode = NumberMode.AUTO, + entity_category: Optional[EntityCategory] = None ): - super().__init__(api, erd_code, erd_override, icon_override, device_class_override) + super().__init__(api, erd_code, erd_override, icon_override, device_class_override, entity_category) self._uom_override = uom_override self._data_type_override = data_type_override self._native_min_value = min_value @@ -38,14 +42,22 @@ def __init__( self._mode = mode @property - def native_value(self): + def icon(self) ->str | None: # type: ignore + return super().icon + + @property + def available(self) -> bool: # type: ignore + return super().available + + @property + def native_value(self) -> float | None: # type: ignore try: value = self.appliance.get_erd_value(self.erd_code) return self._convert_value_from_device(value) except KeyError: return None - @property + @cached_property def native_unit_of_measurement(self) -> Optional[str]: return self._get_uom() @@ -56,29 +68,44 @@ def _data_type(self) -> ErdDataType: return self.appliance.get_erd_code_data_type(self.erd_code) - @property + @cached_property def native_min_value(self) -> float: - return self._convert_value_from_device(self._native_min_value) + return self._native_min_value - @property + @cached_property def native_max_value(self) -> float: - return self._convert_value_from_device(self._native_max_value) + return self._native_max_value - @property + @cached_property def native_step(self) -> float: return self._native_step - @property - def mode(self) -> float: + @cached_property + def mode(self) -> NumberMode: return self._mode + + @cached_property + def device_class(self) -> NumberDeviceClass | None: + # Use GeEntity’s logic, but adapt to HA’s NumberDeviceClass expectations + dc = super(GeErdEntity, self).device_class # call GeEntity version - def _convert_value_from_device(self, value): - """Convert to expected data type""" + if isinstance(dc, str): + try: + return NumberDeviceClass(dc) + except ValueError: + return None - if self._data_type == ErdDataType.INT: - return int(round(value)) - else: - return value + return dc + + def _convert_value_from_device(self, value) -> float | None: + """Convert to expected data type""" + try: + if self._data_type == ErdDataType.INT: + return float(round(value)) + else: + return float(value) + except: + return None def _get_uom(self): """Select appropriate units""" @@ -107,14 +134,6 @@ def _get_device_class(self) -> Optional[str]: 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""" diff --git a/custom_components/ge_home/entities/common/ge_erd_property_binary_sensor.py b/custom_components/ge_home/entities/common/ge_erd_property_binary_sensor.py index d7504ce..bc5411d 100644 --- a/custom_components/ge_home/entities/common/ge_erd_property_binary_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_property_binary_sensor.py @@ -1,32 +1,44 @@ +from propcache.api import cached_property from typing import Optional import magicattr +from homeassistant.const import EntityCategory from gehomesdk import ErdCodeType + from ...devices import ApplianceApi from .ge_erd_binary_sensor import GeErdBinarySensor class GeErdPropertyBinarySensor(GeErdBinarySensor): """GE Entity for property binary sensors""" - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_property: str, erd_override: str = None, icon_on_override: str = None, icon_off_override: str = None, device_class_override: str = None): - super().__init__(api, erd_code, erd_override, icon_on_override, icon_off_override, device_class_override) + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_property: str, erd_override: Optional[str] = None, + icon_on_override: Optional[str] = None, + icon_off_override: Optional[str] = None, + device_class_override: Optional[str] = None, + entity_category: Optional[EntityCategory] = None + ): + super().__init__(api, erd_code, erd_override, icon_on_override, icon_off_override, device_class_override, entity_category) self.erd_property = erd_property self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") - @property - def is_on(self) -> Optional[bool]: - """Return True if entity is on.""" - try: - value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) - except KeyError: - return None - return self._boolify(value) - - @property + @cached_property def unique_id(self) -> Optional[str]: return f"{super().unique_id}_{self._erd_property_cleansed}" - @property + @cached_property def name(self) -> Optional[str]: base_string = super().name property_name = self._erd_property_cleansed.replace("_", " ").title() return f"{base_string} {property_name}" + + @property + def is_on(self) -> Optional[bool]: + """Return True if entity is on.""" + try: + value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) + except KeyError: + return None + return self._boolify(value) \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_property_sensor.py b/custom_components/ge_home/entities/common/ge_erd_property_sensor.py index 70938d0..2e136fb 100644 --- a/custom_components/ge_home/entities/common/ge_erd_property_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_property_sensor.py @@ -1,7 +1,10 @@ +from propcache.api import cached_property from typing import Optional - import magicattr -from gehomesdk import ErdCode, ErdCodeType, ErdMeasurementUnits, ErdDataType + +from homeassistant.const import EntityCategory +from gehomesdk import ErdCodeType, ErdDataType + from ...devices import ApplianceApi from .ge_erd_sensor import GeErdSensor @@ -9,32 +12,44 @@ 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, + erd_override: Optional[str] = None, + icon_override: Optional[str] = None, + device_class_override: Optional[str] = None, + state_class_override: Optional[str] = None, + uom_override: Optional[str] = None, + data_type_override: Optional[ErdDataType] = None, + entity_category: Optional[EntityCategory] = None ): super().__init__( - api, erd_code, erd_override=erd_override, - icon_override=icon_override, device_class_override=device_class_override, + 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 + data_type_override=data_type_override, + entity_category=entity_category ) self.erd_property = erd_property self._erd_property_cleansed = erd_property.replace(".","_").replace("[","_").replace("]","_") - @property + @cached_property def unique_id(self) -> Optional[str]: return f"{super().unique_id}_{self._erd_property_cleansed}" - @property + @cached_property def name(self) -> Optional[str]: base_string = super().name property_name = self._erd_property_cleansed.replace("_", " ").title() return f"{base_string} {property_name}" @property - def native_value(self): + def native_value(self) -> str | float | int | None: # type: ignore try: value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) diff --git a/custom_components/ge_home/entities/common/ge_erd_select.py b/custom_components/ge_home/entities/common/ge_erd_select.py index 833dea2..a6164a7 100644 --- a/custom_components/ge_home/entities/common/ge_erd_select.py +++ b/custom_components/ge_home/entities/common/ge_erd_select.py @@ -1,7 +1,9 @@ import logging +from propcache.api import cached_property from typing import Any, List, Optional +from homeassistant.const import EntityCategory from homeassistant.components.select import SelectEntity from gehomesdk import ErdCodeType @@ -13,17 +15,34 @@ 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) + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + converter: OptionsConverter, + erd_override: Optional[str] = None, + icon_override: Optional[str] = None, + control_erd_code: Optional[ErdCodeType] = None, + entity_category: Optional[EntityCategory] = None + ): + super().__init__(api, erd_code, erd_override=erd_override, icon_override=icon_override, entity_category=entity_category) self._converter = converter + self._control_erd_code = control_erd_code @property - def current_option(self): - return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) + def icon(self) ->str | None: # type: ignore + return super().icon + + @property + def available(self) -> bool: # type: ignore + return super().available @property + def current_option(self) -> str | None: # type: ignore + return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) + + @cached_property def options(self) -> List[str]: "Return a list of options" return self._converter.options @@ -32,4 +51,11 @@ 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)) + await self.appliance.async_set_erd_value(self._writeable_erd_code, self._converter.from_option_string(option)) + + @property + def _writeable_erd_code(self) -> ErdCodeType: + if self._control_erd_code: + return self._control_erd_code + + return self.erd_code \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index 8466dd9..1f4f009 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -1,10 +1,11 @@ import logging +from propcache.api import cached_property from typing import Optional -from gehomesdk.erd.erd_data_type import ErdDataType + from homeassistant.components.sensor import SensorDeviceClass, SensorEntity, SensorStateClass +from homeassistant.const import UnitOfTemperature, EntityCategory +from gehomesdk import ErdCodeType, ErdCodeClass, ErdDataType -from homeassistant.const import UnitOfTemperature -from gehomesdk import ErdCodeType, ErdCodeClass from .ge_erd_entity import GeErdEntity from ...devices import ApplianceApi @@ -17,20 +18,29 @@ 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 + erd_override: Optional[str] = None, + icon_override: Optional[str] = None, + device_class_override: Optional[str] = None, + state_class_override: Optional[str] = None, + uom_override: Optional[str] = None, + data_type_override: Optional[ErdDataType] = None, + entity_category: Optional[EntityCategory] = None ): - super().__init__(api, erd_code, erd_override, icon_override, device_class_override) + super().__init__(api, erd_code, erd_override, icon_override, device_class_override, entity_category) self._uom_override = uom_override self._state_class_override = state_class_override self._data_type_override = data_type_override - + + @property + def icon(self) ->str | None: # type: ignore + return super().icon + @property - def native_value(self): + def available(self) -> bool: # type: ignore + return super().available + + @property + def native_value(self) -> str | int | float | None: # type: ignore try: value = self.appliance.get_erd_value(self.erd_code) @@ -45,13 +55,26 @@ def native_value(self): except KeyError: return None - @property + @cached_property def native_unit_of_measurement(self) -> Optional[str]: return self._get_uom() - @property + @cached_property def state_class(self) -> Optional[str]: return self._get_state_class() + + @cached_property + def device_class(self) -> SensorDeviceClass | None: + # Use GeEntity’s logic, but adapt to HA’s SensorDeviceClass expectations + dc = super(GeErdEntity, self).device_class # call GeEntity version + + if isinstance(dc, str): + try: + return SensorDeviceClass(dc) + except ValueError: + return None + + return dc @property def _data_type(self) -> ErdDataType: @@ -148,17 +171,9 @@ def _get_state_class(self) -> Optional[str]: return None - def _get_icon(self): - if self.erd_code_class == ErdCodeClass.DOOR: - if self.state.lower().endswith("open"): - return "mdi:door-open" - if self.state.lower().endswith("closed"): - return "mdi:door-closed" - return super()._get_icon() - async def set_value(self, value): """Sets the ERD value, assumes that the data type is correct""" try: await self.appliance.async_set_erd_value(self.erd_code, value) except: - _LOGGER.warning(f"Could not set {self.name} to {value}") \ No newline at end of file + _LOGGER.warning(f"Could not set {self.name} to {value}") diff --git a/custom_components/ge_home/entities/common/ge_erd_switch.py b/custom_components/ge_home/entities/common/ge_erd_switch.py index 0fb3703..8b22234 100644 --- a/custom_components/ge_home/entities/common/ge_erd_switch.py +++ b/custom_components/ge_home/entities/common/ge_erd_switch.py @@ -1,33 +1,95 @@ import logging +from propcache.api import cached_property +from typing import Optional +from homeassistant.const import EntityCategory +from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass from gehomesdk import ErdCodeType -from homeassistant.components.switch import SwitchEntity from ...devices import ApplianceApi -from .ge_erd_binary_sensor import GeErdBinarySensor +from .ge_erd_binary_sensor import GeErdEntity from .bool_converter import BoolConverter _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, + bool_converter: BoolConverter = BoolConverter(), + erd_override: Optional[str] = None, + icon_on_override: Optional[str] = None, + icon_off_override: Optional[str] = None, + device_class_override: Optional[str] = None, + control_erd_code: Optional[ErdCodeType] = None, + entity_category: Optional[EntityCategory] = None + ): + super().__init__(api, erd_code, erd_override, icon_on_override, device_class_override, entity_category) + self._icon_on_override = icon_on_override + self._icon_off_override = icon_off_override self._converter = bool_converter + self._control_erd_code = control_erd_code @property - def is_on(self) -> bool: + def icon(self) ->str | None: # type: ignore + return super().icon + + @property + def available(self) -> bool: # type: ignore + return super().available + + @property + def is_on(self) -> bool: # type: ignore """Return True if switch is on.""" return self._converter.boolify(self.appliance.get_erd_value(self.erd_code)) + + @cached_property + def device_class(self) -> SwitchDeviceClass | None: + dc = self._get_device_class() + if dc is None: + return None + + if isinstance(dc, str): + try: + return SwitchDeviceClass(dc) + except ValueError: + return None + if isinstance(dc, SwitchDeviceClass): + return dc + + return None + async def async_turn_on(self, **kwargs): """Turn the switch on.""" _LOGGER.debug(f"Turning on {self.unique_id}") - await self.appliance.async_set_erd_value(self.erd_code, self._converter.true_value()) + + await self.appliance.async_set_erd_value(self._writeable_erd_code, self._converter.true_value()) async def async_turn_off(self, **kwargs): """Turn the switch off.""" _LOGGER.debug(f"Turning off {self.unique_id}") - await self.appliance.async_set_erd_value(self.erd_code, self._converter.false_value()) + await self.appliance.async_set_erd_value(self._writeable_erd_code, self._converter.false_value()) + + 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 + + return super()._get_icon() + + def _get_device_class(self) -> Optional[str]: + if self._device_class_override: + return self._device_class_override + return None + + @property + def _writeable_erd_code(self) -> ErdCodeType: + if self._control_erd_code: + return self._control_erd_code + + return self.erd_code diff --git a/custom_components/ge_home/entities/common/ge_erd_timer_sensor.py b/custom_components/ge_home/entities/common/ge_erd_timer_sensor.py index 3f5e905..4b33b4e 100644 --- a/custom_components/ge_home/entities/common/ge_erd_timer_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_timer_sensor.py @@ -1,14 +1,7 @@ -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__) @@ -19,7 +12,7 @@ async def set_timer(self, duration: timedelta): try: await self.appliance.async_set_erd_value(self.erd_code, duration) except: - _LOGGER.warning("Could not set timer value", exc_info=1) + _LOGGER.warning("Could not set timer value", exc_info=True) async def clear_timer(self): try: @@ -27,4 +20,4 @@ async def clear_timer(self): #won't turn off... I don't see any way around it though. await self.appliance.async_set_erd_value(self.erd_code, timedelta(seconds=0)) except: - _LOGGER.warning("Could not clear timer value", exc_info=1) + _LOGGER.warning("Could not clear timer value", exc_info=True) diff --git a/custom_components/ge_home/entities/common/ge_humidifier.py b/custom_components/ge_home/entities/common/ge_humidifier.py index e102e3b..c32a1fc 100644 --- a/custom_components/ge_home/entities/common/ge_humidifier.py +++ b/custom_components/ge_home/entities/common/ge_humidifier.py @@ -1,6 +1,7 @@ import abc import logging -from typing import Coroutine, Any, Optional +from propcache.api import cached_property +from typing import Any, Optional from homeassistant.components.humidifier import HumidifierEntity, HumidifierDeviceClass from homeassistant.components.humidifier.const import HumidifierEntityFeature @@ -38,43 +39,51 @@ def __init__( self._range_max = range_max self._target_precision = target_precision - @property + @cached_property def unique_id(self) -> str: return f"{DOMAIN}_{self.serial_or_mac}_{self._device_class}" - @property + @cached_property def name(self) -> Optional[str]: return f"{self.serial_or_mac} {self._device_class.title()}" @property - def target_humidity(self) -> int | None: + def icon(self) ->str | None: # type: ignore + return super().icon + + @property + def available(self) -> bool: # type: ignore + return super().available + + @property + def target_humidity(self) -> int | None: # type: ignore return int(self.appliance.get_erd_value(self._target_humidity_erd_code)) @property - def current_humidity(self) -> int | None: + def current_humidity(self) -> int | None: # type: ignore return int(self.appliance.get_erd_value(self._current_humidity_erd_code)) - @property + @cached_property def min_humidity(self) -> int: return self._range_min - @property + @cached_property def max_humidity(self) -> int: return self._range_max - @property + @cached_property def supported_features(self) -> HumidifierEntityFeature: return HumidifierEntityFeature(HumidifierEntityFeature.MODES) @property - def is_on(self) -> bool: + def is_on(self) -> bool: # type: ignore return self.appliance.get_erd_value(self._power_status_erd_code) == ErdOnOff.ON - @property - def device_class(self): + @cached_property + def device_class(self) -> HumidifierDeviceClass | None: return self._device_class - async def async_set_humidity(self, humidity: int) -> Coroutine[Any, Any, None]: + async def async_set_humidity(self, humidity: int) -> None: # round to target precision target = round(humidity / self._target_precision) * self._target_precision @@ -94,12 +103,12 @@ async def async_set_humidity(self, humidity: int) -> Coroutine[Any, Any, None]: target, ) - async def async_turn_on(self): + async def async_turn_on(self, **kwargs: Any): await self.appliance.async_set_erd_value( self._power_status_erd_code, ErdOnOff.ON ) - async def async_turn_off(self): + async def async_turn_off(self, **kwargs: Any): await self.appliance.async_set_erd_value( self._power_status_erd_code, ErdOnOff.OFF ) \ No newline at end of file diff --git a/custom_components/ge_home/entities/common/ge_water_heater.py b/custom_components/ge_home/entities/common/ge_water_heater.py index 88b376a..1637bb9 100644 --- a/custom_components/ge_home/entities/common/ge_water_heater.py +++ b/custom_components/ge_home/entities/common/ge_water_heater.py @@ -1,5 +1,6 @@ import abc import logging +from propcache.api import cached_property from typing import Any, Dict, List, Optional from homeassistant.components.water_heater import WaterHeaterEntity @@ -13,23 +14,31 @@ class GeAbstractWaterHeater(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): """Mock temperature/operation mode supporting device as a water heater""" + @property + def icon(self) ->str | None: # type: ignore + return super().icon + + @property + def available(self) -> bool: # type: ignore + return super().available + @property def heater_type(self) -> str: raise NotImplementedError - @property + @cached_property def operation_list(self) -> List[str]: raise NotImplementedError - @property + @cached_property def unique_id(self) -> str: return f"{DOMAIN}_{self.serial_or_mac}_{self.heater_type}" - @property + @cached_property def name(self) -> Optional[str]: return f"{self.serial_or_mac} {self.heater_type.title()}" - @property + @cached_property def temperature_unit(self): #It appears that the GE API is alwasy Fehrenheit #measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier.py index 1ed2755..b98f101 100644 --- a/custom_components/ge_home/entities/dehumidifier/dehumidifier.py +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier.py @@ -1,9 +1,10 @@ """GE Home Dehumidifier""" import logging +from propcache.api import cached_property +from typing import Optional from homeassistant.components.humidifier import HumidifierDeviceClass from homeassistant.components.humidifier.const import HumidifierEntityFeature - from gehomesdk import ErdCode, DehumidifierTargetRange from ...devices import ApplianceApi @@ -16,12 +17,10 @@ class GeDehumidifier(GeHumidifier): """GE Dehumidifier""" - icon = "mdi:air-humidifier" - def __init__(self, api: ApplianceApi): #try to get the range - range: DehumidifierTargetRange = api.try_get_erd_value(ErdCode.DHUM_TARGET_HUMIDITY_RANGE) + range: DehumidifierTargetRange | None = api.try_get_erd_value(ErdCode.DHUM_TARGET_HUMIDITY_RANGE) low = DEFAULT_MIN_HUMIDITY if range is None else range.min_humidity high = DEFAULT_MAX_HUMIDITY if range is None else range.max_humidity @@ -41,6 +40,10 @@ def __init__(self, api: ApplianceApi): ) @property + def icon(self) -> str | None: + return "mdi:air-humidifier" + + @cached_property def supported_features(self) -> HumidifierEntityFeature: if self._has_fan: return HumidifierEntityFeature(HumidifierEntityFeature.MODES) @@ -48,7 +51,7 @@ def supported_features(self) -> HumidifierEntityFeature: return HumidifierEntityFeature(0) @property - def mode(self) -> str | None: + def mode(self) -> str | None: # type: ignore if not self._has_fan: raise NotImplementedError() @@ -56,7 +59,7 @@ def mode(self) -> str | None: self.appliance.get_erd_value(ErdCode.AC_FAN_SETTING) ) - @property + @cached_property def available_modes(self) -> list[str] | None: if not self._has_fan: raise NotImplementedError() diff --git a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py index dd8424a..2c4fecf 100644 --- a/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py +++ b/custom_components/ge_home/entities/dehumidifier/dehumidifier_fan_speed_sensor.py @@ -1,19 +1,23 @@ +from typing import Optional + +from homeassistant.const import EntityCategory +from gehomesdk import ErdCodeType, ErdDataType, ErdAcFanSetting + from ...devices import ApplianceApi from ..common import GeErdSensor from .dehumidifier_fan_options import DehumidifierFanSettingOptionsConverter -from gehomesdk import ErdCodeType, ErdCodeClass, ErdDataType, ErdAcFanSetting class GeDehumidifierFanSpeedSensor(GeErdSensor): def __init__( self, api: ApplianceApi, erd_code: ErdCodeType, - erd_override: str = None, - icon_override: str = None, - device_class_override: str = None, - state_class_override: str = None, - uom_override: str = None, - data_type_override: ErdDataType = None + erd_override: Optional[str] = None, + icon_override: Optional[str] = None, + device_class_override: Optional[str] = None, + state_class_override: Optional[str] = None, + uom_override: Optional[str] = None, + data_type_override: Optional[ErdDataType] = None ): super().__init__( @@ -24,17 +28,17 @@ def __init__( device_class_override, state_class_override, uom_override, - data_type_override + data_type_override, + entity_category=EntityCategory.DIAGNOSTIC ) self._converter = DehumidifierFanSettingOptionsConverter() @property - def native_value(self): + def native_value(self) -> str | None: try: value: ErdAcFanSetting = self.appliance.get_erd_value(self.erd_code) return self._converter.to_option_string(value) except KeyError: return None - diff --git a/custom_components/ge_home/entities/dishwasher/__init__.py b/custom_components/ge_home/entities/dishwasher/__init__.py index bef929d..6f03ed7 100644 --- a/custom_components/ge_home/entities/dishwasher/__init__.py +++ b/custom_components/ge_home/entities/dishwasher/__init__.py @@ -1 +1,2 @@ -from .ge_dishwasher_control_locked_switch import GeDishwasherControlLockedSwitch \ No newline at end of file +from .ge_dishwasher_control_locked_switch import GeDishwasherControlLockedSwitch +from .ge_dishwasher_command_button import GeDishwasherCommandButton \ No newline at end of file diff --git a/custom_components/ge_home/entities/dishwasher/ge_dishwasher_command_button.py b/custom_components/ge_home/entities/dishwasher/ge_dishwasher_command_button.py new file mode 100644 index 0000000..8f37bbe --- /dev/null +++ b/custom_components/ge_home/entities/dishwasher/ge_dishwasher_command_button.py @@ -0,0 +1,33 @@ +from propcache.api import cached_property +from typing import Optional + +from gehomesdk import ErdCode, ErdRemoteCommand +from ...devices import ApplianceApi +from ..common import GeErdButton + +class GeDishwasherCommandButton(GeErdButton): + def __init__(self, api: ApplianceApi, erd_code: ErdCode, command: ErdRemoteCommand, erd_override: Optional[str] = None): + super().__init__(api, erd_code=erd_code, erd_override=erd_override) + self._command = command + self._command_cleansed = self._command.name.replace(".","_").replace("[","_").replace("]","_") + + @cached_property + def unique_id(self) -> str | None: + return f"{super().unique_id}_{self._command_cleansed}" + + @cached_property + def name(self) -> Optional[str]: + base_string = super().name + property_name = self._command_cleansed.replace("_", " ").title() + return f"{base_string} {property_name}" + + async def async_press(self) -> None: + """Handle the button press.""" + await self.appliance.async_set_erd_value(self.erd_code, self._command) + + def _get_icon(self) -> Optional[str]: + return { + ErdRemoteCommand.START_RESUME: "mdi:play", + ErdRemoteCommand.CANCEL: "mdi:stop", + ErdRemoteCommand.PAUSE: "mdi:pause" + }.get(self._command) \ No newline at end of file diff --git a/custom_components/ge_home/entities/dishwasher/ge_dishwasher_control_locked_switch.py b/custom_components/ge_home/entities/dishwasher/ge_dishwasher_control_locked_switch.py index 55923d8..7ec2cbe 100644 --- a/custom_components/ge_home/entities/dishwasher/ge_dishwasher_control_locked_switch.py +++ b/custom_components/ge_home/entities/dishwasher/ge_dishwasher_control_locked_switch.py @@ -7,6 +7,6 @@ class GeDishwasherControlLockedSwitch(GeErdSwitch): @property def is_on(self) -> bool: - mode: ErdOperatingMode = self.appliance.get_erd_value(ErdCode.OPERATING_MODE) + mode: ErdOperatingMode = self.appliance.get_erd_value(ErdCode.DISHWASHER_OPERATING_MODE) return mode == ErdOperatingMode.CONTROL_LOCKED diff --git a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py index fbd5cba..88112a3 100644 --- a/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py +++ b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py @@ -1,9 +1,10 @@ import logging from typing import List, Any, Optional -from gehomesdk import ErdConvertableDrawerMode from homeassistant.const import UnitOfTemperature -from homeassistant.util.unit_system import UnitSystem, UnitOfTemperature +from homeassistant.util.unit_system import UnitSystem +from gehomesdk import ErdConvertableDrawerMode + from ..common import OptionsConverter _LOGGER = logging.getLogger(__name__) @@ -27,7 +28,13 @@ def __init__(self, units: UnitSystem): @property def options(self) -> List[str]: - return [self.to_option_string(i) for i in ErdConvertableDrawerMode if i not in self._excluded_options] + return [ + s + for i in ErdConvertableDrawerMode + if i not in self._excluded_options + for s in [self.to_option_string(i)] + if s is not None + ] def from_option_string(self, value: str) -> Any: try: @@ -36,6 +43,7 @@ def from_option_string(self, value: str) -> Any: except: _LOGGER.warning(f"Could not set drawer mode to {value.upper()}") return ErdConvertableDrawerMode.NA + def to_option_string(self, value: ErdConvertableDrawerMode) -> Optional[str]: try: if value is not None: diff --git a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py index a024ca1..107a81f 100644 --- a/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_abstract_fridge.py @@ -1,15 +1,13 @@ """GE Home Sensor Entities - Abstract Fridge""" -import importlib -import sys -import os -import abc import logging +from propcache.api import cached_property from typing import Any, Dict, List, Optional from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.util.unit_conversion import TemperatureConverter from gehomesdk import ( ErdCode, + ErdCodeType, ErdOnOff, ErdFullNotFull, FridgeDoorStatus, @@ -18,7 +16,9 @@ FridgeIceBucketStatus, IceMakerControlStatus ) + from ...const import DOMAIN +from ...devices import ApplianceApi from ..common import GeAbstractWaterHeater from .const import * @@ -27,27 +27,33 @@ 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 + def __init__( + self, + api: ApplianceApi + ): + super().__init__(api) + + # 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. + self.temp_limits = {} + self.temp_limits["fridge_min"] = 32 + self.temp_limits["fridge_max"] = 46 + self.temp_limits["freezer_min"] = -6 + self.temp_limits["freezer_max"] = 7 @property def heater_type(self) -> str: raise NotImplementedError @property - def turbo_erd_code(self) -> str: + def turbo_erd_code(self) -> ErdCodeType: raise NotImplementedError @property def turbo_mode(self) -> str: raise NotImplementedError - @property + @cached_property def operation_list(self) -> List[str]: try: return [OP_MODE_NORMAL, OP_MODE_SABBATH, self.turbo_mode] @@ -55,11 +61,11 @@ def operation_list(self) -> List[str]: _LOGGER.debug("Turbo mode not supported.") return [OP_MODE_NORMAL, OP_MODE_SABBATH] - @property + @cached_property def unique_id(self) -> str: return f"{DOMAIN}_{self.serial_number}_{self.heater_type}" - @property + @cached_property def name(self) -> Optional[str]: return f"{self.serial_or_mac} {self.heater_type.title()}" @@ -69,12 +75,12 @@ def target_temps(self) -> FridgeSetPoints: return self.appliance.get_erd_value(ErdCode.TEMPERATURE_SETTING) @property - def target_temperature(self) -> int: + def target_temperature(self) -> int | None: # type: ignore """Return the temperature we try to reach.""" return getattr(self.target_temps, self.heater_type) @property - def current_temperature(self) -> int: + def current_temperature(self) -> int | None: # type: ignore """Return the current temperature.""" try: current_temps = self.appliance.get_erd_value(ErdCode.CURRENT_TEMPERATURE) @@ -129,7 +135,7 @@ def max_temp(self): return TemperatureConverter.convert(self.temp_limits[f"{self.heater_type}_max"], UnitOfTemperature.FAHRENHEIT, self.temperature_unit) @property - def current_operation(self) -> str: + def current_operation(self) -> str: # type: ignore """Get the current operation mode.""" if self.appliance.get_erd_value(ErdCode.SABBATH_MODE): return OP_MODE_SABBATH @@ -168,14 +174,14 @@ def ice_maker_state_attrs(self) -> Dict[str, Any]: 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}") + erd_imbs: FridgeIceBucketStatus = self.appliance.get_erd_value(ErdCode.ICE_MAKER_BUCKET_STATUS) + ice_bucket_status = getattr(erd_imbs, 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}") + erd_imc: IceMakerControlStatus = self.appliance.get_erd_value(ErdCode.ICE_MAKER_CONTROL) + ice_control_status = getattr(erd_imc, f"status_{self.heater_type}") if ice_control_status != ErdOnOff.NA: data["ice_maker"] = self._stringify(ice_control_status) @@ -192,7 +198,7 @@ def other_state_attrs(self) -> Dict[str, Any]: return {} @property - def extra_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> Dict[str, Any]: # type: ignore door_attrs = self.door_state_attrs ice_maker_attrs = self.ice_maker_state_attrs other_state_attrs = self.other_state_attrs diff --git a/custom_components/ge_home/entities/fridge/ge_dispenser.py b/custom_components/ge_home/entities/fridge/ge_dispenser.py index 394af9e..86fee0d 100644 --- a/custom_components/ge_home/entities/fridge/ge_dispenser.py +++ b/custom_components/ge_home/entities/fridge/ge_dispenser.py @@ -1,7 +1,8 @@ """GE Home Sensor Entities - Dispenser""" import logging -from typing import List, Optional, Dict, Any +from propcache.api import cached_property +from typing import List, Dict, Any from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.util.unit_conversion import TemperatureConverter @@ -15,6 +16,7 @@ HotWaterStatus ) +from ...devices import ApplianceApi from ..common import GeAbstractWaterHeater from .const import ( HEATER_TYPE_DISPENSER, @@ -28,14 +30,26 @@ 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 + def __init__( + self, + api: ApplianceApi + ): + super().__init__(api) + + # 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 + self._min_temp = 90 + self._max_temp = 190 #185 + + @property + def heater_type(self) -> str: + return HEATER_TYPE_DISPENSER + + @property + def icon(self) ->str | None: + return "mdi:cup-water" @property def hot_water_status(self) -> HotWaterStatus: @@ -48,7 +62,7 @@ def supports_k_cups(self) -> bool: status = self.hot_water_status return status.pod_status != ErdPodStatus.NA and status.brew_module != ErdPresent.NA - @property + @cached_property def operation_list(self) -> List[str]: """Supported Operations List""" ops_list = [OP_MODE_NORMAL, OP_MODE_SABBATH] @@ -83,19 +97,19 @@ def supported_features(self): return GE_FRIDGE_SUPPORT @property - def current_operation(self) -> str: + def current_operation(self) -> str: # type: ignore """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]: + def current_temperature(self) -> int | None: # type: ignore """Return the current temperature.""" return self.hot_water_status.current_temp @property - def target_temperature(self) -> Optional[int]: + def target_temperature(self) -> int | None: # type: ignore """Return the target temperature.""" return self.appliance.get_erd_value(ErdCode.HOT_WATER_SET_TEMP) @@ -110,7 +124,7 @@ def max_temp(self): return TemperatureConverter.convert(self._max_temp, UnitOfTemperature.FAHRENHEIT, self.temperature_unit) @property - def extra_state_attributes(self) -> Dict[str, Any]: + def extra_state_attributes(self) -> Dict[str, Any]: # type: ignore data = {} data["target_temperature"] = self.target_temperature diff --git a/custom_components/ge_home/entities/fridge/ge_freezer.py b/custom_components/ge_home/entities/fridge/ge_freezer.py index 005dba9..f89bb31 100644 --- a/custom_components/ge_home/entities/fridge/ge_freezer.py +++ b/custom_components/ge_home/entities/fridge/ge_freezer.py @@ -1,9 +1,10 @@ """GE Home Sensor Entities - Freezer""" import logging -from typing import Any, Dict, Optional +from typing import Any, Dict from gehomesdk import ( ErdCode, + ErdCodeType, ErdDoorStatus ) @@ -19,13 +20,24 @@ 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 heater_type(self) -> str: + return HEATER_TYPE_FREEZER + + @property + def icon(self) -> str | None: + return "mdi:fridge-top" + + @property + def turbo_erd_code(self) -> ErdCodeType: + return ErdCode.TURBO_FREEZE_STATUS + + @property + def turbo_mode(self) -> str: + return OP_MODE_TURBO_FREEZE @property - def door_state_attrs(self) -> Optional[Dict[str, Any]]: + def door_state_attrs(self) -> Dict[str, Any]: try: door_status = self.door_status.freezer if door_status and door_status != ErdDoorStatus.NA: diff --git a/custom_components/ge_home/entities/fridge/ge_fridge.py b/custom_components/ge_home/entities/fridge/ge_fridge.py index e24c3e0..de586dd 100644 --- a/custom_components/ge_home/entities/fridge/ge_fridge.py +++ b/custom_components/ge_home/entities/fridge/ge_fridge.py @@ -4,6 +4,7 @@ from gehomesdk import ( ErdCode, + ErdCodeType, ErdDoorStatus, ErdFilterStatus ) @@ -19,10 +20,23 @@ _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 heater_type(self) -> str: + return HEATER_TYPE_FRIDGE + + @property + def icon(self) -> str | None: + return "mdi:fridge-bottom" + + @property + def turbo_erd_code(self) -> ErdCodeType: + return ErdCode.TURBO_COOL_STATUS + + @property + def turbo_mode(self) -> str: + return OP_MODE_TURBO_COOL @property def other_state_attrs(self) -> Dict[str, Any]: diff --git a/custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py b/custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py index f86c59a..8d997a8 100644 --- a/custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py +++ b/custom_components/ge_home/entities/fridge/ge_fridge_ice_control_switch.py @@ -1,4 +1,5 @@ import logging +from homeassistant.const import EntityCategory from gehomesdk import ErdCode, IceMakerControlStatus, ErdOnOff from ...devices import ApplianceApi @@ -8,7 +9,7 @@ class GeFridgeIceControlSwitch(GeErdSwitch): def __init__(self, api: ApplianceApi, control_type: str): - super().__init__(api, ErdCode.ICE_MAKER_CONTROL, BoolConverter()) + super().__init__(api, ErdCode.ICE_MAKER_CONTROL, BoolConverter(), entity_category=EntityCategory.CONFIG) self._control_type = control_type @property diff --git a/custom_components/ge_home/entities/fridge/ge_kcup_switch.py b/custom_components/ge_home/entities/fridge/ge_kcup_switch.py index e50fd03..13c453c 100644 --- a/custom_components/ge_home/entities/fridge/ge_kcup_switch.py +++ b/custom_components/ge_home/entities/fridge/ge_kcup_switch.py @@ -1,7 +1,7 @@ import logging -from typing import Optional +from propcache.api import cached_property -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass from gehomesdk import ErdCode from ...devices import ApplianceApi @@ -20,23 +20,31 @@ def __init__(self, api: ApplianceApi): # Pass the api instance to the base class super().__init__(api) - @property + @cached_property def unique_id(self) -> str: # Create a unique ID for this entity return f"{self.api.serial_or_mac}_kcup_hot_water" - @property - def name(self) -> Optional[str]: + @cached_property + def name(self) -> str | None: # Set the friendly name to match other switches using the device's unique ID return f"{self.api.serial_or_mac} K-Cup Hot Water" @property - def icon(self) -> Optional[str]: + def icon(self) -> str | None: # type: ignore # Set the icon based on the switch's state return "mdi:coffee-maker" if self.is_on else "mdi:coffee-maker-off-outline" + + @property + def available(self) -> bool: # type: ignore + return super().available + + @cached_property + def device_class(self) -> SwitchDeviceClass | None: + return None @property - def is_on(self) -> bool: + def is_on(self) -> bool: # type: ignore """Return true if the hot water is set to a non-zero temperature.""" try: # The switch is "on" if the target temperature is not the "off" value diff --git a/custom_components/ge_home/entities/hood/ge_hood_fan_speed.py b/custom_components/ge_home/entities/hood/ge_hood_fan_speed.py index 0dcfe73..660aacf 100644 --- a/custom_components/ge_home/entities/hood/ge_hood_fan_speed.py +++ b/custom_components/ge_home/entities/hood/ge_hood_fan_speed.py @@ -41,6 +41,19 @@ def to_option_string(self, value: ErdHoodFanSpeed) -> Optional[str]: 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)) + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, control_erd_code: Optional[ErdCodeType] = None): + + # old-style + self._availability: ErdHoodFanSpeedAvailability | None = api.try_get_erd_value(ErdCode.HOOD_FAN_SPEED_AVAILABILITY) + + # new-style + if self._availability is None: + fs: int | None = api.try_get_erd_value(ErdCode.HOOD_AVAILABLE_FAN_SPEEDS) + if fs is not None: + self._availability = ErdHoodFanSpeedAvailability.from_count(fs) + + # default + if self._availability is None: + self._availability = ErdHoodFanSpeedAvailability(off_available=True) + + super().__init__(api, erd_code, HoodFanSpeedOptionsConverter(self._availability), control_erd_code=control_erd_code) diff --git a/custom_components/ge_home/entities/hood/ge_hood_light_level.py b/custom_components/ge_home/entities/hood/ge_hood_light_level.py index a44dccd..1447d9c 100644 --- a/custom_components/ge_home/entities/hood/ge_hood_light_level.py +++ b/custom_components/ge_home/entities/hood/ge_hood_light_level.py @@ -1,7 +1,7 @@ import logging from typing import List, Any, Optional -from gehomesdk import ErdCodeType, ErdHoodLightLevelAvailability, ErdHoodLightLevel, ErdCode +from gehomesdk import ErdCodeType, ErdHoodLightLevelAvailability, ErdHoodLightLevel, ErdHoodLightLevelNew, ErdCode from ...devices import ApplianceApi from ..common import GeErdSelect, OptionsConverter @@ -16,6 +16,8 @@ def __init__(self, availability: ErdHoodLightLevelAvailability): self.excluded_levels.append(ErdHoodLightLevel.OFF) if not availability.dim_available: self.excluded_levels.append(ErdHoodLightLevel.DIM) + if not availability.med_available: + self.excluded_levels.append(ErdHoodLightLevel.MED) if not availability.high_available: self.excluded_levels.append(ErdHoodLightLevel.HIGH) @@ -35,8 +37,51 @@ def to_option_string(self, value: ErdHoodLightLevel) -> Optional[str]: except: pass return ErdHoodLightLevel.OFF.stringify() + +class HoodLightLevelNewOptionsConverter(OptionsConverter): + def __init__(self, availability: ErdHoodLightLevelAvailability): + super().__init__() + self.availability = availability + self.excluded_levels = [] + if not availability.off_available: + self.excluded_levels.append(ErdHoodLightLevelNew.OFF) + if not availability.dim_available: + self.excluded_levels.append(ErdHoodLightLevelNew.L1) + if not availability.med_available: + self.excluded_levels.append(ErdHoodLightLevelNew.L2) + if not availability.high_available: + self.excluded_levels.append(ErdHoodLightLevelNew.L3) + + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdHoodLightLevelNew if i not in self.excluded_levels] + def from_option_string(self, value: str) -> Any: + try: + return ErdHoodLightLevelNew[value.upper()] + except: + _LOGGER.warning(f"Could not set hood light level to {value.upper()}") + return ErdHoodLightLevelNew.OFF + def to_option_string(self, value: ErdHoodLightLevelNew) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdHoodLightLevelNew.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)) + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, control_erd_code: Optional[ErdCodeType] = None): + self._availability, converter = self._detect_availability(api) + super().__init__(api, erd_code, converter, control_erd_code=control_erd_code) + + def _detect_availability(self, api: ApplianceApi): + if (a := api.try_get_erd_value(ErdCode.HOOD_LIGHT_LEVEL_AVAILABILITY)) is not None: + return a, HoodLightLevelOptionsConverter(a) + + if (ll := api.try_get_erd_value(ErdCode.HOOD_AVAILABLE_LIGHT_LEVELS)) is not None: + a = ErdHoodLightLevelAvailability.from_count(ll) + return a, HoodLightLevelNewOptionsConverter(a) + + a = ErdHoodLightLevelAvailability(off_available=True) + return a, HoodLightLevelOptionsConverter(a) \ No newline at end of file diff --git a/custom_components/ge_home/entities/laundry/ge_dryer_cycle_button.py b/custom_components/ge_home/entities/laundry/ge_dryer_cycle_button.py index 7d44184..3afb24d 100644 --- a/custom_components/ge_home/entities/laundry/ge_dryer_cycle_button.py +++ b/custom_components/ge_home/entities/laundry/ge_dryer_cycle_button.py @@ -1,9 +1,8 @@ import logging -from typing import Any +from propcache.api import cached_property from datetime import timedelta -from gehomesdk import ErdCode, ErdMachineState -from homeassistant.components.button import ButtonEntity +from gehomesdk import ErdCode from ..common import GeErdButton _LOGGER = logging.getLogger(__name__) @@ -14,12 +13,12 @@ class GeDryerCycleButton(GeErdButton): def __init__(self, api): super().__init__(api, ErdCode.LAUNDRY_MACHINE_STATE) - @property + @cached_property def unique_id(self) -> str: """Return a unique ID for the button.""" return f"{self.serial_or_mac}_start_cycle_button" - @property + @cached_property def name(self) -> str: """Return the name of the button.""" return f"{self.serial_or_mac} Start Cycle" diff --git a/custom_components/ge_home/entities/laundry/ge_washer_cycle_button.py b/custom_components/ge_home/entities/laundry/ge_washer_cycle_button.py index 2a52a44..83222e9 100644 --- a/custom_components/ge_home/entities/laundry/ge_washer_cycle_button.py +++ b/custom_components/ge_home/entities/laundry/ge_washer_cycle_button.py @@ -1,9 +1,8 @@ import logging -from typing import Any +from propcache.api import cached_property from datetime import timedelta -from gehomesdk import ErdCode, ErdMachineState -from homeassistant.components.button import ButtonEntity +from gehomesdk import ErdCode from ..common import GeErdButton _LOGGER = logging.getLogger(__name__) @@ -14,12 +13,12 @@ class GeWasherCycleButton(GeErdButton): def __init__(self, api): super().__init__(api, ErdCode.LAUNDRY_MACHINE_STATE) - @property + @cached_property def unique_id(self) -> str: """Return a unique ID for the button.""" return f"{self.serial_or_mac}_start_cycle_button" - @property + @cached_property def name(self) -> str: """Return the name of the button.""" return f"{self.serial_or_mac} Start Cycle" diff --git a/custom_components/ge_home/entities/oven/ge_oven.py b/custom_components/ge_home/entities/oven/ge_oven.py index 710178f..6c29ca9 100644 --- a/custom_components/ge_home/entities/oven/ge_oven.py +++ b/custom_components/ge_home/entities/oven/ge_oven.py @@ -1,7 +1,10 @@ """GE Home Sensor Entities - Oven""" import logging +from propcache.api import cached_property from typing import Any, Dict, List, Optional, Set +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature + from gehomesdk import ( ErdCode, ErdMeasurementUnits, @@ -10,7 +13,6 @@ OvenCookSetting ) -from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from ...const import DOMAIN from ...devices import ApplianceApi from ..common import GeAbstractWaterHeater @@ -21,8 +23,6 @@ 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})") @@ -32,19 +32,12 @@ def __init__(self, api: ApplianceApi, oven_select: str = UPPER_OVEN, 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 + @cached_property def unique_id(self) -> str: return f"{DOMAIN}_{self.serial_or_mac}_{self.oven_select.lower()}" - @property - def name(self) -> Optional[str]: + @cached_property + def name(self) -> str | None: if self._two_cavity: oven_title = self.oven_select.replace("_", " ").title() else: @@ -53,10 +46,22 @@ def name(self) -> Optional[str]: return f"{self.serial_or_mac} {oven_title}" @property + def icon(self) -> str | None: + return "mdi:stove" + + @property + def supported_features(self): + if self.remote_enabled: + return GE_OVEN_SUPPORT + else: + return SUPPORT_NONE + + @cached_property def temperature_unit(self): - measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) - if measurement_system == ErdMeasurementUnits.METRIC: - return UnitOfTemperature.CELSIUS + # measurement_system = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) + # if measurement_system == ErdMeasurementUnits.METRIC: + # return UnitOfTemperature.CELSIUS + # APIs always return Fahrenheit, hardcode return UnitOfTemperature.FAHRENHEIT @property @@ -74,7 +79,7 @@ def remote_enabled(self) -> bool: return value == True @property - def current_temperature(self) -> Optional[int]: + def current_temperature(self) -> int | None: # type: ignore #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 @@ -86,7 +91,7 @@ def current_temperature(self) -> Optional[int]: return self.get_erd_value(self._temperature_erd_code) @property - def current_operation(self) -> Optional[str]: + def current_operation(self) -> str | None: # type: ignore cook_setting = self.current_cook_setting cook_mode = cook_setting.cook_mode # TODO: simplify this lookup nonsense somehow @@ -97,7 +102,7 @@ def current_operation(self) -> Optional[str]: _LOGGER.debug(f"Unable to map {current_state} to an operation mode") return OP_MODE_COOK_UNK - @property + @cached_property def operation_list(self) -> List[str]: #lookup all the available cook modes erd_code = self.get_erd_code("AVAILABLE_COOK_MODES") @@ -106,7 +111,7 @@ def operation_list(self) -> List[str]: #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) + ext_cook_modes: Set[ErdOvenCookMode] | None = 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) @@ -126,7 +131,7 @@ def current_cook_setting(self) -> OvenCookSetting: return self.appliance.get_erd_value(erd_code) @property - def target_temperature(self) -> Optional[int]: + def target_temperature(self) -> int | None: # type: ignore """Return the temperature we try to reach.""" cook_mode = self.current_cook_setting if cook_mode.temperature: @@ -172,7 +177,7 @@ async def async_set_temperature(self, **kwargs): return current_op = self.current_operation - if current_op != OP_MODE_OFF: + if current_op is not None and current_op != OP_MODE_OFF: erd_cook_mode = COOK_MODE_OP_MAP.inverse[current_op] else: erd_cook_mode = ErdOvenCookMode.BAKE_NOOPTION @@ -192,7 +197,7 @@ def display_state(self) -> Optional[str]: return self._stringify(erd_value, temp_units=self.temperature_unit) @property - def extra_state_attributes(self) -> Optional[Dict[str, Any]]: + def extra_state_attributes(self) -> Optional[Dict[str, Any]]: # type: ignore probe_present = False if self.api.has_erd_code(self.get_erd_code("PROBE_PRESENT")): probe_present: bool = self.get_erd_value("PROBE_PRESENT") diff --git a/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py b/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py index ca7e0de..a1c3bcc 100644 --- a/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py +++ b/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py @@ -1,14 +1,16 @@ import logging from typing import List, Any, Optional +from homeassistant.const import EntityCategory 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): + def __init__(self, availability: Optional[ErdOvenLightLevelAvailability]): super().__init__() self.availability = availability self.excluded_levels = [ErdOvenLightLevel.NOT_AVAILABLE] @@ -35,24 +37,24 @@ def to_option_string(self, value: ErdOvenLightLevel) -> Optional[str]: 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) + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: Optional[str] = None): + self._availability: ErdOvenLightLevelAvailability | None = 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) + value: ErdOvenLightLevel | None = 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) + super().__init__(api, erd_code, OvenLightLevelOptionsConverter(self._availability), erd_override=erd_override, entity_category=EntityCategory.CONFIG) @property - def assumed_state(self) -> bool: + def assumed_state(self) -> bool: # type: ignore return not self._has_status @property - def current_option(self): + def current_option(self) -> str | None: if self.assumed_state: - return self._assumed_state + return self._assumed_state.name return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) @@ -60,7 +62,7 @@ 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) + new_state: ErdOvenLightLevel = 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_home/entities/oven/ge_oven_warming_state_select.py b/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py index 86da410..a57eb70 100644 --- a/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py +++ b/custom_components/ge_home/entities/oven/ge_oven_warming_state_select.py @@ -1,6 +1,7 @@ import logging from typing import List, Any, Optional +from homeassistant.const import EntityCategory from gehomesdk import ErdCodeType, ErdOvenWarmingState from ...devices import ApplianceApi from ..common import GeErdSelect, OptionsConverter @@ -27,22 +28,22 @@ def to_option_string(self, value: ErdOvenWarmingState) -> Optional[str]: class GeOvenWarmingStateSelect(GeErdSelect): - def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: Optional[str] = None): #check to see if we have a status - value: ErdOvenWarmingState = api.try_get_erd_value(erd_code) + value: ErdOvenWarmingState | None = api.try_get_erd_value(erd_code) self._has_status = value is not None and value != ErdOvenWarmingState.NOT_AVAILABLE self._assumed_state = ErdOvenWarmingState.OFF - super().__init__(api, erd_code, OvenWarmingStateOptionsConverter(self._availability), erd_override=erd_override) + super().__init__(api, erd_code, OvenWarmingStateOptionsConverter(), erd_override=erd_override, entity_category=EntityCategory.CONFIG) @property - def assumed_state(self) -> bool: + def assumed_state(self) -> bool: # type: ignore return not self._has_status @property def current_option(self): if self.assumed_state: - return self._assumed_state + return self._assumed_state.name return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) @@ -50,7 +51,7 @@ 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) + new_state: ErdOvenWarmingState = 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_home/entities/water_filter/filter_position.py b/custom_components/ge_home/entities/water_filter/filter_position.py index 53038af..4dec041 100644 --- a/custom_components/ge_home/entities/water_filter/filter_position.py +++ b/custom_components/ge_home/entities/water_filter/filter_position.py @@ -1,6 +1,7 @@ import logging from typing import List, Any, Optional +from homeassistant.const import EntityCategory from gehomesdk import ErdCodeType, ErdWaterFilterPosition, ErdCode, ErdWaterFilterMode from ...devices import ApplianceApi from ..common import GeErdSelect, OptionsConverter @@ -27,10 +28,10 @@ def to_option_string(self, value: Any) -> Optional[str]: class GeErdFilterPositionSelect(GeErdSelect): def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): - super().__init__(api, erd_code, FilterPositionOptionsConverter(), icon_override="mdi:valve") + super().__init__(api, erd_code, FilterPositionOptionsConverter(), icon_override="mdi:valve", entity_category=EntityCategory.DIAGNOSTIC) @property - def current_option(self): + def current_option(self) -> str | None: """Return the current selected option""" #if we're transitioning or don't know what the mode is, don't allow changes @@ -41,13 +42,13 @@ def current_option(self): return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) @property - def options(self) -> List[str]: + def options(self) -> List[str]: # type: ignore """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 [mode.name.title()] return self._converter.options diff --git a/custom_components/ge_home/entities/water_heater/ge_water_heater.py b/custom_components/ge_home/entities/water_heater/ge_water_heater.py index e9958c4..95916eb 100644 --- a/custom_components/ge_home/entities/water_heater/ge_water_heater.py +++ b/custom_components/ge_home/entities/water_heater/ge_water_heater.py @@ -1,15 +1,15 @@ """GE Home Sensor Entities - Oven""" import logging -from typing import List, Optional +from propcache.api import cached_property +from typing import List +from homeassistant.components.water_heater import WaterHeaterEntityFeature +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from gehomesdk import ( ErdCode, ErdWaterHeaterMode ) -from homeassistant.components.water_heater import WaterHeaterEntityFeature - -from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from ...devices import ApplianceApi from ..common import GeAbstractWaterHeater from .heater_modes import WhHeaterModeConverter @@ -19,8 +19,6 @@ class GeWaterHeater(GeAbstractWaterHeater): """GE Whole Home Water Heater""" - icon = "mdi:water-boiler" - def __init__(self, api: ApplianceApi): super().__init__(api) self._modes_converter = WhHeaterModeConverter() @@ -28,30 +26,34 @@ def __init__(self, api: ApplianceApi): @property def heater_type(self) -> str: return "heater" + + @property + def icon(self) -> str | None: + return "mdi:water-boiler" @property def supported_features(self): return (WaterHeaterEntityFeature.OPERATION_MODE | WaterHeaterEntityFeature.TARGET_TEMPERATURE) - @property + @cached_property def temperature_unit(self): return UnitOfTemperature.FAHRENHEIT @property - def current_temperature(self) -> Optional[int]: + def current_temperature(self) -> int | None: # type: ignore return self.appliance.get_erd_value(ErdCode.WH_HEATER_TEMPERATURE) @property - def current_operation(self) -> Optional[str]: + def current_operation(self) -> str | None: # type: ignore erd_mode = self.appliance.get_erd_value(ErdCode.WH_HEATER_MODE) return self._modes_converter.to_option_string(erd_mode) - @property + @cached_property def operation_list(self) -> List[str]: return self._modes_converter.options @property - def target_temperature(self) -> Optional[int]: + def target_temperature(self) -> int | None: # type: ignore """Return the temperature we try to reach.""" return self.appliance.get_erd_value(ErdCode.WH_HEATER_TARGET_TEMPERATURE) @@ -83,4 +85,3 @@ async def async_set_temperature(self, **kwargs): return await self.appliance.async_set_erd_value(ErdCode.WH_HEATER_TARGET_TEMPERATURE, target_temp) - diff --git a/custom_components/ge_home/entities/water_heater/heater_modes.py b/custom_components/ge_home/entities/water_heater/heater_modes.py index 145bb20..1481b7e 100644 --- a/custom_components/ge_home/entities/water_heater/heater_modes.py +++ b/custom_components/ge_home/entities/water_heater/heater_modes.py @@ -2,6 +2,7 @@ from typing import List, Any, Optional from gehomesdk import ErdWaterHeaterMode + from ..common import OptionsConverter _LOGGER = logging.getLogger(__name__) @@ -9,7 +10,13 @@ class WhHeaterModeConverter(OptionsConverter): @property def options(self) -> List[str]: - return [i.stringify() for i in ErdWaterHeaterMode] + return [ + s + for i in ErdWaterHeaterMode + for s in [i.stringify()] + if s is not None + ] + def from_option_string(self, value: str) -> Any: enum_val = value.upper().replace(" ","_") try: @@ -17,6 +24,7 @@ def from_option_string(self, value: str) -> Any: except: _LOGGER.warning(f"Could not heater mode to {enum_val}") return ErdWaterHeaterMode.UNKNOWN + def to_option_string(self, value: ErdWaterHeaterMode) -> Optional[str]: try: if value is not None: diff --git a/custom_components/ge_home/entities/water_softener/shutoff_position.py b/custom_components/ge_home/entities/water_softener/shutoff_position.py index 53d08a4..015e56a 100644 --- a/custom_components/ge_home/entities/water_softener/shutoff_position.py +++ b/custom_components/ge_home/entities/water_softener/shutoff_position.py @@ -1,6 +1,7 @@ import logging from typing import List, Any, Optional +from homeassistant.const import EntityCategory from gehomesdk import ErdCodeType, ErdWaterSoftenerShutoffValveState, ErdCode from ...devices import ApplianceApi from ..common import GeErdSelect, OptionsConverter @@ -29,7 +30,7 @@ def to_option_string(self, value: Any) -> Optional[str]: class GeErdShutoffPositionSelect(GeErdSelect): def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): - super().__init__(api, erd_code, FilterPositionOptionsConverter(), icon_override="mdi:valve") + super().__init__(api, erd_code, FilterPositionOptionsConverter(), icon_override="mdi:valve", entity_category=EntityCategory.CONFIG) @property def current_option(self): @@ -43,13 +44,13 @@ def current_option(self): return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) @property - def options(self) -> List[str]: + def options(self) -> List[str]: # type: ignore """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 [mode.name.title()] return self._converter.options diff --git a/custom_components/ge_home/exceptions.py b/custom_components/ge_home/exceptions.py index fe7946e..d10706d 100644 --- a/custom_components/ge_home/exceptions.py +++ b/custom_components/ge_home/exceptions.py @@ -7,4 +7,6 @@ class HaCannotConnect(ha_exc.HomeAssistantError): class HaAuthError(ha_exc.HomeAssistantError): """Error to indicate authentication failure.""" class HaAlreadyConfigured(ha_exc.HomeAssistantError): - """Error to indicate that the account is already configured""" \ No newline at end of file + """Error to indicate that the account is already configured""" +class HaInvalidOperation(ha_exc.HomeAssistantError): + """Error to indcate that an invalid operation was attempted""" \ No newline at end of file diff --git a/custom_components/ge_home/humidifier.py b/custom_components/ge_home/humidifier.py index 68aa896..fc53f23 100644 --- a/custom_components/ge_home/humidifier.py +++ b/custom_components/ge_home/humidifier.py @@ -1,5 +1,6 @@ """GE Home Humidifier Entities""" import logging +from collections.abc import Collection from typing import Callable from homeassistant.config_entries import ConfigEntry @@ -15,13 +16,13 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home Water Heaters.""" + """GE Home Humidifiers""" _LOGGER.debug('Adding GE "Humidifiers"') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] registry = er.async_get(hass) @callback - def async_devices_discovered(apis: list[ApplianceApi]): + def async_devices_discovered(apis: Collection[ApplianceApi]): _LOGGER.debug(f'Found {len(apis):d} appliance APIs') entities = [ entity @@ -30,7 +31,7 @@ def async_devices_discovered(apis: list[ApplianceApi]): if isinstance(entity, GeHumidifier) if not registry.async_is_registered(entity.entity_id) ] - _LOGGER.debug(f'Found {len(entities):d} unregistered humidifiers') + _LOGGER.debug(f'Found {len(entities):d} unregistered humidifiers to register') async_add_entities(entities) #if we're already initialized at this point, call device diff --git a/custom_components/ge_home/light.py b/custom_components/ge_home/light.py index ba2a69c..91314de 100644 --- a/custom_components/ge_home/light.py +++ b/custom_components/ge_home/light.py @@ -1,5 +1,6 @@ """GE Home Select Entities""" import logging +from collections.abc import Collection from typing import Callable from homeassistant.config_entries import ConfigEntry @@ -24,7 +25,7 @@ async def async_setup_entry( registry = er.async_get(hass) @callback - def async_devices_discovered(apis: list[ApplianceApi]): + def async_devices_discovered(apis: Collection[ApplianceApi]): _LOGGER.debug(f"Found {len(apis):d} appliance APIs") entities = [ entity @@ -34,7 +35,7 @@ def async_devices_discovered(apis: list[ApplianceApi]): 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") + _LOGGER.debug(f"Found {len(entities):d} unregistered lights to register") async_add_entities(entities) #if we're already initialized at this point, call device diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index c995486..723352f 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==2025.6.0","magicattr==0.1.6","slixmpp==1.8.3"], + "requirements": ["gehomesdk==2025.11.5","magicattr==0.1.6"], "codeowners": ["@simbaja"], - "version": "2025.7.0" + "version": "2025.11.0" } diff --git a/custom_components/ge_home/number.py b/custom_components/ge_home/number.py index e691988..4f148da 100644 --- a/custom_components/ge_home/number.py +++ b/custom_components/ge_home/number.py @@ -1,5 +1,6 @@ """GE Home Number Entities""" import logging +from collections.abc import Collection from typing import Callable from homeassistant.config_entries import ConfigEntry @@ -15,14 +16,14 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home numbers.""" + """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]): + def async_devices_discovered(apis: Collection[ApplianceApi]): _LOGGER.debug(f'Found {len(apis):d} appliance APIs') entities = [ entity @@ -31,7 +32,7 @@ def async_devices_discovered(apis: list[ApplianceApi]): if isinstance(entity, GeErdNumber) if not registry.async_is_registered(entity.entity_id) ] - _LOGGER.debug(f'Found {len(entities):d} unregistered numbers') + _LOGGER.debug(f'Found {len(entities):d} unregistered numbers to register') async_add_entities(entities) #if we're already initialized at this point, call device diff --git a/custom_components/ge_home/select.py b/custom_components/ge_home/select.py index 158af27..ba1e832 100644 --- a/custom_components/ge_home/select.py +++ b/custom_components/ge_home/select.py @@ -1,5 +1,6 @@ """GE Home Select Entities""" import logging +from collections.abc import Collection from typing import Callable from homeassistant.config_entries import ConfigEntry @@ -18,13 +19,13 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable ): - """GE Home selects.""" + """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]): + def async_devices_discovered(apis: Collection[ApplianceApi]): _LOGGER.debug(f"Found {len(apis):d} appliance APIs") entities = [ entity @@ -34,7 +35,7 @@ def async_devices_discovered(apis: list[ApplianceApi]): 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") + _LOGGER.debug(f"Found {len(entities):d} unregistered selects to register") async_add_entities(entities) #if we're already initialized at this point, call device diff --git a/custom_components/ge_home/sensor.py b/custom_components/ge_home/sensor.py index 0bf7ba6..c5b1160 100644 --- a/custom_components/ge_home/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -1,8 +1,9 @@ """GE Home Sensor Entities""" import logging -from typing import Callable import voluptuous as vol +from collections.abc import Collection from datetime import timedelta +from typing import Callable from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -26,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home sensors.""" + """GE Home Sensors.""" _LOGGER.debug('Adding GE Home sensors') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] registry = er.async_get(hass) @@ -35,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn platform = entity_platform.async_get_current_platform() @callback - def async_devices_discovered(apis: list[ApplianceApi]): + def async_devices_discovered(apis: Collection[ApplianceApi]): _LOGGER.debug(f'Found {len(apis):d} appliance APIs') entities = [ entity @@ -44,7 +45,7 @@ def async_devices_discovered(apis: list[ApplianceApi]): if isinstance(entity, GeErdSensor) and entity.erd_code in api.appliance._property_cache if not registry.async_is_registered(entity.entity_id) ] - _LOGGER.debug(f'Found {len(entities):d} unregistered sensors') + _LOGGER.debug(f'Found {len(entities):d} unregistered sensors to register') async_add_entities(entities) #if we're already initialized at this point, call device diff --git a/custom_components/ge_home/strings.json b/custom_components/ge_home/strings.json deleted file mode 100644 index 8e3c913..0000000 --- a/custom_components/ge_home/strings.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "username": "Username", - "password": "Password", - "region": "Region" - } - } - }, - "error": { - "cannot_connect": "Can't connect to SmartHQ", - "invalid_auth": "Invalid authentication provided, please check credentials", - "unknown": "Unknown error occurred" - }, - "abort": { - "already_configured_account": "Account already configured!" - } - } -} diff --git a/custom_components/ge_home/switch.py b/custom_components/ge_home/switch.py index a2b4823..66e606b 100644 --- a/custom_components/ge_home/switch.py +++ b/custom_components/ge_home/switch.py @@ -1,5 +1,6 @@ """GE Home Switch Entities""" import logging +from collections.abc import Collection from typing import Callable from homeassistant.config_entries import ConfigEntry @@ -16,13 +17,13 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): - """GE Home switches.""" + """GE Home Switches.""" _LOGGER.debug('Adding GE Home switches') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] registry = er.async_get(hass) @callback - def async_devices_discovered(apis: list[ApplianceApi]): + def async_devices_discovered(apis: Collection[ApplianceApi]): """Add new switch entities from the device API.""" _LOGGER.debug(f'Found {len(apis):d} appliance APIs') @@ -43,7 +44,7 @@ def async_devices_discovered(apis: list[ApplianceApi]): # For other switche types add them directly new_entities.append(entity) - _LOGGER.debug(f'Found {len(new_entities):d} unregistered switches') + _LOGGER.debug(f'Found {len(new_entities):d} unregistered switches to register') async_add_entities(new_entities) # If we're already initialized at this point, call device diff --git a/custom_components/ge_home/translations/en.json b/custom_components/ge_home/translations/en.json index 50680ea..ca46fc2 100644 --- a/custom_components/ge_home/translations/en.json +++ b/custom_components/ge_home/translations/en.json @@ -1,15 +1,15 @@ { - "title": "GE Home", + "title": "GE Home (SmartHQ)", "config": { "step": { - "init": { + "user": { "data": { "username": "Username", "password": "Password", "region": "Region" } }, - "user": { + "reauth": { "data": { "username": "Username", "password": "Password", @@ -18,12 +18,13 @@ } }, "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, + "cannot_connect": "Can't connect to SmartHQ", + "invalid_auth": "Invalid authentication provided, please check credentials", + "unknown": "Unknown error occurred" + }, "abort": { - "already_configured_account": "Account is already configured" + "already_configured": "Account already configured!", + "reauth_successful": "Re-authentication was successful!" } } } diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 4389a0d..1f40a1b 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -1,11 +1,22 @@ -"""Data update coordinator for GE Home Appliances""" +"""Data update coordinator for GE Home (SmartHQ) Appliances""" import asyncio -import async_timeout +from contextlib import suppress +import random import logging +import time from typing import Any, Callable, Dict, Iterable, Optional, Tuple, List -logging.getLogger('slixmpp.stringprep').setLevel(logging.ERROR) +from homeassistant.components import persistent_notification +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util.ssl import get_default_context from gehomesdk import ( EVENT_APPLIANCE_INITIAL_UPDATE, @@ -16,28 +27,14 @@ ErdCodeType, GeAppliance, GeWebsocketClient, + ErdApplianceType, + GeClientState ) from gehomesdk import GeAuthFailedError, GeGeneralServerError, GeNotAuthenticatedError -from .exceptions import HaAuthError, HaCannotConnect -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util.ssl import get_default_context -from .const import ( - DOMAIN, - EVENT_ALL_APPLIANCES_READY, - UPDATE_INTERVAL, - MIN_RETRY_DELAY, - MAX_RETRY_DELAY, - RETRY_OFFLINE_COUNT, - ASYNC_TIMEOUT, -) +from .const import * from .devices import ApplianceApi, get_appliance_api_type +from .exceptions import HaAuthError, HaCannotConnect PLATFORMS = [ "binary_sensor", @@ -53,7 +50,6 @@ ] _LOGGER = logging.getLogger(__name__) - class GeHomeUpdateCoordinator(DataUpdateCoordinator): """Define a wrapper class to update GE Home data.""" @@ -61,60 +57,42 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Set up the GeHomeUpdateCoordinator class.""" super().__init__(hass, _LOGGER, name=DOMAIN) + self._client : GeWebsocketClient | None = None self._config_entry = config_entry self._username = config_entry.data[CONF_USERNAME] self._password = config_entry.data[CONF_PASSWORD] self._region = config_entry.data[CONF_REGION] - self._appliance_apis = {} # type: Dict[str, ApplianceApi] - self._signal_remove_callbacks = [] # type: List[Callable] - - self._reset_initialization() - - def _reset_initialization(self): - self.client = None # type: Optional[GeWebsocketClient] - - # Mark all appliances as not initialized yet - for a in self.appliance_apis.values(): - a.appliance.initialized = False - - # Some record keeping to let us know when we can start generating entities + self._appliance_apis: Dict[str, ApplianceApi] = {} + self._signal_remove_callbacks: List[Callable] = [] self._got_roster = False self._init_done = False - self._retry_count = 0 + self._all_initial_updates_received = asyncio.Event() - def create_ge_client( - self, event_loop: Optional[asyncio.AbstractEventLoop] - ) -> GeWebsocketClient: - """ - Create a new GeClient object with some helpful callbacks. + self._updater_task: asyncio.Task | None = None + self._reconnect_task: asyncio.Task | None = None + self._last_persistent_log: float = 0.0 + self._retry_count: int = 0 + self._last_ha_refresh: float = 0.0 - :param event_loop: Event loop - :return: GeWebsocketClient - """ - client = GeWebsocketClient( - self._username, - self._password, - self._region, - event_loop=event_loop, - ssl_context=get_default_context() - ) - client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, self.on_device_initial_update) - client.add_event_handler(EVENT_APPLIANCE_UPDATE_RECEIVED, self.on_device_update) - client.add_event_handler(EVENT_GOT_APPLIANCE_LIST, self.on_appliance_list) - client.add_event_handler(EVENT_DISCONNECTED, self.on_disconnect) - client.add_event_handler(EVENT_CONNECTED, self.on_connect) - return client + self._reset_sync_state() + + #region Public Properties @property def appliances(self) -> Iterable[GeAppliance]: - return ( - appliance for appliance in self.client.appliances.values() - if self._is_appliance_valid(appliance) - ) + if self._client is None: + return [] + + return self._client.appliances.values() @property def appliance_apis(self) -> Dict[str, ApplianceApi]: return self._appliance_apis + + @property + def all_appliances_updated(self) -> bool: + """True if all appliances have had an initial update.""" + return all([a.initialized for a in self.appliances]) @property def signal_ready(self) -> str: @@ -138,150 +116,263 @@ def connected(self) -> bool: """ Indicates whether the coordinator is connected """ - return self.client and self.client.connected - - def _get_appliance_api(self, appliance: GeAppliance) -> ApplianceApi: - self._dump_appliance(appliance) - api_type = get_appliance_api_type(appliance.appliance_type) - return api_type(self, appliance) - - def regenerate_appliance_apis(self): - """Regenerate the appliance_apis dictionary, adding elements as necessary.""" - for jid, appliance in self.client.appliances.keys(): - if jid not in self._appliance_apis and self._is_appliance_valid(appliance): - self._appliance_apis[jid] = self._get_appliance_api(appliance) + return self._client is not None and self._client.connected + + @property + def available(self) -> bool: + """ + Indicates whether the coordinator is available + """ + return self._client is not None and self._client.available + + #endregion - def _maybe_add_appliance_api(self, appliance: GeAppliance): - mac_addr = appliance.mac_addr - if mac_addr not in self.appliance_apis: - _LOGGER.debug(f"Adding appliance api for appliance {mac_addr} ({appliance.appliance_type})") - api = self._get_appliance_api(appliance) - api.build_entities_list() - self.appliance_apis[mac_addr] = api - else: - # if we already have the API, switch out its appliance reference for this one - api = self.appliance_apis[mac_addr] - api.appliance = appliance + #region Public Methods def add_signal_remove_callback(self, cb: Callable): self._signal_remove_callbacks.append(cb) - async def get_client(self) -> GeWebsocketClient: - """Get a new GE Websocket client.""" - if self.client: - try: - self.client.clear_event_handlers() - await self.client.disconnect() - except Exception as err: - _LOGGER.warning(f"exception while disconnecting client {err}") - finally: - self._reset_initialization() - - self.client = self.create_ge_client(event_loop=self.hass.loop) - return self.client - async def async_setup(self): """Setup a new coordinator""" - _LOGGER.debug("Setting up coordinator") + _LOGGER.debug("Setting up the coordinator") await self.hass.config_entries.async_forward_entry_setups( self._config_entry, PLATFORMS ) try: - await self.async_start_client() + await self._async_start_client() except (GeNotAuthenticatedError, GeAuthFailedError): raise HaAuthError("Authentication failure") except GeGeneralServerError: raise HaCannotConnect("Cannot connect (server error)") - except Exception: - raise HaCannotConnect("Unknown connection failure") + except Exception as exc: + raise HaCannotConnect("Unknown connection failure") from exc return True - async def async_start_client(self): - """Start a new GeClient in the HASS event loop.""" - try: - _LOGGER.debug("Creating and starting client") - await self.get_client() - await self.async_begin_session() - except: - _LOGGER.debug("could not start the client") - self.client = None - raise - - async def async_begin_session(self): - """Begins the ge_home session.""" - _LOGGER.debug("Beginning session") - session = async_get_clientsession(self.hass) - await self.client.async_get_credentials(session) - fut = asyncio.ensure_future(self.client.async_run_client(), loop=self.hass.loop) - _LOGGER.debug("Client running") - return fut - - async def async_reset(self): + async def async_reset(self) -> bool: """Resets the coordinator.""" - _LOGGER.debug("resetting the coordinator") + _LOGGER.debug("Resetting the coordinator") entry = self._config_entry + + # stop the client + await self._async_stop_client() # remove all the callbacks for this coordinator for c in self._signal_remove_callbacks: c() self._signal_remove_callbacks.clear() + # cancel the notification + try: + persistent_notification.async_dismiss(self.hass, CONNECTION_NOTIFICATION_ID) + except Exception: + pass + + # unload unload_ok = await self.hass.config_entries.async_unload_platforms( self._config_entry, PLATFORMS ) return unload_ok - async def _kill_client(self): - """Kill the client. Leaving this in for testing purposes.""" - await asyncio.sleep(30) - _LOGGER.critical("Killing the connection. Popcorn time.") - await self.client.disconnect() - @callback - def reconnect(self, log=False) -> None: - """Prepare to reconnect ge_home session.""" - if log: - _LOGGER.info("Will try to reconnect to ge_home service") - self.hass.loop.create_task(self.async_reconnect()) - - async def async_reconnect(self) -> None: - """Try to reconnect ge_home session.""" - self._retry_count += 1 - _LOGGER.info( - f"attempting to reconnect to ge_home service (attempt {self._retry_count})" + def shutdown(self, event) -> None: + """ + Close the connection on shutdown. + Used as an argument to EventBus.async_listen_once. + """ + _LOGGER.info("ge_home shutting down") + + #stop the client and existing background tasks + self.hass.loop.create_task(self._async_stop_client()) + + #endregion + + #region Internal Methods + + #region Initialization/Reset/Shutdown + + def _create_ge_client( + self, event_loop: Optional[asyncio.AbstractEventLoop] + ) -> GeWebsocketClient: + """ + Create a new GeClient object with some helpful callbacks. + + :param event_loop: Event loop + :return: GeWebsocketClient + """ + client = GeWebsocketClient( + self._username, + self._password, + self._region, + event_loop=event_loop, + ssl_context=get_default_context() ) + client.add_event_handler(EVENT_APPLIANCE_INITIAL_UPDATE, self._on_device_initial_update) + client.add_event_handler(EVENT_APPLIANCE_UPDATE_RECEIVED, self._on_device_update) + client.add_event_handler(EVENT_GOT_APPLIANCE_LIST, self._on_appliance_list) + client.add_event_handler(EVENT_DISCONNECTED, self._on_disconnect) + client.add_event_handler(EVENT_CONNECTED, self._on_connect) + return client + + async def _async_start_client(self) -> None: + """ + Tear down old client if present, reset state, and create & start a fresh client. + """ + # Teardown old client if present + await self._async_stop_client() + + # Create new client and start it try: - with async_timeout.timeout(ASYNC_TIMEOUT): - await self.async_start_client() + self._client = self._create_ge_client(event_loop=self.hass.loop) + session = async_get_clientsession(self.hass) + await self._client.async_get_credentials(session) except Exception as err: - _LOGGER.warning(f"could not reconnect: {err}, will retry in {self._get_retry_delay()} seconds") - self.hass.loop.call_later(self._get_retry_delay(), self.reconnect) - _LOGGER.debug("forcing a state refresh while disconnected") + _LOGGER.error(f"could not start the client: {err}") + self._client = None + raise + + # Start the client run loop + self.hass.loop.create_task(self._client.async_run_client()) + _LOGGER.debug("Scheduled the client for execution.") + + async def _async_stop_client(self): + """ Teardown the client if it exists """ + if self._client: try: - await self._refresh_ha_state() + self._client.clear_event_handlers() + await self._client.disconnect() except Exception as err: - _LOGGER.debug(f"error refreshing state: {err}") + _LOGGER.warning("Error disconnecting client: %s", err) + finally: + self._client = None - @callback - def shutdown(self, event) -> None: - """Close the connection on shutdown. - Used as an argument to EventBus.async_listen_once. - """ - _LOGGER.info("ge_home shutting down") - if self.client: - self.client.clear_event_handlers() - self.hass.loop.create_task(self.client.disconnect()) + # Reset asynchronous and synchronous states + await self._async_reset_state() + self._reset_sync_state() + + def _reset_sync_state(self): + """ Reset synchronous state """ + + # clear the appliances + self._appliance_apis.clear() + + # reset the initialization + self._all_initial_updates_received.clear() + + # Some record keeping to let us know when we can start generating entities + self._got_roster = False + self._init_done = False + self._retry_count = 0 + + async def _async_reset_state(self): + """ Reset asynchronous state """ + + await self._stop_periodic_updates() + await self._stop_reconnect_worker() + + #endregion + + #region Reconnection Lifecycle + + async def _ensure_client_running(self) -> None: + if self._client is None or self._client.state == GeClientState.DISCONNECTED: + _LOGGER.debug("Client missing or disconnected, starting new client") + await self._async_start_client() + + async def _start_reconnect_worker(self) -> None: + if self._reconnect_task and not self._reconnect_task.done(): + return + self._reconnect_task = self.hass.loop.create_task(self._reconnect_worker()) - async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]): + async def _stop_reconnect_worker(self) -> None: + self._retry_count = 0 + self._last_persistent_log = 0.0 + if self._reconnect_task: + self._reconnect_task.cancel() + with suppress(asyncio.CancelledError): + await self._reconnect_task + self._reconnect_task = None + try: + persistent_notification.async_dismiss(self.hass, CONNECTION_NOTIFICATION_ID) + except Exception: + pass + + async def _reconnect_worker(self) -> None: + _LOGGER.debug("Reconnect worker started") + try: + while True: + if self._client and self._client.state != GeClientState.DISCONNECTED: + _LOGGER.debug("Client no longer disconnected, exiting worker") + return + + self._retry_count += 1 + sleep_time = self._get_retry_delay() + + _LOGGER.info(f"Retrying in {sleep_time:.1f}s (attempt {self._retry_count})") + await asyncio.sleep(sleep_time) + + if self._client and self._client.state != GeClientState.DISCONNECTED: + _LOGGER.debug("Client became healthy before retry, exiting") + return + + try: + await self._async_start_client() + except (GeNotAuthenticatedError, GeAuthFailedError): + self._show_persistent_notification("Authentication failure: please re-authenticate the GE Home integration.") + return + except Exception as err: + _LOGGER.warning(f"Reconnect attempt failed: {err}") + + if self._client and self._client.state != GeClientState.DISCONNECTED: + return + + if self._retry_count >= NOTIFY_AFTER_RETRIES: + self._show_notification_once_per_interval( + title="GE Home: connection issues", + message=f"Unable to connect after {self._retry_count} attempts. Will continue retrying automatically.", + interval=PERSISTENT_RETRY_LOG_INTERVAL, + ) + await self._throttled_refresh_ha_state() + except asyncio.CancelledError: + _LOGGER.debug("Reconnect worker cancelled, ignoring.") + finally: + if self._reconnect_task and self._reconnect_task.done(): + self._reconnect_task = None + + def _get_retry_delay(self) -> float: + delay = float(min(MIN_RETRY_DELAY * (2 ** self._retry_count), MAX_RETRY_DELAY)) + jitter = delay * RECONNECT_JITTER * (random.random() * 2 - 1) + return delay + jitter + + #endregion + + #region Persistent Notifications + + def _show_persistent_notification(self, message: str, title: str = "GE Home Connection"): + try: + persistent_notification.async_create(self.hass, message, title, notification_id=CONNECTION_NOTIFICATION_ID) + except Exception: + _LOGGER.exception("Failed to create persistent notification") + + def _show_notification_once_per_interval(self, title: str, message: str, interval: int = 300): + now = time.time() + if now - self._last_persistent_log > interval: + self._last_persistent_log = now + self._show_persistent_notification(message, title) + + #endregion + + #region Client Event Handlers + + async def _on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]): """Let HA know there's new state.""" self.last_update_success = True - appliance, _ = data + appliance, update_data = data - self._dump_appliance(appliance) + self._dump_appliance(appliance, update_data) if not self._is_appliance_valid(appliance): _LOGGER.debug(f"on_device_update: skipping invalid appliance {appliance.mac_addr}") @@ -295,99 +386,239 @@ async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any] self._update_entity_state(api.entities) - async def _refresh_ha_state(self): - entities = [ - entity for api in self.appliance_apis.values() for entity in api.entities - ] - - self._update_entity_state(entities) - - def _update_entity_state(self, entities: List[Entity]): - from .entities import GeEntity - for entity in entities: - # if this is a GeEntity, check if it's been added - #if not, don't try to refresh this entity - if isinstance(entity, GeEntity): - gee: GeEntity = entity - if not gee.added: - _LOGGER.debug(f"Entity {entity} ({entity.unique_id}, {entity.entity_id}) not yet added, skipping update...") - continue - if entity.enabled: - try: - _LOGGER.debug(f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}") - entity.async_write_ha_state() - except: - _LOGGER.warning(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}", exc_info=1) - - @property - def all_appliances_updated(self) -> bool: - """True if all appliances have had an initial update.""" - return all([a.initialized for a in self.appliances]) - - async def on_appliance_list(self, _): + async def _on_appliance_list(self, _): """When we get an appliance list, mark it and maybe trigger all ready.""" + _LOGGER.debug("Got roster update") self.last_update_success = True if not self._got_roster: self._got_roster = True - # TODO: Probably should have a better way of confirming we're good to go... - await asyncio.sleep(5) - # After the initial roster update, wait a bit and hit go - await self.async_maybe_trigger_all_ready() - async def on_device_initial_update(self, appliance: GeAppliance): + try: + await asyncio.wait_for(self._all_initial_updates_received.wait(), timeout=INITIAL_UPDATE_TIMEOUT) + except asyncio.TimeoutError: + _LOGGER.warning("Timeout waiting for initial appliance updates") + finally: + # Remove stale devices/entities after everything is ready + await self._async_remove_stale_devices() + + # Trigger all-ready signal + await self._async_maybe_trigger_all_ready(True) + + async def _on_device_initial_update(self, appliance: GeAppliance): + """When an appliance first becomes ready, let the system know and schedule periodic updates.""" + self._dump_appliance(appliance) if not self._is_appliance_valid(appliance): _LOGGER.debug(f"on_device_initial_update: skipping invalid appliance {appliance.mac_addr}") return - """When an appliance first becomes ready, let the system know and schedule periodic updates.""" _LOGGER.debug(f"Got initial update for {appliance.mac_addr}") + self.last_update_success = True self._maybe_add_appliance_api(appliance) - await self.async_maybe_trigger_all_ready() - _LOGGER.debug(f"Requesting updates for {appliance.mac_addr}") - while self.connected: - await asyncio.sleep(UPDATE_INTERVAL) - if self.connected and self.client.available: - await appliance.async_request_update() - - _LOGGER.debug(f"No longer requesting updates for {appliance.mac_addr}") + await self._async_maybe_trigger_all_ready() + await self._start_periodic_updates() - async def on_disconnect(self, _): + async def _on_disconnect(self, _): """Handle disconnection.""" - _LOGGER.debug(f"Disconnected. Attempting to reconnect in {MIN_RETRY_DELAY} seconds") + _LOGGER.debug(f"Client has been disconnected, starting reconnection attempts.") self.last_update_success = False - self.hass.loop.call_later(MIN_RETRY_DELAY, self.reconnect, True) + await self._start_reconnect_worker() - async def on_connect(self, _): + async def _on_connect(self, _): """Set state upon connection.""" self.last_update_success = True - self._retry_count = 0 + await self._stop_reconnect_worker() + + #endregion + + #region Appliance Management + + def _is_appliance_valid(self, appliance: GeAppliance) -> bool: + return appliance.appliance_type is not None + + def _get_appliance_api(self, appliance: GeAppliance) -> ApplianceApi: + if appliance is None: + return None + + self._dump_appliance(appliance) + api_type = get_appliance_api_type(appliance.appliance_type or ErdApplianceType.UNKNOWN) + return api_type(self, appliance) - async def async_maybe_trigger_all_ready(self): + def _maybe_add_appliance_api(self, appliance: GeAppliance) -> None: + mac_addr = appliance.mac_addr + if mac_addr not in self.appliance_apis: + _LOGGER.debug(f"Adding appliance api for appliance {mac_addr} ({appliance.appliance_type})") + api = self._get_appliance_api(appliance) + api.build_entities_list() + self.appliance_apis[mac_addr] = api + else: + _LOGGER.debug(f"Already have appliance {mac_addr} ({appliance.appliance_type}), switching reference.") + # if we already have the API, switch out its appliance reference for this one + api = self.appliance_apis[mac_addr] + api.appliance = appliance + + async def _async_maybe_trigger_all_ready(self, force: bool = False) -> None: """See if we're all ready to go, and if so, let the games begin.""" if self._init_done: - # Been here, done this + # Been here, done this + _LOGGER.debug("Already initialized, cannot trigger ready.") + return + + if self._client is None: + _LOGGER.warning("Client is already deallocated, cannot trigger ready.") return - if self._got_roster and self.all_appliances_updated: - _LOGGER.debug("Ready to go, sending ready signal") + + if force or (self._got_roster and self.all_appliances_updated): + _LOGGER.debug("Ready to go, sending ready signal!") self._init_done = True - await self.client.async_event(EVENT_ALL_APPLIANCES_READY, None) + self._all_initial_updates_received.set() + + await self._client.async_event(EVENT_ALL_APPLIANCES_READY, None) async_dispatcher_send( self.hass, self.signal_ready, - list(self.appliance_apis.values())) + list(self.appliance_apis.values())) + + async def _async_remove_stale_devices(self): + """Remove devices/entities from HA that no longer exist in the cloud.""" + if self._client is None: + return - def _get_retry_delay(self) -> int: - delay = MIN_RETRY_DELAY * 2 ** (self._retry_count - 1) - return min(delay, MAX_RETRY_DELAY) + # Device and entity registries + device_registry = dr.async_get(self.hass) + entity_registry = er.async_get(self.hass) - def _is_appliance_valid(self, appliance: GeAppliance) -> bool: - return appliance.appliance_type and appliance.available + # MAC addresses of all currently valid appliances + current_macs = set(self._appliance_apis.keys()) + + # Loop through all devices for this config entry + for device_entry in list(device_registry.devices.values()): + # Skip devices not associated with this config entry + if self._config_entry.entry_id not in device_entry.config_entries: + continue + + # Extract mac_addresses (assumes identifiers contain ("ge_home", mac)) + device_mac = None + for ident in device_entry.identifiers: + if ident[0] == DOMAIN: # DOMAIN = "ge_home" + device_mac = ident[1] + break + + if device_mac and device_mac not in current_macs: + _LOGGER.info(f"Removing stale device {device_entry.name} ({device_mac}) from HA registry") + + # Remove all entities linked to this device + for entity_entry in list(entity_registry.entities.values()): + if entity_entry.device_id == device_entry.id: + entity_registry.async_remove(entity_entry.entity_id) + + # Remove the device itself + device_registry.async_remove_device(device_entry.id) + + #endregion + + #region Background Updates + + async def _start_periodic_updates(self): + + if self._updater_task is not None and not self._updater_task.done(): + _LOGGER.debug("Polling already started, ignoring scheduling request.") + return + + self._updater_task = self.hass.loop.create_task(self._request_periodic_updates()) + _LOGGER.debug("Scheduled background updater for execution.") + + async def _stop_periodic_updates(self) -> None: + if self._updater_task: + self._updater_task.cancel() + with suppress(asyncio.CancelledError): + await self._updater_task + self._updater_task = None + + async def _request_periodic_updates(self): + """Periodic update loop.""" + + _LOGGER.debug("Start requesting periodic updates.") + + try: + while self.connected: + await asyncio.sleep(STATE_UPDATE_INTERVAL) + + if (self._client is None or not self.connected or not self._client.available): + _LOGGER.debug( + f"Connection issue, cannot get update (" + f"client: { self._client is None }," + f"connected: { self.connected }," + f"available: { self.available }" + ) + continue + + for api in self.appliance_apis.values(): + try: + if api.appliance is None: + _LOGGER.debug(f"Appliance {api} is not valid, skipping update.") + continue + + _LOGGER.debug(f"Requesting update for {api.appliance.mac_addr}") + await api.appliance.async_request_update() + except Exception as err: + _LOGGER.debug(f"Poll update failed for [{api.appliance.mac_addr}]: {err}") + + except asyncio.CancelledError: + # Normal exit when shutting down + pass + + _LOGGER.debug("Stopped requesting periodic updates.") - def _dump_appliance(self, appliance: GeAppliance) -> None: + #endregion + + #region State Updates + + async def _refresh_ha_state(self): + """ Performs a full refresh of all appliances """ + entities = [ + entity for api in self.appliance_apis.values() for entity in api.entities + ] + + self._update_entity_state(entities) + + def _update_entity_state(self, entities: List[Entity]): + """ Performs a refresh of the state for a list of entities """ + + from .entities import GeEntity + for entity in entities: + # if this is a GeEntity, check if it's been added + #if not, don't try to refresh this entity + if isinstance(entity, GeEntity): + gee: GeEntity = entity + if not gee.added: + _LOGGER.debug(f"Entity {entity} ({entity.unique_id}, {entity.entity_id}) not yet added, skipping update...") + continue + if entity.enabled: + try: + _LOGGER.debug(f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}), state: {entity.state}") + entity.async_write_ha_state() + except: + _LOGGER.warning(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}", exc_info=True) + + async def _throttled_refresh_ha_state(self): + now = time.time() + if now - self._last_ha_refresh > HA_REFRESH_INTERVAL: + try: + await self._refresh_ha_state() + except Exception: + _LOGGER.debug("Error refreshing HA state during reconnect", exc_info=True) + finally: + self._last_ha_refresh = now + + #endregion + + #region Debugging + + def _dump_appliance(self, appliance: GeAppliance, update_data: Optional[Dict[ErdCodeType, Any]] = None) -> None: if not _LOGGER.isEnabledFor(logging.DEBUG): return @@ -409,7 +640,20 @@ def _dump_appliance(self, appliance: GeAppliance) -> None: except Exception: # some props might fail if called out of context appliance_data[attr_name] = "Error: Could not read attribute" + + # add the internal property cache (i.e. current values) + appliance_data["property_cache"] = appliance._property_cache + + # add the update data if available + if update_data is not None: + appliance_data["update_data"] = update_data + _LOGGER.debug(pprint.pformat(appliance_data)) _LOGGER.debug("--- END OF COMPREHENSIVE DUMP ---") except Exception as e: _LOGGER.error(f"Could not dump appliance {appliance}: {e}") + + #endregion + + #endregion + diff --git a/custom_components/ge_home/water_heater.py b/custom_components/ge_home/water_heater.py index 9bb0a8e..e1b1cfa 100644 --- a/custom_components/ge_home/water_heater.py +++ b/custom_components/ge_home/water_heater.py @@ -1,9 +1,8 @@ """GE Home Sensor Entities""" -import async_timeout import logging +from collections.abc import Collection 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 @@ -23,7 +22,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn registry = er.async_get(hass) @callback - def async_devices_discovered(apis: list[ApplianceApi]): + def async_devices_discovered(apis: Collection[ApplianceApi]): _LOGGER.debug(f'Found {len(apis):d} appliance APIs') entities = [ entity @@ -32,7 +31,7 @@ def async_devices_discovered(apis: list[ApplianceApi]): if isinstance(entity, GeAbstractWaterHeater) if not registry.async_is_registered(entity.entity_id) ] - _LOGGER.debug(f'Found {len(entities):d} unregistered water heaters') + _LOGGER.debug(f'Found {len(entities):d} unregistered water heaters to register') async_add_entities(entities) #if we're already initialized at this point, call device diff --git a/hacs.json b/hacs.json index 6bfcd26..02eff7e 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Home (SmartHQ)", - "homeassistant": "2025.1.0", + "homeassistant": "2025.9.0", "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select", "button", "climate", "light", "number"], "iot_class": "Cloud Polling" } diff --git a/info.md b/info.md index aa54b32..5c2a86d 100644 --- a/info.md +++ b/info.md @@ -46,6 +46,10 @@ A/C Controls: #### Breaking Changes +{% if version_installed.split('.') | map('int') < '2025.11.0'.split('.') | map('int') %} +- changed name of some SAC/WAC entities to have a AC prefix +{% endif %} + {% if version_installed.split('.') | map('int') < '2025.2.0'.split('.') | map('int') %} - Changed dishwasher pods to number - Removed outdated laundry status sensor @@ -72,6 +76,11 @@ A/C Controls: #### Changes +{% if version_installed.split('.') | map('int') < '2025.11.0'.split('.') | map('int') %} +- Refactored code internally to improve reliability +- Cleaned up initialization and config flow +{% endif %} + {% if version_installed.split('.') | map('int') < '2025.7.0'.split('.') | map('int') %} - Silenced string prep warning [#386] (@derekcentrico) {% endif %} @@ -86,6 +95,15 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '2025.11.0'.split('.') | map('int') %} +- Added heat mode for Window ACs +- Added support for Advantium +- Brand inference and stale device cleanup +- Added support for new hoods that require state/control ERDs +- Added entity categorization +- Added dishwasher remote commands +{% endif %} + {% if version_installed.split('.') | map('int') < '2025.7.0'.split('.') | map('int') %} - Enabled Washer/Dryer remote start [#369] (@derekcentrico) - Enabled K-cup refrigerator functionality [#101] (@derekcentrico) @@ -163,74 +181,70 @@ A/C Controls: #### Bugfixes {% if version_installed.split('.') | map('int') < '2025.5.0'.split('.') | map('int') %} -- Fixed helper deprecations +- Fixed temperature unit for ovens [#248, #328, #344] +- Water heater mode setting [#107] {% endif %} +{% if version_installed.split('.') | map('int') < '2025.5.0'.split('.') | map('int') %} +- Fixed helper deprecations +{% endif %} {% if version_installed.split('.') | map('int') < '2025.2.1'.split('.') | map('int') %} - Fix for #339 {% endif %} - {% if version_installed.split('.') | map('int') < '2025.2.0'.split('.') | map('int') %} - Updated SDK to fix broken types {% endif %} - {% if version_installed.split('.') | map('int') < '0.6.14'.split('.') | map('int') %} -- Bugfix: Error checking socket status [#304] -- Bugfix: Error with setup [#301] -- Bugfix: Logger deprecations +- Error checking socket status [#304] +- Error with setup [#301] +- Logger deprecations {% endif %} - {% if version_installed.split('.') | map('int') < '0.6.13'.split('.') | map('int') %} -- Bugfix: Deprecations [#290] [#297] +- Deprecations [#290] [#297] {% endif %} - {% if version_installed.split('.') | map('int') < '0.6.12'.split('.') | map('int') %} -- Bugfix: Deprecations [#271] +- Deprecations [#271] {% endif %} - {% if version_installed.split('.') | map('int') < '0.6.13'.split('.') | map('int') %} -- Bugfix: Deprecations [#290] [#297] +- Deprecations [#290] [#297] {% endif %} - {% if version_installed.split('.') | map('int') < '0.6.12'.split('.') | map('int') %} -- Bugfix: Deprecations [#271] +- Deprecations [#271] {% endif %} - {% if version_installed.split('.') | map('int') < '0.6.11'.split('.') | map('int') %} -- Bugfix: Fixed convertable drawer issue (#243) -- Bugfix: Updated app types to include electric cooktops (#252) -- Bugfix: Updated clientsession to remove deprecation (#253) -- Bugfix: Fixed error strings -- Bugfix: Updated climate support for new flags introduced in 2024.2.0 +- Fixed convertable drawer issue (#243) +- Updated app types to include electric cooktops (#252) +- Updated clientsession to remove deprecation (#253) +- Fixed error strings +- Updated climate support for new flags introduced in 2024.2.0 {% endif %} - {% if version_installed.split('.') | map('int') < '0.6.10'.split('.') | map('int') %} -- Bugfix: Removed additional deprecated constants (#229) -- Bugfix: Fixed issue with climate entities (#228) +- Removed additional deprecated constants (#229) +- Fixed issue with climate entities (#228) {% endif %} {% if version_installed.split('.') | map('int') < '0.6.9'.split('.') | map('int') %} -- Bugfix: Additional auth stability improvements (#215, #211) -- Bugfix: Removed deprecated constants (#218) +- Additional auth stability improvements (#215, #211) +- Removed deprecated constants (#218) {% endif %} {% if version_installed.split('.') | map('int') < '0.6.8'.split('.') | map('int') %} -- Bugfix: Fixed issue with oven lights (#174) -- Bugfix: Fixed issues with dual dishwasher (#161) -- Bugfix: Fixed disconnection issue (#169) +- Fixed issue with oven lights (#174) +- Fixed issues with dual dishwasher (#161) +- Fixed disconnection issue (#169) {% endif %} {% if version_installed.split('.') | map('int') < '0.6.7'.split('.') | map('int') %} -- Bugfix: fixed issues with dishwasher (#155) +- fixed issues with dishwasher (#155) {% endif %} {% if version_installed.split('.') | map('int') < '0.6.6'.split('.') | map('int') %} From 10bf0e8deeca18b0b4c0d869a897dfad16b69fdc Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sun, 7 Dec 2025 10:30:25 -0500 Subject: [PATCH 337/338] 2025.12.0 (#439) - added additional error handling for new HVAC heating mode logic - added duration for timer entities (resolves #312) - added suggested uom/precision properties (resolves #312) - updated laundry enities to use sugested uom (resolves #312) - set suggested units for timers --- CHANGELOG.md | 5 +++ .../ge_home/devices/advantium.py | 4 +- .../ge_home/devices/dishwasher.py | 2 +- custom_components/ge_home/devices/dryer.py | 4 +- .../ge_home/devices/dual_dishwasher.py | 4 +- custom_components/ge_home/devices/hood.py | 2 +- .../ge_home/devices/microwave.py | 4 +- custom_components/ge_home/devices/oven.py | 8 ++-- custom_components/ge_home/devices/washer.py | 4 +- .../ge_home/devices/washer_dryer.py | 4 +- .../ge_home/devices/water_heater.py | 6 +-- .../ge_home/entities/common/ge_climate.py | 4 +- .../ge_home/entities/common/ge_erd_sensor.py | 40 ++++++++++++++++--- custom_components/ge_home/manifest.json | 2 +- info.md | 8 ++++ 15 files changed, 72 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d07d508..93cec56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # GE Home Appliances (SmartHQ) Changelog +## 2025.12.0 + +- Bugfix: Climate heat mode setting [#433, #435] +- Feature: Changed time-related entities to be durations instead of text [#312] + ## 2025.11.0 - Breaking: changed name of some SAC/WAC entities to have a AC prefix diff --git a/custom_components/ge_home/devices/advantium.py b/custom_components/ge_home/devices/advantium.py index abdc837..8dff3a3 100644 --- a/custom_components/ge_home/devices/advantium.py +++ b/custom_components/ge_home/devices/advantium.py @@ -31,8 +31,8 @@ def get_all_entities(self) -> List[Entity]: GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED), entity_category=EntityCategory.DIAGNOSTIC), GeErdBinarySensor(self, ErdCode.MICROWAVE_REMOTE_ENABLE, entity_category=EntityCategory.DIAGNOSTIC), GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE), entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.ADVANTIUM_KITCHEN_TIME_REMAINING), - GeErdSensor(self, ErdCode.ADVANTIUM_COOK_TIME_REMAINING), + GeErdSensor(self, ErdCode.ADVANTIUM_KITCHEN_TIME_REMAINING, suggested_uom="h"), + GeErdSensor(self, ErdCode.ADVANTIUM_COOK_TIME_REMAINING, suggested_uom="h"), GeAdvantium(self), #Cook Status diff --git a/custom_components/ge_home/devices/dishwasher.py b/custom_components/ge_home/devices/dishwasher.py index 07a8a81..db436c8 100644 --- a/custom_components/ge_home/devices/dishwasher.py +++ b/custom_components/ge_home/devices/dishwasher.py @@ -26,7 +26,7 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "add_rinse_aid", icon_override="mdi:shimmer", entity_category=EntityCategory.DIAGNOSTIC), GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "clean_filter", icon_override="mdi:dishwasher-alert", entity_category=EntityCategory.DIAGNOSTIC), GeErdPropertySensor(self, ErdCode.DISHWASHER_REMINDERS, "sanitized", icon_override="mdi:silverware-clean", entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING), + GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING, suggested_uom="h"), GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS, entity_category=EntityCategory.DIAGNOSTIC), GeErdBinarySensor(self, ErdCode.DISHWASHER_IS_CLEAN), diff --git a/custom_components/ge_home/devices/dryer.py b/custom_components/ge_home/devices/dryer.py index 5709299..8fed732 100644 --- a/custom_components/ge_home/devices/dryer.py +++ b/custom_components/ge_home/devices/dryer.py @@ -24,8 +24,8 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdSensor(self, ErdCode.LAUNDRY_TIME_REMAINING, suggested_uom="min"), + GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING, suggested_uom="h"), GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR, entity_category=EntityCategory.DIAGNOSTIC), GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, icon_on_override="mdi:tumble-dryer", icon_off_override="mdi:tumble-dryer", entity_category=EntityCategory.DIAGNOSTIC), GeErdBinarySensor(self, ErdCode.LAUNDRY_DRYER_BLOCKED_VENT_FAULT, icon_on_override="mdi:alert-circle", icon_off_override="mdi:alert-circle", entity_category=EntityCategory.DIAGNOSTIC), diff --git a/custom_components/ge_home/devices/dual_dishwasher.py b/custom_components/ge_home/devices/dual_dishwasher.py index 8fa0604..92970b7 100644 --- a/custom_components/ge_home/devices/dual_dishwasher.py +++ b/custom_components/ge_home/devices/dual_dishwasher.py @@ -20,7 +20,7 @@ def get_all_entities(self) -> List[Entity]: lower_entities = [ GeErdSensor(self, ErdCode.DISHWASHER_CYCLE_STATE, erd_override="lower_cycle_state", icon_override="mdi:state-machine"), - GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING, erd_override="lower_time_remaining"), + GeErdSensor(self, ErdCode.DISHWASHER_TIME_REMAINING, erd_override="lower_time_remaining", suggested_uom="h"), GeErdBinarySensor(self, ErdCode.DISHWASHER_DOOR_STATUS, erd_override="lower_door_status", entity_category=EntityCategory.DIAGNOSTIC), #Reminders @@ -44,7 +44,7 @@ def get_all_entities(self) -> List[Entity]: upper_entities = [ GeErdSensor(self, ErdCode.DISHWASHER_UPPER_CYCLE_STATE, erd_override="upper_cycle_state", icon_override="mdi:state-machine"), - GeErdSensor(self, ErdCode.DISHWASHER_UPPER_TIME_REMAINING, erd_override="upper_time_remaining"), + GeErdSensor(self, ErdCode.DISHWASHER_UPPER_TIME_REMAINING, erd_override="upper_time_remaining", suggested_uom="h"), GeErdBinarySensor(self, ErdCode.DISHWASHER_UPPER_DOOR_STATUS, erd_override="upper_door_status", entity_category=EntityCategory.DIAGNOSTIC), #Reminders diff --git a/custom_components/ge_home/devices/hood.py b/custom_components/ge_home/devices/hood.py index ed7daac..2c03b1e 100644 --- a/custom_components/ge_home/devices/hood.py +++ b/custom_components/ge_home/devices/hood.py @@ -61,7 +61,7 @@ def get_all_entities(self) -> List[Entity]: #timer if timer_availability is not None: - hood_entities.append(GeErdTimerSensor(self, ErdCode.HOOD_TIMER)) + hood_entities.append(GeErdTimerSensor(self, ErdCode.HOOD_TIMER, suggested_uom="h")) entities = base_entities + hood_entities return entities diff --git a/custom_components/ge_home/devices/microwave.py b/custom_components/ge_home/devices/microwave.py index 43471ff..6f43079 100644 --- a/custom_components/ge_home/devices/microwave.py +++ b/custom_components/ge_home/devices/microwave.py @@ -41,8 +41,8 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "cook_mode", icon_override="mdi:food-turkey", entity_category=EntityCategory.DIAGNOSTIC), GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "power_level", icon_override="mdi:gauge", entity_category=EntityCategory.DIAGNOSTIC), GeErdPropertySensor(self, ErdCode.MICROWAVE_STATE, "temperature", icon_override="mdi:thermometer", entity_category=EntityCategory.DIAGNOSTIC), - GeErdTimerSensor(self, ErdCode.MICROWAVE_COOK_TIMER), - GeErdTimerSensor(self, ErdCode.MICROWAVE_KITCHEN_TIMER) + GeErdTimerSensor(self, ErdCode.MICROWAVE_COOK_TIMER, suggested_uom="min"), + GeErdTimerSensor(self, ErdCode.MICROWAVE_KITCHEN_TIMER, suggested_uom="h") ] if fan_availability and fan_availability.is_available: diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index 81188bb..ee9d99e 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -68,8 +68,8 @@ def get_all_entities(self) -> List[Entity]: oven_entities.extend([ GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE, entity_category=EntityCategory.DIAGNOSTIC), GeErdSensor(self, ErdCode.LOWER_OVEN_CURRENT_STATE, entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING), - GeErdTimerSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER), + GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING, suggested_uom="h"), + GeErdTimerSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER, suggested_uom="h"), GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET, entity_category=EntityCategory.DIAGNOSTIC), GeErdSensor(self, ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), GeErdBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED, entity_category=EntityCategory.DIAGNOSTIC), @@ -88,8 +88,8 @@ def get_all_entities(self) -> List[Entity]: oven_entities.extend([ GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), GeErdSensor(self, ErdCode.UPPER_OVEN_CURRENT_STATE, self._single_name(ErdCode.UPPER_OVEN_CURRENT_STATE, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, not oven_config.has_lower_oven)), - GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER, not oven_config.has_lower_oven)), + GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, not oven_config.has_lower_oven), suggested_uom="h"), + GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER, not oven_config.has_lower_oven), suggested_uom="h"), GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, self._single_name(ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), diff --git a/custom_components/ge_home/devices/washer.py b/custom_components/ge_home/devices/washer.py index 6d25595..82cce7c 100644 --- a/custom_components/ge_home/devices/washer.py +++ b/custom_components/ge_home/devices/washer.py @@ -25,8 +25,8 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdSensor(self, ErdCode.LAUNDRY_TIME_REMAINING, suggested_uom="min"), + GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING, suggested_uom="h"), GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR, entity_category=EntityCategory.DIAGNOSTIC), GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, entity_category=EntityCategory.DIAGNOSTIC), ] diff --git a/custom_components/ge_home/devices/washer_dryer.py b/custom_components/ge_home/devices/washer_dryer.py index 73187ec..cad0d13 100644 --- a/custom_components/ge_home/devices/washer_dryer.py +++ b/custom_components/ge_home/devices/washer_dryer.py @@ -23,8 +23,8 @@ def get_all_entities(self) -> List[Entity]: 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), + GeErdSensor(self, ErdCode.LAUNDRY_TIME_REMAINING, suggested_uom="min"), + GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING, suggested_uom="h"), GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR, entity_category=EntityCategory.DIAGNOSTIC), GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, entity_category=EntityCategory.DIAGNOSTIC), ] diff --git a/custom_components/ge_home/devices/water_heater.py b/custom_components/ge_home/devices/water_heater.py index 3fc0c05..7ff9524 100644 --- a/custom_components/ge_home/devices/water_heater.py +++ b/custom_components/ge_home/devices/water_heater.py @@ -34,9 +34,9 @@ def get_all_entities(self) -> List[Entity]: wh_entities = [ GeErdSensor(self, ErdCode.WH_HEATER_TARGET_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), GeErdSensor(self, ErdCode.WH_HEATER_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.WH_HEATER_MODE_HOURS_REMAINING, entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.WH_HEATER_ELECTRIC_MODE_MAX_TIME, entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.WH_HEATER_VACATION_MODE_MAX_TIME, entity_category=EntityCategory.DIAGNOSTIC), + GeErdSensor(self, ErdCode.WH_HEATER_MODE_HOURS_REMAINING, entity_category=EntityCategory.DIAGNOSTIC, suggested_uom="h"), + GeErdSensor(self, ErdCode.WH_HEATER_ELECTRIC_MODE_MAX_TIME, entity_category=EntityCategory.DIAGNOSTIC, suggested_uom="h"), + GeErdSensor(self, ErdCode.WH_HEATER_VACATION_MODE_MAX_TIME, entity_category=EntityCategory.DIAGNOSTIC, suggested_uom="h"), GeWaterHeater(self) ] diff --git a/custom_components/ge_home/entities/common/ge_climate.py b/custom_components/ge_home/entities/common/ge_climate.py index 990738a..06aa63f 100644 --- a/custom_components/ge_home/entities/common/ge_climate.py +++ b/custom_components/ge_home/entities/common/ge_climate.py @@ -55,6 +55,8 @@ def __init__( self._fan_mode_erd_code = api.appliance.translate_erd_code(fan_mode_erd_code) self._target_heating_temperature_erd_code = api.appliance.translate_erd_code(target_heating_temperature_erd_code) + self._has_heat_erd_code = api.try_get_erd_value(self._target_heating_temperature_erd_code) is not None + @cached_property def unique_id(self) -> str: return f"{DOMAIN}_{self.serial_or_mac}_climate" @@ -77,7 +79,7 @@ def power_status_erd_code(self): @property def target_temperature_erd_code(self): - if self.hvac_mode == HVACMode.HEAT: + if self.hvac_mode == HVACMode.HEAT and self._has_heat_erd_code: return self._target_heating_temperature_erd_code return self._target_temperature_erd_code diff --git a/custom_components/ge_home/entities/common/ge_erd_sensor.py b/custom_components/ge_home/entities/common/ge_erd_sensor.py index 1f4f009..b6a9249 100644 --- a/custom_components/ge_home/entities/common/ge_erd_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_sensor.py @@ -1,4 +1,5 @@ import logging +from datetime import timedelta from propcache.api import cached_property from typing import Optional @@ -24,13 +25,17 @@ def __init__( state_class_override: Optional[str] = None, uom_override: Optional[str] = None, data_type_override: Optional[ErdDataType] = None, - entity_category: Optional[EntityCategory] = None + entity_category: Optional[EntityCategory] = None, + suggested_uom: Optional[str] = None, + suggested_precision: Optional[int] = None ): super().__init__(api, erd_code, erd_override, icon_override, device_class_override, entity_category) self._uom_override = uom_override self._state_class_override = state_class_override self._data_type_override = data_type_override - + self._suggested_uom = suggested_uom + self._suggested_precision = suggested_precision + @property def icon(self) ->str | None: # type: ignore return super().icon @@ -48,17 +53,28 @@ def native_value(self) -> str | int | float | None: # type: ignore if self._data_type in [ErdDataType.INT, ErdDataType.FLOAT]: return self._convert_numeric_value_from_device(value) + if self._data_type == ErdDataType.TIMESPAN: + return self._convert_timespan_value_from_device(value) + # otherwise, return a stringified version # TODO: perhaps enhance so that there's a list of variables available # for the stringify function to consume... return self._stringify(value, temp_units=self._temp_units) - except KeyError: + except (KeyError, ValueError): return None @cached_property def native_unit_of_measurement(self) -> Optional[str]: return self._get_uom() + @cached_property + def suggested_unit_of_measurement(self) -> Optional[str]: + return self._suggested_uom + + @cached_property + def suggested_display_precision(self) -> Optional[int]: + return self._suggested_precision + @cached_property def state_class(self) -> Optional[str]: return self._get_state_class() @@ -94,13 +110,21 @@ def _temp_units(self) -> Optional[str]: # return UnitOfTemperature.CELSIUS #return UnitOfTemperature.FAHRENHEIT + def _convert_timespan_value_from_device(self, value): + """Convert to expected data type""" + + if value is None: + return 0 + if not isinstance(value, timedelta): + raise ValueError(f"Expected timedelta, got {type(value)}") + return value.total_seconds() + def _convert_numeric_value_from_device(self, value): """Convert to expected data type""" if self._data_type == ErdDataType.INT: return int(round(value)) - else: - return value + return value def _get_uom(self): """Select appropriate units""" @@ -137,6 +161,8 @@ def _get_uom(self): #if self._measurement_system == ErdMeasurementUnits.METRIC: # return "l" return "gal" + if self.erd_code_class == ErdCodeClass.TIMER: + return "s" return None def _get_device_class(self) -> Optional[str]: @@ -155,7 +181,9 @@ def _get_device_class(self) -> Optional[str]: return SensorDeviceClass.ENERGY if self.erd_code_class == ErdCodeClass.HUMIDITY: return SensorDeviceClass.HUMIDITY - + if self.erd_code_class == ErdCodeClass.TIMER: + return SensorDeviceClass.DURATION + return None def _get_state_class(self) -> Optional[str]: diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 723352f..8a63bbb 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://github.com/simbaja/ha_gehome", "requirements": ["gehomesdk==2025.11.5","magicattr==0.1.6"], "codeowners": ["@simbaja"], - "version": "2025.11.0" + "version": "2025.12.0" } diff --git a/info.md b/info.md index 5c2a86d..569397f 100644 --- a/info.md +++ b/info.md @@ -95,6 +95,10 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '2025.12.0'.split('.') | map('int') %} +- Changed time-related entities to be durations instead of text [#312] +{% endif %} + {% if version_installed.split('.') | map('int') < '2025.11.0'.split('.') | map('int') %} - Added heat mode for Window ACs - Added support for Advantium @@ -180,6 +184,10 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '2025.11.0'.split('.') | map('int') %} +- Climate heat mode setting [#433, #435] +{% endif %} + {% if version_installed.split('.') | map('int') < '2025.5.0'.split('.') | map('int') %} - Fixed temperature unit for ovens [#248, #328, #344] - Water heater mode setting [#107] From 60d82fb816806867dbcdb05579f56049d371dcf1 Mon Sep 17 00:00:00 2001 From: simbaja <59273948+simbaja@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:43:18 -0500 Subject: [PATCH 338/338] 2026.2.0 (#468) * Cooktop Sensor - fixes #440 (#454) * add cooktop binary sensor * fix: use the GeCooktopStatusBinarySensor * fix: aggregate burner status * fix: prioritize reading ErdCode.COOKTOP_STATUS_EXT * fix: indentation * Change mode names for Haier water heaters (#442) * change mode names for Haier water heaters * adapt to spelling mistake in gehomesdk (erd_brand.py) * Add 'DRY' mode to HVAC options and mappings (#441) Adds dry mode to window ACs * Add GeWasherCycleButton to WasherDryerApi (#462) Added the capability to Remote Start WasherDryer Combos, using the existing Washer button entity * Fix: Persist ApplianceApis on reconnect to prevent duplicate entities (#464) This commit addresses an issue where GE Home entities would stop updating and generate 'Unique ID already exists' errors after a network disconnection. Root Cause: Previously, the integration cleared its internal device list upon any connection drop. When the client reconnected, new device objects were created, but existing Home Assistant entities remained linked to the old (destroyed) objects. This caused orphans and unique ID conflicts. Fix: - Modified _reset_sync_state() to PRESERVE the device list on disconnect, allowing existing entities to reconnect. - Moved the device list clearing to async_reset() for proper cleanup on unload. - Updated _async_remove_stale_devices to prune stale devices by cross-referencing against the authoritative cloud roster, preventing 'zombie' devices. Verification: Verified using a standalone reproduction script with mocked components. The simulation confirmed that device objects persist across reconnections and stale devices are correctly removed. * Add DishDrawer User Setting wifi_enabled (read only) (#463) * add wifi_enabled The erd for remote command availability does not appear to be used as it is always available. Remote start is enabled by a button on each drawer. * Update dual_dishwasher.py * import GeErdPropertyBinarySensor * Update dual_dishwasher.py * Update dual_dishwasher.py * Update dual_dishwasher.py * Update dual_dishwasher.py * Update dual_dishwasher.py * Update dual_dishwasher.py * Update ge_erd_property_binary_sensor.py * Update ge_erd_property_binary_sensor.py * Update ge_erd_property_binary_sensor.py * fix import GeErdPropertyBinarySensor * - bumped the version - fixed an issue with the cooktop status binary sensor * - made LAUNDRY_MACHINE_STATE diagnostic on all appliances (resolves #447) * - updated documentation --------- Co-authored-by: Eric Levicky Co-authored-by: Dan1l1 Co-authored-by: PCPSHA Co-authored-by: andyjohnschneider Co-authored-by: Tan Vachiramon Co-authored-by: sgraystar <49742396+sgraystar@users.noreply.github.com> --- CHANGELOG.md | 18 +- custom_components/ge_home/devices/cooktop.py | 66 +++- .../ge_home/devices/dual_dishwasher.py | 16 +- custom_components/ge_home/devices/oven.py | 357 ++++++++++++++---- custom_components/ge_home/devices/washer.py | 2 +- .../ge_home/devices/washer_dryer.py | 4 +- .../ge_home/entities/ac/ge_wac_climate.py | 4 +- .../ge_home/entities/common/__init__.py | 3 +- .../common/ge_cooktop_status_binary_sensor.py | 33 ++ .../common/ge_erd_property_binary_sensor.py | 13 +- .../entities/water_heater/ge_water_heater.py | 2 +- .../entities/water_heater/heater_modes.py | 98 ++++- custom_components/ge_home/manifest.json | 4 +- .../ge_home/update_coordinator.py | 21 +- info.md | 16 + 15 files changed, 530 insertions(+), 127 deletions(-) create mode 100644 custom_components/ge_home/entities/common/ge_cooktop_status_binary_sensor.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 93cec56..e5e36d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # GE Home Appliances (SmartHQ) Changelog +## 2026.2.0 + +- Feature: Added DRY mode to HVAC options and mappings [#441] +- Feature: Added GeWasherCycleButton to WasherDryerApi [#462] +- Feature: Added DishDrawer User Setting wifi_enabled (read only) [#463] +- Change: Changed mode names for Haier water heaters [#442] +- Change: Made LAUNDRY_MACHINE_STATE diagnostic on all appliances [#447] +- Bugfix: Cooktop Sensor fixes [#440, #454] +- Bugfix: Persist ApplianceApis on reconnect to prevent duplicate entities [#464] + ## 2025.12.0 - Bugfix: Climate heat mode setting [#433, #435] @@ -8,7 +18,7 @@ ## 2025.11.0 -- Breaking: changed name of some SAC/WAC entities to have a AC prefix +- Breaking: changed name of some SAC/WAC entities to have a AC prefix - Feature: Added heat mode for Window ACs - Feature: Added support for Advantium - Feature: Brand inference and stale device cleanup @@ -58,7 +68,7 @@ ## 0.6.13 -- Bugfix: Deprecations [#290] [#297] +- Bugfix: Deprecations [#290] [#297] ## 0.6.12 @@ -93,7 +103,6 @@ - Bugfix: Fixed issues with dual dishwasher [#161] - Bugfix: Fixed disconnection issue [#169] - ## 0.6.7 - Bugfix: fixed issues with dishwasher [#155] @@ -152,6 +161,7 @@ - Fixed issue with ovens when raw temperature not available (@chadohalloran) - Fixed issue where Split A/C temperature sensors report UOM incorrectly (@RobertusIT) - Added convertable drawer mode, proximity light, and interior lights to fridge (@groto27, @elwing00) + ## 0.4.3 - Enabled support for appliances without serial numbers @@ -183,4 +193,4 @@ ## 0.3.12 -- Initial tracked version \ No newline at end of file +- Initial tracked version diff --git a/custom_components/ge_home/devices/cooktop.py b/custom_components/ge_home/devices/cooktop.py index f4b08d2..7a81cf0 100644 --- a/custom_components/ge_home/devices/cooktop.py +++ b/custom_components/ge_home/devices/cooktop.py @@ -7,22 +7,24 @@ from gehomesdk import ( ErdCode, ErdDataType, - ErdApplianceType, - ErdCooktopConfig, - CooktopStatus + ErdApplianceType, + ErdCooktopConfig, + CooktopStatus, ) from .base import ApplianceApi from ..entities import ( - GeErdBinarySensor, + GeCooktopStatusBinarySensor, GeErdPropertySensor, - GeErdPropertyBinarySensor + GeErdPropertyBinarySensor, ) _LOGGER = logging.getLogger(__name__) + class CooktopApi(ApplianceApi): """API class for cooktop objects""" + APPLIANCE_TYPE = ErdApplianceType.COOKTOP def get_all_entities(self) -> List[Entity]: @@ -30,34 +32,60 @@ def get_all_entities(self) -> List[Entity]: cooktop_config = ErdCooktopConfig.NONE if self.has_erd_code(ErdCode.COOKTOP_CONFIG): - cooktop_config: ErdCooktopConfig = self.appliance.get_erd_value(ErdCode.COOKTOP_CONFIG) + cooktop_config: ErdCooktopConfig = self.appliance.get_erd_value( + ErdCode.COOKTOP_CONFIG + ) _LOGGER.debug(f"Cooktop Config: {cooktop_config}") cooktop_entities = [] if cooktop_config == ErdCooktopConfig.PRESENT: - # attempt to get the cooktop status using legacy status - cooktop_status_erd = ErdCode.COOKTOP_STATUS - cooktop_status: CooktopStatus | None = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) + # attempt to get cooktop status, preferring extended data when present + cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT + cooktop_status: CooktopStatus | None = self.try_get_erd_value( + ErdCode.COOKTOP_STATUS_EXT + ) - # if we didn't get it, try using the new version + # if we didn't get it, fall back to the legacy status code if cooktop_status is None: - cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT - cooktop_status = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) + cooktop_status_erd = ErdCode.COOKTOP_STATUS + cooktop_status = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) # if we got a status through either mechanism, we can add the entities if cooktop_status is not None: - cooktop_entities.append(GeErdBinarySensor(self, cooktop_status_erd)) - - for (k, v) in cooktop_status.burners.items(): + cooktop_entities.append( + GeCooktopStatusBinarySensor(self, cooktop_status_erd) + ) + + for k, v in cooktop_status.burners.items(): if v.exists: prop = self._camel_to_snake(k) - cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on")) - cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized", entity_category=EntityCategory.DIAGNOSTIC)) + cooktop_entities.append( + GeErdPropertyBinarySensor( + self, cooktop_status_erd, prop + ".on" + ) + ) + cooktop_entities.append( + GeErdPropertyBinarySensor( + self, + cooktop_status_erd, + prop + ".synchronized", + entity_category=EntityCategory.DIAGNOSTIC, + ) + ) if not v.on_off_only: - cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) + cooktop_entities.append( + GeErdPropertySensor( + self, + cooktop_status_erd, + prop + ".power_pct", + icon_override="mdi:fire", + device_class_override=SensorDeviceClass.POWER_FACTOR, + data_type_override=ErdDataType.INT, + ) + ) return base_entities + cooktop_entities def _camel_to_snake(self, s): - return ''.join(['_'+c.lower() if c.isupper() else c for c in s]).lstrip('_') + return "".join(["_" + c.lower() if c.isupper() else c for c in s]).lstrip("_") diff --git a/custom_components/ge_home/devices/dual_dishwasher.py b/custom_components/ge_home/devices/dual_dishwasher.py index 92970b7..11844fd 100644 --- a/custom_components/ge_home/devices/dual_dishwasher.py +++ b/custom_components/ge_home/devices/dual_dishwasher.py @@ -6,14 +6,14 @@ from gehomesdk import ErdCode, ErdApplianceType, ErdRemoteCommand from .base import ApplianceApi -from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeDishwasherCommandButton +from ..entities import GeErdSensor, GeErdBinarySensor, GeErdPropertySensor, GeErdPropertyBinarySensor, GeDishwasherCommandButton _LOGGER = logging.getLogger(__name__) class DualDishwasherApi(ApplianceApi): """API class for dual dishwasher objects""" - APPLIANCE_TYPE = ErdApplianceType.DISH_WASHER + APPLIANCE_TYPE = ErdApplianceType.DUAL_DISH_WASHER def get_all_entities(self) -> List[Entity]: base_entities = super().get_all_entities() @@ -66,22 +66,22 @@ def get_all_entities(self) -> List[Entity]: GeErdPropertySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "delay_hours", erd_override="upper_setting", icon_override="mdi:clock-fast", entity_category=EntityCategory.DIAGNOSTIC) ] - # check for remote command availability and add if present (lower) - if self.has_erd_code(ErdCode.DISHWASHER_REMOTE_START_ENABLE): + # Remote commands are always supported, enabled by a physical button per tub, disabled when the tub is opened (lower) + if True: lower_entities.extend( [ - GeErdBinarySensor(self, ErdCode.DISHWASHER_REMOTE_START_ENABLE, erd_override="lower_remote_command_enable", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertyBinarySensor(self, ErdCode.DISHWASHER_USER_SETTING, "wifi_enabled", erd_override="lower_remote_command_enable", icon_off_override="mdi:wifi-off", icon_on_override="mdi:wifi"), GeDishwasherCommandButton(self, ErdCode.DISHWASHER_REMOTE_START_COMMAND, ErdRemoteCommand.START_RESUME, erd_override="lower_remote_command"), GeDishwasherCommandButton(self, ErdCode.DISHWASHER_REMOTE_START_COMMAND, ErdRemoteCommand.PAUSE, erd_override="lower_remote_command"), GeDishwasherCommandButton(self, ErdCode.DISHWASHER_REMOTE_START_COMMAND, ErdRemoteCommand.CANCEL, erd_override="lower_remote_command") ] ) - # check for remote command availability and add if present (upper) - if self.has_erd_code(ErdCode.DISHWASHER_UPPER_REMOTE_START_ENABLE): + # Remote commands are always supported, enabled by a physical button per tub, disabled when the tub is opened (upper) + if True: upper_entities.extend( [ - GeErdBinarySensor(self, ErdCode.DISHWASHER_UPPER_REMOTE_START_ENABLE, erd_override="upper_remote_command_enable", entity_category=EntityCategory.DIAGNOSTIC), + GeErdPropertyBinarySensor(self, ErdCode.DISHWASHER_UPPER_USER_SETTING, "wifi_enabled", erd_override="upper_remote_command_enable", icon_off_override="mdi:wifi-off", icon_on_override="mdi:wifi"), GeDishwasherCommandButton(self, ErdCode.DISHWASHER_UPPER_REMOTE_START_COMMAND, ErdRemoteCommand.START_RESUME, erd_override="upper_remote_command"), GeDishwasherCommandButton(self, ErdCode.DISHWASHER_UPPER_REMOTE_START_COMMAND, ErdRemoteCommand.PAUSE, erd_override="upper_remote_command"), GeDishwasherCommandButton(self, ErdCode.DISHWASHER_UPPER_REMOTE_START_COMMAND, ErdRemoteCommand.CANCEL, erd_override="upper_remote_command") diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index ee9d99e..bdd0c4c 100644 --- a/custom_components/ge_home/devices/oven.py +++ b/custom_components/ge_home/devices/oven.py @@ -5,59 +5,88 @@ from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.entity import Entity from gehomesdk import ( - ErdCode, + ErdCode, ErdDataType, - ErdApplianceType, - OvenConfiguration, - ErdCooktopConfig, + ErdApplianceType, + OvenConfiguration, + ErdCooktopConfig, CooktopStatus, ErdOvenLightLevel, ErdOvenLightLevelAvailability, - ErdOvenWarmingState + ErdOvenWarmingState, ) from .base import ApplianceApi from ..entities import ( - GeErdSensor, + GeErdSensor, GeErdTimerSensor, - GeErdBinarySensor, + GeErdBinarySensor, + GeCooktopStatusBinarySensor, GeErdPropertySensor, GeErdPropertyBinarySensor, - GeOven, + GeOven, GeOvenLightLevelSelect, GeOvenWarmingStateSelect, - UPPER_OVEN, - LOWER_OVEN + 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) + 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) + cooktop_config: ErdCooktopConfig = self.appliance.get_erd_value( + ErdCode.COOKTOP_CONFIG + ) - has_upper_raw_temperature = self.has_erd_code(ErdCode.UPPER_OVEN_RAW_TEMPERATURE) - has_lower_raw_temperature = self.has_erd_code(ErdCode.LOWER_OVEN_RAW_TEMPERATURE) + has_upper_raw_temperature = self.has_erd_code( + ErdCode.UPPER_OVEN_RAW_TEMPERATURE + ) + has_lower_raw_temperature = self.has_erd_code( + ErdCode.LOWER_OVEN_RAW_TEMPERATURE + ) - has_upper_probe_temperature = self.has_erd_code(ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP) - has_lower_probe_temperature = self.has_erd_code(ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP) + has_upper_probe_temperature = self.has_erd_code( + ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP + ) + has_lower_probe_temperature = self.has_erd_code( + ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP + ) - upper_light: ErdOvenLightLevel | None = self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT) - upper_light_availability: ErdOvenLightLevelAvailability | None = self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT_AVAILABILITY) - lower_light: ErdOvenLightLevel | None = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT) - lower_light_availability: ErdOvenLightLevelAvailability | None = self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT_AVAILABILITY) + upper_light: ErdOvenLightLevel | None = self.try_get_erd_value( + ErdCode.UPPER_OVEN_LIGHT + ) + upper_light_availability: ErdOvenLightLevelAvailability | None = ( + self.try_get_erd_value(ErdCode.UPPER_OVEN_LIGHT_AVAILABILITY) + ) + lower_light: ErdOvenLightLevel | None = self.try_get_erd_value( + ErdCode.LOWER_OVEN_LIGHT + ) + lower_light_availability: ErdOvenLightLevelAvailability | None = ( + self.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT_AVAILABILITY) + ) - upper_warm_drawer : ErdOvenWarmingState | None = self.try_get_erd_value(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE) - lower_warm_drawer : ErdOvenWarmingState | None = self.try_get_erd_value(ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE) - warm_drawer : ErdOvenWarmingState | None = self.try_get_erd_value(ErdCode.WARMING_DRAWER_STATE) + upper_warm_drawer: ErdOvenWarmingState | None = self.try_get_erd_value( + ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE + ) + lower_warm_drawer: ErdOvenWarmingState | None = self.try_get_erd_value( + ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE + ) + warm_drawer: ErdOvenWarmingState | None = self.try_get_erd_value( + ErdCode.WARMING_DRAWER_STATE + ) _LOGGER.debug(f"Oven Config: {oven_config}") _LOGGER.debug(f"Cooktop Config: {cooktop_config}") @@ -65,83 +94,265 @@ def get_all_entities(self) -> List[Entity]: cooktop_entities = [] if oven_config.has_lower_oven: - oven_entities.extend([ - GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE, entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.LOWER_OVEN_CURRENT_STATE, entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING, suggested_uom="h"), - GeErdTimerSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER, suggested_uom="h"), - GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET, entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC), - GeErdBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED, entity_category=EntityCategory.DIAGNOSTIC), - - GeOven(self, LOWER_OVEN, True, self._temperature_code(has_lower_raw_temperature)) - ]) + oven_entities.extend( + [ + GeErdSensor( + self, + ErdCode.LOWER_OVEN_COOK_MODE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + GeErdSensor( + self, + ErdCode.LOWER_OVEN_CURRENT_STATE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + GeErdSensor( + self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING, suggested_uom="h" + ), + GeErdTimerSensor( + self, ErdCode.LOWER_OVEN_KITCHEN_TIMER, suggested_uom="h" + ), + GeErdSensor( + self, + ErdCode.LOWER_OVEN_USER_TEMP_OFFSET, + entity_category=EntityCategory.DIAGNOSTIC, + ), + GeErdSensor( + self, + ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + GeErdBinarySensor( + self, + ErdCode.LOWER_OVEN_REMOTE_ENABLED, + entity_category=EntityCategory.DIAGNOSTIC, + ), + GeOven( + self, + LOWER_OVEN, + True, + self._temperature_code(has_lower_raw_temperature), + ), + ] + ) if has_lower_raw_temperature: - oven_entities.append(GeErdSensor(self, ErdCode.LOWER_OVEN_RAW_TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC)) - 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)) + oven_entities.append( + GeErdSensor( + self, + ErdCode.LOWER_OVEN_RAW_TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + ) + ) + if ( + lower_light_availability is None + or lower_light_availability.is_available + or lower_light is not None + ): + oven_entities.append( + GeOvenLightLevelSelect(self, ErdCode.LOWER_OVEN_LIGHT) + ) if lower_warm_drawer is not None: - oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE)) + oven_entities.append( + GeOvenWarmingStateSelect( + self, ErdCode.LOWER_OVEN_WARMING_DRAWER_STATE + ) + ) if has_lower_probe_temperature: - oven_entities.append(GeErdSensor(self, ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP, entity_category=EntityCategory.DIAGNOSTIC)) - - oven_entities.extend([ - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE, self._single_name(ErdCode.UPPER_OVEN_COOK_MODE, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.UPPER_OVEN_CURRENT_STATE, self._single_name(ErdCode.UPPER_OVEN_CURRENT_STATE, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, self._single_name(ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, not oven_config.has_lower_oven), suggested_uom="h"), - GeErdTimerSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER, self._single_name(ErdCode.UPPER_OVEN_KITCHEN_TIMER, not oven_config.has_lower_oven), suggested_uom="h"), - GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, self._single_name(ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), - GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, self._single_name(ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED, self._single_name(ErdCode.UPPER_OVEN_REMOTE_ENABLED, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC), - - GeOven(self, UPPER_OVEN, False, self._temperature_code(has_upper_raw_temperature)) - ]) + oven_entities.append( + GeErdSensor( + self, + ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP, + entity_category=EntityCategory.DIAGNOSTIC, + ) + ) + + oven_entities.extend( + [ + GeErdSensor( + self, + ErdCode.UPPER_OVEN_COOK_MODE, + self._single_name( + ErdCode.UPPER_OVEN_COOK_MODE, not oven_config.has_lower_oven + ), + entity_category=EntityCategory.DIAGNOSTIC, + ), + GeErdSensor( + self, + ErdCode.UPPER_OVEN_CURRENT_STATE, + self._single_name( + ErdCode.UPPER_OVEN_CURRENT_STATE, not oven_config.has_lower_oven + ), + entity_category=EntityCategory.DIAGNOSTIC, + ), + GeErdSensor( + self, + ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, + self._single_name( + ErdCode.UPPER_OVEN_COOK_TIME_REMAINING, + not oven_config.has_lower_oven, + ), + suggested_uom="h", + ), + GeErdTimerSensor( + self, + ErdCode.UPPER_OVEN_KITCHEN_TIMER, + self._single_name( + ErdCode.UPPER_OVEN_KITCHEN_TIMER, not oven_config.has_lower_oven + ), + suggested_uom="h", + ), + GeErdSensor( + self, + ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, + self._single_name( + ErdCode.UPPER_OVEN_USER_TEMP_OFFSET, + not oven_config.has_lower_oven, + ), + entity_category=EntityCategory.DIAGNOSTIC, + ), + GeErdSensor( + self, + ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, + self._single_name( + ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE, + not oven_config.has_lower_oven, + ), + entity_category=EntityCategory.DIAGNOSTIC, + ), + GeErdBinarySensor( + self, + ErdCode.UPPER_OVEN_REMOTE_ENABLED, + self._single_name( + ErdCode.UPPER_OVEN_REMOTE_ENABLED, + not oven_config.has_lower_oven, + ), + entity_category=EntityCategory.DIAGNOSTIC, + ), + 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, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC)) - 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, not oven_config.has_lower_oven))) + oven_entities.append( + GeErdSensor( + self, + ErdCode.UPPER_OVEN_RAW_TEMPERATURE, + self._single_name( + ErdCode.UPPER_OVEN_RAW_TEMPERATURE, + not oven_config.has_lower_oven, + ), + entity_category=EntityCategory.DIAGNOSTIC, + ) + ) + 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, not oven_config.has_lower_oven + ), + ) + ) if upper_warm_drawer is not None: - oven_entities.append(GeOvenWarmingStateSelect(self, ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, self._single_name(ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, not oven_config.has_lower_oven))) + oven_entities.append( + GeOvenWarmingStateSelect( + self, + ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, + self._single_name( + ErdCode.UPPER_OVEN_WARMING_DRAWER_STATE, + not oven_config.has_lower_oven, + ), + ) + ) if has_upper_probe_temperature: - oven_entities.append(GeErdSensor(self, ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, self._single_name(ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, not oven_config.has_lower_oven), entity_category=EntityCategory.DIAGNOSTIC)) + oven_entities.append( + GeErdSensor( + self, + ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, + self._single_name( + ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP, + not oven_config.has_lower_oven, + ), + entity_category=EntityCategory.DIAGNOSTIC, + ) + ) if oven_config.has_warming_drawer and warm_drawer is not None: - oven_entities.append(GeErdSensor(self, ErdCode.WARMING_DRAWER_STATE, entity_category=EntityCategory.DIAGNOSTIC)) + oven_entities.append( + GeErdSensor( + self, + ErdCode.WARMING_DRAWER_STATE, + entity_category=EntityCategory.DIAGNOSTIC, + ) + ) if cooktop_config == ErdCooktopConfig.PRESENT: - # attempt to get the cooktop status using legacy status - cooktop_status_erd = ErdCode.COOKTOP_STATUS - cooktop_status: CooktopStatus | None = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) + # attempt to get cooktop status, preferring extended data when present + cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT + cooktop_status: CooktopStatus | None = self.try_get_erd_value( + ErdCode.COOKTOP_STATUS_EXT + ) - # if we didn't get it, try using the new version + # if we didn't get it, fall back to the legacy status code if cooktop_status is None: - cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT - cooktop_status = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT) + cooktop_status_erd = ErdCode.COOKTOP_STATUS + cooktop_status = self.try_get_erd_value(ErdCode.COOKTOP_STATUS) # if we got a status through either mechanism, we can add the entities if cooktop_status is not None: - cooktop_entities.append(GeErdBinarySensor(self, cooktop_status_erd)) - - for (k, v) in cooktop_status.burners.items(): + cooktop_entities.append( + GeCooktopStatusBinarySensor(self, cooktop_status_erd) + ) + + for k, v in cooktop_status.burners.items(): if v.exists: prop = self._camel_to_snake(k) - cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on")) - cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized", entity_category=EntityCategory.DIAGNOSTIC)) + cooktop_entities.append( + GeErdPropertyBinarySensor( + self, cooktop_status_erd, prop + ".on" + ) + ) + cooktop_entities.append( + GeErdPropertyBinarySensor( + self, + cooktop_status_erd, + prop + ".synchronized", + entity_category=EntityCategory.DIAGNOSTIC, + ) + ) if not v.on_off_only: - cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT)) + cooktop_entities.append( + GeErdPropertySensor( + self, + cooktop_status_erd, + prop + ".power_pct", + icon_override="mdi:fire", + device_class_override=SensorDeviceClass.POWER_FACTOR, + data_type_override=ErdDataType.INT, + ) + ) return base_entities + oven_entities + cooktop_entities def _single_name(self, erd_code: ErdCode, make_single: bool): name = erd_code.name - + if make_single: - name = name.replace(UPPER_OVEN+"_","") - + name = name.replace(UPPER_OVEN + "_", "") + return name.replace("_", " ").title() def _camel_to_snake(self, s): - return ''.join(['_'+c.lower() if c.isupper() else c for c in s]).lstrip('_') + return "".join(["_" + c.lower() if c.isupper() else c for c in s]).lstrip("_") def _temperature_code(self, has_raw: bool): return "RAW_TEMPERATURE" if has_raw else "DISPLAY_TEMPERATURE" diff --git a/custom_components/ge_home/devices/washer.py b/custom_components/ge_home/devices/washer.py index 82cce7c..810f059 100644 --- a/custom_components/ge_home/devices/washer.py +++ b/custom_components/ge_home/devices/washer.py @@ -21,7 +21,7 @@ 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_MACHINE_STATE, entity_category=EntityCategory.DIAGNOSTIC), 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), diff --git a/custom_components/ge_home/devices/washer_dryer.py b/custom_components/ge_home/devices/washer_dryer.py index cad0d13..af9195e 100644 --- a/custom_components/ge_home/devices/washer_dryer.py +++ b/custom_components/ge_home/devices/washer_dryer.py @@ -8,6 +8,7 @@ from .washer import WasherApi from .dryer import DryerApi from ..entities import GeErdSensor, GeErdBinarySensor +from ..entities.laundry.ge_washer_cycle_button import GeWasherCycleButton _LOGGER = logging.getLogger(__name__) @@ -19,7 +20,7 @@ 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_MACHINE_STATE, entity_category=EntityCategory.DIAGNOSTIC), GeErdSensor(self, ErdCode.LAUNDRY_CYCLE), GeErdSensor(self, ErdCode.LAUNDRY_SUB_CYCLE), GeErdBinarySensor(self, ErdCode.LAUNDRY_END_OF_CYCLE), @@ -27,6 +28,7 @@ def get_all_entities(self) -> List[Entity]: GeErdSensor(self, ErdCode.LAUNDRY_DELAY_TIME_REMAINING, suggested_uom="h"), GeErdBinarySensor(self, ErdCode.LAUNDRY_DOOR, entity_category=EntityCategory.DIAGNOSTIC), GeErdBinarySensor(self, ErdCode.LAUNDRY_REMOTE_STATUS, entity_category=EntityCategory.DIAGNOSTIC), + GeWasherCycleButton(self), ] washer_entities = self.get_washer_entities() diff --git a/custom_components/ge_home/entities/ac/ge_wac_climate.py b/custom_components/ge_home/entities/ac/ge_wac_climate.py index e240842..7bab345 100644 --- a/custom_components/ge_home/entities/ac/ge_wac_climate.py +++ b/custom_components/ge_home/entities/ac/ge_wac_climate.py @@ -16,7 +16,7 @@ def __init__(self, available_modes: Optional[ErdAcAvailableModes] = None): @property def options(self) -> List[str]: - modes = [HVACMode.AUTO, HVACMode.COOL, HVACMode.FAN_ONLY] + modes = [HVACMode.AUTO, HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY] if self._available_modes and self._available_modes.has_heat: modes.append(HVACMode.HEAT) return [i.value for i in modes] @@ -28,6 +28,7 @@ def from_option_string(self, value: str) -> Any: HVACMode.AUTO: ErdAcOperationMode.ENERGY_SAVER, HVACMode.COOL: ErdAcOperationMode.COOL, HVACMode.HEAT: ErdAcOperationMode.HEAT, + HVACMode.DRY: ErdAcOperationMode.DRY, HVACMode.FAN_ONLY: ErdAcOperationMode.FAN_ONLY }.get(hvac) except ValueError: @@ -40,6 +41,7 @@ def to_option_string(self, value: Any) -> Optional[str]: ErdAcOperationMode.AUTO: HVACMode.AUTO, ErdAcOperationMode.COOL: HVACMode.COOL, ErdAcOperationMode.HEAT: HVACMode.HEAT, + ErdAcOperationMode.DRY: HVACMode.DRY, ErdAcOperationMode.FAN_ONLY: HVACMode.FAN_ONLY }.get(value) diff --git a/custom_components/ge_home/entities/common/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 546ef73..3b7bf9e 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -3,6 +3,7 @@ from .ge_entity import GeEntity from .ge_erd_entity import GeErdEntity from .ge_erd_binary_sensor import GeErdBinarySensor +from .ge_cooktop_status_binary_sensor import GeCooktopStatusBinarySensor from .ge_erd_property_binary_sensor import GeErdPropertyBinarySensor from .ge_erd_sensor import GeErdSensor from .ge_erd_light import GeErdLight @@ -14,4 +15,4 @@ from .ge_water_heater import GeAbstractWaterHeater from .ge_erd_select import GeErdSelect from .ge_climate import GeClimate -from .ge_humidifier import GeHumidifier \ No newline at end of file +from .ge_humidifier import GeHumidifier diff --git a/custom_components/ge_home/entities/common/ge_cooktop_status_binary_sensor.py b/custom_components/ge_home/entities/common/ge_cooktop_status_binary_sensor.py new file mode 100644 index 0000000..be4395e --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_cooktop_status_binary_sensor.py @@ -0,0 +1,33 @@ +from typing import Optional + +from gehomesdk import CooktopStatus + +from .ge_erd_binary_sensor import GeErdBinarySensor + + +class GeCooktopStatusBinarySensor(GeErdBinarySensor): + """Binary sensor that reports if any cooktop burner is active.""" + + @property + def is_on(self) -> Optional[bool]: + """Return True when any burner that exists is reported as on.""" + try: + status = self.appliance.get_erd_value(self.erd_code) + except KeyError: + return None + + if status is None: + return None + + if not isinstance(status, CooktopStatus): + return self._boolify(status) + + burners = status.burners or {} + + for burner in burners.values(): + exists = getattr(burner, "exists", False) + is_on = self._boolify(getattr(burner, "on", None)) + if exists and is_on: + return True + + return status.boolify() diff --git a/custom_components/ge_home/entities/common/ge_erd_property_binary_sensor.py b/custom_components/ge_home/entities/common/ge_erd_property_binary_sensor.py index bc5411d..ef0689e 100644 --- a/custom_components/ge_home/entities/common/ge_erd_property_binary_sensor.py +++ b/custom_components/ge_home/entities/common/ge_erd_property_binary_sensor.py @@ -39,6 +39,17 @@ def is_on(self) -> Optional[bool]: """Return True if entity is on.""" try: value = magicattr.get(self.appliance.get_erd_value(self.erd_code), self.erd_property) + + """ + Handle the property 'wifi_enabled' + May be safe for other appliances with UserSetting.ENABLE/DISABLE but not sure + """ + if self.erd_property == 'wifi_enabled': + if self._stringify(value) == 'Enable': + return True + else: + return False + except KeyError: return None - return self._boolify(value) \ No newline at end of file + return self._boolify(value) diff --git a/custom_components/ge_home/entities/water_heater/ge_water_heater.py b/custom_components/ge_home/entities/water_heater/ge_water_heater.py index 95916eb..3027f70 100644 --- a/custom_components/ge_home/entities/water_heater/ge_water_heater.py +++ b/custom_components/ge_home/entities/water_heater/ge_water_heater.py @@ -21,7 +21,7 @@ class GeWaterHeater(GeAbstractWaterHeater): def __init__(self, api: ApplianceApi): super().__init__(api) - self._modes_converter = WhHeaterModeConverter() + self._modes_converter = WhHeaterModeConverter(api) @property def heater_type(self) -> str: diff --git a/custom_components/ge_home/entities/water_heater/heater_modes.py b/custom_components/ge_home/entities/water_heater/heater_modes.py index 1481b7e..1b1c857 100644 --- a/custom_components/ge_home/entities/water_heater/heater_modes.py +++ b/custom_components/ge_home/entities/water_heater/heater_modes.py @@ -1,34 +1,106 @@ import logging from typing import List, Any, Optional -from gehomesdk import ErdWaterHeaterMode - +from gehomesdk import ErdWaterHeaterMode, ErdCode, ErdBrand from ..common import OptionsConverter _LOGGER = logging.getLogger(__name__) class WhHeaterModeConverter(OptionsConverter): + def __init__(self, api=None): + """Initialize with optional API reference to check brand.""" + self._api = api + self._brand = None + if api: + try: + self._brand = api.try_get_erd_value(ErdCode.BRAND) + except: + pass + + @property + def is_haier(self) -> bool: + """Check if this is a Haier brand water heater.""" + return self._brand in [ErdBrand.HEIER, ErdBrand.HEIER_FPA] + + def _get_mode_name(self, mode: ErdWaterHeaterMode) -> Optional[str]: + """Get the display name for a mode, considering brand.""" + if mode == ErdWaterHeaterMode.UNKNOWN: + return None + + # Haier-specific mode names (High Demand not supported on Haier) + if self.is_haier: + # High Demand mode doesn't exist on Haier water heaters + if mode == ErdWaterHeaterMode.HIGH_DEMAND: + return None + + haier_names = { + ErdWaterHeaterMode.HYBRID: "Auto", + ErdWaterHeaterMode.STANDARD_ELECTRIC: "Electric", + ErdWaterHeaterMode.HEAT_PUMP: "Eco", + ErdWaterHeaterMode.VACATION: "Vacation" + } + return haier_names.get(mode, mode.stringify()) + + # Default GE mode names + return mode.stringify() + + def get_mode_name(self, mode: ErdWaterHeaterMode) -> Optional[str]: + """Public method to get the display name for a mode.""" + return self._get_mode_name(mode) + + def get_available_options(self) -> List[str]: + """Get list of available mode options with brand-specific names.""" + return self.options + + def get_mode_from_name(self, name: str) -> Optional[ErdWaterHeaterMode]: + """Get the mode enum from display name.""" + return self.from_option_string(name) + + def set_appliance(self, appliance): + """Set the appliance reference to check brand.""" + if appliance and not self._brand: + try: + self._brand = appliance.get_erd_value(ErdCode.BRAND) + except: + pass + @property def options(self) -> List[str]: - return [ - s - for i in ErdWaterHeaterMode - for s in [i.stringify()] - if s is not None - ] + """Get list of available mode options with brand-specific names.""" + options = [] + for mode in ErdWaterHeaterMode: + name = self._get_mode_name(mode) + if name: + options.append(name) + return options def from_option_string(self, value: str) -> Any: - enum_val = value.upper().replace(" ","_") + """Convert from display string to ErdWaterHeaterMode.""" + # Handle Haier-specific names + if self.is_haier: + haier_mapping = { + "AUTO": ErdWaterHeaterMode.HYBRID, + "ELECTRIC": ErdWaterHeaterMode.STANDARD_ELECTRIC, + "ECO": ErdWaterHeaterMode.HEAT_PUMP, + "VACATION": ErdWaterHeaterMode.VACATION + } + normalized = value.upper() + if normalized in haier_mapping: + return haier_mapping[normalized] + + # Try standard mapping + enum_val = value.upper().replace(" ", "_") try: return ErdWaterHeaterMode[enum_val] except: - _LOGGER.warning(f"Could not heater mode to {enum_val}") + _LOGGER.warning(f"Could not convert heater mode from {value}") return ErdWaterHeaterMode.UNKNOWN - + def to_option_string(self, value: ErdWaterHeaterMode) -> Optional[str]: + """Convert from ErdWaterHeaterMode to display string.""" try: if value is not None: - return value.stringify() + return self._get_mode_name(value) except: pass - return ErdWaterHeaterMode.UNKNOWN.stringify() + return self._get_mode_name(ErdWaterHeaterMode.UNKNOWN) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 8a63bbb..88d643f 100644 --- a/custom_components/ge_home/manifest.json +++ b/custom_components/ge_home/manifest.json @@ -5,7 +5,7 @@ "integration_type": "hub", "iot_class": "cloud_push", "documentation": "https://github.com/simbaja/ha_gehome", - "requirements": ["gehomesdk==2025.11.5","magicattr==0.1.6"], + "requirements": ["gehomesdk>=2026.2.0","magicattr==0.1.6"], "codeowners": ["@simbaja"], - "version": "2025.12.0" + "version": "2026.2.0" } diff --git a/custom_components/ge_home/update_coordinator.py b/custom_components/ge_home/update_coordinator.py index 1f40a1b..2f2a908 100644 --- a/custom_components/ge_home/update_coordinator.py +++ b/custom_components/ge_home/update_coordinator.py @@ -163,6 +163,9 @@ async def async_reset(self) -> bool: for c in self._signal_remove_callbacks: c() self._signal_remove_callbacks.clear() + + # clear the appliances (moved from _reset_sync_state to ensure proper cleanup on unload) + self._appliance_apis.clear() # cancel the notification try: @@ -257,7 +260,7 @@ def _reset_sync_state(self): """ Reset synchronous state """ # clear the appliances - self._appliance_apis.clear() + # self._appliance_apis.clear() # MOVED to async_reset to allow for persistence across reconnections # reset the initialization self._all_initial_updates_received.clear() @@ -459,6 +462,7 @@ def _maybe_add_appliance_api(self, appliance: GeAppliance) -> None: # if we already have the API, switch out its appliance reference for this one api = self.appliance_apis[mac_addr] api.appliance = appliance + api.build_entities_list() async def _async_maybe_trigger_all_ready(self, force: bool = False) -> None: """See if we're all ready to go, and if so, let the games begin.""" @@ -492,7 +496,20 @@ async def _async_remove_stale_devices(self): entity_registry = er.async_get(self.hass) # MAC addresses of all currently valid appliances - current_macs = set(self._appliance_apis.keys()) + # we need to look at the cloud list, not our internal list, as we may have stale entries in our internal list + if self._client and self._client.appliances: + valid_macs = set(self._client.appliances.keys()) + else: + valid_macs = set() + + # Remove stale appliance APIs from our internal list + for mac in list(self._appliance_apis.keys()): + if mac not in valid_macs: + _LOGGER.info(f"Removing stale appliance API {mac}") + self._appliance_apis.pop(mac) + + # Update current macs for HA registry cleanup + current_macs = valid_macs # Loop through all devices for this config entry for device_entry in list(device_registry.devices.values()): diff --git a/info.md b/info.md index 569397f..31540f6 100644 --- a/info.md +++ b/info.md @@ -76,6 +76,11 @@ A/C Controls: #### Changes +{% if version_installed.split('.') | map('int') < '2026.2.0'.split('.') | map('int') %} +- Changed mode names for Haier water heaters [#442] +- Made LAUNDRY_MACHINE_STATE diagnostic on all appliances [#447] +{% endif %} + {% if version_installed.split('.') | map('int') < '2025.11.0'.split('.') | map('int') %} - Refactored code internally to improve reliability - Cleaned up initialization and config flow @@ -95,6 +100,12 @@ A/C Controls: #### Features +{% if version_installed.split('.') | map('int') < '2026.2.0'.split('.') | map('int') %} +- Added DRY mode to HVAC options and mappings [#441] +- Added GeWasherCycleButton to WasherDryerApi [#462] +- Added DishDrawer User Setting wifi_enabled (read only) [#463] +{% endif %} + {% if version_installed.split('.') | map('int') < '2025.12.0'.split('.') | map('int') %} - Changed time-related entities to be durations instead of text [#312] {% endif %} @@ -184,6 +195,11 @@ A/C Controls: #### Bugfixes +{% if version_installed.split('.') | map('int') < '2026.2.0'.split('.') | map('int') %} +- Cooktop Sensor fixes [#440, #454] +- Persist ApplianceApis on reconnect to prevent duplicate entities [#464] +{% endif %} + {% if version_installed.split('.') | map('int') < '2025.11.0'.split('.') | map('int') %} - Climate heat mode setting [#433, #435] {% endif %}