Add econet thermostat support and use getattr for sensors (#45564)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
William Scanlon 2021-03-25 12:06:51 -04:00 committed by GitHub
parent 9f07ca069d
commit 4f4a6fd6a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 338 additions and 91 deletions

View file

@ -221,6 +221,7 @@ omit =
homeassistant/components/ecobee/weather.py
homeassistant/components/econet/__init__.py
homeassistant/components/econet/binary_sensor.py
homeassistant/components/econet/climate.py
homeassistant/components/econet/const.py
homeassistant/components/econet/sensor.py
homeassistant/components/econet/water_heater.py

View file

@ -13,7 +13,7 @@ from pyeconet.errors import (
PyeconetError,
)
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, TEMP_FAHRENHEIT
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import dispatcher_send
@ -24,7 +24,7 @@ from .const import API_CLIENT, DOMAIN, EQUIPMENT
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["binary_sensor", "sensor", "water_heater"]
PLATFORMS = ["climate", "binary_sensor", "sensor", "water_heater"]
PUSH_UPDATE = "econet.push_update"
INTERVAL = timedelta(minutes=60)
@ -54,7 +54,9 @@ async def async_setup_entry(hass, config_entry):
raise ConfigEntryNotReady from err
try:
equipment = await api.get_equipment_by_type([EquipmentType.WATER_HEATER])
equipment = await api.get_equipment_by_type(
[EquipmentType.WATER_HEATER, EquipmentType.THERMOSTAT]
)
except (ClientError, GenericHTTPError, InvalidResponseFormat) as err:
raise ConfigEntryNotReady from err
hass.data[DOMAIN][API_CLIENT][config_entry.entry_id] = api
@ -74,6 +76,9 @@ async def async_setup_entry(hass, config_entry):
for _eqip in equipment[EquipmentType.WATER_HEATER]:
_eqip.set_update_callback(update_published)
for _eqip in equipment[EquipmentType.THERMOSTAT]:
_eqip.set_update_callback(update_published)
async def resubscribe(now):
"""Resubscribe to the MQTT updates."""
await hass.async_add_executor_job(api.unsubscribe)
@ -149,6 +154,11 @@ class EcoNetEntity(Entity):
"""Return the unique ID of the entity."""
return f"{self._econet.device_id}_{self._econet.device_name}"
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state.

View file

@ -2,8 +2,10 @@
from pyeconet.equipment import EquipmentType
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_LOCK,
DEVICE_CLASS_OPENING,
DEVICE_CLASS_POWER,
DEVICE_CLASS_SOUND,
BinarySensorEntity,
)
@ -12,27 +14,40 @@ from .const import DOMAIN, EQUIPMENT
SENSOR_NAME_RUNNING = "running"
SENSOR_NAME_SHUTOFF_VALVE = "shutoff_valve"
SENSOR_NAME_VACATION = "vacation"
SENSOR_NAME_RUNNING = "running"
SENSOR_NAME_SCREEN_LOCKED = "screen_locked"
SENSOR_NAME_BEEP_ENABLED = "beep_enabled"
ATTR = "attr"
DEVICE_CLASS = "device_class"
SENSORS = {
SENSOR_NAME_SHUTOFF_VALVE: {
ATTR: "shutoff_valve_open",
DEVICE_CLASS: DEVICE_CLASS_OPENING,
},
SENSOR_NAME_RUNNING: {ATTR: "running", DEVICE_CLASS: DEVICE_CLASS_POWER},
SENSOR_NAME_SCREEN_LOCKED: {
ATTR: "screen_locked",
DEVICE_CLASS: DEVICE_CLASS_LOCK,
},
SENSOR_NAME_BEEP_ENABLED: {
ATTR: "beep_enabled",
DEVICE_CLASS: DEVICE_CLASS_SOUND,
},
}
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up EcoNet binary sensor based on a config entry."""
equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id]
binary_sensors = []
for water_heater in equipment[EquipmentType.WATER_HEATER]:
if water_heater.has_shutoff_valve:
binary_sensors.append(
EcoNetBinarySensor(
water_heater,
SENSOR_NAME_SHUTOFF_VALVE,
)
)
if water_heater.running is not None:
binary_sensors.append(EcoNetBinarySensor(water_heater, SENSOR_NAME_RUNNING))
if water_heater.vacation is not None:
binary_sensors.append(
EcoNetBinarySensor(water_heater, SENSOR_NAME_VACATION)
)
all_equipment = equipment[EquipmentType.WATER_HEATER].copy()
all_equipment.extend(equipment[EquipmentType.THERMOSTAT].copy())
for _equip in all_equipment:
for sensor_name, sensor in SENSORS.items():
if getattr(_equip, sensor[ATTR], None) is not None:
binary_sensors.append(EcoNetBinarySensor(_equip, sensor_name))
async_add_entities(binary_sensors)
@ -48,22 +63,12 @@ class EcoNetBinarySensor(EcoNetEntity, BinarySensorEntity):
@property
def is_on(self):
"""Return true if the binary sensor is on."""
if self._device_name == SENSOR_NAME_SHUTOFF_VALVE:
return self._econet.shutoff_valve_open
if self._device_name == SENSOR_NAME_RUNNING:
return self._econet.running
if self._device_name == SENSOR_NAME_VACATION:
return self._econet.vacation
return False
return getattr(self._econet, SENSORS[self._device_name][ATTR])
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
if self._device_name == SENSOR_NAME_SHUTOFF_VALVE:
return DEVICE_CLASS_OPENING
if self._device_name == SENSOR_NAME_RUNNING:
return DEVICE_CLASS_POWER
return None
return SENSORS[self._device_name][DEVICE_CLASS]
@property
def name(self):

View file

@ -0,0 +1,241 @@
"""Support for Rheem EcoNet thermostats."""
import logging
from pyeconet.equipment import EquipmentType
from pyeconet.equipment.thermostat import ThermostatFanMode, ThermostatOperationMode
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
HVAC_MODE_COOL,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
SUPPORT_AUX_HEAT,
SUPPORT_FAN_MODE,
SUPPORT_TARGET_HUMIDITY,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.const import ATTR_TEMPERATURE
from . import EcoNetEntity
from .const import DOMAIN, EQUIPMENT
_LOGGER = logging.getLogger(__name__)
ECONET_STATE_TO_HA = {
ThermostatOperationMode.HEATING: HVAC_MODE_HEAT,
ThermostatOperationMode.COOLING: HVAC_MODE_COOL,
ThermostatOperationMode.OFF: HVAC_MODE_OFF,
ThermostatOperationMode.AUTO: HVAC_MODE_HEAT_COOL,
ThermostatOperationMode.FAN_ONLY: HVAC_MODE_FAN_ONLY,
}
HA_STATE_TO_ECONET = {value: key for key, value in ECONET_STATE_TO_HA.items()}
ECONET_FAN_STATE_TO_HA = {
ThermostatFanMode.AUTO: FAN_AUTO,
ThermostatFanMode.LOW: FAN_LOW,
ThermostatFanMode.MEDIUM: FAN_MEDIUM,
ThermostatFanMode.HIGH: FAN_HIGH,
}
HA_FAN_STATE_TO_ECONET = {value: key for key, value in ECONET_FAN_STATE_TO_HA.items()}
SUPPORT_FLAGS_THERMOSTAT = (
SUPPORT_TARGET_TEMPERATURE
| SUPPORT_TARGET_TEMPERATURE_RANGE
| SUPPORT_FAN_MODE
| SUPPORT_AUX_HEAT
)
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up EcoNet thermostat based on a config entry."""
equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id]
async_add_entities(
[
EcoNetThermostat(thermostat)
for thermostat in equipment[EquipmentType.THERMOSTAT]
],
)
class EcoNetThermostat(EcoNetEntity, ClimateEntity):
"""Define a Econet thermostat."""
def __init__(self, thermostat):
"""Initialize."""
super().__init__(thermostat)
self._running = thermostat.running
self._poll = True
self.econet_state_to_ha = {}
self.ha_state_to_econet = {}
self.op_list = []
for mode in self._econet.modes:
if mode not in [
ThermostatOperationMode.UNKNOWN,
ThermostatOperationMode.EMERGENCY_HEAT,
]:
ha_mode = ECONET_STATE_TO_HA[mode]
self.op_list.append(ha_mode)
@property
def supported_features(self):
"""Return the list of supported features."""
if self._econet.supports_humidifier:
return SUPPORT_FLAGS_THERMOSTAT | SUPPORT_TARGET_HUMIDITY
return SUPPORT_FLAGS_THERMOSTAT
@property
def current_temperature(self):
"""Return the current temperature."""
return self._econet.set_point
@property
def current_humidity(self):
"""Return the current humidity."""
return self._econet.humidity
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
if self._econet.supports_humidifier:
return self._econet.dehumidifier_set_point
return None
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
if self.hvac_mode == HVAC_MODE_COOL:
return self._econet.cool_set_point
if self.hvac_mode == HVAC_MODE_HEAT:
return self._econet.heat_set_point
return None
@property
def target_temperature_low(self):
"""Return the lower bound temperature we try to reach."""
if self.hvac_mode == HVAC_MODE_HEAT_COOL:
return self._econet.heat_set_point
return None
@property
def target_temperature_high(self):
"""Return the higher bound temperature we try to reach."""
if self.hvac_mode == HVAC_MODE_HEAT_COOL:
return self._econet.cool_set_point
return None
def set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temp = kwargs.get(ATTR_TEMPERATURE)
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if target_temp:
self._econet.set_set_point(target_temp, None, None)
if target_temp_low or target_temp_high:
self._econet.set_set_point(None, target_temp_high, target_temp_low)
@property
def is_aux_heat(self):
"""Return true if aux heater."""
return self._econet.mode == ThermostatOperationMode.EMERGENCY_HEAT
@property
def hvac_modes(self):
"""Return hvac operation ie. heat, cool mode.
Needs to be one of HVAC_MODE_*.
"""
return self.op_list
@property
def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool, mode.
Needs to be one of HVAC_MODE_*.
"""
econet_mode = self._econet.mode
_current_op = HVAC_MODE_OFF
if econet_mode is not None:
_current_op = ECONET_STATE_TO_HA[econet_mode]
return _current_op
def set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode."""
hvac_mode_to_set = HA_STATE_TO_ECONET.get(hvac_mode)
if hvac_mode_to_set is None:
raise ValueError(f"{hvac_mode} is not a valid mode.")
self._econet.set_mode(hvac_mode_to_set)
def set_humidity(self, humidity: int):
"""Set new target humidity."""
self._econet.set_dehumidifier_set_point(humidity)
@property
def fan_mode(self):
"""Return the current fan mode."""
econet_fan_mode = self._econet.fan_mode
# Remove this after we figure out how to handle med lo and med hi
if econet_fan_mode in [ThermostatFanMode.MEDHI, ThermostatFanMode.MEDLO]:
econet_fan_mode = ThermostatFanMode.MEDIUM
_current_fan_mode = FAN_AUTO
if econet_fan_mode is not None:
_current_fan_mode = ECONET_FAN_STATE_TO_HA[econet_fan_mode]
return _current_fan_mode
@property
def fan_modes(self):
"""Return the fan modes."""
econet_fan_modes = self._econet.fan_modes
fan_list = []
for mode in econet_fan_modes:
# Remove the MEDLO MEDHI once we figure out how to handle it
if mode not in [
ThermostatFanMode.UNKNOWN,
ThermostatFanMode.MEDLO,
ThermostatFanMode.MEDHI,
]:
fan_list.append(ECONET_FAN_STATE_TO_HA[mode])
return fan_list
def set_fan_mode(self, fan_mode):
"""Set the fan mode."""
self._econet.set_fan_mode(HA_FAN_STATE_TO_ECONET[fan_mode])
def turn_aux_heat_on(self):
"""Turn auxiliary heater on."""
self._econet.set_mode(ThermostatOperationMode.EMERGENCY_HEAT)
def turn_aux_heat_off(self):
"""Turn auxiliary heater off."""
self._econet.set_mode(ThermostatOperationMode.HEATING)
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._econet.set_point_limits[0]
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._econet.set_point_limits[1]
@property
def min_humidity(self) -> int:
"""Return the minimum humidity."""
return self._econet.dehumidifier_set_point_limits[0]
@property
def max_humidity(self) -> int:
"""Return the maximum humidity."""
return self._econet.dehumidifier_set_point_limits[1]

