Update plugwise to async and config_flow sensor part (#36219)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Tom 2020-06-01 14:01:17 +02:00 committed by GitHub
parent 5d7720832b
commit d0fedad000
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 439 additions and 83 deletions

View File

@ -606,6 +606,7 @@ omit =
homeassistant/components/plex/sensor.py
homeassistant/components/plugwise/__init__.py
homeassistant/components/plugwise/climate.py
homeassistant/components/plugwise/sensor.py
homeassistant/components/plum_lightpad/*
homeassistant/components/pocketcasts/sensor.py
homeassistant/components/point/*

View File

@ -3,13 +3,14 @@
import asyncio
from datetime import timedelta
import logging
from typing import Dict
from Plugwise_Smile.Smile import Smile
import async_timeout
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -22,7 +23,8 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)
ALL_PLATFORMS = ["climate"]
SENSOR_PLATFORMS = ["sensor"]
ALL_PLATFORMS = ["climate", "sensor"]
async def async_setup(hass: HomeAssistant, config: dict):
@ -100,7 +102,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
sw_version=api.smile_version[0],
)
for component in ALL_PLATFORMS:
platforms = ALL_PLATFORMS
single_master_thermostat = api.single_master_thermostat()
if single_master_thermostat is None:
platforms = SENSOR_PLATFORMS
for component in platforms:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
@ -127,11 +135,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
class SmileGateway(Entity):
"""Represent Smile Gateway."""
def __init__(self, api, coordinator):
"""Initialise the sensor."""
def __init__(self, api, coordinator, name, dev_id):
"""Initialise the gateway."""
self._api = api
self._coordinator = coordinator
self._name = name
self._dev_id = dev_id
self._unique_id = None
self._model = None
self._entity_name = self._name
@property
def unique_id(self):
@ -148,11 +162,40 @@ class SmileGateway(Entity):
"""Return True if entity is available."""
return self._coordinator.last_update_success
@property
def name(self):
"""Return the name of the entity, if any."""
if not self._name:
return None
return self._name
@property
def device_info(self) -> Dict[str, any]:
"""Return the device information."""
device_information = {
"identifiers": {(DOMAIN, self._dev_id)},
"name": self._entity_name,
"manufacturer": "Plugwise",
}
if self._model is not None:
device_information["model"] = self._model.replace("_", " ").title()
if self._dev_id != self._api.gateway_id:
device_information["via_device"] = (DOMAIN, self._api.gateway_id)
return device_information
async def async_added_to_hass(self):
"""Subscribe to updates."""
self.async_on_remove(self._coordinator.async_add_listener(self._process_data))
self._async_process_data()
self.async_on_remove(
self._coordinator.async_add_listener(self._async_process_data)
)
def _process_data(self):
@callback
def _async_process_data(self):
"""Interpret and process API data."""
raise NotImplementedError

View File

@ -1,7 +1,6 @@
"""Plugwise Climate component for Home Assistant."""
import logging
from typing import Dict
from Plugwise_Smile.Smile import Smile
@ -17,16 +16,10 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.core import callback
from . import SmileGateway
from .const import (
DEFAULT_MAX_TEMP,
DEFAULT_MIN_TEMP,
DOMAIN,
SCHEDULE_OFF,
SCHEDULE_ON,
THERMOSTAT_ICON,
)
from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SCHEDULE_OFF, SCHEDULE_ON
HVAC_MODES_HEAT_ONLY = [HVAC_MODE_HEAT, HVAC_MODE_AUTO]
HVAC_MODES_HEAT_COOL = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO]
@ -47,20 +40,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"zone_thermostat",
"thermostatic_radiator_valve",
]
all_entities = api.get_all_devices()
all_devices = api.get_all_devices()
for dev_id, device in all_entities.items():
for dev_id, device_properties in all_devices.items():
if device["class"] not in thermostat_classes:
if device_properties["class"] not in thermostat_classes:
continue
thermostat = PwThermostat(
api,
coordinator,
device["name"],
device_properties["name"],
dev_id,
device["location"],
device["class"],
device_properties["location"],
device_properties["class"],
DEFAULT_MIN_TEMP,
DEFAULT_MAX_TEMP,
)
@ -77,11 +70,9 @@ class PwThermostat(SmileGateway, ClimateEntity):
self, api, coordinator, name, dev_id, loc_id, model, min_temp, max_temp
):
"""Set up the Plugwise API."""
super().__init__(api, coordinator)
super().__init__(api, coordinator, name, dev_id)
self._api = api
self._name = name
self._dev_id = dev_id
self._loc_id = loc_id
self._model = model
self._min_temp = min_temp
@ -92,9 +83,9 @@ class PwThermostat(SmileGateway, ClimateEntity):
self._preset_mode = None
self._presets = None
self._presets_list = None
self._boiler_state = None
self._heating_state = None
self._cooling_state = None
self._compressor_state = None
self._dhw_state = None
self._hvac_mode = None
self._schema_names = None
@ -111,43 +102,16 @@ class PwThermostat(SmileGateway, ClimateEntity):
def hvac_action(self):
"""Return the current action."""
if self._single_thermostat:
if self._heating_state or self._boiler_state:
if self._heating_state:
return CURRENT_HVAC_HEAT
if self._cooling_state:
return CURRENT_HVAC_COOL
return CURRENT_HVAC_IDLE
if self._heating_state is not None or self._boiler_state is not None:
if self._heating_state is not None:
if self._setpoint > self._temperature:
return CURRENT_HVAC_HEAT
return CURRENT_HVAC_IDLE
@property
def name(self):
"""Return the name of the thermostat, if any."""
return self._name
@property
def device_info(self) -> Dict[str, any]:
"""Return the device information."""
device_information = {
"identifiers": {(DOMAIN, self._dev_id)},
"name": self._name,
"manufacturer": "Plugwise",
"model": self._model.replace("_", " ").title(),
}
if self._dev_id != self._api.gateway_id:
device_information["via_device"] = (DOMAIN, self._api.gateway_id)
del device_information["via_device"]
return device_information
@property
def icon(self):
"""Return the icon to use in the frontend."""
return THERMOSTAT_ICON
@property
def supported_features(self):
"""Return the list of supported features."""
@ -172,8 +136,8 @@ class PwThermostat(SmileGateway, ClimateEntity):
@property
def hvac_modes(self):
"""Return the available hvac modes list."""
if self._heating_state is not None or self._boiler_state is not None:
if self._cooling_state is not None:
if self._heating_state is not None:
if self._compressor_state is not None:
return HVAC_MODES_HEAT_COOL
return HVAC_MODES_HEAT_ONLY
@ -258,7 +222,8 @@ class PwThermostat(SmileGateway, ClimateEntity):
except Smile.PlugwiseError:
_LOGGER.error("Error while communicating to device")
def _process_data(self):
@callback
def _async_process_data(self):
"""Update the data for this climate device."""
climate_data = self._api.get_device_data(self._dev_id)
heater_central_data = self._api.get_device_data(self._api.heater_id)
@ -286,21 +251,18 @@ class PwThermostat(SmileGateway, ClimateEntity):
if "active_preset" in climate_data:
self._preset_mode = climate_data["active_preset"]
if "boiler_state" in heater_central_data:
if heater_central_data["boiler_state"] is not None:
self._boiler_state = heater_central_data["boiler_state"]
if "heating_state" in heater_central_data:
if heater_central_data["heating_state"] is not None:
self._heating_state = heater_central_data["heating_state"]
if "cooling_state" in heater_central_data:
if heater_central_data["cooling_state"] is not None:
self._cooling_state = heater_central_data["cooling_state"]
if heater_central_data.get("heating_state") is not None:
self._heating_state = heater_central_data["heating_state"]
if heater_central_data.get("cooling_state") is not None:
self._cooling_state = heater_central_data["cooling_state"]
if heater_central_data.get("compressor_state") is not None:
self._compressor_state = heater_central_data["compressor_state"]
if self._schema_status:
self._hvac_mode = HVAC_MODE_AUTO
elif self._heating_state is not None or self._boiler_state is not None:
elif self._heating_state is not None:
self._hvac_mode = HVAC_MODE_HEAT
if self._cooling_state is not None:
if self._compressor_state is not None:
self._hvac_mode = HVAC_MODE_HEAT_COOL
self.async_write_ha_state()

View File

@ -47,7 +47,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
api = None
if user_input is not None:
try:

View File

@ -1,6 +1,11 @@
"""Constant for Plugwise component."""
DOMAIN = "plugwise"
# Sensor mapping
SENSOR_MAP_MODEL = 0
SENSOR_MAP_UOM = 1
SENSOR_MAP_DEVICE_CLASS = 2
# Default directives
DEFAULT_NAME = "Smile"
DEFAULT_USERNAME = "smile"
@ -10,8 +15,6 @@ DEFAULT_MIN_TEMP = 4
DEFAULT_MAX_TEMP = 30
DEFAULT_SCAN_INTERVAL = {"thermostat": 60, "power": 10}
DEVICE_CLASS_GAS = "gas"
# Configuration directives
CONF_MIN_TEMP = "min_temp"
CONF_MAX_TEMP = "max_temp"
@ -22,21 +25,15 @@ CONF_SOLAR = "solar"
CONF_GAS = "gas"
ATTR_ILLUMINANCE = "illuminance"
UNIT_LUMEN = "lm"
CURRENT_HVAC_DHW = "hot_water"
DEVICE_STATE = "device_state"
SCHEDULE_ON = "true"
SCHEDULE_OFF = "false"
# Icons
SWITCH_ICON = "mdi:electric-switch"
THERMOSTAT_ICON = "mdi:thermometer"
WATER_ICON = "mdi:water-pump"
FLAME_ICON = "mdi:fire"
COOL_ICON = "mdi:snowflake"
FLAME_ICON = "mdi:fire"
IDLE_ICON = "mdi:circle-off-outline"
GAS_ICON = "mdi:fire"
POWER_ICON = "mdi:flash"
POWER_FAILURE_ICON = "mdi:flash-off"
SWELL_SAG_ICON = "mdi:pulse"
VALVE_ICON = "mdi:valve"

View File

@ -3,7 +3,6 @@
"name": "Plugwise",
"documentation": "https://www.home-assistant.io/integrations/plugwise",
"requirements": ["Plugwise_Smile==0.2.10"],
"dependencies": [],
"codeowners": ["@CoMPaTech", "@bouwew"],
"config_flow": true
}

View File

@ -0,0 +1,354 @@
"""Plugwise Sensor component for Home Assistant."""
import logging
from homeassistant.const import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
ENERGY_WATT_HOUR,
POWER_WATT,
PRESSURE_BAR,
TEMP_CELSIUS,
UNIT_PERCENTAGE,
VOLUME_CUBIC_METERS,
)
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from . import SmileGateway
from .const import (
COOL_ICON,
DEVICE_STATE,
DOMAIN,
FLAME_ICON,
IDLE_ICON,
SENSOR_MAP_DEVICE_CLASS,
SENSOR_MAP_MODEL,
SENSOR_MAP_UOM,
UNIT_LUMEN,
)
_LOGGER = logging.getLogger(__name__)
ATTR_TEMPERATURE = [
"Temperature",
TEMP_CELSIUS,
DEVICE_CLASS_TEMPERATURE,
]
ATTR_BATTERY_LEVEL = [
"Charge",
UNIT_PERCENTAGE,
DEVICE_CLASS_BATTERY,
]
ATTR_ILLUMINANCE = [
"Illuminance",
UNIT_LUMEN,
DEVICE_CLASS_ILLUMINANCE,
]
ATTR_PRESSURE = ["Pressure", PRESSURE_BAR, DEVICE_CLASS_PRESSURE]
TEMP_SENSOR_MAP = {
"setpoint": ATTR_TEMPERATURE,
"temperature": ATTR_TEMPERATURE,
"intended_boiler_temperature": ATTR_TEMPERATURE,
"temperature_difference": ATTR_TEMPERATURE,
"outdoor_temperature": ATTR_TEMPERATURE,
"water_temperature": ATTR_TEMPERATURE,
"return_temperature": ATTR_TEMPERATURE,
}
ENERGY_SENSOR_MAP = {
"electricity_consumed": ["Current Consumed Power", POWER_WATT, DEVICE_CLASS_POWER],
"electricity_produced": ["Current Produced Power", POWER_WATT, DEVICE_CLASS_POWER],
"electricity_consumed_interval": [
"Consumed Power Interval",
ENERGY_WATT_HOUR,
DEVICE_CLASS_POWER,
],
"electricity_produced_interval": [
"Produced Power Interval",
ENERGY_WATT_HOUR,
DEVICE_CLASS_POWER,
],
"electricity_consumed_off_peak_point": [
"Current Consumed Power (off peak)",
POWER_WATT,
DEVICE_CLASS_POWER,
],
"electricity_consumed_peak_point": [
"Current Consumed Power",
POWER_WATT,
DEVICE_CLASS_POWER,
],
"electricity_consumed_off_peak_cumulative": [
"Cumulative Consumed Power (off peak)",
ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_POWER,
],
"electricity_consumed_peak_cumulative": [
"Cumulative Consumed Power",
ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_POWER,
],
"electricity_produced_off_peak_point": [
"Current Consumed Power (off peak)",
POWER_WATT,
DEVICE_CLASS_POWER,
],
"electricity_produced_peak_point": [
"Current Consumed Power",
POWER_WATT,
DEVICE_CLASS_POWER,
],
"electricity_produced_off_peak_cumulative": [
"Cumulative Consumed Power (off peak)",
ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_POWER,
],
"electricity_produced_peak_cumulative": [
"Cumulative Consumed Power",
ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_POWER,
],
"gas_consumed_interval": ["Current Consumed Gas", VOLUME_CUBIC_METERS, None],
"gas_consumed_cumulative": ["Cumulative Consumed Gas", VOLUME_CUBIC_METERS, None],
"net_electricity_point": ["Current net Power", POWER_WATT, DEVICE_CLASS_POWER],
"net_electricity_cumulative": [
"Cumulative net Power",
ENERGY_KILO_WATT_HOUR,
DEVICE_CLASS_POWER,
],
}
MISC_SENSOR_MAP = {
"battery": ATTR_BATTERY_LEVEL,
"illuminance": ATTR_ILLUMINANCE,
"modulation_level": ["Heater Modulation Level", UNIT_PERCENTAGE, None],
"valve_position": ["Valve Position", UNIT_PERCENTAGE, None],
"water_pressure": ATTR_PRESSURE,
}
INDICATE_ACTIVE_LOCAL_DEVICE = [
"cooling_state",
"flame_state",
]
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Smile sensors from a config entry."""
api = hass.data[DOMAIN][config_entry.entry_id]["api"]
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
entities = []
all_devices = api.get_all_devices()
single_thermostat = api.single_master_thermostat()
for dev_id, device_properties in all_devices.items():
data = api.get_device_data(dev_id)
for sensor, sensor_type in {
**TEMP_SENSOR_MAP,
**ENERGY_SENSOR_MAP,
**MISC_SENSOR_MAP,
}.items():
if sensor in data:
if data[sensor] is None:
continue
if "power" in device_properties["types"]:
model = None
if "plug" in device_properties["types"]:
model = "Metered Switch"
entities.append(
PwPowerSensor(
api,
coordinator,
device_properties["name"],
dev_id,
sensor,
sensor_type,
model,
)
)
else:
entities.append(
PwThermostatSensor(
api,
coordinator,
device_properties["name"],
dev_id,
sensor,
sensor_type,
)
)
if single_thermostat is False:
for state in INDICATE_ACTIVE_LOCAL_DEVICE:
if state in data:
entities.append(
PwAuxDeviceSensor(
api,
coordinator,
device_properties["name"],
dev_id,
DEVICE_STATE,
)
)
break
async_add_entities(entities, True)
class SmileSensor(SmileGateway):
"""Represent Smile Sensors."""
def __init__(self, api, coordinator, name, dev_id, sensor):
"""Initialise the sensor."""
super().__init__(api, coordinator, name, dev_id)
self._sensor = sensor
self._dev_class = None
self._state = None
self._unit_of_measurement = None
if dev_id == self._api.heater_id:
self._entity_name = "Auxiliary"
sensorname = sensor.replace("_", " ").title()
self._name = f"{self._entity_name} {sensorname}"
if dev_id == self._api.gateway_id:
self._entity_name = f"Smile {self._entity_name}"
self._unique_id = f"{dev_id}-{sensor}"
@property
def device_class(self):
"""Device class of this entity."""
return self._dev_class
@property
def state(self):
"""Device class of this entity."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
class PwThermostatSensor(SmileSensor, Entity):
"""Thermostat and climate sensor entities."""
def __init__(self, api, coordinator, name, dev_id, sensor, sensor_type):
"""Set up the Plugwise API."""
super().__init__(api, coordinator, name, dev_id, sensor)
self._model = sensor_type[SENSOR_MAP_MODEL]
self._unit_of_measurement = sensor_type[SENSOR_MAP_UOM]
self._dev_class = sensor_type[SENSOR_MAP_DEVICE_CLASS]
@callback
def _async_process_data(self):
"""Update the entity."""
data = self._api.get_device_data(self._dev_id)
if not data:
_LOGGER.error("Received no data for device %s.", self._entity_name)
self.async_write_ha_state()
return
if data.get(self._sensor) is not None:
measurement = data[self._sensor]
if self._sensor == "battery" or self._sensor == "valve_position":
measurement = measurement * 100
if self._unit_of_measurement == UNIT_PERCENTAGE:
measurement = int(measurement)
self._state = measurement
self.async_write_ha_state()
class PwAuxDeviceSensor(SmileSensor, Entity):
"""Auxiliary sensor entities for the heating/cooling device."""
def __init__(self, api, coordinator, name, dev_id, sensor):
"""Set up the Plugwise API."""
super().__init__(api, coordinator, name, dev_id, sensor)
self._cooling_state = False
self._heating_state = False
self._icon = None
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._icon
@callback
def _async_process_data(self):
"""Update the entity."""
data = self._api.get_device_data(self._dev_id)
if not data:
_LOGGER.error("Received no data for device %s.", self._entity_name)
self.async_write_ha_state()
return
if data.get("heating_state") is not None:
self._heating_state = data["heating_state"]
if data.get("cooling_state") is not None:
self._cooling_state = data["cooling_state"]
self._state = "idle"
self._icon = IDLE_ICON
if self._heating_state:
self._state = "heating"
self._icon = FLAME_ICON
if self._cooling_state:
self._state = "cooling"
self._icon = COOL_ICON
self.async_write_ha_state()
class PwPowerSensor(SmileSensor, Entity):
"""Power sensor entities."""
def __init__(self, api, coordinator, name, dev_id, sensor, sensor_type, model):
"""Set up the Plugwise API."""
super().__init__(api, coordinator, name, dev_id, sensor)
self._model = model
if model is None:
self._model = sensor_type[SENSOR_MAP_MODEL]
self._unit_of_measurement = sensor_type[SENSOR_MAP_UOM]
self._dev_class = sensor_type[SENSOR_MAP_DEVICE_CLASS]
if dev_id == self._api.gateway_id:
self._model = "P1 DSMR"
@callback
def _async_process_data(self):
"""Update the entity."""
data = self._api.get_device_data(self._dev_id)
if not data:
_LOGGER.error("Received no data for device %s.", self._entity_name)
self.async_write_ha_state()
return
if data.get(self._sensor) is not None:
measurement = data[self._sensor]
if self._unit_of_measurement == ENERGY_KILO_WATT_HOUR:
measurement = int(measurement / 1000)
self._state = measurement
self.async_write_ha_state()