Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions homeassistant/components/sense/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"config": {
"title": "Sense",
"step": {
"user": {
"title": "Connect to your Sense Energy Monitor",
"data": {
"email": "Email Address",
"password": "Password"
}
}
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Device is already configured"
}
}
}
105 changes: 82 additions & 23 deletions homeassistant/components/sense/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Support for monitoring a Sense energy sensor."""
import asyncio
from datetime import timedelta
import logging

Expand All @@ -9,21 +10,25 @@
)
import voluptuous as vol

from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval

_LOGGER = logging.getLogger(__name__)

ACTIVE_UPDATE_RATE = 60
from .const import (
ACTIVE_UPDATE_RATE,
DEFAULT_TIMEOUT,
DOMAIN,
SENSE_DATA,
SENSE_DEVICE_UPDATE,
)

DEFAULT_TIMEOUT = 5
DOMAIN = "sense"
_LOGGER = logging.getLogger(__name__)

SENSE_DATA = "sense_data"
SENSE_DEVICE_UPDATE = "sense_devices_update"
PLATFORMS = ["sensor", "binary_sensor"]

CONFIG_SCHEMA = vol.Schema(
{
Expand All @@ -39,34 +44,88 @@
)


async def async_setup(hass, config):
"""Set up the Sense sensor."""
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Sense component."""
hass.data.setdefault(DOMAIN, {})
conf = config.get(DOMAIN)
if not conf:
return True

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_EMAIL: conf[CONF_EMAIL],
CONF_PASSWORD: conf[CONF_PASSWORD],
CONF_TIMEOUT: conf.get[CONF_TIMEOUT],
},
)
)
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Sense from a config entry."""

entry_data = entry.data
email = entry_data[CONF_EMAIL]
password = entry_data[CONF_PASSWORD]
timeout = entry_data[CONF_TIMEOUT]

username = config[DOMAIN][CONF_EMAIL]
password = config[DOMAIN][CONF_PASSWORD]
gateway = ASyncSenseable(api_timeout=timeout, wss_timeout=timeout)
gateway.rate_limit = ACTIVE_UPDATE_RATE

timeout = config[DOMAIN][CONF_TIMEOUT]
try:
hass.data[SENSE_DATA] = ASyncSenseable(api_timeout=timeout, wss_timeout=timeout)
hass.data[SENSE_DATA].rate_limit = ACTIVE_UPDATE_RATE
await hass.data[SENSE_DATA].authenticate(username, password)
await gateway.authenticate(email, password)
except SenseAuthenticationException:
Comment thread
bdraco marked this conversation as resolved.
_LOGGER.error("Could not authenticate with sense server")
return False
hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
hass.async_create_task(
async_load_platform(hass, "binary_sensor", DOMAIN, {}, config)
)
except SenseAPITimeoutException:
raise ConfigEntryNotReady

hass.data[DOMAIN][entry.entry_id] = {SENSE_DATA: gateway}

for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)

async def async_sense_update(now):
"""Retrieve latest state."""
try:
await hass.data[SENSE_DATA].update_realtime()
async_dispatcher_send(hass, SENSE_DEVICE_UPDATE)
gateway = hass.data[DOMAIN][entry.entry_id][SENSE_DATA]
await gateway.update_realtime()
async_dispatcher_send(
hass, f"{SENSE_DEVICE_UPDATE}-{gateway.sense_monitor_id}"
)
except SenseAPITimeoutException:
_LOGGER.error("Timeout retrieving data")

async_track_time_interval(
hass.data[DOMAIN][entry.entry_id][
"track_time_remove_callback"
] = async_track_time_interval(
hass, async_sense_update, timedelta(seconds=ACTIVE_UPDATE_RATE)
)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
track_time_remove_callback = hass.data[DOMAIN][entry.entry_id][
"track_time_remove_callback"
]
track_time_remove_callback()

if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
50 changes: 40 additions & 10 deletions homeassistant/components/sense/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_registry import async_get_registry

from . import SENSE_DATA, SENSE_DEVICE_UPDATE
from .const import DOMAIN, SENSE_DATA, SENSE_DEVICE_UPDATE

_LOGGER = logging.getLogger(__name__)

ATTR_WATTS = "watts"
DEVICE_ID_SOLAR = "solar"
BIN_SENSOR_CLASS = "power"
MDI_ICONS = {
"ac": "air-conditioner",
Expand Down Expand Up @@ -41,6 +44,7 @@
"skillet": "pot",
"smartcamera": "webcam",
"socket": "power-plug",
"solar_alt": "solar-power",
"sound": "speaker",
"stove": "stove",
"trash": "trash-can",
Expand All @@ -50,21 +54,40 @@
}


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Sense binary sensor."""
if discovery_info is None:
return
data = hass.data[SENSE_DATA]
data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DATA]
sense_monitor_id = data.sense_monitor_id

