Merge branch 'dev' into update-constants

This commit is contained in:
myztillx 2024-01-16 09:21:34 -05:00 committed by GitHub
commit b9f2f66e72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 234 additions and 42 deletions

View File

@ -1,6 +1,10 @@
# GE Home Appliances (SmartHQ) Changelog
## 0.6.9
- Added additional fridge controls [#200]
## 0.6.8
- Added Dehumidifier [#114]

View File

@ -35,5 +35,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
]
_LOGGER.debug(f'Found {len(entities):d} unregistered binary sensors')
async_add_entities(entities)
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)
#if we're already initialized at this point, call device
#discovery directly, otherwise add a callback based on the
#ready signal
if coordinator.initialized:
async_devices_discovered(coordinator.appliance_apis.values())
else:
# add the ready signal and register the remove callback
coordinator.add_signal_remove_callback(
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered))

View File

@ -34,4 +34,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
_LOGGER.debug(f'Found {len(entities):d} unregistered buttons ')
async_add_entities(entities)
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)
#if we're already initialized at this point, call device
#discovery directly, otherwise add a callback based on the
#ready signal
if coordinator.initialized:
async_devices_discovered(coordinator.appliance_apis.values())
else:
# add the ready signal and register the remove callback
coordinator.add_signal_remove_callback(
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered))

View File

@ -36,4 +36,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
_LOGGER.debug(f'Found {len(entities):d} unregistered climate entities')
async_add_entities(entities)
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)
#if we're already initialized at this point, call device
#discovery directly, otherwise add a callback based on the
#ready signal
if coordinator.initialized:
async_devices_discovered(coordinator.appliance_apis.values())
else:
# add the ready signal and register the remove callback
coordinator.add_signal_remove_callback(
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered))

View File

@ -31,7 +31,8 @@ from ..entities import (
GeDispenser,
GeErdPropertySensor,
GeErdPropertyBinarySensor,
ConvertableDrawerModeOptionsConverter
ConvertableDrawerModeOptionsConverter,
GeFridgeIceControlSwitch
)
_LOGGER = logging.getLogger(__name__)
@ -61,7 +62,10 @@ class FridgeApi(ApplianceApi):
proximity_light: ErdOnOff = self.try_get_erd_value(ErdCode.PROXIMITY_LIGHT)
display_mode: ErdOnOff = self.try_get_erd_value(ErdCode.DISPLAY_MODE)
lockout_mode: ErdOnOff = self.try_get_erd_value(ErdCode.LOCKOUT_MODE)
turbo_cool: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_COOL_STATUS)
turbo_freeze: ErdOnOff = self.try_get_erd_value(ErdCode.TURBO_FREEZE_STATUS)
ice_boost: ErdOnOff = self.try_get_erd_value(ErdCode.FRIDGE_ICE_BOOST)
units = self.hass.config.units
# Common entities
@ -80,8 +84,11 @@ class FridgeApi(ApplianceApi):
GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "fridge"),
GeFridge(self),
])
if turbo_cool is not None:
fridge_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST))
if(ice_maker_control and ice_maker_control.status_fridge != ErdOnOff.NA):
fridge_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_fridge"))
fridge_entities.append(GeFridgeIceControlSwitch(self, "fridge"))
if(water_filter and water_filter != ErdFilterStatus.NA):
fridge_entities.append(GeErdSensor(self, ErdCode.WATER_FILTER_STATUS))
if(air_filter and air_filter != ErdFilterStatus.NA):
@ -105,8 +112,13 @@ class FridgeApi(ApplianceApi):
GeErdPropertySensor(self, ErdCode.CURRENT_TEMPERATURE, "freezer"),
GeFreezer(self),
])
if turbo_freeze is not None:
freezer_entities.append(GeErdSwitch(self, ErdCode.TURBO_FREEZE_STATUS))
if ice_boost is not None:
freezer_entities.append(GeErdSwitch(self, ErdCode.FRIDGE_ICE_BOOST))
if(ice_maker_control and ice_maker_control.status_freezer != ErdOnOff.NA):
freezer_entities.append(GeErdPropertyBinarySensor(self, ErdCode.ICE_MAKER_CONTROL, "status_freezer"))
freezer_entities.append(GeFridgeIceControlSwitch(self, "freezer"))
if(ice_bucket_status and ice_bucket_status.is_present_freezer):
freezer_entities.append(GeErdPropertySensor(self, ErdCode.ICE_MAKER_BUCKET_STATUS, "state_full_freezer"))

