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

New EDP re:dy component #16426

Merged
merged 15 commits into from Sep 15, 2018
3 changes: 3 additions & 0 deletions .coveragerc
Expand Up @@ -92,6 +92,9 @@ omit =
homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py

homeassistant/components/edp_redy.py
homeassistant/components/*/edp_redy.py

homeassistant/components/egardia.py
homeassistant/components/*/egardia.py

Expand Down
134 changes: 134 additions & 0 deletions homeassistant/components/edp_redy.py
@@ -0,0 +1,134 @@
"""
Support for EDP re:dy.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/edp_redy/
"""

import logging
from datetime import timedelta

import voluptuous as vol

from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD,
EVENT_HOMEASSISTANT_START)
from homeassistant.core import callback
from homeassistant.helpers import discovery, dispatcher, aiohttp_client
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_time
from homeassistant.util import dt as dt_util

_LOGGER = logging.getLogger(__name__)

DOMAIN = 'edp_redy'
EDP_REDY = 'edp_redy'
DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN)
UPDATE_INTERVAL = 30

REQUIREMENTS = ['edp_redy==0.0.2']

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
}, extra=vol.ALLOW_EXTRA)


async def async_setup(hass, config):
"""Set up the EDP re:dy component."""
from edp_redy import EdpRedySession

session = EdpRedySession(config[DOMAIN][CONF_USERNAME],
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
config[DOMAIN][CONF_PASSWORD],
aiohttp_client.async_get_clientsession(hass),
hass.loop)
hass.data[EDP_REDY] = session
platform_loaded = False

async def async_update_and_sched(time):
update_success = await session.async_update()

if update_success:
dispatcher.async_dispatcher_send(hass, DATA_UPDATE_TOPIC)

nonlocal platform_loaded
if not platform_loaded:
for component in ['sensor', 'switch']:
await discovery.async_load_platform(hass, component,
abmantis marked this conversation as resolved.
Show resolved Hide resolved
DOMAIN, {}, config)
platform_loaded = True

# schedule next update
async_track_point_in_time(hass, async_update_and_sched,
time + timedelta(seconds=UPDATE_INTERVAL))

async def start_component(event):
_LOGGER.debug("Starting updates")
await async_update_and_sched(dt_util.utcnow())

# only start fetching data after HA boots to prevent delaying the boot
# process
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_component)

return True


class EdpRedyDevice(Entity):
"""Representation a base re:dy device."""

def __init__(self, session, device_id, name):
"""Initialize the device."""
self._session = session
self._state = None
self._is_available = True
self._device_state_attributes = {}
self._id = device_id
self._unique_id = device_id
self._name = name if name else device_id

async def async_added_to_hass(self):
"""Subscribe to the data updates topic."""
dispatcher.async_dispatcher_connect(
self.hass, DATA_UPDATE_TOPIC, self._data_updated)

@property
def name(self):
"""Return the name of the device."""
return self._name

@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id

@property
def available(self):
"""Return True if entity is available."""
return self._is_available

@property
def should_poll(self):
"""Return the polling state. No polling needed."""
return False

@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._device_state_attributes

@callback
def _data_updated(self):
"""Update state, trigger updates."""
self.async_schedule_update_ha_state(True)

def _parse_data(self, data):
"""Parse data received from the server."""
if "OutOfOrder" in data:
try:
self._is_available = not data['OutOfOrder']
except ValueError:
_LOGGER.error(
"Could not parse OutOfOrder for %s", self._id)
self._is_available = False
115 changes: 115 additions & 0 deletions homeassistant/components/sensor/edp_redy.py
@@ -0,0 +1,115 @@
"""Support for EDP re:dy sensors."""
import logging

from homeassistant.helpers.entity import Entity

from homeassistant.components.edp_redy import EdpRedyDevice, EDP_REDY

_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ['edp_redy']

# Load power in watts (W)
ATTR_ACTIVE_POWER = 'active_power'


