Upgrade econet to use new API (#44427)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
William Scanlon 2021-01-26 03:18:20 -05:00 committed by GitHub
parent f2a8ccdbae
commit 260d9f8e16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 717 additions and 195 deletions

View file

@ -215,7 +215,11 @@ omit =
homeassistant/components/ecobee/notify.py homeassistant/components/ecobee/notify.py
homeassistant/components/ecobee/sensor.py homeassistant/components/ecobee/sensor.py
homeassistant/components/ecobee/weather.py homeassistant/components/ecobee/weather.py
homeassistant/components/econet/* homeassistant/components/econet/__init__.py
homeassistant/components/econet/binary_sensor.py
homeassistant/components/econet/const.py
homeassistant/components/econet/sensor.py
homeassistant/components/econet/water_heater.py
homeassistant/components/ecovacs/* homeassistant/components/ecovacs/*
homeassistant/components/edl21/* homeassistant/components/edl21/*
homeassistant/components/eddystone_temperature/sensor.py homeassistant/components/eddystone_temperature/sensor.py

View file

@ -120,6 +120,7 @@ homeassistant/components/dweet/* @fabaff
homeassistant/components/dynalite/* @ziv1234 homeassistant/components/dynalite/* @ziv1234
homeassistant/components/eafm/* @Jc2k homeassistant/components/eafm/* @Jc2k
homeassistant/components/ecobee/* @marthoc homeassistant/components/ecobee/* @marthoc
homeassistant/components/econet/* @vangorra @w1ll1am23
homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/ecovacs/* @OverloadUT
homeassistant/components/edl21/* @mtdcr homeassistant/components/edl21/* @mtdcr
homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/egardia/* @jeroenterheerdt

View file

@ -1 +1,158 @@
"""The econet component.""" """Support for EcoNet products."""
import asyncio
from datetime import timedelta
import logging
from aiohttp.client_exceptions import ClientError
from pyeconet import EcoNetApiInterface
from pyeconet.equipment import EquipmentType
from pyeconet.errors import (
GenericHTTPError,
InvalidCredentialsError,
InvalidResponseFormat,
PyeconetError,
)
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
from .const import API_CLIENT, DOMAIN, EQUIPMENT
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["binary_sensor", "sensor", "water_heater"]
PUSH_UPDATE = "econet.push_update"
INTERVAL = timedelta(minutes=60)
async def async_setup(hass, config):
"""Set up the EcoNet component."""
hass.data[DOMAIN] = {}
hass.data[DOMAIN][API_CLIENT] = {}
hass.data[DOMAIN][EQUIPMENT] = {}
return True
async def async_setup_entry(hass, config_entry):
"""Set up EcoNet as config entry."""
email = config_entry.data[CONF_EMAIL]
password = config_entry.data[CONF_PASSWORD]
try:
api = await EcoNetApiInterface.login(email, password=password)
except InvalidCredentialsError:
_LOGGER.error("Invalid credentials provided")
return False
except PyeconetError as err:
_LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady from err
try:
equipment = await api.get_equipment_by_type([EquipmentType.WATER_HEATER])
except (ClientError, GenericHTTPError, InvalidResponseFormat) as err:
raise ConfigEntryNotReady from err
hass.data[DOMAIN][API_CLIENT][config_entry.entry_id] = api
hass.data[DOMAIN][EQUIPMENT][config_entry.entry_id] = equipment
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
)
api.subscribe()
def update_published():
"""Handle a push update."""
dispatcher_send(hass, PUSH_UPDATE)
for _eqip in equipment[EquipmentType.WATER_HEATER]:
_eqip.set_update_callback(update_published)
async def resubscribe(now):
"""Resubscribe to the MQTT updates."""
await hass.async_add_executor_job(api.unsubscribe)
api.subscribe()
async def fetch_update(now):
"""Fetch the latest changes from the API."""
await api.refresh_equipment()
async_track_time_interval(hass, resubscribe, INTERVAL)
async_track_time_interval(hass, fetch_update, INTERVAL + timedelta(minutes=1))
return True
async def async_unload_entry(hass, entry):
"""Unload a EcoNet config entry."""
tasks = [
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
await asyncio.gather(*tasks)
hass.data[DOMAIN][API_CLIENT].pop(entry.entry_id)
hass.data[DOMAIN][EQUIPMENT].pop(entry.entry_id)
return True
class EcoNetEntity(Entity):
"""Define a base EcoNet entity."""
def __init__(self, econet):
"""Initialize."""
self._econet = econet
async def async_added_to_hass(self):
"""Subscribe to device events."""
await super().async_added_to_hass()
self.async_on_remove(
self.hass.helpers.dispatcher.async_dispatcher_connect(
PUSH_UPDATE, self.on_update_received
)
)
@callback
def on_update_received(self):
"""Update was pushed from the ecoent API."""
self.async_write_ha_state()
@property
def available(self):
"""Return if the the device is online or not."""
return self._econet.connected
@property
def device_info(self):
"""Return device registry information for this entity."""
return {
"identifiers": {(DOMAIN, self._econet.device_id)},
"manufacturer": "Rheem",
"name": self._econet.device_name,
}
@property
def name(self):
"""Return the name of the entity."""
return self._econet.device_name
@property
def unique_id(self):
"""Return the unique ID of the entity."""
return f"{self._econet.device_id}_{self._econet.device_name}"
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state.
False if entity pushes its state to HA.
"""
return False

View file

@ -0,0 +1,82 @@
"""Support for Rheem EcoNet water heaters."""
import logging
from pyeconet.equipment import EquipmentType
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_OPENING,
DEVICE_CLASS_POWER,
BinarySensorEntity,
)
from . import EcoNetEntity
from .const import DOMAIN, EQUIPMENT
_LOGGER = logging.getLogger(__name__)
SENSOR_NAME_RUNNING = "running"
SENSOR_NAME_SHUTOFF_VALVE = "shutoff_valve"
SENSOR_NAME_VACATION = "vacation"
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)
)
async_add_entities(binary_sensors)
class EcoNetBinarySensor(EcoNetEntity, BinarySensorEntity):
"""Define a Econet binary sensor."""
def __init__(self, econet_device, device_name):
"""Initialize."""
super().__init__(econet_device)
self._econet = econet_device
self._device_name = device_name
@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
@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
@property
def name(self):
"""Return the name of the entity."""
return f"{self._econet.device_name}_{self._device_name}"
@property
def unique_id(self):
"""Return the unique ID of the entity."""
return (
f"{self._econet.device_id}_{self._econet.device_name}_{self._device_name}"
)

View file

@ -0,0 +1,61 @@
"""Config flow to configure the EcoNet component."""
from pyeconet import EcoNetApiInterface
from pyeconet.errors import InvalidCredentialsError, PyeconetError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from .const import DOMAIN # pylint: disable=unused-import
class EcoNetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle an EcoNet config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize the config flow."""
self.data_schema = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
}
)
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
if not user_input:
return self.async_show_form(
step_id="user",
data_schema=self.data_schema,
)
await self.async_set_unique_id(user_input[CONF_EMAIL])
self._abort_if_unique_id_configured()
errors = {}
try:
await EcoNetApiInterface.login(
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except InvalidCredentialsError:
errors["base"] = "invalid_auth"
except PyeconetError:
errors["base"] = "cannot_connect"
if errors:
return self.async_show_form(
step_id="user",
data_schema=self.data_schema,
errors=errors,
)
return self.async_create_entry(
title=user_input[CONF_EMAIL],
data={
CONF_EMAIL: user_input[CONF_EMAIL],
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)

View file

@ -1,5 +1,5 @@
"""Constants for Econet integration.""" """Constants for Econet integration."""
DOMAIN = "econet" DOMAIN = "econet"
SERVICE_ADD_VACATION = "add_vacation" API_CLIENT = "api_client"
SERVICE_DELETE_VACATION = "delete_vacation" EQUIPMENT = "equipment"

View file

@ -1,7 +1,9 @@
{ {
"domain": "econet", "domain": "econet",
"name": "Rheem EcoNET Water Products", "name": "Rheem EcoNet Products",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/econet", "documentation": "https://www.home-assistant.io/integrations/econet",
"requirements": ["pyeconet==0.0.11"], "requirements": ["pyeconet==0.1.12"],
"codeowners": [] "codeowners": ["@vangorra", "@w1ll1am23"]
} }

View file

@ -0,0 +1,122 @@
"""Support for Rheem EcoNet water heaters."""
import logging
from pyeconet.equipment import EquipmentType
from homeassistant.const import (
ENERGY_KILO_WATT_HOUR,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
VOLUME_GALLONS,
)
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"
OVERRIDE_STATUS = "oveerride_status"
WATER_USAGE_TODAY = "water_usage_today"
POWER_USAGE_TODAY = "power_usage_today"
ALERT_COUNT = "alert_count"
WIFI_SIGNAL = "wifi_signal"
RUNNING_STATE = "running_state"
_LOGGER = logging.getLogger(__name__)
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 = []
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)
class EcoNetSensor(EcoNetEntity):
"""Define a Econet sensor."""
def __init__(self, econet_device, device_name):
"""Initialize."""
super().__init__(econet_device)
self._econet = econet_device
self._device_name = device_name
@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
@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
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
@property
def name(self):
"""Return the name of the entity."""
return f"{self._econet.device_name}_{self._device_name}"
@property
def unique_id(self):
"""Return the unique ID of the entity."""
return (
f"{self._econet.device_id}_{self._econet.device_name}_{self._device_name}"
)

View file

@ -1,19 +0,0 @@
add_vacation:
description: Add a vacation to your water heater.
fields:
entity_id:
description: Name(s) of entities to change.
example: "water_heater.econet"
start_date:
description: The timestamp of when the vacation should start. (Optional, defaults to now)
example: 1513186320
end_date:
description: The timestamp of when the vacation should end.
example: 1513445520
delete_vacation:
description: Delete your existing vacation from your water heater.
fields:
entity_id:
description: Name(s) of entities to change.
example: "water_heater.econet"

View file

@ -0,0 +1,22 @@
{
"config": {
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"step": {
"user": {
"title": "Setup Rheem EcoNet Account",
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
}
}
}

View file

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication"
},
"step": {
"user": {
"data": {
"email": "Email",
"password": "Password"
},
"title": "Setup Rheem EcoNet Account"
}
}
}
}

View file

@ -1,12 +1,11 @@
"""Support for Rheem EcoNet water heaters.""" """Support for Rheem EcoNet water heaters."""
import datetime
import logging import logging
from pyeconet.api import PyEcoNet from pyeconet.equipment import EquipmentType
import voluptuous as vol from pyeconet.equipment.water_heater import WaterHeaterOperationMode
from homeassistant.components.water_heater import ( from homeassistant.components.water_heater import (
PLATFORM_SCHEMA, ATTR_TEMPERATURE,
STATE_ECO, STATE_ECO,
STATE_ELECTRIC, STATE_ELECTRIC,
STATE_GAS, STATE_GAS,
@ -14,222 +13,125 @@ from homeassistant.components.water_heater import (
STATE_HIGH_DEMAND, STATE_HIGH_DEMAND,
STATE_OFF, STATE_OFF,
STATE_PERFORMANCE, STATE_PERFORMANCE,
SUPPORT_AWAY_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
WaterHeaterEntity, WaterHeaterEntity,
) )
from homeassistant.const import ( from homeassistant.const import TEMP_FAHRENHEIT
ATTR_ENTITY_ID, from homeassistant.core import callback
ATTR_TEMPERATURE,
CONF_PASSWORD,
CONF_USERNAME,
TEMP_FAHRENHEIT,
)
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN, SERVICE_ADD_VACATION, SERVICE_DELETE_VACATION from . import EcoNetEntity
from .const import DOMAIN, EQUIPMENT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_VACATION_START = "next_vacation_start_date" ECONET_STATE_TO_HA = {
ATTR_VACATION_END = "next_vacation_end_date" WaterHeaterOperationMode.ENERGY_SAVING: STATE_ECO,
ATTR_ON_VACATION = "on_vacation" WaterHeaterOperationMode.HIGH_DEMAND: STATE_HIGH_DEMAND,
ATTR_TODAYS_ENERGY_USAGE = "todays_energy_usage" WaterHeaterOperationMode.OFF: STATE_OFF,
ATTR_IN_USE = "in_use" WaterHeaterOperationMode.HEAT_PUMP_ONLY: STATE_HEAT_PUMP,
WaterHeaterOperationMode.ELECTRIC_MODE: STATE_ELECTRIC,
ATTR_START_DATE = "start_date" WaterHeaterOperationMode.GAS: STATE_GAS,
ATTR_END_DATE = "end_date" WaterHeaterOperationMode.PERFORMANCE: STATE_PERFORMANCE,
}
ATTR_LOWER_TEMP = "lower_temp" HA_STATE_TO_ECONET = {value: key for key, value in ECONET_STATE_TO_HA.items()}
ATTR_UPPER_TEMP = "upper_temp"
ATTR_IS_ENABLED = "is_enabled"
SUPPORT_FLAGS_HEATER = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE SUPPORT_FLAGS_HEATER = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
ADD_VACATION_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_START_DATE): cv.positive_int,
vol.Required(ATTR_END_DATE): cv.positive_int,
}
)
DELETE_VACATION_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) async def async_setup_entry(hass, entry, async_add_entities):
"""Set up EcoNet water heater based on a config entry."""
ECONET_DATA = "econet" equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id]
async_add_entities(
ECONET_STATE_TO_HA = { [
"Energy Saver": STATE_ECO, EcoNetWaterHeater(water_heater)
"gas": STATE_GAS, for water_heater in equipment[EquipmentType.WATER_HEATER]
"High Demand": STATE_HIGH_DEMAND, ],
"Off": STATE_OFF,
"Performance": STATE_PERFORMANCE,
"Heat Pump Only": STATE_HEAT_PUMP,
"Electric-Only": STATE_ELECTRIC,
"Electric": STATE_ELECTRIC,
"Heat Pump": STATE_HEAT_PUMP,
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the EcoNet water heaters."""
hass.data[ECONET_DATA] = {}
hass.data[ECONET_DATA]["water_heaters"] = []
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
econet = PyEcoNet(username, password)
water_heaters = econet.get_water_heaters()
hass_water_heaters = [
EcoNetWaterHeater(water_heater) for water_heater in water_heaters
]
add_entities(hass_water_heaters)
hass.data[ECONET_DATA]["water_heaters"].extend(hass_water_heaters)
def service_handle(service):
"""Handle the service calls."""
entity_ids = service.data.get("entity_id")
all_heaters = hass.data[ECONET_DATA]["water_heaters"]
_heaters = [
x for x in all_heaters if not entity_ids or x.entity_id in entity_ids
]
for _water_heater in _heaters:
if service.service == SERVICE_ADD_VACATION:
start = service.data.get(ATTR_START_DATE)
end = service.data.get(ATTR_END_DATE)
_water_heater.add_vacation(start, end)
if service.service == SERVICE_DELETE_VACATION:
for vacation in _water_heater.water_heater.vacations:
vacation.delete()
_water_heater.schedule_update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_ADD_VACATION, service_handle, schema=ADD_VACATION_SCHEMA
)
hass.services.register(
DOMAIN, SERVICE_DELETE_VACATION, service_handle, schema=DELETE_VACATION_SCHEMA
) )
class EcoNetWaterHeater(WaterHeaterEntity): class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity):
"""Representation of an EcoNet water heater.""" """Define a Econet water heater."""
def __init__(self, water_heater): def __init__(self, water_heater):
"""Initialize the water heater.""" """Initialize."""
super().__init__(water_heater)
self._running = water_heater.running
self._poll = True
self.water_heater = water_heater self.water_heater = water_heater
self.supported_modes = self.water_heater.supported_modes
self.econet_state_to_ha = {} self.econet_state_to_ha = {}
self.ha_state_to_econet = {} self.ha_state_to_econet = {}
for mode in ECONET_STATE_TO_HA:
if mode in self.supported_modes: @callback
self.econet_state_to_ha[mode] = ECONET_STATE_TO_HA.get(mode) def on_update_received(self):
for key, value in self.econet_state_to_ha.items(): """Update was pushed from the ecoent API."""
self.ha_state_to_econet[value] = key if self._running != self.water_heater.running:
for mode in self.supported_modes: # Water heater running state has changed so check usage on next update
if mode not in ECONET_STATE_TO_HA: self._poll = True
error = f"Invalid operation mode mapping. {mode} doesn't map. Please report this." self._running = self.water_heater.running
_LOGGER.error(error) self.async_write_ha_state()
@property @property
def name(self): def is_away_mode_on(self):
"""Return the device name.""" """Return true if away mode is on."""
return self.water_heater.name return self._econet.away
@property
def available(self):
"""Return if the the device is online or not."""
return self.water_heater.is_connected
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT
@property
def device_state_attributes(self):
"""Return the optional device state attributes."""
data = {}
vacations = self.water_heater.get_vacations()
if vacations:
data[ATTR_VACATION_START] = vacations[0].start_date
data[ATTR_VACATION_END] = vacations[0].end_date
data[ATTR_ON_VACATION] = self.water_heater.is_on_vacation
todays_usage = self.water_heater.total_usage_for_today
if todays_usage:
data[ATTR_TODAYS_ENERGY_USAGE] = todays_usage
data[ATTR_IN_USE] = self.water_heater.in_use
if self.water_heater.lower_temp is not None:
data[ATTR_LOWER_TEMP] = round(self.water_heater.lower_temp, 2)
if self.water_heater.upper_temp is not None:
data[ATTR_UPPER_TEMP] = round(self.water_heater.upper_temp, 2)
if self.water_heater.is_enabled is not None:
data[ATTR_IS_ENABLED] = self.water_heater.is_enabled
return data
@property @property
def current_operation(self): def current_operation(self):
""" """Return current operation."""
Return current operation as one of the following. econet_mode = self.water_heater.mode
_current_op = STATE_OFF
if econet_mode is not None:
_current_op = ECONET_STATE_TO_HA[econet_mode]
["eco", "heat_pump", "high_demand", "electric_only"] return _current_op
"""
current_op = self.econet_state_to_ha.get(self.water_heater.mode)
return current_op
@property @property
def operation_list(self): def operation_list(self):
"""List of available operation modes.""" """List of available operation modes."""
econet_modes = self.water_heater.modes
op_list = [] op_list = []
for mode in self.supported_modes: for mode in econet_modes:
ha_mode = self.econet_state_to_ha.get(mode) if (
if ha_mode is not None: mode is not WaterHeaterOperationMode.UNKNOWN
and mode is not WaterHeaterOperationMode.VACATION
):
ha_mode = ECONET_STATE_TO_HA[mode]
op_list.append(ha_mode) op_list.append(ha_mode)
return op_list return op_list
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
return SUPPORT_FLAGS_HEATER if self.water_heater.modes:
if self.water_heater.supports_away:
return SUPPORT_FLAGS_HEATER | SUPPORT_AWAY_MODE
return SUPPORT_FLAGS_HEATER
if self.water_heater.supports_away:
return SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE
return SUPPORT_TARGET_TEMPERATURE
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
target_temp = kwargs.get(ATTR_TEMPERATURE) target_temp = kwargs.get(ATTR_TEMPERATURE)
if target_temp is not None: if target_temp is not None:
self.water_heater.set_target_set_point(target_temp) self.water_heater.set_set_point(target_temp)
else: else:
_LOGGER.error("A target temperature must be provided") _LOGGER.error("A target temperature must be provided")
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set operation mode.""" """Set operation mode."""
op_mode_to_set = self.ha_state_to_econet.get(operation_mode) op_mode_to_set = HA_STATE_TO_ECONET.get(operation_mode)
if op_mode_to_set is not None: if op_mode_to_set is not None:
self.water_heater.set_mode(op_mode_to_set) self.water_heater.set_mode(op_mode_to_set)
else: else:
_LOGGER.error("An operation mode must be provided") _LOGGER.error("Invalid operation mode: %s", operation_mode)
def add_vacation(self, start, end):
"""Add a vacation to this water heater."""
if not start:
start = datetime.datetime.now()
else:
start = datetime.datetime.fromtimestamp(start)
end = datetime.datetime.fromtimestamp(end)
self.water_heater.set_vacation_mode(start, end)
def update(self):
"""Get the latest date."""
self.water_heater.update_state()
@property @property
def target_temperature(self): def target_temperature(self):
@ -239,9 +141,31 @@ class EcoNetWaterHeater(WaterHeaterEntity):
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return self.water_heater.min_set_point return self.water_heater.set_point_limits[0]
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return self.water_heater.max_set_point return self.water_heater.set_point_limits[1]
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state.
False if entity pushes its state to HA.
"""
return self._poll
async def async_update(self):
"""Get the latest energy usage."""
await self.water_heater.get_energy_usage()
await self.water_heater.get_water_usage()
self._poll = False
def turn_away_mode_on(self):
"""Turn away mode on."""
self.water_heater.set_away_mode(True)
def turn_away_mode_off(self):
"""Turn away mode off."""
self.water_heater.set_away_mode(False)

View file

@ -54,6 +54,7 @@ FLOWS = [
"dynalite", "dynalite",
"eafm", "eafm",
"ecobee", "ecobee",
"econet",
"elgato", "elgato",
"elkm1", "elkm1",
"emulated_roku", "emulated_roku",

View file

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

View file

@ -684,6 +684,9 @@ pydexcom==0.2.0
# homeassistant.components.zwave # homeassistant.components.zwave
pydispatcher==2.0.5 pydispatcher==2.0.5
# homeassistant.components.econet
pyeconet==0.1.12
# homeassistant.components.everlights # homeassistant.components.everlights
pyeverlights==0.1.0 pyeverlights==0.1.0

View file

@ -0,0 +1 @@
"""Tests for the Econet component."""

View file

@ -0,0 +1,140 @@
"""Tests for the Econet component."""
from unittest.mock import patch
from pyeconet.api import EcoNetApiInterface
from pyeconet.errors import InvalidCredentialsError, PyeconetError
from homeassistant.components.econet import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from tests.common import MockConfigEntry
async def test_bad_credentials(hass):
"""Test when provided credentials are rejected."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
with patch(
"pyeconet.EcoNetApiInterface.login",
side_effect=InvalidCredentialsError(),
), patch("homeassistant.components.econet.async_setup", return_value=True), patch(
"homeassistant.components.econet.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_EMAIL: "admin@localhost.com",
CONF_PASSWORD: "password0",
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {
"base": "invalid_auth",
}
async def test_generic_error_from_library(hass):
"""Test when connection fails."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
with patch(
"pyeconet.EcoNetApiInterface.login",
side_effect=PyeconetError(),
), patch("homeassistant.components.econet.async_setup", return_value=True), patch(
"homeassistant.components.econet.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_EMAIL: "admin@localhost.com",
CONF_PASSWORD: "password0",
},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {
"base": "cannot_connect",
}
async def test_auth_worked(hass):
"""Test when provided credentials are accepted."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
with patch(
"pyeconet.EcoNetApiInterface.login",
return_value=EcoNetApiInterface,
), patch("homeassistant.components.econet.async_setup", return_value=True), patch(
"homeassistant.components.econet.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_EMAIL: "admin@localhost.com",
CONF_PASSWORD: "password0",
},
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["data"] == {
CONF_EMAIL: "admin@localhost.com",
CONF_PASSWORD: "password0",
}
async def test_already_configured(hass):
"""Test when provided credentials are already configured."""
config = {
CONF_EMAIL: "admin@localhost.com",
CONF_PASSWORD: "password0",
}
MockConfigEntry(
domain=DOMAIN, data=config, unique_id="admin@localhost.com"
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
with patch(
"pyeconet.EcoNetApiInterface.login",
return_value=EcoNetApiInterface,
), patch("homeassistant.components.econet.async_setup", return_value=True), patch(
"homeassistant.components.econet.async_setup_entry", return_value=True
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_EMAIL: "admin@localhost.com",
CONF_PASSWORD: "password0",
},
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"