View File

@ -100,18 +100,27 @@ class OvenApi(ApplianceApi):
if oven_config.has_warming_drawer and warm_drawer is not None:
oven_entities.append(GeErdSensor(self, ErdCode.WARMING_DRAWER_STATE))
if cooktop_config == ErdCooktopConfig.PRESENT:
if cooktop_config == ErdCooktopConfig.PRESENT:
# attempt to get the cooktop status using legacy status
cooktop_status_erd = ErdCode.COOKTOP_STATUS
cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS)
# if we didn't get it, try using the new version
if cooktop_status is None:
cooktop_status_erd = ErdCode.COOKTOP_STATUS_EXT
cooktop_status: CooktopStatus = self.try_get_erd_value(ErdCode.COOKTOP_STATUS_EXT)
# if we got a status through either mechanism, we can add the entities
if cooktop_status is not None:
cooktop_entities.append(GeErdBinarySensor(self, ErdCode.COOKTOP_STATUS))
cooktop_entities.append(GeErdBinarySensor(self, cooktop_status_erd))
for (k, v) in cooktop_status.burners.items():
if v.exists:
prop = self._camel_to_snake(k)
cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, prop+".on"))
cooktop_entities.append(GeErdPropertyBinarySensor(self, ErdCode.COOKTOP_STATUS, prop+".synchronized"))
cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".on"))
cooktop_entities.append(GeErdPropertyBinarySensor(self, cooktop_status_erd, prop+".synchronized"))
if not v.on_off_only:
cooktop_entities.append(GeErdPropertySensor(self, ErdCode.COOKTOP_STATUS, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT))
cooktop_entities.append(GeErdPropertySensor(self, cooktop_status_erd, prop+".power_pct", icon_override="mdi:fire", device_class_override=SensorDeviceClass.POWER_FACTOR, data_type_override=ErdDataType.INT))
return base_entities + oven_entities + cooktop_entities

View File

@ -30,7 +30,7 @@ class WaterFilterApi(ApplianceApi):
GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE, icon_on_override="mdi:human", icon_off_override="mdi:robot"),
GeErdBinarySensor(self, ErdCode.WH_FILTER_LEAK_VALIDITY, device_class_override="moisture"),
GeErdPropertySensor(self, ErdCode.WH_FILTER_FLOW_RATE, "flow_rate"),
GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE),
GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE, device_class_override="water"),
GeErdPropertySensor(self, ErdCode.WH_FILTER_LIFE_REMAINING, "life_remaining"),
GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"),
]

View File

@ -27,7 +27,7 @@ class WaterSoftenerApi(ApplianceApi):
GeErdBinarySensor(self, ErdCode.WH_FILTER_MANUAL_MODE, icon_on_override="mdi:human", icon_off_override="mdi:robot"),
GeErdPropertySensor(self, ErdCode.WH_FILTER_FLOW_RATE, "flow_rate"),
GeErdBinarySensor(self, ErdCode.WH_FILTER_FLOW_ALERT, device_class_override="moisture"),
GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE),
GeErdSensor(self, ErdCode.WH_FILTER_DAY_USAGE, device_class_override="water"),
GeErdSensor(self, ErdCode.WH_SOFTENER_ERROR_CODE, icon_override="mdi:alert-circle"),
GeErdBinarySensor(self, ErdCode.WH_SOFTENER_LOW_SALT, icon_on_override="mdi:alert", icon_off_override="mdi:grain"),
GeErdSensor(self, ErdCode.WH_SOFTENER_SHUTOFF_VALVE_STATE, icon_override="mdi:state-machine"),

View File