async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Perform the setup for re:dy devices."""
from edp_redy.session import ACTIVE_POWER_ID

session = hass.data[EDP_REDY]
devices = []

# Create sensors for modules
for device_json in session.modules_dict.values():
if 'HA_POWER_METER' not in device_json['Capabilities']:
continue
devices.append(EdpRedyModuleSensor(session, device_json))

# Create a sensor for global active power
devices.append(EdpRedySensor(session, ACTIVE_POWER_ID, "Power Home",
'mdi:flash', 'W'))

async_add_entities(devices, True)


class EdpRedySensor(EdpRedyDevice, Entity):
"""Representation of a EDP re:dy generic sensor."""

def __init__(self, session, sensor_id, name, icon, unit):
"""Initialize the sensor."""
super().__init__(session, sensor_id, name)

self._icon = icon
self._unit = unit

@property
def state(self):
"""Return the state of the sensor."""
return self._state

@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._icon

@property
def unit_of_measurement(self):
"""Return the unit of measurement of this sensor."""
return self._unit

async def async_update(self):
"""Parse the data for this sensor."""
if self._id in self._session.values_dict:
self._state = self._session.values_dict[self._id]
self._is_available = True
else:
self._is_available = False


class EdpRedyModuleSensor(EdpRedyDevice, Entity):
"""Representation of a EDP re:dy module sensor."""

def __init__(self, session, device_json):
"""Initialize the sensor."""
super().__init__(session, device_json['PKID'],
"Power {0}".format(device_json['Name']))

@property
def state(self):
"""Return the state of the sensor."""
return self._state

@property
def icon(self):
"""Return the icon to use in the frontend."""
return 'mdi:flash'

@property
def unit_of_measurement(self):
"""Return the unit of measurement of this sensor."""
return 'W'

async def async_update(self):
"""Parse the data for this sensor."""
if self._id in self._session.modules_dict:
device_json = self._session.modules_dict[self._id]
self._parse_data(device_json)
else:
self._is_available = False

def _parse_data(self, data):
"""Parse data received from the server."""
super()._parse_data(data)

_LOGGER.debug("Sensor data: %s", str(data))

for state_var in data['StateVars']:
if state_var['Name'] == 'ActivePower':
try:
self._state = float(state_var['Value']) * 1000
except ValueError:
_LOGGER.error("Could not parse power for %s", self._id)
self._state = 0
self._is_available = False
94 changes: 94 additions & 0 deletions homeassistant/components/switch/edp_redy.py
@@ -0,0 +1,94 @@
"""Support for EDP re:dy plugs/switches."""
import logging

from homeassistant.components.edp_redy import EdpRedyDevice, EDP_REDY
from homeassistant.components.switch import SwitchDevice

_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ['edp_redy']

# Load power in watts (W)
ATTR_ACTIVE_POWER = 'active_power'


async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Perform the setup for re:dy devices."""
session = hass.data[EDP_REDY]
devices = []
for device_json in session.modules_dict.values():
if 'HA_SWITCH' not in device_json['Capabilities']:
continue
devices.append(EdpRedySwitch(session, device_json))

async_add_entities(devices, True)


class EdpRedySwitch(EdpRedyDevice, SwitchDevice):
"""Representation of a Edp re:dy switch (plugs, switches, etc)."""

def __init__(self, session, device_json):
"""Initialize the switch."""
super().__init__(session, device_json['PKID'], device_json['Name'])

self._active_power = None

@property
def icon(self):
"""Return the icon to use in the frontend."""
return 'mdi:power-plug'

@property
def is_on(self):
"""Return true if it is on."""
return self._state

@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._active_power is not None:
attrs = {ATTR_ACTIVE_POWER: self._active_power}
else:
attrs = {}
attrs.update(super().device_state_attributes)
return attrs

async def async_turn_on(self, **kwargs):
"""Turn the switch on."""
if await self._async_send_state_cmd(True):
self._state = True
self.async_schedule_update_ha_state()

async def async_turn_off(self, **kwargs):
"""Turn the switch off."""
if await self._async_send_state_cmd(False):
self._state = False
self.async_schedule_update_ha_state()

async def _async_send_state_cmd(self, state):
state_json = {'devModuleId': self._id, 'key': 'RelayState',
'value': state}
return await self._session.async_set_state_var(state_json)

async def async_update(self):
"""Parse the data for this switch."""
if self._id in self._session.modules_dict:
device_json = self._session.modules_dict[self._id]
self._parse_data(device_json)
else:
self._is_available = False

def _parse_data(self, data):
"""Parse data received from the server."""
super()._parse_data(data)

for state_var in data['StateVars']:
if state_var['Name'] == 'RelayState':
self._state = state_var['Value'] == 'true'
elif state_var['Name'] == 'ActivePower':
try:
self._active_power = float(state_var['Value']) * 1000
except ValueError:
_LOGGER.error("Could not parse power for %s", self._id)
self._active_power = None
3 changes: 3 additions & 0 deletions requirements_all.txt
Expand Up @@ -308,6 +308,9 @@ dsmr_parser==0.11
# homeassistant.components.sensor.dweet
dweepy==0.3.0

# homeassistant.components.edp_redy
edp_redy==0.0.2

# homeassistant.components.media_player.horizon
einder==0.3.1

Expand Down