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

Added OptionFlow and fixed formatting and typing issues #316

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
10 changes: 8 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
.vscode
/.vscode/*
.vs
**/__pycache__
/dist
/build
*.egg-info
*.egg-info

/config
/.venv

!/.vscode/settings.json
!/.vscode/launch.json
19 changes: 19 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: HomeAssistant",
"type": "debugpy",
"request": "launch",
"module": "homeassistant",
"args": [
"-v",
"-c",
"config"
]
}
]
}
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"hass"
]
}
208 changes: 99 additions & 109 deletions custom_components/emporia_vue/__init__.py

Large diffs are not rendered by default.

25 changes: 12 additions & 13 deletions custom_components/emporia_vue/charger_entity.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Emporia Charger Entity."""
from typing import Any, Optional

from pyemvue import pyemvue
from pyemvue.device import ChargerDevice, VueDevice
from functools import cached_property
from typing import Any, Optional

from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from pyemvue import pyemvue
from pyemvue.device import ChargerDevice, VueDevice

from .const import DOMAIN

Expand All @@ -25,9 +26,9 @@ def __init__(
"""Initialize the sensor."""
super().__init__(coordinator)
self._coordinator = coordinator
self._device = device
self._vue = vue
self._enabled_default = enabled_default
self._device: VueDevice = device
self._vue: pyemvue.PyEmVue = vue
self._enabled_default: bool = enabled_default

self._attr_unit_of_measurement = units
self._attr_device_class = device_class
Expand All @@ -37,14 +38,14 @@ def __init__(
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._device
return self._device is not None

@property
@cached_property
def entity_registry_enabled_default(self) -> bool:
"""Return whether the entity should be enabled when first added to the entity registry."""
return self._enabled_default

@property
@cached_property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the device."""
data: ChargerDevice = self._coordinator.data[self._device.device_gid]
Expand All @@ -61,12 +62,12 @@ def extra_state_attributes(self) -> dict[str, Any]:
}
return {}

@property
@cached_property
def unique_id(self) -> str:
"""Unique ID for the charger."""
return f"charger.emporia_vue.{self._device.device_gid}"

@property
@cached_property
def device_info(self) -> DeviceInfo:
"""Return the device information."""
return DeviceInfo(
Expand All @@ -76,5 +77,3 @@ def device_info(self) -> DeviceInfo:
sw_version=self._device.firmware,
manufacturer="Emporia",
)


77 changes: 56 additions & 21 deletions custom_components/emporia_vue/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,14 @@
import asyncio
import logging

from pyemvue import PyEmVue
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from pyemvue import PyEmVue

from .const import DOMAIN, ENABLE_1D, ENABLE_1M, ENABLE_1MON

_LOGGER = logging.getLogger(__name__)
from .const import DOMAIN, DOMAIN_SCHEMA, ENABLE_1D, ENABLE_1M, ENABLE_1MON

DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(ENABLE_1M, default=True): bool,
vol.Optional(ENABLE_1D, default=True): bool,
vol.Optional(ENABLE_1MON, default=True): bool,
}
)
_LOGGER: logging.Logger = logging.getLogger(__name__)


class VueHub:
Expand All @@ -33,24 +22,27 @@ def __init__(self) -> None:

async def authenticate(self, username, password) -> bool:
"""Test if we can authenticate with the host."""
loop = asyncio.get_event_loop()
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
return await loop.run_in_executor(None, self.vue.login, username, password)


