mirror of
https://github.com/magico13/ha-emporia-vue.git
synced 2026-01-08 20:07:56 -05:00
Initial commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.vscode
|
||||
**/__pycache__
|
||||
/dist
|
||||
/build
|
||||
*.egg-info
|
||||
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal 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
3
README.md
Normal 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.
|
||||
21
custom_components/emporia_vue/.translations/en.json
Normal file
21
custom_components/emporia_vue/.translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
101
custom_components/emporia_vue/__init__.py
Normal file
101
custom_components/emporia_vue/__init__.py
Normal 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
|
||||
87
custom_components/emporia_vue/config_flow.py
Normal file
87
custom_components/emporia_vue/config_flow.py
Normal 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."""
|
||||
4
custom_components/emporia_vue/const.py
Normal file
4
custom_components/emporia_vue/const.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Constants for the Emporia Vue integration."""
|
||||
|
||||
DOMAIN = "emporia_vue"
|
||||
VUE_DATA = "vue_data"
|
||||
16
custom_components/emporia_vue/manifest.json
Normal file
16
custom_components/emporia_vue/manifest.json
Normal 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"
|
||||
]
|
||||
}
|
||||
77
custom_components/emporia_vue/sensor.py
Normal file
77
custom_components/emporia_vue/sensor.py
Normal 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
|
||||
21
custom_components/emporia_vue/strings.json
Normal file
21
custom_components/emporia_vue/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user