@ -10,7 +10,7 @@ class GeEntity:
def __init__(self, api: ApplianceApi):
self._api = api
self.hass = None
self._added = False
@property
def unique_id(self) -> str:
@ -56,6 +56,18 @@ class GeEntity:
def device_class(self) -> Optional[str]:
return self._get_device_class()
@property
def added(self) -> bool:
return self._added
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
self._added = True
async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
self._added = False
def _stringify(self, value: any, **kwargs) -> Optional[str]:
if isinstance(value, timedelta):
return str(value)[:-3] if value else ""

View File

@ -29,5 +29,5 @@ class GeErdSwitch(GeErdBinarySensor, SwitchEntity):
async def async_turn_off(self, **kwargs):
"""Turn the switch off."""
_LOGGER.debug(f"Turning on {self.unique_id}")
_LOGGER.debug(f"Turning off {self.unique_id}")
await self.appliance.async_set_erd_value(self.erd_code, self._converter.false_value())

View File

@ -1,4 +1,5 @@
from .ge_fridge import GeFridge
from .ge_freezer import GeFreezer
from .ge_dispenser import GeDispenser
from .convertable_drawer_mode_options import ConvertableDrawerModeOptionsConverter
from .convertable_drawer_mode_options import ConvertableDrawerModeOptionsConverter
from .ge_fridge_ice_control_switch import GeFridgeIceControlSwitch

View File

@ -0,0 +1,47 @@
import logging
from gehomesdk import ErdCode, IceMakerControlStatus, ErdOnOff
from ...devices import ApplianceApi
from ..common import GeErdSwitch, BoolConverter
_LOGGER = logging.getLogger(__name__)
class GeFridgeIceControlSwitch(GeErdSwitch):
def __init__(self, api: ApplianceApi, control_type: str):
super().__init__(api, ErdCode.ICE_MAKER_CONTROL, BoolConverter())
self._control_type = control_type
@property
def control_status(self) -> IceMakerControlStatus:
return self.appliance.get_erd_value(ErdCode.ICE_MAKER_CONTROL)
@property
def is_on(self) -> bool:
if self._control_type == "fridge":
return self.control_status.status_fridge == ErdOnOff.ON
else:
return self.control_status.status_freezer == ErdOnOff.ON
async def async_turn_on(self, **kwargs):
"""Turn the switch on."""
_LOGGER.debug(f"Turning on {self.unique_id}")
old_status = self.control_status
if self._control_type == "fridge":
new_status = IceMakerControlStatus(ErdOnOff.ON, old_status.status_freezer)
else:
new_status = IceMakerControlStatus(old_status.status_fridge, ErdOnOff.ON)
await self.appliance.async_set_erd_value(self.erd_code, new_status)
async def async_turn_off(self, **kwargs):
"""Turn the switch off."""
_LOGGER.debug(f"Turning off {self.unique_id}")
old_status = self.control_status
if self._control_type == "fridge":
new_status = IceMakerControlStatus(ErdOnOff.OFF, old_status.status_freezer)
else:
new_status = IceMakerControlStatus(old_status.status_fridge, ErdOnOff.OFF)
await self.appliance.async_set_erd_value(self.erd_code, new_status)

View File

@ -33,4 +33,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
_LOGGER.debug(f'Found {len(entities):d} unregistered humidifiers')
async_add_entities(entities)
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)
#if we're already initialized at this point, call device
#discovery directly, otherwise add a callback based on the
#ready signal
if coordinator.initialized:
async_devices_discovered(coordinator.appliance_apis.values())
else:
# add the ready signal and register the remove callback
coordinator.add_signal_remove_callback(
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered))

View File

@ -37,4 +37,12 @@ async def async_setup_entry(
_LOGGER.debug(f"Found {len(entities):d} unregistered lights")
async_add_entities(entities)
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)
#if we're already initialized at this point, call device
#discovery directly, otherwise add a callback based on the
#ready signal
if coordinator.initialized:
async_devices_discovered(coordinator.appliance_apis.values())
else:
# add the ready signal and register the remove callback
coordinator.add_signal_remove_callback(
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered))

