Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade econet to use new API #44427

Merged
merged 9 commits into from Jan 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion .coveragerc
Expand Up @@ -215,7 +215,11 @@ omit =
homeassistant/components/ecobee/notify.py
homeassistant/components/ecobee/sensor.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/edl21/*
homeassistant/components/eddystone_temperature/sensor.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -120,6 +120,7 @@ homeassistant/components/dweet/* @fabaff
homeassistant/components/dynalite/* @ziv1234
homeassistant/components/eafm/* @Jc2k
homeassistant/components/ecobee/* @marthoc
homeassistant/components/econet/* @vangorra @w1ll1am23
homeassistant/components/ecovacs/* @OverloadUT
homeassistant/components/edl21/* @mtdcr
homeassistant/components/egardia/* @jeroenterheerdt
Expand Down
159 changes: 158 additions & 1 deletion homeassistant/components/econet/__init__.py
@@ -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()
w1ll1am23 marked this conversation as resolved.
Show resolved Hide resolved

def update_published():
w1ll1am23 marked this conversation as resolved.
Show resolved Hide resolved
"""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)
w1ll1am23 marked this conversation as resolved.
Show resolved Hide resolved
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
82 changes: 82 additions & 0 deletions homeassistant/components/econet/binary_sensor.py
@@ -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
w1ll1am23 marked this conversation as resolved.
Show resolved Hide resolved
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}"
)
61 changes: 61 additions & 0 deletions homeassistant/components/econet/config_flow.py
@@ -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],
},
)
4 changes: 2 additions & 2 deletions homeassistant/components/econet/const.py
@@ -1,5 +1,5 @@
"""Constants for Econet integration."""

DOMAIN = "econet"
SERVICE_ADD_VACATION = "add_vacation"
SERVICE_DELETE_VACATION = "delete_vacation"
API_CLIENT = "api_client"
EQUIPMENT = "equipment"
10 changes: 6 additions & 4 deletions homeassistant/components/econet/manifest.json
@@ -1,7 +1,9 @@

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