View file

@ -4,6 +4,6 @@
"name": "Rheem EcoNet Products",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/econet",
"requirements": ["pyeconet==0.1.12"],
"requirements": ["pyeconet==0.1.13"],
"codeowners": ["@vangorra", "@w1ll1am23"]
}

View file

@ -3,9 +3,9 @@ from pyeconet.equipment import EquipmentType
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import (
DEVICE_CLASS_SIGNAL_STRENGTH,
ENERGY_KILO_WATT_HOUR,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
VOLUME_GALLONS,
)
@ -13,6 +13,7 @@ from . import EcoNetEntity
from .const import DOMAIN, EQUIPMENT
ENERGY_KILO_BRITISH_THERMAL_UNIT = "kBtu"
TANK_HEALTH = "tank_health"
AVAILIBLE_HOT_WATER = "availible_hot_water"
COMPRESSOR_HEALTH = "compressor_health"
@ -23,28 +24,51 @@ ALERT_COUNT = "alert_count"
WIFI_SIGNAL = "wifi_signal"
RUNNING_STATE = "running_state"
SENSOR_NAMES_TO_ATTRIBUTES = {
TANK_HEALTH: "tank_health",
AVAILIBLE_HOT_WATER: "tank_hot_water_availability",
COMPRESSOR_HEALTH: "compressor_health",
OVERRIDE_STATUS: "override_status",
WATER_USAGE_TODAY: "todays_water_usage",
POWER_USAGE_TODAY: "todays_energy_usage",
ALERT_COUNT: "alert_count",
WIFI_SIGNAL: "wifi_signal",
RUNNING_STATE: "running_state",
}
SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT = {
TANK_HEALTH: PERCENTAGE,
AVAILIBLE_HOT_WATER: PERCENTAGE,
COMPRESSOR_HEALTH: PERCENTAGE,
OVERRIDE_STATUS: None,
WATER_USAGE_TODAY: VOLUME_GALLONS,
POWER_USAGE_TODAY: None, # Depends on unit type
ALERT_COUNT: None,
WIFI_SIGNAL: DEVICE_CLASS_SIGNAL_STRENGTH,
RUNNING_STATE: None, # This is just a string
}
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up EcoNet sensor based on a config entry."""
equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id]
sensors = []
all_equipment = equipment[EquipmentType.WATER_HEATER].copy()
all_equipment.extend(equipment[EquipmentType.THERMOSTAT].copy())
for _equip in all_equipment:
for name, attribute in SENSOR_NAMES_TO_ATTRIBUTES.items():
if getattr(_equip, attribute, None) is not None:
sensors.append(EcoNetSensor(_equip, name))
# This is None to start with and all device have it
sensors.append(EcoNetSensor(_equip, WIFI_SIGNAL))
for water_heater in equipment[EquipmentType.WATER_HEATER]:
if water_heater.tank_hot_water_availability is not None:
sensors.append(EcoNetSensor(water_heater, AVAILIBLE_HOT_WATER))
if water_heater.tank_health is not None:
sensors.append(EcoNetSensor(water_heater, TANK_HEALTH))
if water_heater.compressor_health is not None:
sensors.append(EcoNetSensor(water_heater, COMPRESSOR_HEALTH))
if water_heater.override_status:
sensors.append(EcoNetSensor(water_heater, OVERRIDE_STATUS))
if water_heater.running_state is not None:
sensors.append(EcoNetSensor(water_heater, RUNNING_STATE))
# All units have this
sensors.append(EcoNetSensor(water_heater, ALERT_COUNT))
# These aren't part of the device and start off as None in pyeconet so always add them
sensors.append(EcoNetSensor(water_heater, WATER_USAGE_TODAY))
sensors.append(EcoNetSensor(water_heater, POWER_USAGE_TODAY))
sensors.append(EcoNetSensor(water_heater, WIFI_SIGNAL))
async_add_entities(sensors)
@ -60,50 +84,21 @@ class EcoNetSensor(EcoNetEntity, SensorEntity):
@property
def state(self):
"""Return sensors state."""
if self._device_name == AVAILIBLE_HOT_WATER:
return self._econet.tank_hot_water_availability
if self._device_name == TANK_HEALTH:
return self._econet.tank_health
if self._device_name == COMPRESSOR_HEALTH:
return self._econet.compressor_health
if self._device_name == OVERRIDE_STATUS:
return self._econet.oveerride_status
if self._device_name == WATER_USAGE_TODAY:
if self._econet.todays_water_usage:
return round(self._econet.todays_water_usage, 2)
return None
if self._device_name == POWER_USAGE_TODAY:
if self._econet.todays_energy_usage:
return round(self._econet.todays_energy_usage, 2)
return None
if self._device_name == WIFI_SIGNAL:
if self._econet.wifi_signal:
return self._econet.wifi_signal
return None
if self._device_name == ALERT_COUNT:
return self._econet.alert_count
if self._device_name == RUNNING_STATE:
return self._econet.running_state
return None
value = getattr(self._econet, SENSOR_NAMES_TO_ATTRIBUTES[self._device_name])
if isinstance(value, float):
value = round(value, 2)
return value
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
if self._device_name == AVAILIBLE_HOT_WATER:
return PERCENTAGE
if self._device_name == TANK_HEALTH:
return PERCENTAGE
if self._device_name == COMPRESSOR_HEALTH:
return PERCENTAGE
if self._device_name == WATER_USAGE_TODAY:
return VOLUME_GALLONS
unit_of_measurement = SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT[self._device_name]
if self._device_name == POWER_USAGE_TODAY:
if self._econet.energy_type == ENERGY_KILO_BRITISH_THERMAL_UNIT.upper():
return ENERGY_KILO_BRITISH_THERMAL_UNIT
return ENERGY_KILO_WATT_HOUR
if self._device_name == WIFI_SIGNAL:
return SIGNAL_STRENGTH_DECIBELS_MILLIWATT
return None
unit_of_measurement = ENERGY_KILO_BRITISH_THERMAL_UNIT
else:
unit_of_measurement = ENERGY_KILO_WATT_HOUR
return unit_of_measurement
@property
def name(self):

View file

@ -18,7 +18,6 @@ from homeassistant.components.water_heater import (
SUPPORT_TARGET_TEMPERATURE,
WaterHeaterEntity,
)
from homeassistant.const import TEMP_FAHRENHEIT
from homeassistant.core import callback
from . import EcoNetEntity
@ -77,11 +76,6 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity):
"""Return true if away mode is on."""
return self._econet.away
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def current_operation(self):
"""Return current operation."""
@ -160,6 +154,7 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity):
"""Get the latest energy usage."""
await self.water_heater.get_energy_usage()
await self.water_heater.get_water_usage()
self.async_write_ha_state()
self._poll = False
def turn_away_mode_on(self):

View file

@ -1355,7 +1355,7 @@ pydroid-ipcam==0.8
pyebox==1.1.4
# homeassistant.components.econet
pyeconet==0.1.12
pyeconet==0.1.13
# homeassistant.components.edimax
pyedimax==0.2.1

View file

@ -708,7 +708,7 @@ pydexcom==0.2.0
pydispatcher==2.0.5
# homeassistant.components.econet
pyeconet==0.1.12
pyeconet==0.1.13
# homeassistant.components.everlights
pyeverlights==0.1.0