diff --git a/CHANGELOG.md b/CHANGELOG.md index 1695382..18a3b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ # GE Home Appliances (SmartHQ) Changelog +## 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) +- 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 - Initial support for oven hoods (@digitalbites) @@ -9,7 +21,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/README.md b/README.md index ad30d3d..42ad817 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,18 @@ 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 +- 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/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/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/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 aiohttp 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 @@ from .exceptions import HaAuthError, HaCannotConnect, HaAlreadyConfigured _LOGGER = logging.getLogger(__name__) GEHOME_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_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/const.py b/custom_components/ge_home/const.py index e8511f5..76cef76 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,4 @@ 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 +SERVICE_SET_INT_VALUE = "set_int_value" diff --git a/custom_components/ge_home/devices/__init__.py b/custom_components/ge_home/devices/__init__.py index 293fe3c..1acc228 100644 --- a/custom_components/ge_home/devices/__init__.py +++ b/custom_components/ge_home/devices/__init__.py @@ -16,6 +16,10 @@ from .wac import WacApi 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 +from .coffee_maker import CcmApi _LOGGER = logging.getLogger(__name__) @@ -37,6 +41,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: @@ -47,6 +53,12 @@ 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 + if appliance_type == ErdApplianceType.CAFE_COFFEE_MAKER: + return CcmApi # Fallback return ApplianceApi 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 @@ import logging 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 @@ class AdvantiumApi(ApplianceApi): 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/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 @@ class ApplianceApi: 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 new file mode 100644 index 0000000..d95af55 --- /dev/null +++ b/custom_components/ge_home/devices/coffee_maker.py @@ -0,0 +1,66 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + GeAppliance, + ErdCode, + ErdApplianceType, + ErdCcmBrewSettings +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .base import ApplianceApi +from ..entities import ( + GeCcmPotNotPresentBinarySensor, + GeErdSensor, + GeErdBinarySensor, + GeErdButton, + GeCcmBrewStrengthSelect, + GeCcmBrewTemperatureNumber, + GeCcmBrewCupsNumber, + GeCcmBrewSettingsButton +) + +_LOGGER = logging.getLogger(__name__) + + +class CcmApi(ApplianceApi): + """API class for Cafe Coffee Maker objects""" + APPLIANCE_TYPE = ErdApplianceType.CAFE_COFFEE_MAKER + + def __init__(self, coordinator: DataUpdateCoordinator, appliance: GeAppliance): + super().__init__(coordinator, appliance) + + self._brew_strengh_entity = GeCcmBrewStrengthSelect(self) + self._brew_temperature_entity = GeCcmBrewTemperatureNumber(self) + self._brew_cups_entity = GeCcmBrewCupsNumber(self) + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + ccm_entities = [ + GeErdBinarySensor(self, ErdCode.CCM_IS_BREWING), + GeErdBinarySensor(self, ErdCode.CCM_IS_DESCALING), + GeCcmBrewSettingsButton(self), + GeErdButton(self, ErdCode.CCM_CANCEL_DESCALING), + GeErdButton(self, ErdCode.CCM_START_DESCALING), + GeErdButton(self, ErdCode.CCM_CANCEL_BREWING), + self._brew_strengh_entity, + self._brew_temperature_entity, + self._brew_cups_entity, + GeErdSensor(self, ErdCode.CCM_CURRENT_WATER_TEMPERATURE), + GeErdBinarySensor(self, ErdCode.CCM_OUT_OF_WATER, device_class_override="problem"), + GeCcmPotNotPresentBinarySensor(self, ErdCode.CCM_POT_PRESENT, device_class_override="problem") + ] + + entities = base_entities + ccm_entities + return entities + + async def start_brewing(self) -> None: + """Aggregate brew settings and start brewing.""" + + new_mode = ErdCcmBrewSettings(self._brew_cups_entity.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/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 @@ class DishwasherApi(ApplianceApi): 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/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 @@ from gehomesdk import ( ErdFilterStatus, HotWaterStatus, FridgeModelInfo, - ErdConvertableDrawerMode + ErdConvertableDrawerMode, + ErdDataType ) from .base import ApplianceApi @@ -110,7 +111,7 @@ class FridgeApi(ApplianceApi): 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/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 .base import ApplianceApi from ..entities import ( GeHoodLightLevelSelect, GeHoodFanSpeedSelect, - GeErdSensor, + GeErdTimerSensor, GeErdSwitch, ErdOnOffBoolConverter ) @@ -45,7 +45,7 @@ class HoodApi(ApplianceApi): 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/devices/oim.py b/custom_components/ge_home/devices/oim.py new file mode 100644 index 0000000..ad1bd06 --- /dev/null +++ b/custom_components/ge_home/devices/oim.py @@ -0,0 +1,40 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ( + ErdCode, + ErdApplianceType, + ErdOnOff +) + +from .base import ApplianceApi +from ..entities import ( + OimLightLevelOptionsConverter, + GeErdSensor, + GeErdBinarySensor, + GeErdSelect, + GeErdSwitch, + ErdOnOffBoolConverter +) + +_LOGGER = logging.getLogger(__name__) + + +class OimApi(ApplianceApi): + """API class for 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), + 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"), + ] + + entities = base_entities + oim_entities + return entities + diff --git a/custom_components/ge_home/devices/oven.py b/custom_components/ge_home/devices/oven.py index f2a2074..ff2d077 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 @@ -8,7 +9,8 @@ from gehomesdk import ( ErdApplianceType, OvenConfiguration, ErdCooktopConfig, - CooktopStatus + CooktopStatus, + ErdOvenLightLevelAvailability ) from .base import ApplianceApi @@ -19,6 +21,7 @@ from ..entities import ( GeErdPropertySensor, GeErdPropertyBinarySensor, GeOven, + GeOvenLightLevelSelect, UPPER_OVEN, LOWER_OVEN ) @@ -40,6 +43,9 @@ class OvenApi(ApplianceApi): 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 = [] @@ -68,6 +74,8 @@ class OvenApi(ApplianceApi): 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)), @@ -80,6 +88,8 @@ class OvenApi(ApplianceApi): ]) 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: @@ -92,7 +102,7 @@ class OvenApi(ApplianceApi): 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..a1dfad5 100644 --- a/custom_components/ge_home/devices/sac.py +++ b/custom_components/ge_home/devices/sac.py @@ -5,7 +5,7 @@ from homeassistant.helpers.entity import Entity 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__) @@ -19,8 +19,8 @@ class SacApi(ApplianceApi): 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/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 gehomesdk import ErdCode, ErdApplianceType from .base import ApplianceApi from ..entities import ( GeErdSensor, + GeErdPropertySensor, GeErdBinarySensor, GeErdFilterPositionSelect, ) @@ -28,7 +29,7 @@ class WaterFilterApi(ApplianceApi): 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 new file mode 100644 index 0000000..a730471 --- /dev/null +++ b/custom_components/ge_home/devices/water_softener.py @@ -0,0 +1,38 @@ +import logging +from typing import List + +from homeassistant.helpers.entity import Entity +from gehomesdk import ErdCode, ErdApplianceType + +from .base import ApplianceApi +from ..entities import ( + GeErdSensor, + GeErdPropertySensor, + GeErdBinarySensor, + GeErdShutoffPositionSelect, +) + +_LOGGER = logging.getLogger(__name__) + + +class WaterSoftenerApi(ApplianceApi): + """API class for water softener objects""" + + APPLIANCE_TYPE = ErdApplianceType.WATER_SOFTENER + + def get_all_entities(self) -> List[Entity]: + base_entities = super().get_all_entities() + + ws_entities = [ + GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE, icon_on_override="mdi:human", icon_off_override="mdi:robot"), + GeErdPropertySensor(self, ErdCode.WH_FILTER_FLOW_RATE, "flow_rate"), + GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"), + GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE), + GeErdSensor(self, ErdCode.WH_SOFTENER_ERROR_CODE, icon_override="mdi:alert-circle"), + GeErdBinarySensor(self, ErdCode.WH_SOFTENER_LOW_SALT, icon_on_override="mdi:alert", icon_off_override="mdi:grain"), + GeErdSensor(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_STATE, icon_override="mdi:state-machine"), + GeErdSensor(self, ErdCode.WH_SOFTENER_SALT_LIFE_REMAINING, icon_override="mdi:calendar-clock"), + GeErdShutoffPositionSelect(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_CONTROL), + ] + entities = base_entities + ws_entities + return entities diff --git a/custom_components/ge_home/entities/__init__.py b/custom_components/ge_home/entities/__init__.py index b03b590..eabcc59 100644 --- a/custom_components/ge_home/entities/__init__.py +++ b/custom_components/ge_home/entities/__init__.py @@ -5,4 +5,7 @@ from .oven import * 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 * +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/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 @@ class GePacClimate(GeClimate): #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 @@ class GeSacClimate(GeClimate): #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/ccm/__init__.py b/custom_components/ge_home/entities/ccm/__init__.py new file mode 100644 index 0000000..614d130 --- /dev/null +++ b/custom_components/ge_home/entities/ccm/__init__.py @@ -0,0 +1,5 @@ +from .ge_ccm_pot_not_present_binary_sensor import GeCcmPotNotPresentBinarySensor +from .ge_ccm_brew_strength import GeCcmBrewStrengthSelect +from .ge_ccm_brew_temperature import GeCcmBrewTemperatureNumber +from .ge_ccm_brew_cups import GeCcmBrewCupsNumber +from .ge_ccm_brew_settings import GeCcmBrewSettingsButton \ No newline at end of file diff --git a/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..4dc20e9 --- /dev/null +++ b/custom_components/ge_home/entities/ccm/ge_ccm_brew_temperature.py @@ -0,0 +1,22 @@ +from gehomesdk import ErdCode +from ...devices import ApplianceApi +from ..common import GeErdNumber +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: + return self.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_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)) + 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_light import GeErdLight from .ge_erd_timer_sensor import GeErdTimerSensor from .ge_erd_property_sensor import GeErdPropertySensor from .ge_erd_switch import GeErdSwitch +from .ge_erd_button import GeErdButton +from .ge_erd_number import GeErdNumber from .ge_water_heater import 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 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 @@ class GeClimate(GeEntity, ClimateEntity): @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_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 a23a36c..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 @@ class GeErdEntity(GeEntity): try: value = self.appliance.get_erd_value(ErdCode.TEMPERATURE_UNIT) except KeyError: - return None + return ErdMeasurementUnits.Imperial return value def _get_icon(self): @@ -104,7 +104,7 @@ class GeErdEntity(GeEntity): 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: @@ -138,6 +138,12 @@ class GeErdEntity(GeEntity): 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" + 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..bea3f69 --- /dev/null +++ b/custom_components/ge_home/entities/common/ge_erd_number.py @@ -0,0 +1,128 @@ +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 gehomesdk import ErdCodeType, ErdCodeClass +from .ge_erd_entity import GeErdEntity +from ...devices import ApplianceApi + +_LOGGER = logging.getLogger(__name__) + +class GeErdNumber(GeErdEntity, NumberEntity): + """GE Entity for numbers""" + + def __init__( + self, + api: ApplianceApi, + erd_code: ErdCodeType, + erd_override: str = None, + icon_override: str = None, + device_class_override: str = None, + uom_override: str = None, + data_type_override: ErdDataType = None, + min_value: float = 1, + max_value: float = 100, + step_value: float = 1, + mode: str = "auto" + ): + super().__init__(api, erd_code, erd_override, icon_override, device_class_override) + self._uom_override = uom_override + self._data_type_override = data_type_override + self._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 data type""" + + if self._data_type == ErdDataType.INT: + return int(round(value)) + else: + return value + + def _get_uom(self): + """Select appropriate units""" + + #if we have an override, just use it + if self._uom_override: + return self._uom_override + + if self.device_class == 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 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._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_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 @@ class GeErdPropertySensor(GeErdSensor): 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..ffb16f0 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,7 @@ import logging from typing import Optional -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from gehomesdk.erd.erd_data_type import ErdDataType +from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.const import ( DEVICE_CLASS_ENERGY, @@ -8,29 +9,15 @@ from homeassistant.const import ( 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 gehomesdk import ErdCodeType, ErdCodeClass 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__( @@ -42,34 +29,62 @@ class GeErdSensor(GeErdEntity, Entity): 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 self._convert_numeric_value_from_device(value) + + # otherwise, return a stringified version + # TODO: perhaps enhance so that there's a list of variables available + # for the stringify function to consume... + return self._stringify(value, temp_units=self._temp_units) except KeyError: return None - # 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 _convert_numeric_value_from_device(self, value): + """Convert to expected data type""" + + if self._data_type == ErdDataType.INT: + return int(round(value)) + else: + return value def _get_uom(self): """Select appropriate units""" @@ -83,7 +98,10 @@ class GeErdSensor(GeErdEntity, Entity): 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 +112,12 @@ class GeErdSensor(GeErdEntity, Entity): 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 @@ -125,11 +143,11 @@ class GeErdSensor(GeErdEntity, Entity): 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 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 @@ class GeWaterHeater(GeEntity, WaterHeaterEntity, metaclass=abc.ABCMeta): @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/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() 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..8c63973 --- /dev/null +++ b/custom_components/ge_home/entities/oven/ge_oven_light_level_select.py @@ -0,0 +1,66 @@ +import logging +from typing import List, Any, Optional + +from gehomesdk import ErdCodeType, ErdOvenLightLevelAvailability, ErdOvenLightLevel, ErdCode +from ...devices import ApplianceApi +from ..common import GeErdSelect, OptionsConverter + +_LOGGER = logging.getLogger(__name__) + +class OvenLightLevelOptionsConverter(OptionsConverter): + def __init__(self, availability: ErdOvenLightLevelAvailability): + super().__init__() + self.availability = availability + self.excluded_levels = [ErdOvenLightLevel.NOT_AVAILABLE] + + if not availability or not availability.dim_available: + self.excluded_levels.append(ErdOvenLightLevel.DIM) + + @property + def options(self) -> List[str]: + return [i.stringify() for i in ErdOvenLightLevel if i not in self.excluded_levels] + def from_option_string(self, value: str) -> Any: + try: + return ErdOvenLightLevel[value.upper()] + except: + _LOGGER.warn(f"Could not set Oven light level to {value.upper()}") + return ErdOvenLightLevel.OFF + def to_option_string(self, value: ErdOvenLightLevel) -> Optional[str]: + try: + if value is not None: + return value.stringify() + except: + pass + return ErdOvenLightLevel.OFF.stringify() + +class GeOvenLightLevelSelect(GeErdSelect): + + def __init__(self, api: ApplianceApi, erd_code: ErdCodeType, erd_override: str = None): + self._availability: ErdOvenLightLevelAvailability = api.try_get_erd_value(ErdCode.LOWER_OVEN_LIGHT_AVAILABILITY) + + #check to see if we have a status + value: ErdOvenLightLevel = api.try_get_erd_value(erd_code) + self._has_status = value is not None and value != ErdOvenLightLevel.NOT_AVAILABLE + self._assumed_state = ErdOvenLightLevel.OFF + + super().__init__(api, erd_code, OvenLightLevelOptionsConverter(self._availability), erd_override=erd_override) + + @property + def assumed_state(self) -> bool: + return not self._has_status + + @property + def current_option(self): + if self.assumed_state: + return self._assumed_state + + return self._converter.to_option_string(self.appliance.get_erd_value(self.erd_code)) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + _LOGGER.debug(f"Setting select from {self.current_option} to {option}") + + new_state = self._converter.from_option_string(option) + await self.appliance.async_set_erd_value(self.erd_code, new_state) + self._assumed_state = new_state + \ No newline at end of file diff --git a/custom_components/ge_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) diff --git a/custom_components/ge_home/manifest.json b/custom_components/ge_home/manifest.json index 9d6b89c..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.12","magicattr==0.1.5"], + "requirements": ["gehomesdk==0.4.25","magicattr==0.1.5","slixmpp==1.7.1"], "codeowners": ["@simbaja"], - "version": "0.5.0" + "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 .const import ( ) 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 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator): 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})") diff --git a/hacs.json b/hacs.json index f508818..7c919dc 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "name": "GE Home (SmartHQ)", - "homeassistant": "2021.7.1", - "domains": ["binary_sensor", "sensor", "switch", "water_heater", "select"], + "homeassistant": "2021.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 75ed07d..f99e99d 100644 --- a/info.md +++ b/info.md @@ -2,15 +2,25 @@ 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 +- 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: @@ -34,6 +44,13 @@ 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 +- 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 %} + {% 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 +63,13 @@ A/C Controls: #### Features +{% 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) +- 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') %} - Support for Oven Hood units (@digitalbites) - Added extended mode support for ovens @@ -67,12 +91,16 @@ 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) - 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') %}