diff --git a/README.md b/README.md index 6c8eab2..6dcdc2b 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,17 @@ Integration for Shark IQ Robot Vacuums ![Shark Vacuum Lovelace Card](https://raw.githubusercontent.com/ajmarks/ajmarks_ha_components/master/img/shark_vacuum_control.png) ## `ge_kitchen` -Integration for GE WiFi-enabled kitchen appliances. Right now, this is largely a proof of concept as HA doesn't yet -have entity types for ovens, refrigerators, etc., but I'm hoping this may provide some motivation to expand the platform -roster. Currently, I've only really built out ovens, and that's purely as sensors, though I'll add some switches soon. -If anybody who has other GE appliances sees this and wants to pitch in, please shoot me a message or make a PR. +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. -Oven sensors in action: +Entities card: -![Oven Sensor Example](https://raw.githubusercontent.com/ajmarks/ajmarks_ha_components/master/img/oven_sensors.jpg) +![Entities](https://raw.githubusercontent.com/ajmarks/ajmarks_ha_components/master/img/appliance_entities.png) + +Fridge Controls: +![Fridge controls](https://raw.githubusercontent.com/ajmarks/ajmarks_ha_components/master/img/fridge_controls.png) + +Oven Controls: +![Fridge controls](https://raw.githubusercontent.com/ajmarks/ajmarks_ha_components/master/img/fridge_controls.png) \ No newline at end of file diff --git a/ge_kitchen/__init__.py b/ge_kitchen/__init__.py index 9a8ca53..96a3eb9 100644 --- a/ge_kitchen/__init__.py +++ b/ge_kitchen/__init__.py @@ -22,7 +22,7 @@ from .exceptions import AuthError, CannotConnect from .update_coordinator import GeKitchenUpdateCoordinator CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) -PLATFORMS = ["sensor", "binary_sensor", "switch"] +PLATFORMS = ["binary_sensor", "sensor", "switch", "water_heater"] _LOGGER = logging.getLogger(__name__) diff --git a/ge_kitchen/appliance_api.py b/ge_kitchen/appliance_api.py index 92c694a..33a64e8 100644 --- a/ge_kitchen/appliance_api.py +++ b/ge_kitchen/appliance_api.py @@ -11,11 +11,18 @@ from gekitchen.erd_types import * from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from .binary_sensor import GeErdBinarySensor, GeErdPropertyBinarySensor +from .binary_sensor import GeErdBinarySensor from .const import DOMAIN from .entities import GeErdEntity -from .sensor import GeErdPropertySensor, GeErdSensor +from .sensor import GeErdSensor from .switch import GeErdSwitch +from .water_heater import ( + GeFreezerEntity, + GeFridgeEntity, + GeOvenHeaterEntity, + LOWER_OVEN, + UPPER_OVEN, +) _LOGGER = logging.getLogger(__name__) @@ -125,15 +132,8 @@ class OvenApi(ApplianceApi): oven_entities = [ GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_MODE), GeErdSensor(self, ErdCode.UPPER_OVEN_COOK_TIME_REMAINING), - GeErdSensor(self, ErdCode.UPPER_OVEN_CURRENT_STATE), - GeErdSensor(self, ErdCode.UPPER_OVEN_DELAY_TIME_REMAINING), - GeErdSensor(self, ErdCode.UPPER_OVEN_DISPLAY_TEMPERATURE), - GeErdSensor(self, ErdCode.UPPER_OVEN_ELAPSED_COOK_TIME), GeErdSensor(self, ErdCode.UPPER_OVEN_KITCHEN_TIMER), - GeErdSensor(self, ErdCode.UPPER_OVEN_PROBE_DISPLAY_TEMP), GeErdSensor(self, ErdCode.UPPER_OVEN_USER_TEMP_OFFSET), - GeErdSensor(self, ErdCode.UPPER_OVEN_RAW_TEMPERATURE), - GeErdBinarySensor(self, ErdCode.UPPER_OVEN_PROBE_PRESENT), GeErdBinarySensor(self, ErdCode.UPPER_OVEN_REMOTE_ENABLED), ] @@ -141,17 +141,13 @@ class OvenApi(ApplianceApi): oven_entities.extend([ GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_MODE), GeErdSensor(self, ErdCode.LOWER_OVEN_COOK_TIME_REMAINING), - GeErdSensor(self, ErdCode.LOWER_OVEN_CURRENT_STATE), - GeErdSensor(self, ErdCode.LOWER_OVEN_DELAY_TIME_REMAINING), - GeErdSensor(self, ErdCode.LOWER_OVEN_DISPLAY_TEMPERATURE), - GeErdSensor(self, ErdCode.LOWER_OVEN_ELAPSED_COOK_TIME), - GeErdSensor(self, ErdCode.LOWER_OVEN_KITCHEN_TIMER), - GeErdSensor(self, ErdCode.LOWER_OVEN_PROBE_DISPLAY_TEMP), GeErdSensor(self, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET), - GeErdSensor(self, ErdCode.LOWER_OVEN_RAW_TEMPERATURE), - GeErdBinarySensor(self, ErdCode.LOWER_OVEN_PROBE_PRESENT), GeErdBinarySensor(self, ErdCode.LOWER_OVEN_REMOTE_ENABLED), + GeOvenHeaterEntity(self, LOWER_OVEN, True), + GeOvenHeaterEntity(self, UPPER_OVEN, True), ]) + else: + oven_entities.append(GeOvenHeaterEntity(self, UPPER_OVEN, False)) return base_entities + oven_entities @@ -169,26 +165,9 @@ class FridgeApi(ApplianceApi): # GeErdSensor(self, ErdCode.HOT_WATER_LOCAL_USE), # GeErdSensor(self, ErdCode.HOT_WATER_SET_TEMP), # GeErdSensor(self, ErdCode.HOT_WATER_STATUS), - GeErdSensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS), - # GeErdSensor(self, ErdCode.ICE_MAKER_CONTROL), - # GeErdSensor(self, ErdCode.SETPOINT_LIMITS), - GeErdPropertySensor(self, ErdCode.TEMPERATURE_SETTING, "fridge"), - GeErdPropertySensor(self, ErdCode.TEMPERATURE_SETTING, "freezer"), - GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "fridge"), - GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "freezer"), - GeErdSwitch(self, ErdCode.TURBO_COOL_STATUS), - GeErdSwitch(self, ErdCode.TURBO_FREEZE_STATUS), - GeErdSensor(self, ErdCode.WATER_FILTER_STATUS), + GeErdSwitch(self, ErdCode.SABBATH_MODE), + GeFreezerEntity(self), + GeFridgeEntity(self), ] entities = base_entities + fridge_entities - door_status = self.appliance.get_erd_value(ErdCode.DOOR_STATUS) # type: ErdDoorStatus - if door_status.fridge_right != ErdDoorStatus.NA: - entities.append(GeErdPropertyBinarySensor(self, ErdCode.DOOR_STATUS, "fridge_right")) - if door_status.fridge_left != ErdDoorStatus.NA: - entities.append(GeErdPropertyBinarySensor(self, ErdCode.DOOR_STATUS, "fridge_left")) - if door_status.freezer != ErdDoorStatus.NA: - entities.append(GeErdPropertyBinarySensor(self, ErdCode.DOOR_STATUS, "freezer")) - if door_status.drawer != ErdDoorStatus.NA: - entities.append(GeErdPropertyBinarySensor(self, ErdCode.DOOR_STATUS, "drawer")) - return entities diff --git a/ge_kitchen/entities.py b/ge_kitchen/entities.py index b43b6c7..fe2b109 100644 --- a/ge_kitchen/entities.py +++ b/ge_kitchen/entities.py @@ -6,7 +6,7 @@ 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 DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant @@ -23,7 +23,6 @@ DOOR_ERD_CODES = { ErdCode.DOOR_STATUS } RAW_TEMPERATURE_ERD_CODES = { - ErdCode.HOT_WATER_SET_TEMP, ErdCode.LOWER_OVEN_RAW_TEMPERATURE, ErdCode.LOWER_OVEN_USER_TEMP_OFFSET, ErdCode.UPPER_OVEN_RAW_TEMPERATURE, @@ -32,6 +31,7 @@ RAW_TEMPERATURE_ERD_CODES = { 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, @@ -72,7 +72,7 @@ def boolify_erd_value(erd_code: ErdCodeType, value: Any) -> Optional[bool]: return bool(value) -def stringify_erd_value(erd_code: ErdCodeType, value: Any, units: str) -> Optional[str]: +def stringify_erd_value(erd_code: ErdCodeType, value: Any, units: Optional[str] = None) -> Optional[str]: """ Convert an erd property value to a nice string diff --git a/ge_kitchen/manifest.json b/ge_kitchen/manifest.json index 04211d7..30aab96 100644 --- a/ge_kitchen/manifest.json +++ b/ge_kitchen/manifest.json @@ -3,7 +3,7 @@ "name": "GE Kitchen", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ge_kitchen", - "requirements": ["gekitchen==0.2.8"], + "requirements": ["gekitchen==0.2.10"], "dependencies": [], "codeowners": ["@ajmarks"] } diff --git a/ge_kitchen/water_heater.py b/ge_kitchen/water_heater.py new file mode 100644 index 0000000..645bf2a --- /dev/null +++ b/ge_kitchen/water_heater.py @@ -0,0 +1,540 @@ +"""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 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 +from .const import DOMAIN + +if TYPE_CHECKING: + from .appliance_api import ApplianceApi + from .update_coordinator import GeKitchenUpdateCoordinator + +_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) + return getattr(current_temps, self.heater_type) + + 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("_", " ").title() + + 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 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"] = elapsed_time + if cook_time_left: + data["cook_time_left"] = cook_time_left + if kitchen_timer: + data["cook_time_remaining"] = kitchen_timer + if delay_time: + data["delay_time_remaining"] = 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"') + coordinator: "GeKitchenUpdateCoordinator" = 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, WaterHeaterEntity) + ] + _LOGGER.debug(f'Found {len(entities):d} "water heaters"') + async_add_entities(entities) diff --git a/img/appliance_entities.png b/img/appliance_entities.png new file mode 100644 index 0000000..5f29203 Binary files /dev/null and b/img/appliance_entities.png differ diff --git a/img/fridge_control.png b/img/fridge_control.png new file mode 100644 index 0000000..cd6c87e Binary files /dev/null and b/img/fridge_control.png differ diff --git a/img/oven_controls.png b/img/oven_controls.png new file mode 100644 index 0000000..5c5b396 Binary files /dev/null and b/img/oven_controls.png differ diff --git a/img/oven_sensors.jpg b/img/oven_sensors.jpg deleted file mode 100644 index 9b2f29d..0000000 Binary files a/img/oven_sensors.jpg and /dev/null differ