View File

@ -5,7 +5,7 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"documentation": "https://github.com/simbaja/ha_gehome",
"requirements": ["gehomesdk==0.5.23","magicattr==0.1.6","slixmpp==1.8.3"],
"requirements": ["gehomesdk==0.5.26","magicattr==0.1.6","slixmpp==1.8.3"],
"codeowners": ["@simbaja"],
"version": "0.6.8"
"version": "0.6.9"
}

View File

@ -34,4 +34,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
_LOGGER.debug(f'Found {len(entities):d} unregisterd numbers')
async_add_entities(entities)
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)
#if we're already initialized at this point, call device
#discovery directly, otherwise add a callback based on the
#ready signal
if coordinator.initialized:
async_devices_discovered(coordinator.appliance_apis.values())
else:
# add the ready signal and register the remove callback
coordinator.add_signal_remove_callback(
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered))

View File

@ -37,4 +37,12 @@ async def async_setup_entry(
_LOGGER.debug(f"Found {len(entities):d} unregistered selects")
async_add_entities(entities)
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)
#if we're already initialized at this point, call device
#discovery directly, otherwise add a callback based on the
#ready signal
if coordinator.initialized:
async_devices_discovered(coordinator.appliance_apis.values())
else:
# add the ready signal and register the remove callback
coordinator.add_signal_remove_callback(
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered))

View File

@ -47,8 +47,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
_LOGGER.debug(f'Found {len(entities):d} unregistered sensors')
async_add_entities(entities)
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)
#if we're already initialized at this point, call device
#discovery directly, otherwise add a callback based on the
#ready signal
if coordinator.initialized:
async_devices_discovered(coordinator.appliance_apis.values())
else:
# add the ready signal and register the remove callback
coordinator.add_signal_remove_callback(
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered))
# register set_timer entity service
platform.async_register_entity_service(
SERVICE_SET_TIMER,

View File

@ -33,4 +33,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
_LOGGER.debug(f'Found {len(entities):d} unregistered switches')
async_add_entities(entities)
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)
#if we're already initialized at this point, call device
#discovery directly, otherwise add a callback based on the
#ready signal
if coordinator.initialized:
async_devices_discovered(coordinator.appliance_apis.values())
else:
# add the ready signal and register the remove callback
coordinator.add_signal_remove_callback(
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered))

View File

