Skip to content

Commit

Permalink
Added OptionFlow and fixed formatting and typing issues (#316)
Browse files Browse the repository at this point in the history
* Added OptionsFlow support
* Formatted code
* Corrected and added typing
* Removed linting errors where possible
* Added VSCode configuration files
  • Loading branch information
timlaing authored Jan 19, 2025
1 parent 86804e1 commit 4467393
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 203 deletions.
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

0 comments on commit 4467393

Please sign in to comment.