sense_devices = await data.get_discovered_device_data()
devices = [
SenseDevice(data, device)
SenseDevice(data, device, sense_monitor_id)
for device in sense_devices
if device["tags"]["DeviceListAllowed"] == "true"
if device["id"] == DEVICE_ID_SOLAR
or device["tags"]["DeviceListAllowed"] == "true"
]

await _migrate_old_unique_ids(hass, devices)

async_add_entities(devices)


async def _migrate_old_unique_ids(hass, devices):
registry = await async_get_registry(hass)
for device in devices:
# Migration of old not so unique ids
old_entity_id = registry.async_get_entity_id(
"binary_sensor", DOMAIN, device.old_unique_id
)
if old_entity_id is not None:
_LOGGER.debug(
"Migrating unique_id from [%s] to [%s]",
device.old_unique_id,
device.unique_id,
)
registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id)


def sense_to_mdi(sense_icon):
"""Convert sense icon to mdi icon."""
return "mdi:{}".format(MDI_ICONS.get(sense_icon, "power-plug"))
Expand All @@ -73,10 +96,12 @@ def sense_to_mdi(sense_icon):
class SenseDevice(BinarySensorDevice):
"""Implementation of a Sense energy device binary sensor."""

def __init__(self, data, device):
def __init__(self, data, device, sense_monitor_id):
"""Initialize the Sense binary sensor."""
self._name = device["name"]
self._id = device["id"]
self._sense_monitor_id = sense_monitor_id
self._unique_id = f"{sense_monitor_id}-{self._id}"
self._icon = sense_to_mdi(device["icon"])
self._data = data
self._undo_dispatch_subscription = None
Expand All @@ -93,7 +118,12 @@ def name(self):

@property
def unique_id(self):
"""Return the id of the binary sensor."""
"""Return the unique id of the binary sensor."""
return self._unique_id

@property
def old_unique_id(self):
"""Return the old not so unique id of the binary sensor."""
return self._id

@property
Expand All @@ -120,7 +150,7 @@ def update():
self.async_schedule_update_ha_state(True)

self._undo_dispatch_subscription = async_dispatcher_connect(
self.hass, SENSE_DEVICE_UPDATE, update
self.hass, f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}", update
)

async def async_will_remove_from_hass(self):
Expand Down
75 changes: 75 additions & 0 deletions homeassistant/components/sense/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Config flow for Sense integration."""
import logging

from sense_energy import (
ASyncSenseable,
SenseAPITimeoutException,
SenseAuthenticationException,
)
import voluptuous as vol

from homeassistant import config_entries, core
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT

from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT

from .const import DOMAIN # pylint:disable=unused-import; pylint:disable=unused-import

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
}
)


async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.

Data has the keys from DATA_SCHEMA with values provided by the user.
"""
timeout = data[CONF_TIMEOUT]

gateway = ASyncSenseable(api_timeout=timeout, wss_timeout=timeout)
gateway.rate_limit = ACTIVE_UPDATE_RATE
await gateway.authenticate(data[CONF_EMAIL], data[CONF_PASSWORD])

# Return info that you want to store in the config entry.
return {"title": data[CONF_EMAIL]}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Sense."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
await self.async_set_unique_id(user_input[CONF_EMAIL])
return self.async_create_entry(title=info["title"], data=user_input)
except SenseAPITimeoutException:
errors["base"] = "cannot_connect"
except SenseAuthenticationException:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)

async def async_step_import(self, user_input):
"""Handle import."""
await self.async_set_unique_id(user_input[CONF_EMAIL])
self._abort_if_unique_id_configured()

return await self.async_step_user(user_input)
7 changes: 7 additions & 0 deletions homeassistant/components/sense/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Constants for monitoring a Sense energy sensor."""
DOMAIN = "sense"
DEFAULT_TIMEOUT = 10
ACTIVE_UPDATE_RATE = 60
DEFAULT_NAME = "Sense"
SENSE_DATA = "sense_data"
SENSE_DEVICE_UPDATE = "sense_devices_update"
11 changes: 8 additions & 3 deletions homeassistant/components/sense/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
"domain": "sense",
"name": "Sense",
"documentation": "https://www.home-assistant.io/integrations/sense",
"requirements": ["sense_energy==0.7.0"],
"requirements": [
"sense_energy==0.7.0"
],
"dependencies": [],
"codeowners": ["@kbickar"]
}
"codeowners": [
"@kbickar"
],
"config_flow": true
}
Loading