@ -3,7 +3,7 @@
import asyncio
import async_timeout
import logging
from typing import Any, Dict, Iterable, Optional, Tuple
from typing import Any, Callable, Dict, Iterable, Optional, Tuple, List
from gehomesdk import (
EVENT_APPLIANCE_INITIAL_UPDATE,
@ -22,6 +22,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_REGION
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import (
@ -55,17 +56,17 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Set up the GeHomeUpdateCoordinator class."""
self._hass = hass
super().__init__(hass, _LOGGER, name=DOMAIN)
self._config_entry = config_entry
self._username = config_entry.data[CONF_USERNAME]
self._password = config_entry.data[CONF_PASSWORD]
self._region = config_entry.data[CONF_REGION]
self._appliance_apis = {} # type: Dict[str, ApplianceApi]
self._signal_remove_callbacks = [] # type: List[Callable]
self._reset_initialization()
super().__init__(hass, _LOGGER, name=DOMAIN)
def _reset_initialization(self):
self.client = None # type: Optional[GeWebsocketClient]
@ -111,7 +112,11 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator):
@property
def signal_ready(self) -> str:
"""Event specific per entry to signal readiness"""
return f"{DOMAIN}-ready-{self._config_entry.entry_id}"
return f"{DOMAIN}-ready-{self._config_entry.entry_id}"
@property
def initialized(self) -> bool:
return self._init_done
@property
def online(self) -> bool:
@ -150,6 +155,9 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator):
api = self.appliance_apis[mac_addr]
api.appliance = appliance
def add_signal_remove_callback(self, cb: Callable):
self._signal_remove_callbacks.append(cb)
async def get_client(self) -> GeWebsocketClient:
"""Get a new GE Websocket client."""
if self.client:
@ -161,8 +169,7 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator):
finally:
self._reset_initialization()
loop = self._hass.loop
self.client = self.create_ge_client(event_loop=loop)
self.client = self.create_ge_client(event_loop=self.hass.loop)
return self.client
async def async_setup(self):
@ -201,9 +208,9 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator):
async def async_begin_session(self):
"""Begins the ge_home session."""
_LOGGER.debug("Beginning session")
session = self._hass.helpers.aiohttp_client.async_get_clientsession()
session = self.hass.helpers.aiohttp_client.async_get_clientsession()
await self.client.async_get_credentials(session)
fut = asyncio.ensure_future(self.client.async_run_client(), loop=self._hass.loop)
fut = asyncio.ensure_future(self.client.async_run_client(), loop=self.hass.loop)
_LOGGER.debug("Client running")
return fut
@ -211,6 +218,12 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator):
"""Resets the coordinator."""
_LOGGER.debug("resetting the coordinator")
entry = self._config_entry
# remove all the callbacks for this coordinator
for c in self._signal_remove_callbacks:
c()
self._signal_remove_callbacks.clear()
unload_ok = all(
await asyncio.gather(
*[
@ -263,7 +276,7 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator):
_LOGGER.info("ge_home shutting down")
if self.client:
self.client.clear_event_handlers()
self._hass.loop.create_task(self.client.disconnect())
self.hass.loop.create_task(self.client.disconnect())
async def on_device_update(self, data: Tuple[GeAppliance, Dict[ErdCodeType, Any]]):
"""Let HA know there's new state."""
@ -272,24 +285,34 @@ class GeHomeUpdateCoordinator(DataUpdateCoordinator):
try:
api = self.appliance_apis[appliance.mac_addr]
except KeyError:
_LOGGER.warn(f"Could not find appliance {appliance.mac_addr} in known device list.")
return
for entity in api.entities:
if entity.enabled:
_LOGGER.debug(f"Updating {entity} ({entity.unique_id}, {entity.entity_id})")
entity.async_write_ha_state()
self._update_entity_state(api.entities)
async def _refresh_ha_state(self):
entities = [
entity for api in self.appliance_apis.values() for entity in api.entities
]
self._update_entity_state(entities)
def _update_entity_state(self, entities: List[Entity]):
from .entities import GeEntity
for entity in entities:
# if this is a GeEntity, check if it's been added
#if not, don't try to refresh this entity
if isinstance(entity, GeEntity):
gee: GeEntity = entity
if not gee.added:
_LOGGER.debug(f"Entity {entity} ({entity.unique_id}, {entity.entity_id}) not yet added, skipping update...")
continue
if entity.enabled:
try:
_LOGGER.debug(f"Refreshing state for {entity} ({entity.unique_id}, {entity.entity_id}")
entity.async_write_ha_state()
except:
_LOGGER.debug(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}")
_LOGGER.warn(f"Could not refresh state for {entity} ({entity.unique_id}, {entity.entity_id}", exc_info=1)
@property
def all_appliances_updated(self) -> bool:

View File

@ -35,4 +35,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
_LOGGER.debug(f'Found {len(entities):d} unregistered water heaters')
async_add_entities(entities)
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered)
#if we're already initialized at this point, call device
#discovery directly, otherwise add a callback based on the
#ready signal
if coordinator.initialized:
async_devices_discovered(coordinator.appliance_apis.values())
else:
# add the ready signal and register the remove callback
coordinator.add_signal_remove_callback(
async_dispatcher_connect(hass, coordinator.signal_ready, async_devices_discovered))

View File

@ -69,6 +69,10 @@ A/C Controls:
#### Features
{% if version_installed.split('.') | map('int') < '0.6.9'.split('.') | map('int') %}
- Added additional fridge controls (#200)
{% endif %}
{% if version_installed.split('.') | map('int') < '0.6.8'.split('.') | map('int') %}
- Added Dehumidifier (#114)
- Added oven drawer sensors