diff --git a/CHANGELOG.md b/CHANGELOG.md index 3627ce0..1695382 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # GE Home Appliances (SmartHQ) Changelog +## 0.5.0 + +- 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) +- 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/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/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/const.py b/custom_components/ge_home/const.py index 87fe381..e8511f5 100644 --- a/custom_components/ge_home/const.py +++ b/custom_components/ge_home/const.py @@ -11,3 +11,6 @@ MAX_RETRY_DELAY = 1800 RETRY_OFFLINE_COUNT = 5 +SERVICE_SET_TIMER = "set_timer" +SERVICE_CLEAR_TIMER = "clear_timer" +SERVICE_SET_INT_VALUE = "set_int_value" \ 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 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/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/devices/fridge.py b/custom_components/ge_home/devices/fridge.py index cf3d620..6dd2f4c 100644 --- a/custom_components/ge_home/devices/fridge.py +++ b/custom_components/ge_home/devices/fridge.py @@ -13,19 +13,24 @@ IceMakerControlStatus, ErdFilterStatus, HotWaterStatus, - FridgeModelInfo + FridgeModelInfo, + ErdConvertableDrawerMode ) from .base import ApplianceApi from ..entities import ( + ErdOnOffBoolConverter, GeErdSensor, GeErdBinarySensor, GeErdSwitch, + GeErdSelect, + GeErdLight, GeFridge, GeFreezer, GeDispenser, GeErdPropertySensor, - GeErdPropertyBinarySensor + GeErdPropertyBinarySensor, + ConvertableDrawerModeOptionsConverter ) _LOGGER = logging.getLogger(__name__) @@ -49,6 +54,12 @@ 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) + + units = self.hass.config.units # Common entities common_entities = [ @@ -74,7 +85,13 @@ 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 != 255): + fridge_entities.append(GeErdLight(self, ErdCode.INTERIOR_LIGHT)) + if(proximity_light and proximity_light != ErdOnOff.NA): + fridge_entities.append(GeErdSwitch(self, ErdCode.PROXIMITY_LIGHT, ErdOnOffBoolConverter(), icon_on_override="mdi:lightbulb-on", icon_off_override="mdi:lightbulb")) + if(convertable_drawer and convertable_drawer != ErdConvertableDrawerMode.NA): + fridge_entities.append(GeErdSelect(self, ErdCode.CONVERTABLE_DRAWER_MODE, ConvertableDrawerModeOptionsConverter(units))) + # Freezer entities if fridge_model_info is None or fridge_model_info.has_freezer: freezer_entities.extend([ 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/devices/oven.py b/custom_components/ge_home/devices/oven.py index d1067a0..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, @@ -36,6 +37,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 = [] @@ -45,31 +49,38 @@ 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_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), + GeErdTimerSensor(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)), + 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_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 +101,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/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/__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/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..854a239 --- /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 _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/__init__.py b/custom_components/ge_home/entities/common/__init__.py index 691629a..7db556b 100644 --- a/custom_components/ge_home/entities/common/__init__.py +++ b/custom_components/ge_home/entities/common/__init__.py @@ -5,6 +5,8 @@ from .ge_erd_binary_sensor import GeErdBinarySensor from .ge_erd_property_binary_sensor import GeErdPropertyBinarySensor from .ge_erd_sensor import GeErdSensor +from .ge_erd_light import GeErdLight +from .ge_erd_timer_sensor import GeErdTimerSensor from .ge_erd_property_sensor import GeErdPropertySensor from .ge_erd_switch import GeErdSwitch from .ge_water_heater import GeWaterHeater 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]: 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..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) @@ -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 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..1d9d714 --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_erd_light.py @@ -0,0 +1,70 @@ +import logging + +from gehomesdk import ErdCodeType +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + COLOR_MODE_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity +) + +from ...devices import ApplianceApi +from .ge_erd_entity import GeErdEntity + +_LOGGER = logging.getLogger(__name__) + + +def to_ge_level(level): + """Convert the given Home Assistant light level (0-255) to GE (0-100).""" + return int(round((level * 100) / 255)) + +def to_hass_level(level): + """Convert the given GE (0-100) light level to Home Assistant (0-255).""" + return int((level * 255) // 100) + +class GeErdLight(GeErdEntity, LightEntity): + """Lights for ERD codes.""" + + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None, color_mode = COLOR_MODE_BRIGHTNESS): + super().__init__(api, erd_code, erd_override) + self._color_mode = color_mode + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + @property + def supported_color_modes(self): + """Flag supported color modes.""" + return {COLOR_MODE_BRIGHTNESS} + + @property + def color_mode(self): + """Return the color mode of the light.""" + return self._color_mode + + @property + def brightness(self): + """Return the brightness of the light.""" + return to_hass_level(self.appliance.get_erd_value(self.erd_code)) + + async def _set_brightness(self, brightness, **kwargs): + await self.appliance.async_set_erd_value(self.erd_code, to_ge_level(brightness)) + + @property + def is_on(self) -> bool: + """Return True if light is on.""" + return self.appliance.get_erd_value(self.erd_code) > 0 + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + brightness = kwargs.pop(ATTR_BRIGHTNESS, 255) + + _LOGGER.debug(f"Turning on {self.unique_id}") + await self._set_brightness(brightness, **kwargs) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + _LOGGER.debug(f"Turning off {self.unique_id}") + await self._set_brightness(0, **kwargs) diff --git a/custom_components/ge_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..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,4 +1,6 @@ +import logging from typing import Optional +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT from homeassistant.const import ( DEVICE_CLASS_ENERGY, @@ -6,15 +8,28 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_TIMESTAMP, 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): """GE Entity for sensors""" @@ -25,10 +40,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 +61,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 +120,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"): @@ -106,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/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/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..b9b933c --- /dev/null +++ b/custom_components/ge_home/entities/fridge/convertable_drawer_mode_options.py @@ -0,0 +1,56 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdConvertableDrawerMode +from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.util.unit_system import UnitSystem +from ..common import OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +_TEMP_MAP = { + ErdConvertableDrawerMode.MEAT: 29, + ErdConvertableDrawerMode.BEVERAGE: 33, + ErdConvertableDrawerMode.SNACK: 37, + ErdConvertableDrawerMode.WINE: 42 +} + +class ConvertableDrawerModeOptionsConverter(OptionsConverter): + def __init__(self, units: UnitSystem): + super().__init__() + self._excluded_options = [ + ErdConvertableDrawerMode.UNKNOWN0, + ErdConvertableDrawerMode.UNKNOWN1, + ErdConvertableDrawerMode.NA + ] + self._units = units + + @property + def options(self) -> List[str]: + return [self.to_option_string(i) for i in ErdConvertableDrawerMode if i not in self._excluded_options] + + def from_option_string(self, value: str) -> Any: + try: + v = value.split(" ")[0] + return ErdConvertableDrawerMode[v.upper()] + except: + _LOGGER.warn(f"Could not set hood light level to {value.upper()}") + return ErdConvertableDrawerMode.NA + def to_option_string(self, value: ErdConvertableDrawerMode) -> Optional[str]: + try: + if value is not None: + v = value.stringify() + t = _TEMP_MAP.get(value, None) + + if t and self._units.is_metric: + t = self._units.temperature(float(t), TEMP_FAHRENHEIT) + t = round(t,1) + + if t: + return f"{v} ({t}{self._units.temperature_unit})" + return v + except: + pass + + return ErdConvertableDrawerMode.NA.stringify() + diff --git a/custom_components/ge_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()) 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..52e2516 --- /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.high_available: + self.excluded_levels.append(ErdHoodLightLevel.HIGH) + + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdHoodLightLevel if i not in self.excluded_levels] + def from_option_string(self, value: str) -> Any: + try: + return ErdHoodLightLevel[value.upper()] + except: + _LOGGER.warn(f"Could not set hood light level to {value.upper()}") + return ErdHoodLightLevel.OFF + def to_option_string(self, value: ErdHoodLightLevel) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdHoodLightLevel.OFF.stringify() + +class GeHoodLightLevelSelect(GeErdSelect): + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType): + self._availability: ErdHoodLightLevelAvailability = api.try_get_erd_value(ErdCode.HOOD_LIGHT_LEVEL_AVAILABILITY) + super().__init__(api, erd_code, HoodLightLevelOptionsConverter(self._availability)) diff --git a/custom_components/ge_home/entities/oven/const.py b/custom_components/ge_home/entities/oven/const.py index 11bd991..af9d602 100644 --- a/custom_components/ge_home/entities/oven/const.py +++ b/custom_components/ge_home/entities/oven/const.py @@ -20,6 +20,12 @@ OP_MODE_BAKED_GOODS = "Baked Goods" OP_MODE_FROZEN_PIZZA_MULTI = "Frozen Pizza Multi" OP_MODE_FROZEN_SNACKS_MULTI = "Frozen Snacks Multi" +OP_MODE_BROIL_HIGH = "Broil High" +OP_MODE_BROIL_LOW = "Broil Low" +OP_MODE_PROOF = "Proof" +OP_MODE_WARM = "Warm" + +OP_MODE_AIRFRY = "Air Fry" UPPER_OVEN = "UPPER_OVEN" LOWER_OVEN = "LOWER_OVEN" @@ -29,11 +35,16 @@ ErdOvenCookMode.CONVMULTIBAKE_NOOPTION: OP_MODE_CONVMULTIBAKE, ErdOvenCookMode.CONVBAKE_NOOPTION: OP_MODE_CONVBAKE, ErdOvenCookMode.CONVROAST_NOOPTION: OP_MODE_CONVROAST, + ErdOvenCookMode.BROIL_LOW: OP_MODE_BROIL_LOW, + ErdOvenCookMode.BROIL_HIGH: OP_MODE_BROIL_HIGH, ErdOvenCookMode.BAKE_NOOPTION: OP_MODE_BAKE, + ErdOvenCookMode.PROOF_NOOPTION: OP_MODE_PROOF, + ErdOvenCookMode.WARM_NOOPTION: OP_MODE_WARM, ErdOvenCookMode.FROZEN_PIZZA: OP_MODE_PIZZA, ErdOvenCookMode.FROZEN_SNACKS: OP_MODE_FROZEN_SNACKS, ErdOvenCookMode.BAKED_GOODS: OP_MODE_BAKED_GOODS, ErdOvenCookMode.FROZEN_PIZZA_MULTI: OP_MODE_FROZEN_PIZZA_MULTI, - ErdOvenCookMode.FROZEN_SNACKS_MULTI: OP_MODE_FROZEN_SNACKS_MULTI + ErdOvenCookMode.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..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]: @@ -99,10 +102,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 = 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 @@ -187,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") 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/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 69bcb7e..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.3","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.12","magicattr==0.1.5"], "codeowners": ["@simbaja"], - "version": "0.4.3" + "version": "0.5.0" } diff --git a/custom_components/ge_home/sensor.py b/custom_components/ge_home/sensor.py index 17a037d..2dea9a1 100644 --- a/custom_components/ge_home/sensor.py +++ b/custom_components/ge_home/sensor.py @@ -2,20 +2,34 @@ 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, + SERVICE_SET_INT_VALUE +) from .entities import GeErdSensor from .update_coordinator import GeHomeUpdateCoordinator +ATTR_DURATION = "duration" +ATTR_VALUE = "value" + _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable): """GE Home sensors.""" _LOGGER.debug('Adding GE Home sensors') coordinator: GeHomeUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + # 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 +46,33 @@ 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 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 new file mode 100644 index 0000000..1ca48ea --- /dev/null +++ b/custom_components/ge_home/services.yaml @@ -0,0 +1,47 @@ +# GE Home Services + +set_timer: + name: Set Timer + description: Sets a timer value (timespan) + target: + entity: + integration: "ge_home" + domain: "sensor" + fields: + duration: + name: Duration + description: Duration of the timer (minutes) + required: true + example: "90" + default: "30" + selector: + number: + min: 1 + max: 360 + unit_of_measurement: minutes + mode: slider +clear_timer: + name: Clear Timer + description: Clears a timer value (sets to zero) + target: + entity: + integration: "ge_home" + domain: "sensor" + +set_int_value: + name: Set Int Value + description: Sets an integer value (also can be used with ERD enums) + target: + entity: + integration: "ge_home" + domain: "sensor" + fields: + value: + name: Value + description: The value to set + required: true + selector: + number: + min: 0 + max: 65535 + \ No newline at end of file diff --git a/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" } } } 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__) diff --git a/info.md b/info.md index c6104e7..75ed07d 100644 --- a/info.md +++ b/info.md @@ -40,8 +40,17 @@ 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') %} +- 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') %} - Support for Portable, Split, and Window AC units (@swcrawford1, @mbrentrowe, @RobertusIT, @luddystefenson) {% endif %} @@ -58,6 +67,14 @@ A/C Controls: #### Bugfixes +{% 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) +- 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') %} - Bug fixes for laundry (@steveredden, @sweichbr) - Fixed startup issue when encountering an unknown unit type(@chansearrington, @opie546)