Skip to content

Commit

Permalink
Merge pull request #51 from simbaja/dev
Browse files Browse the repository at this point in the history
v0.5.0 Dev -> Master
  • Loading branch information
simbaja authored Oct 17, 2021
2 parents c1e01b7 + a8e161c commit f36f5a7
Show file tree
Hide file tree
Showing 36 changed files with 662 additions and 47 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).**
Expand Down
26 changes: 20 additions & 6 deletions custom_components/ge_home/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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."""
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions custom_components/ge_home/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 3 additions & 0 deletions custom_components/ge_home/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .wac import WacApi
from .sac import SacApi
from .pac import PacApi
from .hood import HoodApi

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -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
14 changes: 12 additions & 2 deletions custom_components/ge_home/devices/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand Down
23 changes: 20 additions & 3 deletions custom_components/ge_home/devices/fridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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 = [
Expand All @@ -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([
Expand Down
52 changes: 52 additions & 0 deletions custom_components/ge_home/devices/hood.py
Original file line number Diff line number Diff line change
@@ -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

32 changes: 23 additions & 9 deletions custom_components/ge_home/devices/oven.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .base import ApplianceApi
from ..entities import (
GeErdSensor,
GeErdTimerSensor,
GeErdBinarySensor,
GeErdPropertySensor,
GeErdPropertyBinarySensor,
Expand All @@ -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 = []
Expand All @@ -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)
Expand All @@ -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"
6 changes: 3 additions & 3 deletions custom_components/ge_home/devices/sac.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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"),
Expand Down
3 changes: 2 additions & 1 deletion custom_components/ge_home/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
from .oven import *
from .water_filter import *
from .advantium import *
from .ac import *
from .ac import *
from .hood import *
3 changes: 2 additions & 1 deletion custom_components/ge_home/entities/ac/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .ge_wac_climate import GeWacClimate
from .ge_sac_climate import GeSacClimate
from .ge_pac_climate import GePacClimate
from .ge_pac_climate import GePacClimate
from .ge_sac_temperature_sensor import GeSacTemperatureSensor
15 changes: 15 additions & 0 deletions custom_components/ge_home/entities/ac/ge_sac_temperature_sensor.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions custom_components/ge_home/entities/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions custom_components/ge_home/entities/common/ge_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
Loading

0 comments on commit f36f5a7

Please sign in to comment.