Initial commit

This commit is contained in:
magico13
2020-03-14 18:24:27 -04:00
commit c06d286b3c
10 changed files with 356 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.vscode
**/__pycache__
/dist
/build
*.egg-info

21
LICENSE.txt Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Michael Marvin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# emporia_vue Home Assistant Integration
Reads data from the Emporia Vue energy monitor. Creates a sensor for each device channel showing average usage over each minute.

View File

@@ -0,0 +1,21 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"host": "Host"
},
"title": "Connect to the device"
}
},
"title": "Emporia Vue"
}
}

View File

@@ -0,0 +1,101 @@
"""The Emporia Vue integration."""
import asyncio
import logging
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from pyemvue import PyEmVue
from pyemvue.device import VueDevice, VueDeviceChannel
from .const import DOMAIN, VUE_DATA
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_EMAIL): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"]
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Emporia Vue 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],
},
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Emporia Vue from a config entry."""
entry_data = entry.data
email = entry_data[CONF_EMAIL]
password = entry_data[CONF_PASSWORD]
_LOGGER.info(entry_data)
vue = PyEmVue()
try:
result = vue.login(username=email, password=password)
if not result:
raise Exception("Could not authenticate with Emporia API")
return False
except Exception:
_LOGGER.error("Could not authenticate with Emporia API")
return False
# Get device data from Emporia API
#discovered_devices = vue.get_devices()
hass.data[DOMAIN][entry.entry_id] = {
VUE_DATA: vue,
#VUE_DEVICES_DATA: VueDeviceData(),
#VUE_DISCOVERED_DEVICES_DATA: discovered_devices,
}
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
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
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -0,0 +1,87 @@
"""Config flow for Emporia Vue integration."""
import logging
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from .const import DOMAIN # pylint:disable=unused-import
from pyemvue import PyEmVue
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema({CONF_EMAIL: str, CONF_PASSWORD: str})
class VueHub:
"""Hub for the Emporia Vue Integration."""
def __init__(self):
"""Initialize."""
self.vue = PyEmVue()
pass
async def authenticate(self, username, password) -> bool:
"""Test if we can authenticate with the host."""
return self.vue.login(username=username, password=password)
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.
"""
hub = VueHub()
if not await hub.authenticate(data[CONF_EMAIL], data[CONF_PASSWORD]):
raise InvalidAuth
# If you cannot connect:
# throw CannotConnect
# If the authentication is wrong:
# InvalidAuth
# Return info that you want to store in the config entry.
return {
"title": f"Customer {hub.vue.customer.customer_gid}",
"gid": f"{hub.vue.customer.customer_gid}"
}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Emporia Vue."""
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)
#prevent setting up the same account twice
await self.async_set_unique_id(info["gid"])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
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
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@@ -0,0 +1,4 @@
"""Constants for the Emporia Vue integration."""
DOMAIN = "emporia_vue"
VUE_DATA = "vue_data"

View File

@@ -0,0 +1,16 @@
{
"domain": "emporia_vue",
"name": "Emporia Vue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/emporia_vue",
"requirements": [
"pyemvue==0.9.3"
],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": [
"@magico13"
]
}

View File

@@ -0,0 +1,77 @@
"""Platform for sensor integration."""
from homeassistant.const import DEVICE_CLASS_POWER, POWER_WATT, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR
from homeassistant.helpers.entity import Entity
from .const import DOMAIN, VUE_DATA
from pyemvue import pyemvue
from pyemvue.enums import Scale
from pyemvue.device import VueDevice, VueDeviceChannel, VuewDeviceChannelUsage
#def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_entry(hass, config_entry, add_entities):
"""Set up the sensor platform."""
vue = hass.data[DOMAIN][config_entry.entry_id][VUE_DATA]
vue_devices = vue.get_devices()
# Add a sensor for each device channel
devices = []
for device in vue_devices:
for channel in device.channels:
devices.append(CurrentVuePowerSensor(vue, channel))
add_entities(devices)
class CurrentVuePowerSensor(Entity):
"""Representation of a Vue Sensor's current power."""
def __init__(self, vue, channel):
"""Initialize the sensor."""
self._state = None
self._vue = vue
self._channel = channel
@property
def name(self):
"""Return the name of the sensor."""
return f'Power {self._channel.device_gid} {self._channel.channel_num}'
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return POWER_WATT
@property
def device_class(self):
"""The type of sensor"""
return DEVICE_CLASS_POWER
@property
def unique_id(self):
"""Unique ID for the sensor"""
return f"sensor.emporia_vue.instant.{self._channel.device_gid}-{self._channel.channel_num}"
def update(self):
"""Fetch new state data for the sensor.
This is the only method that should fetch new data for Home Assistant.
"""
gid = self._channel.device_gid
num = self._channel.channel_num
# TODO: each sensor shouldn't do this separately
channels = self._vue.get_recent_usage(scale=Scale.MINUTE.value)
if channels:
for channel in channels:
if channel.device_gid == gid and channel.channel_num == num:
self._state = round(channel.usage)
return
self._state = None
return

View File

@@ -0,0 +1,21 @@
{
"config": {
"title": "Emporia Vue",
"step": {
"user": {
"title": "Connect to the device",
"data": {
"host": "Host"
}
}
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Device is already configured"
}
}
}