mirror of
https://github.com/magico13/ha-emporia-vue.git
synced 2026-01-06 19:13:51 -05:00
Added OptionFlow and fixed formatting and typing issues (#316)
* Added OptionsFlow support * Formatted code * Corrected and added typing * Removed linting errors where possible * Added VSCode configuration files
This commit is contained in:
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,6 +1,12 @@
|
||||
.vscode
|
||||
/.vscode/*
|
||||
.vs
|
||||
**/__pycache__
|
||||
/dist
|
||||
/build
|
||||
*.egg-info
|
||||
*.egg-info
|
||||
|
||||
/config
|
||||
/.venv
|
||||
|
||||
!/.vscode/settings.json
|
||||
!/.vscode/launch.json
|
||||
19
.vscode/launch.json
vendored
Normal file
19
.vscode/launch.json
vendored
Normal file
@@ -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
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"hass"
|
||||
]
|
||||
}
|
||||
@@ -1,53 +1,36 @@
|
||||
"""The Emporia Vue integration."""
|
||||
|
||||
import asyncio
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import logging
|
||||
import re
|
||||
from datetime import UTC, datetime, timedelta, tzinfo
|
||||
from typing import Any
|
||||
|
||||
import dateutil.relativedelta
|
||||
import dateutil.tz
|
||||
import requests
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from pyemvue import PyEmVue
|
||||
from pyemvue.device import (
|
||||
ChargerDevice,
|
||||
VueDevice,
|
||||
VueDeviceChannel,
|
||||
VueDeviceChannelUsage,
|
||||
VueUsageDevice,
|
||||
)
|
||||
from pyemvue.enums import Scale
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, ENABLE_1D, ENABLE_1M, ENABLE_1MON, VUE_DATA
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(ENABLE_1M, default=True): cv.boolean,
|
||||
vol.Optional(ENABLE_1D, default=True): cv.boolean,
|
||||
vol.Optional(ENABLE_1MON, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = ["sensor", "switch"]
|
||||
PLATFORMS: list[str] = ["sensor", "switch"]
|
||||
|
||||
DEVICE_GIDS: list[str] = []
|
||||
DEVICE_INFORMATION: dict[int, VueDevice] = {}
|
||||
@@ -88,27 +71,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
DEVICE_INFORMATION = {}
|
||||
|
||||
entry_data = entry.data
|
||||
email = entry_data[CONF_EMAIL]
|
||||
password = entry_data[CONF_PASSWORD]
|
||||
email: str = entry_data[CONF_EMAIL]
|
||||
password: str = entry_data[CONF_PASSWORD]
|
||||
vue = PyEmVue()
|
||||
loop = asyncio.get_event_loop()
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||
try:
|
||||
result = await loop.run_in_executor(None, vue.login, email, password)
|
||||
# result = await loop.run_in_executor(None, vue.login_simulator, "http://localhost:8000", email, password)
|
||||
result: bool = await loop.run_in_executor(None, vue.login, email, password)
|
||||
if not result:
|
||||
_LOGGER.error("Failed to login to Emporia Vue")
|
||||
return False
|
||||
except Exception as err:
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.error("Failed to login to Emporia Vue: %s", err)
|
||||
return False
|
||||
|
||||
try:
|
||||
devices = await loop.run_in_executor(None, vue.get_devices)
|
||||
devices: list[VueDevice] = await loop.run_in_executor(None, vue.get_devices)
|
||||
for device in devices:
|
||||
if str(device.device_gid) not in DEVICE_GIDS:
|
||||
DEVICE_GIDS.append(str(device.device_gid))
|
||||
_LOGGER.info("Adding gid %s to DEVICE_GIDS list", device.device_gid)
|
||||
# await loop.run_in_executor(None, vue.populate_device_properties, device)
|
||||
DEVICE_INFORMATION[device.device_gid] = device
|
||||
else:
|
||||
DEVICE_INFORMATION[device.device_gid].channels += device.channels
|
||||
@@ -122,13 +103,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
total_channels,
|
||||
)
|
||||
|
||||
async def async_update_data_1min():
|
||||
async def async_update_data_1min() -> dict:
|
||||
"""Fetch data from API endpoint at a 1 minute interval.
|
||||
|
||||
This is the place to pre-process the data to lookup tables
|
||||
so entities can quickly look up their data.
|
||||
"""
|
||||
data = await update_sensors(vue, [Scale.MINUTE.value])
|
||||
data: dict = await update_sensors(vue, [Scale.MINUTE.value])
|
||||
# store this, then have the daily sensors pull from it and integrate
|
||||
# then the daily can "true up" hourly (or more frequent) in case it's incorrect
|
||||
if data:
|
||||
@@ -136,7 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
LAST_MINUTE_DATA = data
|
||||
return data
|
||||
|
||||
async def async_update_data_1mon():
|
||||
async def async_update_data_1mon() -> dict:
|
||||
"""Fetch data from API endpoint at a 1 hour interval.
|
||||
|
||||
This is the place to pre-process the data to lookup tables
|
||||
@@ -144,10 +125,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""
|
||||
return await update_sensors(vue, [Scale.MONTH.value])
|
||||
|
||||
async def async_update_day_sensors():
|
||||
async def async_update_day_sensors() -> dict:
|
||||
global LAST_DAY_UPDATE
|
||||
global LAST_DAY_DATA
|
||||
now = datetime.now(UTC)
|
||||
now: datetime = datetime.now(UTC)
|
||||
if not LAST_DAY_UPDATE or (now - LAST_DAY_UPDATE) > timedelta(minutes=15):
|
||||
_LOGGER.info("Updating day sensors")
|
||||
LAST_DAY_UPDATE = now
|
||||
@@ -158,7 +139,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if LAST_MINUTE_DATA:
|
||||
for identifier, data in LAST_MINUTE_DATA.items():
|
||||
device_gid, channel_gid, _ = identifier.split("-")
|
||||
day_id = f"{device_gid}-{channel_gid}-{Scale.DAY.value}"
|
||||
day_id: str = f"{device_gid}-{channel_gid}-{Scale.DAY.value}"
|
||||
if (
|
||||
data
|
||||
and LAST_DAY_DATA
|
||||
@@ -217,7 +198,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await coordinator_day_sensor.async_config_entry_first_refresh()
|
||||
|
||||
# Setup custom services
|
||||
async def handle_set_charger_current(call):
|
||||
async def handle_set_charger_current(call) -> None:
|
||||
"""Handle setting the EV Charger current."""
|
||||
_LOGGER.debug(
|
||||
"executing set_charger_current: %s %s",
|
||||
@@ -226,8 +207,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
current = call.data.get("current")
|
||||
current = int(current)
|
||||
device_id = call.data.get("device_id", None)
|
||||
entity_id = call.data.get("entity_id", None)
|
||||
device_id: str | list[str] | None = call.data.get("device_id", None)
|
||||
entity_id: str | list[str] | None = call.data.get("entity_id", None)
|
||||
|
||||
# if device or entity ids are strings, convert to list
|
||||
if isinstance(device_id, str):
|
||||
@@ -237,10 +218,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
# technically we should loop through all the passed device and entities and update all
|
||||
# but for now we'll just use the first one
|
||||
charger_entity = None
|
||||
entity_registry = er.async_get(hass)
|
||||
charger_entity: er.RegistryEntry | None = None
|
||||
entity_registry: er.EntityRegistry = er.async_get(hass)
|
||||
if device_id:
|
||||
entities = er.async_entries_for_device(entity_registry, device_id[0])
|
||||
entities: list[er.RegistryEntry] = er.async_entries_for_device(
|
||||
entity_registry, device_id[0]
|
||||
)
|
||||
for entity in entities:
|
||||
_LOGGER.info("Entity is %s", str(entity))
|
||||
if entity.entity_id.startswith("switch"):
|
||||
@@ -253,8 +236,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if not charger_entity:
|
||||
raise HomeAssistantError("Target device or Entity required.")
|
||||
|
||||
unique_entity_id = charger_entity.unique_id
|
||||
gid_match = re.search(r"\d+", unique_entity_id)
|
||||
unique_entity_id: str = charger_entity.unique_id
|
||||
gid_match: re.Match[str] | None = re.search(r"\d+", unique_entity_id)
|
||||
if not gid_match:
|
||||
raise HomeAssistantError(
|
||||
f"Could not find device gid from unique id {unique_entity_id}"
|
||||
@@ -266,7 +249,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
or not DEVICE_INFORMATION[charger_gid].ev_charger
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
f"Set Charging Current called on invalid device with entity id {charger_entity.entity_id} (unique id {unique_entity_id})"
|
||||
"Set Charging Current called on invalid device with entity id"
|
||||
f" {charger_entity.entity_id} (unique id {unique_entity_id})"
|
||||
)
|
||||
|
||||
state = hass.states.get(charger_entity.entity_id)
|
||||
@@ -275,20 +259,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
raise HomeAssistantError(
|
||||
f"Could not find state for entity {charger_entity.entity_id}"
|
||||
)
|
||||
charger_info = DEVICE_INFORMATION[charger_gid]
|
||||
charger_info: VueDevice = DEVICE_INFORMATION[charger_gid]
|
||||
if charger_info.ev_charger is None:
|
||||
raise HomeAssistantError(
|
||||
f"Could not find charger info for device {charger_gid}"
|
||||
)
|
||||
# Scale the current to a minimum of 6 amps and max of the circuit max
|
||||
current = max(6, current)
|
||||
current: int = max(6, current)
|
||||
current = min(current, charger_info.ev_charger.max_charging_rate)
|
||||
_LOGGER.info(
|
||||
"Setting charger %s to current of %d amps", charger_gid, current
|
||||
)
|
||||
|
||||
try:
|
||||
updated_charger = await loop.run_in_executor(
|
||||
updated_charger: ChargerDevice = await loop.run_in_executor(
|
||||
None,
|
||||
vue.update_charger,
|
||||
charger_info.ev_charger,
|
||||
@@ -297,14 +281,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
DEVICE_INFORMATION[charger_gid].ev_charger = updated_charger
|
||||
# update the state of the charger entity using the updated data
|
||||
state = hass.states.get(charger_entity.entity_id)
|
||||
state: State | None = hass.states.get(charger_entity.entity_id)
|
||||
if state:
|
||||
newState = "on" if updated_charger.charger_on else "off"
|
||||
newAttributes = state.attributes.copy()
|
||||
newAttributes["charging_rate"] = updated_charger.charging_rate
|
||||
new_state: str = "on" if updated_charger.charger_on else "off"
|
||||
new_attributes: dict = state.attributes.copy()
|
||||
new_attributes["charging_rate"] = updated_charger.charging_rate
|
||||
# good enough for now, update the state in the registry
|
||||
hass.states.async_set(
|
||||
charger_entity.entity_id, newState, newAttributes
|
||||
charger_entity.entity_id, new_state, new_attributes
|
||||
)
|
||||
|
||||
except requests.exceptions.HTTPError as err:
|
||||
@@ -343,7 +327,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
unload_ok: bool = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
@@ -357,16 +341,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def update_sensors(vue: PyEmVue, scales: list[str]):
|
||||
async def update_sensors(vue: PyEmVue, scales: list[str]) -> dict:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
|
||||
# handled by the data update coordinator.
|
||||
data = {}
|
||||
loop = asyncio.get_event_loop()
|
||||
data: dict = {}
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||
for scale in scales:
|
||||
utcnow = datetime.now(UTC)
|
||||
usage_dict = await loop.run_in_executor(
|
||||
utcnow: datetime = datetime.now(UTC)
|
||||
usage_dict: dict[int, VueUsageDevice] = await loop.run_in_executor(
|
||||
None, vue.get_device_list_usage, DEVICE_GIDS, utcnow, scale
|
||||
)
|
||||
if not usage_dict:
|
||||
@@ -405,7 +389,7 @@ def flatten_usage_data(
|
||||
data_time = usage.timestamp or data_time
|
||||
if usage.channels:
|
||||
for channel in usage.channels.values():
|
||||
identifier = make_channel_id(channel, scale)
|
||||
identifier: str = make_channel_id(channel, scale)
|
||||
flattened[identifier] = channel
|
||||
if channel.nested_devices:
|
||||
nested_flattened, _ = flatten_usage_data(
|
||||
@@ -421,24 +405,25 @@ async def parse_flattened_usage_data(
|
||||
data: dict[str, Any],
|
||||
requested_time: datetime,
|
||||
data_time: datetime,
|
||||
):
|
||||
) -> None:
|
||||
"""Loop through the device list and find the corresponding update data."""
|
||||
unused_data = flattened_data.copy()
|
||||
unused_data: dict[str, VueDeviceChannelUsage] = flattened_data.copy()
|
||||
for gid, info in DEVICE_INFORMATION.items():
|
||||
local_time = await change_time_to_local(data_time, info.time_zone)
|
||||
requested_time_local = await change_time_to_local(
|
||||
local_time: datetime = await change_time_to_local(data_time, info.time_zone)
|
||||
requested_time_local: datetime = await change_time_to_local(
|
||||
requested_time, info.time_zone
|
||||
)
|
||||
if abs((local_time - requested_time_local).total_seconds()) > 30:
|
||||
_LOGGER.warning(
|
||||
"More than 30 seconds have passed between the requested datetime and the returned datetime. Requested: %s Returned: %s",
|
||||
"More than 30 seconds have passed between the requested datetime"
|
||||
" and the returned datetime. Requested: %s Returned: %s",
|
||||
requested_time,
|
||||
data_time,
|
||||
)
|
||||
for info_channel in info.channels:
|
||||
identifier = make_channel_id(info_channel, scale)
|
||||
identifier: str = make_channel_id(info_channel, scale)
|
||||
channel_num = info_channel.channel_num
|
||||
channel = flattened_data.get(identifier)
|
||||
channel: VueDeviceChannelUsage | None = flattened_data.get(identifier)
|
||||
if not channel:
|
||||
_LOGGER.info(
|
||||
"Could not find usage info for device %s channel %s",
|
||||
@@ -446,12 +431,13 @@ async def parse_flattened_usage_data(
|
||||
channel_num,
|
||||
)
|
||||
unused_data.pop(identifier, None)
|
||||
reset_datetime = None
|
||||
reset_datetime: datetime | None = None
|
||||
|
||||
if scale in [Scale.DAY.value, Scale.MONTH.value]:
|
||||
# We need to know when the value reset
|
||||
# For day, that should be midnight local time, but we need to use the timestamp returned to us
|
||||
# for month, that should be midnight of the reset day they specify in the app
|
||||
# For day, that should be midnight local time, but we need to use the timestamp
|
||||
# returnedto us for month, that should be midnight of the reset day they specify
|
||||
# in the app
|
||||
reset_datetime = determine_reset_datetime(
|
||||
local_time,
|
||||
info.billing_cycle_start_day,
|
||||
@@ -460,11 +446,12 @@ async def parse_flattened_usage_data(
|
||||
|
||||
# Fix the usage if we got None
|
||||
# Use the last value if we have it, otherwise use zero
|
||||
fixed_usage = channel.usage if channel else 0
|
||||
fixed_usage: float = channel.usage if channel else 0.0
|
||||
if fixed_usage is None:
|
||||
fixed_usage = handle_none_usage(scale, identifier)
|
||||
_LOGGER.info(
|
||||
"Got None usage for device %s channel %s scale %s and timestamp %s. Instead using a value of %s",
|
||||
"Got None usage for device %s channel %s scale %s and timestamp %s. "
|
||||
"Instead using a value of %s",
|
||||
gid,
|
||||
channel_num,
|
||||
scale,
|
||||
@@ -487,8 +474,8 @@ async def parse_flattened_usage_data(
|
||||
"timestamp": local_time,
|
||||
}
|
||||
if unused_data:
|
||||
# unused_data is not json serializable because VueDeviceChannelUsage is not JSON serializable
|
||||
# instead print out dictionary as a string
|
||||
# unused_data is not json serializable because VueDeviceChannelUsage
|
||||
# is not JSON serializable instead print out dictionary as a string
|
||||
_LOGGER.info(
|
||||
"Unused data found during update. Unused data: %s",
|
||||
str(unused_data),
|
||||
@@ -496,8 +483,9 @@ async def parse_flattened_usage_data(
|
||||
channels_were_added = False
|
||||
for channel in unused_data.values():
|
||||
channels_were_added |= await handle_special_channels_for_device(channel)
|
||||
# we'll also need to register these entities I think. They might show up automatically on the first run
|
||||
# When we're done handling the unused data we need to rerun the update
|
||||
# we'll also need to register these entities I think. They might show up
|
||||
# automatically on the first run When we're done handling the unused data
|
||||
# we need to rerun the update
|
||||
if channels_were_added:
|
||||
_LOGGER.info("Rerunning update due to added channels")
|
||||
await parse_flattened_usage_data(
|
||||
@@ -507,9 +495,8 @@ async def parse_flattened_usage_data(
|
||||
|
||||
async def handle_special_channels_for_device(channel: VueDeviceChannel) -> bool:
|
||||
"""Handle the special channels for a device, if they exist."""
|
||||
device_info = None
|
||||
if channel.device_gid in DEVICE_INFORMATION:
|
||||
device_info = DEVICE_INFORMATION[channel.device_gid]
|
||||
device_info: VueDevice = DEVICE_INFORMATION[channel.device_gid]
|
||||
# if channel.channel_num in [
|
||||
# "MainsFromGrid",
|
||||
# "MainsToGrid",
|
||||
@@ -517,7 +504,7 @@ async def handle_special_channels_for_device(channel: VueDeviceChannel) -> bool:
|
||||
# "TotalUsage",
|
||||
# ]:
|
||||
found = False
|
||||
channel_123 = None
|
||||
channel_123: VueDeviceChannel | None = None
|
||||
for device_channel in device_info.channels:
|
||||
if device_channel.channel_num == channel.channel_num:
|
||||
found = True
|
||||
@@ -546,37 +533,33 @@ async def handle_special_channels_for_device(channel: VueDeviceChannel) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
# register the entity
|
||||
# registry = await async_get_registry(hass)
|
||||
# registry.async_get_or_create(
|
||||
# domain='your_domain',
|
||||
# platform='your_platform',
|
||||
# unique_id=entity_id,
|
||||
# name=entity_name,
|
||||
# config_entry=config_entry,
|
||||
# device_id=device_id,
|
||||
# )
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def make_channel_id(channel: VueDeviceChannel, scale: str):
|
||||
def make_channel_id(channel: VueDeviceChannel, scale: str) -> str:
|
||||
"""Format the channel id for a channel and scale."""
|
||||
return f"{channel.device_gid}-{channel.channel_num}-{scale}"
|
||||
|
||||
|
||||
def fix_usage_sign(channel_num: str, usage: float, bidirectional: bool):
|
||||
"""If the channel is not '1,2,3' or 'Balance' we need it to be positive (see https://github.com/magico13/ha-emporia-vue/issues/57)."""
|
||||
def fix_usage_sign(channel_num: str, usage: float, bidirectional: bool) -> float:
|
||||
"""
|
||||
If the channel is not '1,2,3' or 'Balance' we need it to be positive
|
||||
(see https://github.com/magico13/ha-emporia-vue/issues/57).
|
||||
"""
|
||||
if usage and not bidirectional and channel_num not in ["1,2,3", "Balance"]:
|
||||
# With bidirectionality, we need to also check if bidirectional. If yes, we either don't abs, or we flip the sign.
|
||||
# With bidirectionality, we need to also check if bidirectional. If yes,
|
||||
# we either don't abs, or we flip the sign.
|
||||
return abs(usage)
|
||||
return usage
|
||||
|
||||
|
||||
async def change_time_to_local(time: datetime, tz_string: str):
|
||||
async def change_time_to_local(time: datetime, tz_string: str) -> datetime:
|
||||
"""Change the datetime to the provided timezone, if not already."""
|
||||
loop = asyncio.get_event_loop()
|
||||
tz_info = await loop.run_in_executor(None, dateutil.tz.gettz, tz_string)
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||
tz_info: tzinfo | None = await loop.run_in_executor(
|
||||
None, dateutil.tz.gettz, tz_string
|
||||
)
|
||||
if not time.tzinfo or time.tzinfo.utcoffset(time) is None:
|
||||
# unaware, assume it's already utc
|
||||
time = time.replace(tzinfo=UTC)
|
||||
@@ -586,14 +569,19 @@ async def change_time_to_local(time: datetime, tz_string: str):
|
||||
async def check_for_midnight(timestamp: datetime, device_gid: int, day_id: str):
|
||||
"""If midnight has recently passed, reset the LAST_DAY_DATA for Day sensors to zero."""
|
||||
if device_gid in DEVICE_INFORMATION:
|
||||
device_info = DEVICE_INFORMATION[device_gid]
|
||||
local_time = await change_time_to_local(timestamp, device_info.time_zone)
|
||||
local_midnight = local_time.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
device_info: VueDevice = DEVICE_INFORMATION[device_gid]
|
||||
local_time: datetime = await change_time_to_local(
|
||||
timestamp, device_info.time_zone
|
||||
)
|
||||
local_midnight: datetime = local_time.replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
last_reset = LAST_DAY_DATA[day_id]["reset"]
|
||||
if local_midnight > last_reset:
|
||||
# New reset time found
|
||||
_LOGGER.info(
|
||||
"Midnight happened recently for id %s! Timestamp is %s, midnight is %s, previous reset was %s",
|
||||
"Midnight happened recently for id %s! Timestamp is %s, midnight is %s, "
|
||||
"previous reset was %s",
|
||||
day_id,
|
||||
local_time,
|
||||
local_midnight,
|
||||
@@ -605,9 +593,11 @@ async def check_for_midnight(timestamp: datetime, device_gid: int, day_id: str):
|
||||
|
||||
def determine_reset_datetime(
|
||||
local_time: datetime, monthly_cycle_start: int, is_month: bool
|
||||
):
|
||||
) -> datetime:
|
||||
"""Determine the last reset datetime (aware) based on the passed time and cycle start date."""
|
||||
reset_datetime = local_time.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
reset_datetime: datetime = local_time.replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
if is_month:
|
||||
# Month should use the last billing_cycle_start_day of either this or last month
|
||||
reset_datetime = reset_datetime.replace(day=monthly_cycle_start)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,9 +26,9 @@ class EmporiaChargerEntity(CoordinatorEntity):
|
||||
"""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
|
||||
@@ -37,14 +38,14 @@ class EmporiaChargerEntity(CoordinatorEntity):
|
||||
@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]
|
||||
@@ -61,12 +62,12 @@ class EmporiaChargerEntity(CoordinatorEntity):
|
||||
}
|
||||
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(
|
||||
@@ -76,5 +77,3 @@ class EmporiaChargerEntity(CoordinatorEntity):
|
||||
sw_version=self._device.firmware,
|
||||
manufacturer="Emporia",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
from .const import DOMAIN, DOMAIN_SCHEMA, ENABLE_1D, ENABLE_1M, ENABLE_1MON
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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:
|
||||
@@ -33,24 +22,27 @@ class VueHub:
|
||||
|
||||
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}",
|
||||
@@ -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:
|
||||
@@ -87,13 +81,54 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
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."""
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
@@ -17,10 +15,12 @@ from homeassistant.core import HomeAssistant
|
||||
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):
|
||||
@@ -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:
|
||||
@@ -100,7 +100,7 @@ class CurrentVuePowerSensor(CoordinatorEntity, SensorEntity):
|
||||
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
|
||||
@@ -114,14 +114,14 @@ class CurrentVuePowerSensor(CoordinatorEntity, SensorEntity):
|
||||
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:
|
||||
@@ -129,12 +129,18 @@ class CurrentVuePowerSensor(CoordinatorEntity, SensorEntity):
|
||||
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."""
|
||||
@@ -155,7 +161,7 @@ class CurrentVuePowerSensor(CoordinatorEntity, SensorEntity):
|
||||
Scale.SECOND.value,
|
||||
Scale.MINUTES_15.value,
|
||||
)
|
||||
|
||||
|
||||
def scale_readable(self):
|
||||
"""Return a human readable scale."""
|
||||
if self._scale == Scale.MINUTE.value:
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"enable_1s": "One Second Sensor",
|
||||
"enable_1m": "One Minute Sensor",
|
||||
"enable_1d": "One Day Sensor",
|
||||
"enable_1mon": "One Month Sensor"
|
||||
@@ -20,5 +19,18 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"enable_1m": "One Minute Sensor",
|
||||
"enable_1d": "One Day Sensor",
|
||||
"enable_1mon": "One Month Sensor"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Platform for switch integration."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from requests import exceptions
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -16,25 +16,52 @@ from homeassistant.helpers.update_coordinator import (
|
||||
DataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
from pyemvue import PyEmVue
|
||||
from pyemvue.device import ChargerDevice, OutletDevice, VueDevice
|
||||
from requests import exceptions
|
||||
|
||||
from .charger_entity import EmporiaChargerEntity
|
||||
from .const import DOMAIN, VUE_DATA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
device_information: dict[int, VueDevice] = {} # data is the populated device objects
|
||||
|
||||
|
||||
async def __async_update_data(vue: PyEmVue):
|
||||
"""Fetch data from API endpoint.
|
||||
|
||||
This is the place to pre-process the data to lookup tables
|
||||
so entities can quickly look up their data.
|
||||
"""
|
||||
try:
|
||||
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
|
||||
# handled by the data update coordinator.
|
||||
data = {}
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||
outlets: list[OutletDevice]
|
||||
chargers: list[ChargerDevice]
|
||||
(outlets, chargers) = await loop.run_in_executor(None, vue.get_devices_status)
|
||||
if outlets:
|
||||
for outlet in outlets:
|
||||
data[outlet.device_gid] = outlet
|
||||
if chargers:
|
||||
for charger in chargers:
|
||||
data[charger.device_gid] = charger
|
||||
return data
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Error communicating with Emporia API: {err}") from err
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
vue = hass.data[DOMAIN][config_entry.entry_id][VUE_DATA]
|
||||
vue: PyEmVue = hass.data[DOMAIN][config_entry.entry_id][VUE_DATA]
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||
devices: list[VueDevice] = await loop.run_in_executor(None, vue.get_devices)
|
||||
for device in devices:
|
||||
if device.outlet or device.ev_charger:
|
||||
@@ -42,30 +69,9 @@ async def async_setup_entry(
|
||||
device_information[device.device_gid] = device
|
||||
|
||||
async def async_update_data():
|
||||
"""Fetch data from API endpoint.
|
||||
|
||||
This is the place to pre-process the data to lookup tables
|
||||
so entities can quickly look up their data.
|
||||
"""
|
||||
try:
|
||||
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
|
||||
# handled by the data update coordinator.
|
||||
data = {}
|
||||
loop = asyncio.get_event_loop()
|
||||
outlets: list[OutletDevice]
|
||||
chargers: list[ChargerDevice]
|
||||
(outlets, chargers) = await loop.run_in_executor(
|
||||
None, vue.get_devices_status
|
||||
)
|
||||
if outlets:
|
||||
for outlet in outlets:
|
||||
data[outlet.device_gid] = outlet
|
||||
if chargers:
|
||||
for charger in chargers:
|
||||
data[charger.device_gid] = charger
|
||||
return data
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Error communicating with Emporia API: {err}") from err
|
||||
return await __async_update_data(
|
||||
vue=vue,
|
||||
)
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
@@ -85,7 +91,7 @@ async def async_setup_entry(
|
||||
continue
|
||||
if device_information[gid].outlet:
|
||||
switches.append(EmporiaOutletSwitch(coordinator, vue, gid))
|
||||
elif device_information[gid].ev_charger:
|
||||
elif device_information[gid].ev_charger:
|
||||
switches.append(
|
||||
EmporiaChargerSwitch(
|
||||
coordinator,
|
||||
@@ -99,23 +105,22 @@ async def async_setup_entry(
|
||||
async_add_entities(switches)
|
||||
|
||||
|
||||
class EmporiaOutletSwitch(CoordinatorEntity, SwitchEntity):
|
||||
class EmporiaOutletSwitch(CoordinatorEntity, SwitchEntity): # type: ignore
|
||||
"""Representation of an Emporia Smart Outlet state."""
|
||||
|
||||
def __init__(self, coordinator, vue, gid) -> None:
|
||||
"""Pass coordinator to CoordinatorEntity."""
|
||||
super().__init__(coordinator)
|
||||
# self._state = coordinator.data[index]['usage']
|
||||
self._vue = vue
|
||||
self._device_gid = gid
|
||||
self._device = device_information[gid]
|
||||
self._device: VueDevice = device_information[gid]
|
||||
self._attr_has_entity_name = True
|
||||
self._attr_name = None
|
||||
self._attr_device_class = SwitchDeviceClass.OUTLET
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
loop = asyncio.get_event_loop()
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(
|
||||
None, self._vue.update_outlet, self.coordinator.data[self._device_gid], True
|
||||
)
|
||||
@@ -123,7 +128,7 @@ class EmporiaOutletSwitch(CoordinatorEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
loop = asyncio.get_event_loop()
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(
|
||||
None,
|
||||
self._vue.update_outlet,
|
||||
@@ -132,7 +137,7 @@ class EmporiaOutletSwitch(CoordinatorEntity, SwitchEntity):
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device information."""
|
||||
return DeviceInfo(
|
||||
@@ -143,17 +148,24 @@ class EmporiaOutletSwitch(CoordinatorEntity, SwitchEntity):
|
||||
manufacturer="Emporia",
|
||||
)
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the switch."""
|
||||
return self.coordinator.data[self._device_gid].outlet_on
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def unique_id(self) -> str:
|
||||
"""Unique ID for the switch."""
|
||||
return f"switch.emporia_vue.{self._device_gid}"
|
||||
|
||||
class EmporiaChargerSwitch(EmporiaChargerEntity, SwitchEntity):
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class EmporiaChargerSwitch(EmporiaChargerEntity, SwitchEntity): # type: ignore
|
||||
"""Representation of an Emporia Charger switch state."""
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
@@ -164,14 +176,14 @@ class EmporiaChargerSwitch(EmporiaChargerEntity, SwitchEntity):
|
||||
"""Turn the charger off."""
|
||||
await self._update_switch(False)
|
||||
|
||||
@property
|
||||
@cached_property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the switch."""
|
||||
return self.coordinator.data[self._device.device_gid].charger_on
|
||||
|
||||
async def _update_switch(self, on: bool):
|
||||
async def _update_switch(self, on: bool) -> None:
|
||||
"""Update the switch."""
|
||||
loop = asyncio.get_event_loop()
|
||||
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||
try:
|
||||
await loop.run_in_executor(
|
||||
None,
|
||||
@@ -187,3 +199,9 @@ class EmporiaChargerSwitch(EmporiaChargerEntity, SwitchEntity):
|
||||
)
|
||||
raise
|
||||
await self._coordinator.async_request_refresh()
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -15,7 +15,19 @@
|
||||
"enable_1d": "Energy Today Sensor",
|
||||
"enable_1m": "Power Minute Average Sensor",
|
||||
"enable_1mon": "Energy This Month Sensor",
|
||||
"enable_1s": "Power Second Average Sensor",
|
||||
"password": "Password"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"email": "Email",
|
||||
"enable_1d": "Energy Today Sensor",
|
||||
"enable_1m": "Power Minute Average Sensor",
|
||||
"enable_1mon": "Energy This Month Sensor",
|
||||
"password": "Password"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user