Renew Smappee (sensors and switches) (#36445)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
fd67a079db
commit
5228282f69
16 changed files with 709 additions and 571 deletions
|
@ -728,7 +728,10 @@ omit =
|
||||||
homeassistant/components/sinch/*
|
homeassistant/components/sinch/*
|
||||||
homeassistant/components/slide/*
|
homeassistant/components/slide/*
|
||||||
homeassistant/components/sma/sensor.py
|
homeassistant/components/sma/sensor.py
|
||||||
homeassistant/components/smappee/*
|
homeassistant/components/smappee/__init__.py
|
||||||
|
homeassistant/components/smappee/api.py
|
||||||
|
homeassistant/components/smappee/sensor.py
|
||||||
|
homeassistant/components/smappee/switch.py
|
||||||
homeassistant/components/smarty/*
|
homeassistant/components/smarty/*
|
||||||
homeassistant/components/smarthab/*
|
homeassistant/components/smarthab/*
|
||||||
homeassistant/components/sms/*
|
homeassistant/components/sms/*
|
||||||
|
|
|
@ -366,6 +366,7 @@ homeassistant/components/sinch/* @bendikrb
|
||||||
homeassistant/components/sisyphus/* @jkeljo
|
homeassistant/components/sisyphus/* @jkeljo
|
||||||
homeassistant/components/slide/* @ualex73
|
homeassistant/components/slide/* @ualex73
|
||||||
homeassistant/components/sma/* @kellerza
|
homeassistant/components/sma/* @kellerza
|
||||||
|
homeassistant/components/smappee/* @bsmappee
|
||||||
homeassistant/components/smarthab/* @outadoc
|
homeassistant/components/smarthab/* @outadoc
|
||||||
homeassistant/components/smartthings/* @andrewsayre
|
homeassistant/components/smartthings/* @andrewsayre
|
||||||
homeassistant/components/smarty/* @z0mbieprocess
|
homeassistant/components/smarty/* @z0mbieprocess
|
||||||
|
|
|
@ -1,334 +1,109 @@
|
||||||
"""Support for Smappee energy monitor."""
|
"""The Smappee integration."""
|
||||||
from datetime import datetime, timedelta
|
import asyncio
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
|
|
||||||
from requests.exceptions import RequestException
|
from pysmappee import Smappee
|
||||||
import smappy
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.config_entries import ConfigEntry
|
||||||
CONF_CLIENT_ID,
|
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
CONF_CLIENT_SECRET,
|
from homeassistant.core import HomeAssistant
|
||||||
CONF_HOST,
|
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_USERNAME,
|
|
||||||
)
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.helpers.discovery import load_platform
|
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
from . import api, config_flow
|
||||||
|
from .const import (
|
||||||
DEFAULT_NAME = "Smappee"
|
AUTHORIZE_URL,
|
||||||
DEFAULT_HOST_PASSWORD = "admin"
|
BASE,
|
||||||
|
DOMAIN,
|
||||||
CONF_HOST_PASSWORD = "host_password"
|
MIN_TIME_BETWEEN_UPDATES,
|
||||||
|
SMAPPEE_PLATFORMS,
|
||||||
DOMAIN = "smappee"
|
TOKEN_URL,
|
||||||
DATA_SMAPPEE = "SMAPPEE"
|
)
|
||||||
|
|
||||||
_SENSOR_REGEX = re.compile(r"(?P<key>([A-Za-z]+))\=(?P<value>([0-9\.]+))")
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
DOMAIN: vol.Schema(
|
DOMAIN: vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Inclusive(CONF_CLIENT_ID, "Server credentials"): cv.string,
|
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||||
vol.Inclusive(CONF_CLIENT_SECRET, "Server credentials"): cv.string,
|
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||||
vol.Inclusive(CONF_USERNAME, "Server credentials"): cv.string,
|
|
||||||
vol.Inclusive(CONF_PASSWORD, "Server credentials"): cv.string,
|
|
||||||
vol.Optional(CONF_HOST): cv.string,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_HOST_PASSWORD, default=DEFAULT_HOST_PASSWORD
|
|
||||||
): cv.string,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
|
"""Set up the Smappee component."""
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
|
||||||
def setup(hass, config):
|
if DOMAIN not in config:
|
||||||
"""Set up the Smapee component."""
|
return True
|
||||||
client_id = config.get(DOMAIN).get(CONF_CLIENT_ID)
|
|
||||||
client_secret = config.get(DOMAIN).get(CONF_CLIENT_SECRET)
|
|
||||||
username = config.get(DOMAIN).get(CONF_USERNAME)
|
|
||||||
password = config.get(DOMAIN).get(CONF_PASSWORD)
|
|
||||||
host = config.get(DOMAIN).get(CONF_HOST)
|
|
||||||
host_password = config.get(DOMAIN).get(CONF_HOST_PASSWORD)
|
|
||||||
|
|
||||||
smappee = Smappee(client_id, client_secret, username, password, host, host_password)
|
config_flow.SmappeeFlowHandler.async_register_implementation(
|
||||||
|
hass,
|
||||||
|
config_entry_oauth2_flow.LocalOAuth2Implementation(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
config[DOMAIN][CONF_CLIENT_ID],
|
||||||
|
config[DOMAIN][CONF_CLIENT_SECRET],
|
||||||
|
AUTHORIZE_URL,
|
||||||
|
TOKEN_URL,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if not smappee.is_local_active and not smappee.is_remote_active:
|
|
||||||
_LOGGER.error("Neither Smappee server or local integration enabled.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
hass.data[DATA_SMAPPEE] = smappee
|
|
||||||
load_platform(hass, "switch", DOMAIN, {}, config)
|
|
||||||
load_platform(hass, "sensor", DOMAIN, {}, config)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Smappee:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Stores data retrieved from Smappee sensor."""
|
"""Set up Smappee from a config entry."""
|
||||||
|
implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||||
|
hass, entry
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(
|
smappee_api = api.ConfigEntrySmappeeApi(hass, entry, implementation)
|
||||||
self, client_id, client_secret, username, password, host, host_password
|
|
||||||
):
|
|
||||||
"""Initialize the data."""
|
|
||||||
|
|
||||||
self._remote_active = False
|
smappee = Smappee(smappee_api)
|
||||||
self._local_active = False
|
await hass.async_add_executor_job(smappee.load_service_locations)
|
||||||
if client_id is not None:
|
|
||||||
try:
|
|
||||||
self._smappy = smappy.Smappee(client_id, client_secret)
|
|
||||||
self._smappy.authenticate(username, password)
|
|
||||||
self._remote_active = True
|
|
||||||
except RequestException as error:
|
|
||||||
self._smappy = None
|
|
||||||
_LOGGER.exception("Smappee server authentication failed (%s)", error)
|
|
||||||
else:
|
|
||||||
_LOGGER.warning("Smappee server integration init skipped.")
|
|
||||||
|
|
||||||
if host is not None:
|
hass.data[DOMAIN][BASE] = SmappeeBase(hass, smappee)
|
||||||
try:
|
|
||||||
self._localsmappy = smappy.LocalSmappee(host)
|
|
||||||
self._localsmappy.logon(host_password)
|
|
||||||
self._local_active = True
|
|
||||||
except RequestException as error:
|
|
||||||
self._localsmappy = None
|
|
||||||
_LOGGER.exception(
|
|
||||||
"Local Smappee device authentication failed (%s)", error
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
_LOGGER.warning("Smappee local integration init skipped.")
|
|
||||||
|
|
||||||
self.locations = {}
|
for component in SMAPPEE_PLATFORMS:
|
||||||
self.info = {}
|
hass.async_create_task(
|
||||||
self.consumption = {}
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||||
self.sensor_consumption = {}
|
)
|
||||||
self.instantaneous = {}
|
|
||||||
|
|
||||||
if self._remote_active or self._local_active:
|
return True
|
||||||
self.update()
|
|
||||||
|
|
||||||
|
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 SMAPPEE_PLATFORMS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(BASE, None)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
class SmappeeBase:
|
||||||
|
"""An object to hold the PySmappee instance."""
|
||||||
|
|
||||||
|
def __init__(self, hass, smappee):
|
||||||
|
"""Initialize the Smappee API wrapper class."""
|
||||||
|
self.hass = hass
|
||||||
|
self.smappee = smappee
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
def update(self):
|
async def async_update(self):
|
||||||
"""Update data from Smappee API."""
|
"""Update all Smappee trends and appliance states."""
|
||||||
if self.is_remote_active:
|
await self.hass.async_add_executor_job(
|
||||||
service_locations = self._smappy.get_service_locations().get(
|
self.smappee.update_trends_and_appliance_states
|
||||||
"serviceLocations"
|
)
|
||||||
)
|
|
||||||
for location in service_locations:
|
|
||||||
location_id = location.get("serviceLocationId")
|
|
||||||
if location_id is not None:
|
|
||||||
self.sensor_consumption[location_id] = {}
|
|
||||||
self.locations[location_id] = location.get("name")
|
|
||||||
self.info[location_id] = self._smappy.get_service_location_info(
|
|
||||||
location_id
|
|
||||||
)
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Remote info %s %s", self.locations, self.info[location_id]
|
|
||||||
)
|
|
||||||
|
|
||||||
for sensors in self.info[location_id].get("sensors"):
|
|
||||||
sensor_id = sensors.get("id")
|
|
||||||
self.sensor_consumption[location_id].update(
|
|
||||||
{
|
|
||||||
sensor_id: self.get_sensor_consumption(
|
|
||||||
location_id, sensor_id, aggregation=3, delta=1440
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Remote sensors %s %s",
|
|
||||||
self.locations,
|
|
||||||
self.sensor_consumption[location_id],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.consumption[location_id] = self.get_consumption(
|
|
||||||
location_id, aggregation=3, delta=1440
|
|
||||||
)
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Remote consumption %s %s",
|
|
||||||
self.locations,
|
|
||||||
self.consumption[location_id],
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.is_local_active:
|
|
||||||
self.local_devices = self.get_switches()
|
|
||||||
_LOGGER.debug("Local switches %s", self.local_devices)
|
|
||||||
|
|
||||||
self.instantaneous = self.load_instantaneous()
|
|
||||||
_LOGGER.debug("Local values %s", self.instantaneous)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_remote_active(self):
|
|
||||||
"""Return true if Smappe server is configured and working."""
|
|
||||||
return self._remote_active
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_local_active(self):
|
|
||||||
"""Return true if Smappe local device is configured and working."""
|
|
||||||
return self._local_active
|
|
||||||
|
|
||||||
def get_switches(self):
|
|
||||||
"""Get switches from local Smappee."""
|
|
||||||
if not self.is_local_active:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self._localsmappy.load_command_control_config()
|
|
||||||
except RequestException as error:
|
|
||||||
_LOGGER.error("Error getting switches from local Smappee. (%s)", error)
|
|
||||||
|
|
||||||
def get_consumption(self, location_id, aggregation, delta):
|
|
||||||
"""Update data from Smappee."""
|
|
||||||
# Start & End accept epoch (in milliseconds),
|
|
||||||
# datetime and pandas timestamps
|
|
||||||
# Aggregation:
|
|
||||||
# 1 = 5 min values (only available for the last 14 days),
|
|
||||||
# 2 = hourly values,
|
|
||||||
# 3 = daily values,
|
|
||||||
# 4 = monthly values,
|
|
||||||
# 5 = quarterly values
|
|
||||||
if not self.is_remote_active:
|
|
||||||
return
|
|
||||||
|
|
||||||
end = datetime.utcnow()
|
|
||||||
start = end - timedelta(minutes=delta)
|
|
||||||
try:
|
|
||||||
return self._smappy.get_consumption(location_id, start, end, aggregation)
|
|
||||||
except RequestException as error:
|
|
||||||
_LOGGER.error("Error getting consumption from Smappee cloud. (%s)", error)
|
|
||||||
|
|
||||||
def get_sensor_consumption(self, location_id, sensor_id, aggregation, delta):
|
|
||||||
"""Update data from Smappee."""
|
|
||||||
# Start & End accept epoch (in milliseconds),
|
|
||||||
# datetime and pandas timestamps
|
|
||||||
# Aggregation:
|
|
||||||
# 1 = 5 min values (only available for the last 14 days),
|
|
||||||
# 2 = hourly values,
|
|
||||||
# 3 = daily values,
|
|
||||||
# 4 = monthly values,
|
|
||||||
# 5 = quarterly values
|
|
||||||
if not self.is_remote_active:
|
|
||||||
return
|
|
||||||
|
|
||||||
end = datetime.utcnow()
|
|
||||||
start = end - timedelta(minutes=delta)
|
|
||||||
try:
|
|
||||||
return self._smappy.get_sensor_consumption(
|
|
||||||
location_id, sensor_id, start, end, aggregation
|
|
||||||
)
|
|
||||||
except RequestException as error:
|
|
||||||
_LOGGER.error("Error getting consumption from Smappee cloud. (%s)", error)
|
|
||||||
|
|
||||||
def actuator_on(self, location_id, actuator_id, is_remote_switch, duration=None):
|
|
||||||
"""Turn on actuator."""
|
|
||||||
# Duration = 300,900,1800,3600
|
|
||||||
# or any other value for an undetermined period of time.
|
|
||||||
#
|
|
||||||
# The comport plugs have a tendency to ignore the on/off signal.
|
|
||||||
# And because you can't read the status of a plug, it's more
|
|
||||||
# reliable to execute the command twice.
|
|
||||||
try:
|
|
||||||
if is_remote_switch:
|
|
||||||
self._smappy.actuator_on(location_id, actuator_id, duration)
|
|
||||||
self._smappy.actuator_on(location_id, actuator_id, duration)
|
|
||||||
else:
|
|
||||||
self._localsmappy.on_command_control(actuator_id)
|
|
||||||
self._localsmappy.on_command_control(actuator_id)
|
|
||||||
except RequestException as error:
|
|
||||||
_LOGGER.error("Error turning actuator on. (%s)", error)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def actuator_off(self, location_id, actuator_id, is_remote_switch, duration=None):
|
|
||||||
"""Turn off actuator."""
|
|
||||||
# Duration = 300,900,1800,3600
|
|
||||||
# or any other value for an undetermined period of time.
|
|
||||||
#
|
|
||||||
# The comport plugs have a tendency to ignore the on/off signal.
|
|
||||||
# And because you can't read the status of a plug, it's more
|
|
||||||
# reliable to execute the command twice.
|
|
||||||
try:
|
|
||||||
if is_remote_switch:
|
|
||||||
self._smappy.actuator_off(location_id, actuator_id, duration)
|
|
||||||
self._smappy.actuator_off(location_id, actuator_id, duration)
|
|
||||||
else:
|
|
||||||
self._localsmappy.off_command_control(actuator_id)
|
|
||||||
self._localsmappy.off_command_control(actuator_id)
|
|
||||||
except RequestException as error:
|
|
||||||
_LOGGER.error("Error turning actuator on. (%s)", error)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def active_power(self):
|
|
||||||
"""Get sum of all instantaneous active power values from local hub."""
|
|
||||||
if not self.is_local_active:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self._localsmappy.active_power()
|
|
||||||
except RequestException as error:
|
|
||||||
_LOGGER.error("Error getting data from Local Smappee unit. (%s)", error)
|
|
||||||
|
|
||||||
def active_cosfi(self):
|
|
||||||
"""Get the average of all instantaneous cosfi values."""
|
|
||||||
if not self.is_local_active:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self._localsmappy.active_cosfi()
|
|
||||||
except RequestException as error:
|
|
||||||
_LOGGER.error("Error getting data from Local Smappee unit. (%s)", error)
|
|
||||||
|
|
||||||
def instantaneous_values(self):
|
|
||||||
"""ReportInstantaneousValues."""
|
|
||||||
if not self.is_local_active:
|
|
||||||
return
|
|
||||||
|
|
||||||
report_instantaneous_values = self._localsmappy.report_instantaneous_values()
|
|
||||||
|
|
||||||
report_result = report_instantaneous_values["report"].split("<BR>")
|
|
||||||
properties = {}
|
|
||||||
for lines in report_result:
|
|
||||||
lines_result = lines.split(",")
|
|
||||||
for prop in lines_result:
|
|
||||||
match = _SENSOR_REGEX.search(prop)
|
|
||||||
if match:
|
|
||||||
properties[match.group("key")] = match.group("value")
|
|
||||||
_LOGGER.debug(properties)
|
|
||||||
return properties
|
|
||||||
|
|
||||||
def active_current(self):
|
|
||||||
"""Get current active Amps."""
|
|
||||||
if not self.is_local_active:
|
|
||||||
return
|
|
||||||
|
|
||||||
properties = self.instantaneous_values()
|
|
||||||
return float(properties["current"])
|
|
||||||
|
|
||||||
def active_voltage(self):
|
|
||||||
"""Get current active Voltage."""
|
|
||||||
if not self.is_local_active:
|
|
||||||
return
|
|
||||||
|
|
||||||
properties = self.instantaneous_values()
|
|
||||||
return float(properties["voltage"])
|
|
||||||
|
|
||||||
def load_instantaneous(self):
|
|
||||||
"""LoadInstantaneous."""
|
|
||||||
if not self.is_local_active:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self._localsmappy.load_instantaneous()
|
|
||||||
except RequestException as error:
|
|
||||||
_LOGGER.error("Error getting data from Local Smappee unit. (%s)", error)
|
|
||||||
|
|
33
homeassistant/components/smappee/api.py
Normal file
33
homeassistant/components/smappee/api.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
"""API for Smappee bound to Home Assistant OAuth."""
|
||||||
|
from asyncio import run_coroutine_threadsafe
|
||||||
|
|
||||||
|
from pysmappee import api
|
||||||
|
|
||||||
|
from homeassistant import config_entries, core
|
||||||
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigEntrySmappeeApi(api.SmappeeApi):
|
||||||
|
"""Provide Smappee authentication tied to an OAuth2 based config entry."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: core.HomeAssistant,
|
||||||
|
config_entry: config_entries.ConfigEntry,
|
||||||
|
implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation,
|
||||||
|
):
|
||||||
|
"""Initialize Smappee Auth."""
|
||||||
|
self.hass = hass
|
||||||
|
self.config_entry = config_entry
|
||||||
|
self.session = config_entry_oauth2_flow.OAuth2Session(
|
||||||
|
hass, config_entry, implementation
|
||||||
|
)
|
||||||
|
super().__init__(None, None, token=self.session.token)
|
||||||
|
|
||||||
|
def refresh_tokens(self) -> dict:
|
||||||
|
"""Refresh and return new Smappee tokens using Home Assistant OAuth2 session."""
|
||||||
|
run_coroutine_threadsafe(
|
||||||
|
self.session.async_ensure_token_valid(), self.hass.loop
|
||||||
|
).result()
|
||||||
|
|
||||||
|
return self.session.token
|
30
homeassistant/components/smappee/config_flow.py
Normal file
30
homeassistant/components/smappee/config_flow.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
"""Config flow for Smappee."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from .const import DOMAIN # pylint: disable=unused-import
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SmappeeFlowHandler(
|
||||||
|
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
||||||
|
):
|
||||||
|
"""Config flow to handle Smappee OAuth2 authentication."""
|
||||||
|
|
||||||
|
DOMAIN = DOMAIN
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logger(self) -> logging.Logger:
|
||||||
|
"""Return logger."""
|
||||||
|
return logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow start."""
|
||||||
|
if self.hass.config_entries.async_entries(DOMAIN):
|
||||||
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
|
||||||
|
return await super().async_step_user(user_input)
|
15
homeassistant/components/smappee/const.py
Normal file
15
homeassistant/components/smappee/const.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
"""Constants for the Smappee integration."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
DOMAIN = "smappee"
|
||||||
|
DATA_CLIENT = "smappee_data"
|
||||||
|
|
||||||
|
BASE = "BASE"
|
||||||
|
|
||||||
|
SMAPPEE_PLATFORMS = ["sensor", "switch"]
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
||||||
|
|
||||||
|
AUTHORIZE_URL = "https://app1pub.smappee.net/dev/v1/oauth2/authorize"
|
||||||
|
TOKEN_URL = "https://app1pub.smappee.net/dev/v3/oauth2/token"
|
|
@ -1,7 +1,13 @@
|
||||||
{
|
{
|
||||||
"domain": "smappee",
|
"domain": "smappee",
|
||||||
"name": "Smappee",
|
"name": "Smappee",
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/smappee",
|
"documentation": "https://www.home-assistant.io/integrations/smappee",
|
||||||
"requirements": ["smappy==0.2.16"],
|
"dependencies": ["http"],
|
||||||
"codeowners": []
|
"requirements": [
|
||||||
|
"pysmappee==0.1.0"
|
||||||
|
],
|
||||||
|
"codeowners": [
|
||||||
|
"@bsmappee"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,170 +1,248 @@
|
||||||
"""Support for monitoring a Smappee energy sensor."""
|
"""Support for monitoring a Smappee energy sensor."""
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_WATT_HOUR, POWER_WATT, VOLT
|
||||||
DEGREE,
|
|
||||||
ELECTRICAL_CURRENT_AMPERE,
|
|
||||||
ENERGY_KILO_WATT_HOUR,
|
|
||||||
POWER_WATT,
|
|
||||||
UNIT_PERCENTAGE,
|
|
||||||
VOLT,
|
|
||||||
VOLUME_CUBIC_METERS,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from . import DATA_SMAPPEE
|
from .const import BASE, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SENSOR_PREFIX = "Smappee"
|
TREND_SENSORS = {
|
||||||
SENSOR_TYPES = {
|
"total_power": [
|
||||||
"solar": ["Solar", "mdi:white-balance-sunny", "local", POWER_WATT, "solar"],
|
"Total consumption - Active power",
|
||||||
"active_power": [
|
None,
|
||||||
"Active Power",
|
|
||||||
"mdi:power-plug",
|
|
||||||
"local",
|
|
||||||
POWER_WATT,
|
POWER_WATT,
|
||||||
"active_power",
|
"total_power",
|
||||||
|
DEVICE_CLASS_POWER,
|
||||||
],
|
],
|
||||||
"current": ["Current", "mdi:gauge", "local", ELECTRICAL_CURRENT_AMPERE, "current"],
|
"total_reactive_power": [
|
||||||
"voltage": ["Voltage", "mdi:gauge", "local", VOLT, "voltage"],
|
"Total consumption - Reactive power",
|
||||||
"active_cosfi": [
|
None,
|
||||||
"Power Factor",
|
POWER_WATT,
|
||||||
"mdi:gauge",
|
"total_reactive_power",
|
||||||
"local",
|
DEVICE_CLASS_POWER,
|
||||||
UNIT_PERCENTAGE,
|
|
||||||
"active_cosfi",
|
|
||||||
],
|
],
|
||||||
"alwayson_today": [
|
"alwayson": [
|
||||||
"Always On Today",
|
"Always on - Active power",
|
||||||
"mdi:gauge",
|
None,
|
||||||
"remote",
|
POWER_WATT,
|
||||||
ENERGY_KILO_WATT_HOUR,
|
"alwayson",
|
||||||
"alwaysOn",
|
DEVICE_CLASS_POWER,
|
||||||
],
|
|
||||||
"solar_today": [
|
|
||||||
"Solar Today",
|
|
||||||
"mdi:white-balance-sunny",
|
|
||||||
"remote",
|
|
||||||
ENERGY_KILO_WATT_HOUR,
|
|
||||||
"solar",
|
|
||||||
],
|
],
|
||||||
"power_today": [
|
"power_today": [
|
||||||
"Power Today",
|
"Total consumption - Today",
|
||||||
"mdi:power-plug",
|
"mdi:power-plug",
|
||||||
"remote",
|
ENERGY_WATT_HOUR,
|
||||||
ENERGY_KILO_WATT_HOUR,
|
"power_today",
|
||||||
"consumption",
|
None,
|
||||||
],
|
],
|
||||||
"water_sensor_1": [
|
"power_current_hour": [
|
||||||
"Water Sensor 1",
|
"Total consumption - Current hour",
|
||||||
"mdi:water",
|
"mdi:power-plug",
|
||||||
"water",
|
ENERGY_WATT_HOUR,
|
||||||
VOLUME_CUBIC_METERS,
|
"power_current_hour",
|
||||||
"value1",
|
None,
|
||||||
],
|
],
|
||||||
"water_sensor_2": [
|
"power_last_5_minutes": [
|
||||||
"Water Sensor 2",
|
"Total consumption - Last 5 minutes",
|
||||||
"mdi:water",
|
"mdi:power-plug",
|
||||||
"water",
|
ENERGY_WATT_HOUR,
|
||||||
VOLUME_CUBIC_METERS,
|
"power_last_5_minutes",
|
||||||
"value2",
|
None,
|
||||||
],
|
],
|
||||||
"water_sensor_temperature": [
|
"alwayson_today": [
|
||||||
"Water Sensor Temperature",
|
"Always on - Today",
|
||||||
"mdi:temperature-celsius",
|
"mdi:sleep",
|
||||||
"water",
|
ENERGY_WATT_HOUR,
|
||||||
DEGREE,
|
"alwayson_today",
|
||||||
"temperature",
|
None,
|
||||||
],
|
],
|
||||||
"water_sensor_humidity": [
|
}
|
||||||
"Water Sensor Humidity",
|
SOLAR_SENSORS = {
|
||||||
"mdi:water-percent",
|
"solar_power": [
|
||||||
"water",
|
"Total production - Active power",
|
||||||
UNIT_PERCENTAGE,
|
None,
|
||||||
"humidity",
|
POWER_WATT,
|
||||||
|
"solar_power",
|
||||||
|
DEVICE_CLASS_POWER,
|
||||||
],
|
],
|
||||||
"water_sensor_battery": [
|
"solar_today": [
|
||||||
"Water Sensor Battery",
|
"Total production - Today",
|
||||||
"mdi:battery",
|
"mdi:white-balance-sunny",
|
||||||
"water",
|
ENERGY_WATT_HOUR,
|
||||||
UNIT_PERCENTAGE,
|
"solar_today",
|
||||||
"battery",
|
None,
|
||||||
|
],
|
||||||
|
"solar_current_hour": [
|
||||||
|
"Total production - Current hour",
|
||||||
|
"mdi:white-balance-sunny",
|
||||||
|
ENERGY_WATT_HOUR,
|
||||||
|
"solar_current_hour",
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
VOLTAGE_SENSORS = {
|
||||||
|
"phase_voltages_a": [
|
||||||
|
"Phase voltages - A",
|
||||||
|
"mdi:flash",
|
||||||
|
VOLT,
|
||||||
|
"phase_voltage_a",
|
||||||
|
["ONE", "TWO", "THREE_STAR", "THREE_DELTA"],
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
"phase_voltages_b": [
|
||||||
|
"Phase voltages - B",
|
||||||
|
"mdi:flash",
|
||||||
|
VOLT,
|
||||||
|
"phase_voltage_b",
|
||||||
|
["TWO", "THREE_STAR", "THREE_DELTA"],
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
"phase_voltages_c": [
|
||||||
|
"Phase voltages - C",
|
||||||
|
"mdi:flash",
|
||||||
|
VOLT,
|
||||||
|
"phase_voltage_c",
|
||||||
|
["THREE_STAR"],
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
"line_voltages_a": [
|
||||||
|
"Line voltages - A",
|
||||||
|
"mdi:flash",
|
||||||
|
VOLT,
|
||||||
|
"line_voltage_a",
|
||||||
|
["ONE", "TWO", "THREE_STAR", "THREE_DELTA"],
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
"line_voltages_b": [
|
||||||
|
"Line voltages - B",
|
||||||
|
"mdi:flash",
|
||||||
|
VOLT,
|
||||||
|
"line_voltage_b",
|
||||||
|
["TWO", "THREE_STAR", "THREE_DELTA"],
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
"line_voltages_c": [
|
||||||
|
"Line voltages - C",
|
||||||
|
"mdi:flash",
|
||||||
|
VOLT,
|
||||||
|
"line_voltage_c",
|
||||||
|
["THREE_STAR", "THREE_DELTA"],
|
||||||
|
None,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
|
||||||
"""Set up the Smappee sensor."""
|
"""Set up the Smappee sensor."""
|
||||||
smappee = hass.data[DATA_SMAPPEE]
|
smappee_base = hass.data[DOMAIN][BASE]
|
||||||
|
|
||||||
dev = []
|
entities = []
|
||||||
if smappee.is_remote_active:
|
for service_location in smappee_base.smappee.service_locations.values():
|
||||||
for location_id in smappee.locations.keys():
|
# Add all basic sensors (realtime values and aggregators)
|
||||||
for sensor in SENSOR_TYPES:
|
for sensor in TREND_SENSORS:
|
||||||
if "remote" in SENSOR_TYPES[sensor]:
|
entities.append(
|
||||||
dev.append(
|
SmappeeSensor(
|
||||||
SmappeeSensor(
|
smappee_base=smappee_base,
|
||||||
smappee, location_id, sensor, SENSOR_TYPES[sensor]
|
service_location=service_location,
|
||||||
)
|
sensor=sensor,
|
||||||
|
attributes=TREND_SENSORS[sensor],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add solar sensors
|
||||||
|
if service_location.has_solar_production:
|
||||||
|
for sensor in SOLAR_SENSORS:
|
||||||
|
entities.append(
|
||||||
|
SmappeeSensor(
|
||||||
|
smappee_base=smappee_base,
|
||||||
|
service_location=service_location,
|
||||||
|
sensor=sensor,
|
||||||
|
attributes=SOLAR_SENSORS[sensor],
|
||||||
)
|
)
|
||||||
elif "water" in SENSOR_TYPES[sensor]:
|
)
|
||||||
for items in smappee.info[location_id].get("sensors"):
|
|
||||||
dev.append(
|
|
||||||
SmappeeSensor(
|
|
||||||
smappee,
|
|
||||||
location_id,
|
|
||||||
"{}:{}".format(sensor, items.get("id")),
|
|
||||||
SENSOR_TYPES[sensor],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if smappee.is_local_active:
|
# Add all CT measurements
|
||||||
if smappee.is_remote_active:
|
for measurement_id, measurement in service_location.measurements.items():
|
||||||
location_keys = smappee.locations.keys()
|
entities.append(
|
||||||
else:
|
SmappeeSensor(
|
||||||
location_keys = [None]
|
smappee_base=smappee_base,
|
||||||
for location_id in location_keys:
|
service_location=service_location,
|
||||||
for sensor in SENSOR_TYPES:
|
sensor="load",
|
||||||
if "local" in SENSOR_TYPES[sensor]:
|
attributes=[
|
||||||
dev.append(
|
measurement.name,
|
||||||
SmappeeSensor(
|
None,
|
||||||
smappee, location_id, sensor, SENSOR_TYPES[sensor]
|
POWER_WATT,
|
||||||
)
|
measurement_id,
|
||||||
|
DEVICE_CLASS_POWER,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add phase- and line voltages
|
||||||
|
for sensor_name, sensor in VOLTAGE_SENSORS.items():
|
||||||
|
if service_location.phase_type in sensor[4]:
|
||||||
|
entities.append(
|
||||||
|
SmappeeSensor(
|
||||||
|
smappee_base=smappee_base,
|
||||||
|
service_location=service_location,
|
||||||
|
sensor=sensor_name,
|
||||||
|
attributes=sensor,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
add_entities(dev, True)
|
# Add Gas and Water sensors
|
||||||
|
for sensor_id, sensor in service_location.sensors.items():
|
||||||
|
for channel in sensor.channels:
|
||||||
|
gw_icon = "mdi:gas-cylinder"
|
||||||
|
if channel.get("type") == "water":
|
||||||
|
gw_icon = "mdi:water"
|
||||||
|
|
||||||
|
entities.append(
|
||||||
|
SmappeeSensor(
|
||||||
|
smappee_base=smappee_base,
|
||||||
|
service_location=service_location,
|
||||||
|
sensor="sensor",
|
||||||
|
attributes=[
|
||||||
|
channel.get("name"),
|
||||||
|
gw_icon,
|
||||||
|
channel.get("uom"),
|
||||||
|
f"{sensor_id}-{channel.get('channel')}",
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
|
|
||||||
class SmappeeSensor(Entity):
|
class SmappeeSensor(Entity):
|
||||||
"""Implementation of a Smappee sensor."""
|
"""Implementation of a Smappee sensor."""
|
||||||
|
|
||||||
def __init__(self, smappee, location_id, sensor, attributes):
|
def __init__(self, smappee_base, service_location, sensor, attributes):
|
||||||
"""Initialize the Smappee sensor."""
|
"""Initialize the Smappee sensor."""
|
||||||
self._smappee = smappee
|
self._smappee_base = smappee_base
|
||||||
self._location_id = location_id
|
self._service_location = service_location
|
||||||
self._attributes = attributes
|
|
||||||
self._sensor = sensor
|
self._sensor = sensor
|
||||||
self.data = None
|
self.data = None
|
||||||
self._state = None
|
self._state = None
|
||||||
self._name = self._attributes[0]
|
self._name = attributes[0]
|
||||||
self._icon = self._attributes[1]
|
self._icon = attributes[1]
|
||||||
self._type = self._attributes[2]
|
self._unit_of_measurement = attributes[2]
|
||||||
self._unit_of_measurement = self._attributes[3]
|
self._sensor_id = attributes[3]
|
||||||
self._smappe_name = self._attributes[4]
|
self._device_class = attributes[4]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name for this sensor."""
|
||||||
if self._location_id:
|
if self._sensor in ["sensor", "load"]:
|
||||||
location_name = self._smappee.locations[self._location_id]
|
return (
|
||||||
else:
|
f"{self._service_location.service_location_name} - "
|
||||||
location_name = "Local"
|
f"{self._sensor.title()} - {self._name}"
|
||||||
|
)
|
||||||
|
|
||||||
return f"{SENSOR_PREFIX} {location_name} {self._name}"
|
return f"{self._service_location.service_location_name} - {self._name}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
|
@ -176,97 +254,94 @@ class SmappeeSensor(Entity):
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||||
|
return self._device_class
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
"""Return the unit of measurement of this entity, if any."""
|
"""Return the unit of measurement of this entity, if any."""
|
||||||
return self._unit_of_measurement
|
return self._unit_of_measurement
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def unique_id(self,):
|
||||||
"""Return the state attributes of the device."""
|
"""Return the unique ID for this sensor."""
|
||||||
attr = {}
|
if self._sensor in ["load", "sensor"]:
|
||||||
if self._location_id:
|
return (
|
||||||
attr["Location Id"] = self._location_id
|
f"{self._service_location.device_serial_number}-"
|
||||||
attr["Location Name"] = self._smappee.locations[self._location_id]
|
f"{self._service_location.service_location_id}-"
|
||||||
return attr
|
f"{self._sensor}-{self._sensor_id}"
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Get the latest data from Smappee and update the state."""
|
|
||||||
self._smappee.update()
|
|
||||||
|
|
||||||
if self._sensor in ["alwayson_today", "solar_today", "power_today"]:
|
|
||||||
data = self._smappee.consumption[self._location_id]
|
|
||||||
if data:
|
|
||||||
consumption = data.get("consumptions")[-1]
|
|
||||||
_LOGGER.debug("%s %s", self._sensor, consumption)
|
|
||||||
value = consumption.get(self._smappe_name)
|
|
||||||
self._state = round(value / 1000, 2)
|
|
||||||
elif self._sensor == "active_cosfi":
|
|
||||||
cosfi = self._smappee.active_cosfi()
|
|
||||||
_LOGGER.debug("%s %s", self._sensor, cosfi)
|
|
||||||
if cosfi:
|
|
||||||
self._state = round(cosfi, 2)
|
|
||||||
elif self._sensor == "current":
|
|
||||||
current = self._smappee.active_current()
|
|
||||||
_LOGGER.debug("%s %s", self._sensor, current)
|
|
||||||
if current:
|
|
||||||
self._state = round(current, 2)
|
|
||||||
elif self._sensor == "voltage":
|
|
||||||
voltage = self._smappee.active_voltage()
|
|
||||||
_LOGGER.debug("%s %s", self._sensor, voltage)
|
|
||||||
if voltage:
|
|
||||||
self._state = round(voltage, 3)
|
|
||||||
elif self._sensor == "active_power":
|
|
||||||
data = self._smappee.instantaneous
|
|
||||||
_LOGGER.debug("%s %s", self._sensor, data)
|
|
||||||
if data:
|
|
||||||
value1 = [
|
|
||||||
float(i["value"])
|
|
||||||
for i in data
|
|
||||||
if i["key"].endswith("phase0ActivePower")
|
|
||||||
]
|
|
||||||
value2 = [
|
|
||||||
float(i["value"])
|
|
||||||
for i in data
|
|
||||||
if i["key"].endswith("phase1ActivePower")
|
|
||||||
]
|
|
||||||
value3 = [
|
|
||||||
float(i["value"])
|
|
||||||
for i in data
|
|
||||||
if i["key"].endswith("phase2ActivePower")
|
|
||||||
]
|
|
||||||
active_power = sum(value1 + value2 + value3) / 1000
|
|
||||||
self._state = round(active_power, 2)
|
|
||||||
elif self._sensor == "solar":
|
|
||||||
data = self._smappee.instantaneous
|
|
||||||
_LOGGER.debug("%s %s", self._sensor, data)
|
|
||||||
if data:
|
|
||||||
value1 = [
|
|
||||||
float(i["value"])
|
|
||||||
for i in data
|
|
||||||
if i["key"].endswith("phase3ActivePower")
|
|
||||||
]
|
|
||||||
value2 = [
|
|
||||||
float(i["value"])
|
|
||||||
for i in data
|
|
||||||
if i["key"].endswith("phase4ActivePower")
|
|
||||||
]
|
|
||||||
value3 = [
|
|
||||||
float(i["value"])
|
|
||||||
for i in data
|
|
||||||
if i["key"].endswith("phase5ActivePower")
|
|
||||||
]
|
|
||||||
power = sum(value1 + value2 + value3) / 1000
|
|
||||||
self._state = round(power, 2)
|
|
||||||
elif self._type == "water":
|
|
||||||
sensor_name, sensor_id = self._sensor.split(":")
|
|
||||||
data = self._smappee.sensor_consumption[self._location_id].get(
|
|
||||||
int(sensor_id)
|
|
||||||
)
|
)
|
||||||
if data:
|
|
||||||
tempdata = data.get("records")
|
return (
|
||||||
if tempdata:
|
f"{self._service_location.device_serial_number}-"
|
||||||
consumption = tempdata[-1]
|
f"{self._service_location.service_location_id}-"
|
||||||
_LOGGER.debug("%s (%s) %s", sensor_name, sensor_id, consumption)
|
f"{self._sensor}"
|
||||||
value = consumption.get(self._smappe_name)
|
)
|
||||||
self._state = value
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device info for this sensor."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._service_location.device_serial_number)},
|
||||||
|
"name": self._service_location.service_location_name,
|
||||||
|
"manufacturer": "Smappee",
|
||||||
|
"model": self._service_location.device_model,
|
||||||
|
"sw_version": self._service_location.firmware_version,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Get the latest data from Smappee and update the state."""
|
||||||
|
await self._smappee_base.async_update()
|
||||||
|
|
||||||
|
if self._sensor == "total_power":
|
||||||
|
self._state = self._service_location.total_power
|
||||||
|
elif self._sensor == "total_reactive_power":
|
||||||
|
self._state = self._service_location.total_reactive_power
|
||||||
|
elif self._sensor == "solar_power":
|
||||||
|
self._state = self._service_location.solar_power
|
||||||
|
elif self._sensor == "alwayson":
|
||||||
|
self._state = self._service_location.alwayson
|
||||||
|
elif self._sensor in [
|
||||||
|
"phase_voltages_a",
|
||||||
|
"phase_voltages_b",
|
||||||
|
"phase_voltages_c",
|
||||||
|
]:
|
||||||
|
phase_voltages = self._service_location.phase_voltages
|
||||||
|
if phase_voltages is not None:
|
||||||
|
if self._sensor == "phase_voltages_a":
|
||||||
|
self._state = phase_voltages[0]
|
||||||
|
elif self._sensor == "phase_voltages_b":
|
||||||
|
self._state = phase_voltages[1]
|
||||||
|
elif self._sensor == "phase_voltages_c":
|
||||||
|
self._state = phase_voltages[2]
|
||||||
|
elif self._sensor in ["line_voltages_a", "line_voltages_b", "line_voltages_c"]:
|
||||||
|
line_voltages = self._service_location.line_voltages
|
||||||
|
if line_voltages is not None:
|
||||||
|
if self._sensor == "line_voltages_a":
|
||||||
|
self._state = line_voltages[0]
|
||||||
|
elif self._sensor == "line_voltages_b":
|
||||||
|
self._state = line_voltages[1]
|
||||||
|
elif self._sensor == "line_voltages_c":
|
||||||
|
self._state = line_voltages[2]
|
||||||
|
elif self._sensor in [
|
||||||
|
"power_today",
|
||||||
|
"power_current_hour",
|
||||||
|
"power_last_5_minutes",
|
||||||
|
"solar_today",
|
||||||
|
"solar_current_hour",
|
||||||
|
"alwayson_today",
|
||||||
|
]:
|
||||||
|
trend_value = self._service_location.aggregated_values.get(self._sensor)
|
||||||
|
self._state = round(trend_value) if trend_value is not None else None
|
||||||
|
elif self._sensor == "load":
|
||||||
|
self._state = self._service_location.measurements.get(
|
||||||
|
self._sensor_id
|
||||||
|
).active_total
|
||||||
|
elif self._sensor == "sensor":
|
||||||
|
sensor_id, channel_id = self._sensor_id.split("-")
|
||||||
|
sensor = self._service_location.sensors.get(int(sensor_id))
|
||||||
|
for channel in sensor.channels:
|
||||||
|
if channel.get("channel") == int(channel_id):
|
||||||
|
self._state = channel.get("value_today")
|
||||||
|
|
14
homeassistant/components/smappee/strings.json
Normal file
14
homeassistant/components/smappee/strings.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"pick_implementation": {
|
||||||
|
"title": "Pick Authentication Method"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"authorize_url_timeout": "Timeout generating authorize url.",
|
||||||
|
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||||
|
"missing_configuration": "The component is not configured. Please follow the documentation."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,62 +1,105 @@
|
||||||
"""Support for interacting with Smappee Comport Plugs."""
|
"""Support for interacting with Smappee Comport Plugs, Switches and Output Modules."""
|
||||||
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity
|
||||||
|
|
||||||
from . import DATA_SMAPPEE
|
from .const import BASE, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ICON = "mdi:power-plug"
|
SWITCH_PREFIX = "Switch"
|
||||||
|
ICON = "mdi:toggle-switch"
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=5)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up the Smappee Comfort Plugs."""
|
"""Set up the Smappee Comfort Plugs."""
|
||||||
smappee = hass.data[DATA_SMAPPEE]
|
smappee_base = hass.data[DOMAIN][BASE]
|
||||||
|
|
||||||
dev = []
|
entities = []
|
||||||
if smappee.is_remote_active:
|
for service_location in smappee_base.smappee.service_locations.values():
|
||||||
for location_id in smappee.locations.keys():
|
for actuator_id, actuator in service_location.actuators.items():
|
||||||
for items in smappee.info[location_id].get("actuators"):
|
if actuator.type in ["SWITCH", "COMFORT_PLUG"]:
|
||||||
if items.get("name") != "":
|
entities.append(
|
||||||
_LOGGER.debug("Remote actuator %s", items)
|
SmappeeActuator(
|
||||||
dev.append(
|
smappee_base,
|
||||||
SmappeeSwitch(
|
service_location,
|
||||||
smappee, items.get("name"), location_id, items.get("id")
|
actuator.name,
|
||||||
|
actuator_id,
|
||||||
|
actuator.type,
|
||||||
|
actuator.serialnumber,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif actuator.type == "INFINITY_OUTPUT_MODULE":
|
||||||
|
for option in actuator.state_options:
|
||||||
|
entities.append(
|
||||||
|
SmappeeActuator(
|
||||||
|
smappee_base,
|
||||||
|
service_location,
|
||||||
|
actuator.name,
|
||||||
|
actuator_id,
|
||||||
|
actuator.type,
|
||||||
|
actuator.serialnumber,
|
||||||
|
actuator_state_option=option,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif smappee.is_local_active:
|
|
||||||
for items in smappee.local_devices:
|
async_add_entities(entities, True)
|
||||||
_LOGGER.debug("Local actuator %s", items)
|
|
||||||
dev.append(
|
|
||||||
SmappeeSwitch(smappee, items.get("value"), None, items.get("key"))
|
|
||||||
)
|
|
||||||
add_entities(dev)
|
|
||||||
|
|
||||||
|
|
||||||
class SmappeeSwitch(SwitchEntity):
|
class SmappeeActuator(SwitchEntity):
|
||||||
"""Representation of a Smappee Comport Plug."""
|
"""Representation of a Smappee Comport Plug."""
|
||||||
|
|
||||||
def __init__(self, smappee, name, location_id, switch_id):
|
def __init__(
|
||||||
|
self,
|
||||||
|
smappee_base,
|
||||||
|
service_location,
|
||||||
|
name,
|
||||||
|
actuator_id,
|
||||||
|
actuator_type,
|
||||||
|
actuator_serialnumber,
|
||||||
|
actuator_state_option=None,
|
||||||
|
):
|
||||||
"""Initialize a new Smappee Comfort Plug."""
|
"""Initialize a new Smappee Comfort Plug."""
|
||||||
self._name = name
|
self._smappee_base = smappee_base
|
||||||
self._state = False
|
self._service_location = service_location
|
||||||
self._smappee = smappee
|
self._actuator_name = name
|
||||||
self._location_id = location_id
|
self._actuator_id = actuator_id
|
||||||
self._switch_id = switch_id
|
self._actuator_type = actuator_type
|
||||||
self._remoteswitch = True
|
self._actuator_serialnumber = actuator_serialnumber
|
||||||
if location_id is None:
|
self._actuator_state_option = actuator_state_option
|
||||||
self._remoteswitch = False
|
self._state = self._service_location.actuators.get(actuator_id).state
|
||||||
|
self._connection_state = self._service_location.actuators.get(
|
||||||
|
actuator_id
|
||||||
|
).connection_state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the switch."""
|
"""Return the name of the switch."""
|
||||||
return self._name
|
if self._actuator_type == "INFINITY_OUTPUT_MODULE":
|
||||||
|
return (
|
||||||
|
f"{self._service_location.service_location_name} - "
|
||||||
|
f"Output module - {self._actuator_name} - {self._actuator_state_option}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Switch or comfort plug
|
||||||
|
return (
|
||||||
|
f"{self._service_location.service_location_name} - "
|
||||||
|
f"{self._actuator_type.title()} - {self._actuator_name}"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if switch is on."""
|
"""Return true if switch is on."""
|
||||||
return self._state
|
if self._actuator_type == "INFINITY_OUTPUT_MODULE":
|
||||||
|
return (
|
||||||
|
self._service_location.actuators.get(self._actuator_id).state
|
||||||
|
== self._actuator_state_option
|
||||||
|
)
|
||||||
|
|
||||||
|
# Switch or comfort plug
|
||||||
|
return self._state == "ON_ON"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
|
@ -65,24 +108,80 @@ class SmappeeSwitch(SwitchEntity):
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn on Comport Plug."""
|
"""Turn on Comport Plug."""
|
||||||
if self._smappee.actuator_on(
|
if self._actuator_type in ["SWITCH", "COMFORT_PLUG"]:
|
||||||
self._location_id, self._switch_id, self._remoteswitch
|
self._service_location.set_actuator_state(self._actuator_id, state="ON_ON")
|
||||||
):
|
elif self._actuator_type == "INFINITY_OUTPUT_MODULE":
|
||||||
self._state = True
|
self._service_location.set_actuator_state(
|
||||||
|
self._actuator_id, state=self._actuator_state_option
|
||||||
|
)
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn off Comport Plug."""
|
"""Turn off Comport Plug."""
|
||||||
if self._smappee.actuator_off(
|
if self._actuator_type in ["SWITCH", "COMFORT_PLUG"]:
|
||||||
self._location_id, self._switch_id, self._remoteswitch
|
self._service_location.set_actuator_state(
|
||||||
):
|
self._actuator_id, state="OFF_OFF"
|
||||||
self._state = False
|
)
|
||||||
|
elif self._actuator_type == "INFINITY_OUTPUT_MODULE":
|
||||||
|
self._service_location.set_actuator_state(
|
||||||
|
self._actuator_id, state="PLACEHOLDER", api=False
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def available(self):
|
||||||
"""Return the state attributes of the device."""
|
"""Return True if entity is available. Unavailable for COMFORT_PLUGS."""
|
||||||
attr = {}
|
return (
|
||||||
if self._remoteswitch:
|
self._connection_state == "CONNECTED"
|
||||||
attr["Location Id"] = self._location_id
|
or self._actuator_type == "COMFORT_PLUG"
|
||||||
attr["Location Name"] = self._smappee.locations[self._location_id]
|
)
|
||||||
attr["Switch Id"] = self._switch_id
|
|
||||||
return attr
|
@property
|
||||||
|
def today_energy_kwh(self):
|
||||||
|
"""Return the today total energy usage in kWh."""
|
||||||
|
if self._actuator_type == "SWITCH":
|
||||||
|
cons = self._service_location.actuators.get(
|
||||||
|
self._actuator_id
|
||||||
|
).consumption_today
|
||||||
|
if cons is not None:
|
||||||
|
return round(cons / 1000.0, 2)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self,):
|
||||||
|
"""Return the unique ID for this switch."""
|
||||||
|
if self._actuator_type == "INFINITY_OUTPUT_MODULE":
|
||||||
|
return (
|
||||||
|
f"{self._service_location.device_serial_number}-"
|
||||||
|
f"{self._service_location.service_location_id}-actuator-"
|
||||||
|
f"{self._actuator_id}-{self._actuator_state_option}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Switch or comfort plug
|
||||||
|
return (
|
||||||
|
f"{self._service_location.device_serial_number}-"
|
||||||
|
f"{self._service_location.service_location_id}-actuator-"
|
||||||
|
f"{self._actuator_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device info for this switch."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._service_location.device_serial_number)},
|
||||||
|
"name": self._service_location.service_location_name,
|
||||||
|
"manufacturer": "Smappee",
|
||||||
|
"model": self._service_location.device_model,
|
||||||
|
"sw_version": self._service_location.firmware_version,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Get the latest data from Smappee and update the state."""
|
||||||
|
await self._smappee_base.async_update()
|
||||||
|
|
||||||
|
new_state = self._service_location.actuators.get(self._actuator_id).state
|
||||||
|
if new_state != self._state:
|
||||||
|
self._state = new_state
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
self._connection_state = self._service_location.actuators.get(
|
||||||
|
self._actuator_id
|
||||||
|
).connection_state
|
||||||
|
|
14
homeassistant/components/smappee/translations/en.json
Normal file
14
homeassistant/components/smappee/translations/en.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"pick_implementation": {
|
||||||
|
"title": "Pick Authentication Method"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"authorize_url_timeout": "Timeout generating authorize url.",
|
||||||
|
"single_instance_allowed": "Already configured. Only a single configuration possible.",
|
||||||
|
"missing_configuration": "The component is not configured. Please follow the documentation."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -135,6 +135,7 @@ FLOWS = [
|
||||||
"sentry",
|
"sentry",
|
||||||
"shopping_list",
|
"shopping_list",
|
||||||
"simplisafe",
|
"simplisafe",
|
||||||
|
"smappee",
|
||||||
"smartthings",
|
"smartthings",
|
||||||
"smhi",
|
"smhi",
|
||||||
"solaredge",
|
"solaredge",
|
||||||
|
|
|
@ -1609,6 +1609,9 @@ pysignalclirestapi==0.3.4
|
||||||
# homeassistant.components.sma
|
# homeassistant.components.sma
|
||||||
pysma==0.3.5
|
pysma==0.3.5
|
||||||
|
|
||||||
|
# homeassistant.components.smappee
|
||||||
|
pysmappee==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartapp==0.3.2
|
pysmartapp==0.3.2
|
||||||
|
|
||||||
|
@ -1964,9 +1967,6 @@ sleepyq==0.7
|
||||||
# homeassistant.components.xmpp
|
# homeassistant.components.xmpp
|
||||||
slixmpp==1.5.1
|
slixmpp==1.5.1
|
||||||
|
|
||||||
# homeassistant.components.smappee
|
|
||||||
smappy==0.2.16
|
|
||||||
|
|
||||||
# homeassistant.components.smarthab
|
# homeassistant.components.smarthab
|
||||||
smarthab==0.20
|
smarthab==0.20
|
||||||
|
|
||||||
|
|
|
@ -702,6 +702,9 @@ pysignalclirestapi==0.3.4
|
||||||
# homeassistant.components.sma
|
# homeassistant.components.sma
|
||||||
pysma==0.3.5
|
pysma==0.3.5
|
||||||
|
|
||||||
|
# homeassistant.components.smappee
|
||||||
|
pysmappee==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.smartthings
|
# homeassistant.components.smartthings
|
||||||
pysmartapp==0.3.2
|
pysmartapp==0.3.2
|
||||||
|
|
||||||
|
|
1
tests/components/smappee/__init__.py
Normal file
1
tests/components/smappee/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the Smappee integration."""
|
68
tests/components/smappee/test_config_flow.py
Normal file
68
tests/components/smappee/test_config_flow.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
"""Test the Smappee config flow."""
|
||||||
|
from homeassistant import config_entries, data_entry_flow, setup
|
||||||
|
from homeassistant.components.smappee.const import AUTHORIZE_URL, DOMAIN, TOKEN_URL
|
||||||
|
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from tests.async_mock import patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
CLIENT_ID = "1234"
|
||||||
|
CLIENT_SECRET = "5678"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_abort_if_existing_entry(hass):
|
||||||
|
"""Check flow abort when an entry already exist."""
|
||||||
|
MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "single_instance_allowed"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_flow(hass, aiohttp_client, aioclient_mock):
|
||||||
|
"""Check full flow."""
|
||||||
|
assert await setup.async_setup_component(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
{
|
||||||
|
DOMAIN: {CONF_CLIENT_ID: CLIENT_ID, CONF_CLIENT_SECRET: CLIENT_SECRET},
|
||||||
|
"http": {"base_url": "https://example.com"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
|
||||||
|
|
||||||
|
assert result["url"] == (
|
||||||
|
f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}"
|
||||||
|
"&redirect_uri=https://example.com/auth/external/callback"
|
||||||
|
f"&state={state}"
|
||||||
|
)
|
||||||
|
|
||||||
|
client = await aiohttp_client(hass.http.app)
|
||||||
|
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||||
|
assert resp.status == 200
|
||||||
|
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||||
|
|
||||||
|
aioclient_mock.post(
|
||||||
|
TOKEN_URL,
|
||||||
|
json={
|
||||||
|
"refresh_token": "mock-refresh-token",
|
||||||
|
"access_token": "mock-access-token",
|
||||||
|
"type": "Bearer",
|
||||||
|
"expires_in": 60,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.smappee.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup:
|
||||||
|
await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
Loading…
Add table
Reference in a new issue