async def validate_input(hass: core.HomeAssistant, data):
async def validate_input(data: dict):
"""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
raise InvalidAuth()

# If you cannot connect:
# throw CannotConnect
# If the authentication is wrong:
# InvalidAuth

if not hub.vue.customer:
raise InvalidAuth()

# Return info that you want to store in the config entry.
return {
"title": f"Customer {hub.vue.customer.customer_gid}",
Expand All @@ -67,17 +59,19 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

async def async_step_user(self, user_input=None) -> config_entries.ConfigFlowResult:
async def async_step_user(self, user_input=None) -> config_entries.FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
info = await validate_input(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)
return self.async_create_entry(
title=info["title"], data=user_input, options=user_input
)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
Expand All @@ -87,13 +81,54 @@ async def async_step_user(self, user_input=None) -> config_entries.ConfigFlowRes
errors["base"] = "unknown"

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

@staticmethod
@core.callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return OptionsFlowHandler(config_entry)


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""


class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a options flow for Emporia Vue."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry: config_entries.ConfigEntry = config_entry

async def async_step_init(self, user_input=None) -> config_entries.FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
ENABLE_1M,
default=self.config_entry.options.get(ENABLE_1M, True),
): bool,
vol.Optional(
ENABLE_1D,
default=self.config_entry.options.get(ENABLE_1D, True),
): bool,
vol.Optional(
ENABLE_1MON,
default=self.config_entry.options.get(ENABLE_1MON, True),
): bool,
}
),
)


class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""
21 changes: 21 additions & 0 deletions custom_components/emporia_vue/const.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
"""Constants for the Emporia Vue integration."""

import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD

DOMAIN = "emporia_vue"
VUE_DATA = "vue_data"
ENABLE_1S = "enable_1s"
ENABLE_1M = "enable_1m"
ENABLE_1D = "enable_1d"
ENABLE_1MON = "enable_1mon"

DOMAIN_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(ENABLE_1M, default=True): cv.boolean, # type: ignore
vol.Optional(ENABLE_1D, default=True): cv.boolean, # type: ignore
vol.Optional(ENABLE_1MON, default=True): cv.boolean, # type: ignore
}
)

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: DOMAIN_SCHEMA,
},
extra=vol.ALLOW_EXTRA,
)
32 changes: 19 additions & 13 deletions custom_components/emporia_vue/sensor.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
"""Platform for sensor integration."""

from datetime import datetime
import logging

from pyemvue.device import VueDevice, VueDeviceChannel
from pyemvue.enums import Scale
from datetime import datetime
from functools import cached_property

from homeassistant.components.sensor import (
SensorDeviceClass,
Expand All @@ -17,10 +15,12 @@
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from pyemvue.device import VueDevice, VueDeviceChannel
from pyemvue.enums import Scale

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)
_LOGGER: logging.Logger = logging.getLogger(__name__)


# def setup_platform(hass, config, add_entities, discovery_info=None):
Expand Down Expand Up @@ -57,7 +57,7 @@ async def async_setup_entry(
)


class CurrentVuePowerSensor(CoordinatorEntity, SensorEntity):
class CurrentVuePowerSensor(CoordinatorEntity, SensorEntity): # type: ignore
"""Representation of a Vue Sensor's current power."""

def __init__(self, coordinator, identifier) -> None:
Expand Down Expand Up @@ -100,7 +100,7 @@ def __init__(self, coordinator, identifier) -> None:
self._attr_suggested_display_precision = 1
self._attr_name = f"Power {self.scale_readable()}"

@property
@cached_property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
device_name = self._channel.name or self._device.device_name
Expand All @@ -114,27 +114,33 @@ def device_info(self) -> DeviceInfo:
manufacturer="Emporia",
)

@property
@cached_property
def last_reset(self) -> datetime | None:
"""The time when the daily/monthly sensor was reset. Midnight local time."""
if self._id in self.coordinator.data:
return self.coordinator.data[self._id]["reset"]
return None

@property
@cached_property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
if self._id in self.coordinator.data:
usage = self.coordinator.data[self._id]["usage"]
return self.scale_usage(usage) if usage is not None else None
return None

@property
@cached_property
def unique_id(self) -> str:
"""Unique ID for the sensor."""
if self._scale == Scale.MINUTE.value:
return f"sensor.emporia_vue.instant.{self._channel.device_gid}-{self._channel.channel_num}"
return f"sensor.emporia_vue.{self._scale}.{self._channel.device_gid}-{self._channel.channel_num}"
return (
"sensor.emporia_vue.instant."
f"{self._channel.device_gid}-{self._channel.channel_num}"
)
return (
"sensor.emporia_vue.{self._scale}."
f"{self._channel.device_gid}-{self._channel.channel_num}"
)

def scale_usage(self, usage):
"""Scales the usage to the correct timescale and magnitude."""
Expand All @@ -155,7 +161,7 @@ def scale_is_energy(self):
Scale.SECOND.value,
Scale.MINUTES_15.value,
)

def scale_readable(self):
"""Return a human readable scale."""
if self._scale == Scale.MINUTE.value:
Expand Down
Loading