Add Pentair ScreenLogic integration (#47933)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Kevin Worrel 2021-03-16 16:32:02 -07:00 committed by GitHub
parent f605a3c149
commit d21d9951ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1136 additions and 0 deletions

View file

@ -842,6 +842,11 @@ omit =
homeassistant/components/satel_integra/* homeassistant/components/satel_integra/*
homeassistant/components/schluter/* homeassistant/components/schluter/*
homeassistant/components/scrape/sensor.py homeassistant/components/scrape/sensor.py
homeassistant/components/screenlogic/__init__.py
homeassistant/components/screenlogic/binary_sensor.py
homeassistant/components/screenlogic/sensor.py
homeassistant/components/screenlogic/switch.py
homeassistant/components/screenlogic/water_heater.py
homeassistant/components/scsgate/* homeassistant/components/scsgate/*
homeassistant/components/scsgate/cover.py homeassistant/components/scsgate/cover.py
homeassistant/components/sendgrid/notify.py homeassistant/components/sendgrid/notify.py

View file

@ -401,6 +401,7 @@ homeassistant/components/samsungtv/* @escoand
homeassistant/components/scene/* @home-assistant/core homeassistant/components/scene/* @home-assistant/core
homeassistant/components/schluter/* @prairieapps homeassistant/components/schluter/* @prairieapps
homeassistant/components/scrape/* @fabaff homeassistant/components/scrape/* @fabaff
homeassistant/components/screenlogic/* @dieselrabbit
homeassistant/components/script/* @home-assistant/core homeassistant/components/script/* @home-assistant/core
homeassistant/components/search/* @home-assistant/core homeassistant/components/search/* @home-assistant/core
homeassistant/components/sense/* @kbickar homeassistant/components/sense/* @kbickar

View file

@ -0,0 +1,202 @@
"""The Screenlogic integration."""
import asyncio
from collections import defaultdict
from datetime import timedelta
import logging
from screenlogicpy import ScreenLogicError, ScreenLogicGateway
from screenlogicpy.const import (
CONTROLLER_HARDWARE,
SL_GATEWAY_IP,
SL_GATEWAY_NAME,
SL_GATEWAY_PORT,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .config_flow import async_discover_gateways_by_unique_id, name_for_mac
from .const import DEFAULT_SCAN_INTERVAL, DISCOVERED_GATEWAYS, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["switch", "sensor", "binary_sensor", "water_heater"]
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Screenlogic component."""
domain_data = hass.data[DOMAIN] = {}
domain_data[DISCOVERED_GATEWAYS] = await async_discover_gateways_by_unique_id(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Screenlogic from a config entry."""
mac = entry.unique_id
# Attempt to re-discover named gateway to follow IP changes
discovered_gateways = hass.data[DOMAIN][DISCOVERED_GATEWAYS]
if mac in discovered_gateways:
connect_info = discovered_gateways[mac]
else:
_LOGGER.warning("Gateway rediscovery failed.")
# Static connection defined or fallback from discovery
connect_info = {
SL_GATEWAY_NAME: name_for_mac(mac),
SL_GATEWAY_IP: entry.data[CONF_IP_ADDRESS],
SL_GATEWAY_PORT: entry.data[CONF_PORT],
}
try:
gateway = ScreenLogicGateway(**connect_info)
except ScreenLogicError as ex:
_LOGGER.error("Error while connecting to the gateway %s: %s", connect_info, ex)
raise ConfigEntryNotReady from ex
except AttributeError as ex:
_LOGGER.exception(
"Unexpected error while connecting to the gateway %s", connect_info
)
raise ConfigEntryNotReady from ex
coordinator = ScreenlogicDataUpdateCoordinator(
hass, config_entry=entry, gateway=gateway
)
device_data = defaultdict(list)
await coordinator.async_refresh()
for circuit in coordinator.data["circuits"]:
device_data["switch"].append(circuit)
for sensor in coordinator.data["sensors"]:
if sensor == "chem_alarm":
device_data["binary_sensor"].append(sensor)
else:
if coordinator.data["sensors"][sensor]["value"] != 0:
device_data["sensor"].append(sensor)
for pump in coordinator.data["pumps"]:
if (
coordinator.data["pumps"][pump]["data"] != 0
and "currentWatts" in coordinator.data["pumps"][pump]
):
device_data["pump"].append(pump)
for body in coordinator.data["bodies"]:
device_data["water_heater"].append(body)
hass.data[DOMAIN][entry.entry_id] = {
"coordinator": coordinator,
"devices": device_data,
"listener": entry.add_update_listener(async_update_listener),
}
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
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 PLATFORMS
]
)
)
hass.data[DOMAIN][entry.entry_id]["listener"]()
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage the data update for the Screenlogic component."""
def __init__(self, hass, *, config_entry, gateway):
"""Initialize the Screenlogic Data Update Coordinator."""
self.config_entry = config_entry
self.gateway = gateway
self.screenlogic_data = {}
interval = timedelta(
seconds=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
)
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=interval,
)
async def _async_update_data(self):
"""Fetch data from the Screenlogic gateway."""
try:
await self.hass.async_add_executor_job(self.gateway.update)
return self.gateway.get_data()
except ScreenLogicError as error:
raise UpdateFailed(error) from error
class ScreenlogicEntity(CoordinatorEntity):
"""Base class for all ScreenLogic entities."""
def __init__(self, coordinator, datakey):
"""Initialize of the entity."""
super().__init__(coordinator)
self._data_key = datakey
@property
def mac(self):
"""Mac address."""
return self.coordinator.config_entry.unique_id
@property
def unique_id(self):
"""Entity Unique ID."""
return f"{self.mac}_{self._data_key}"
@property
def config_data(self):
"""Shortcut for config data."""
return self.coordinator.data["config"]
@property
def gateway(self):
"""Return the gateway."""
return self.coordinator.gateway
@property
def gateway_name(self):
"""Return the configured name of the gateway."""
return self.gateway.name
@property
def device_info(self):
"""Return device information for the controller."""
controller_type = self.config_data["controler_type"]
hardware_type = self.config_data["hardware_type"]
return {
"connections": {(dr.CONNECTION_NETWORK_MAC, self.mac)},
"name": self.gateway_name,
"manufacturer": "Pentair",
"model": CONTROLLER_HARDWARE[controller_type][hardware_type],
}

View file

@ -0,0 +1,54 @@
"""Support for a ScreenLogic Binary Sensor."""
import logging
from screenlogicpy.const import ON_OFF
from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity
from . import ScreenlogicEntity
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up entry."""
entities = []
data = hass.data[DOMAIN][config_entry.entry_id]
coordinator = data["coordinator"]
for binary_sensor in data["devices"]["binary_sensor"]:
entities.append(ScreenLogicBinarySensor(coordinator, binary_sensor))
async_add_entities(entities, True)
class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity):
"""Representation of a ScreenLogic binary sensor entity."""
@property
def name(self):
"""Return the sensor name."""
return f"{self.gateway_name} {self.sensor['name']}"
@property
def device_class(self):
"""Return the device class."""
device_class = self.sensor.get("hass_type")
if device_class in DEVICE_CLASSES:
return device_class
return None
@property
def is_on(self) -> bool:
"""Determine if the sensor is on."""
return self.sensor["value"] == ON_OFF.ON
@property
def sensor(self):
"""Shortcut to access the sensor data."""
return self.sensor_data[self._data_key]
@property
def sensor_data(self):
"""Shortcut to access the sensors data."""
return self.coordinator.data["sensors"]

View file

@ -0,0 +1,218 @@
"""Config flow for ScreenLogic."""
import logging
from screenlogicpy import ScreenLogicError, discover
from screenlogicpy.const import SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT
from screenlogicpy.requests import login
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from .const import DEFAULT_SCAN_INTERVAL, MIN_SCAN_INTERVAL
from .const import DOMAIN # pylint: disable=unused-import
_LOGGER = logging.getLogger(__name__)
GATEWAY_SELECT_KEY = "selected_gateway"
GATEWAY_MANUAL_ENTRY = "manual"
PENTAIR_OUI = "00-C0-33"
async def async_discover_gateways_by_unique_id(hass):
"""Discover gateways and return a dict of them by unique id."""
discovered_gateways = {}
try:
hosts = await hass.async_add_executor_job(discover)
_LOGGER.debug("Discovered hosts: %s", hosts)
except ScreenLogicError as ex:
_LOGGER.debug(ex)
return discovered_gateways
for host in hosts:
mac = _extract_mac_from_name(host[SL_GATEWAY_NAME])
discovered_gateways[mac] = host
_LOGGER.debug("Discovered gateways: %s", discovered_gateways)
return discovered_gateways
def _extract_mac_from_name(name):
return format_mac(f"{PENTAIR_OUI}-{name.split(':')[1].strip()}")
def short_mac(mac):
"""Short version of the mac as seen in the app."""
return "-".join(mac.split(":")[3:]).upper()
def name_for_mac(mac):
"""Derive the gateway name from the mac."""
return f"Pentair: {short_mac(mac)}"
async def async_get_mac_address(hass, ip_address, port):
"""Connect to a screenlogic gateway and return the mac address."""
connected_socket = await hass.async_add_executor_job(
login.create_socket,
ip_address,
port,
)
if not connected_socket:
raise ScreenLogicError("Unknown socket error")
return await hass.async_add_executor_job(login.gateway_connect, connected_socket)
class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow to setup screen logic devices."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self):
"""Initialize ScreenLogic ConfigFlow."""
self.discovered_gateways = {}
self.discovered_ip = None
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for ScreenLogic."""
return ScreenLogicOptionsFlowHandler(config_entry)
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
self.discovered_gateways = await async_discover_gateways_by_unique_id(self.hass)
return await self.async_step_gateway_select()
async def async_step_dhcp(self, dhcp_discovery):
"""Handle dhcp discovery."""
mac = _extract_mac_from_name(dhcp_discovery[HOSTNAME])
await self.async_set_unique_id(mac)
self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: dhcp_discovery[IP_ADDRESS]}
)
self.discovered_ip = dhcp_discovery[IP_ADDRESS]
self.context["title_placeholders"] = {"name": dhcp_discovery[HOSTNAME]}
return await self.async_step_gateway_entry()
async def async_step_gateway_select(self, user_input=None):
"""Handle the selection of a discovered ScreenLogic gateway."""
existing = self._async_current_ids()
unconfigured_gateways = {
mac: gateway[SL_GATEWAY_NAME]
for mac, gateway in self.discovered_gateways.items()
if mac not in existing
}
if not unconfigured_gateways:
return await self.async_step_gateway_entry()
errors = {}
if user_input is not None:
if user_input[GATEWAY_SELECT_KEY] == GATEWAY_MANUAL_ENTRY:
return await self.async_step_gateway_entry()
mac = user_input[GATEWAY_SELECT_KEY]
selected_gateway = self.discovered_gateways[mac]
await self.async_set_unique_id(mac)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=name_for_mac(mac),
data={
CONF_IP_ADDRESS: selected_gateway[SL_GATEWAY_IP],
CONF_PORT: selected_gateway[SL_GATEWAY_PORT],
},
)
return self.async_show_form(
step_id="gateway_select",
data_schema=vol.Schema(
{
vol.Required(GATEWAY_SELECT_KEY): vol.In(
{
**unconfigured_gateways,
GATEWAY_MANUAL_ENTRY: "Manually configure a ScreenLogic gateway",
}
)
}
),
errors=errors,
description_placeholders={},
)
async def async_step_gateway_entry(self, user_input=None):
"""Handle the manual entry of a ScreenLogic gateway."""
errors = {}
ip_address = self.discovered_ip
port = 80
if user_input is not None:
ip_address = user_input[CONF_IP_ADDRESS]
port = user_input[CONF_PORT]
try:
mac = format_mac(
await async_get_mac_address(self.hass, ip_address, port)
)
except ScreenLogicError as ex:
_LOGGER.debug(ex)
errors[CONF_IP_ADDRESS] = "cannot_connect"
if not errors:
await self.async_set_unique_id(mac)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=name_for_mac(mac),
data={
CONF_IP_ADDRESS: ip_address,
CONF_PORT: port,
},
)
return self.async_show_form(
step_id="gateway_entry",
data_schema=vol.Schema(
{
vol.Required(CONF_IP_ADDRESS, default=ip_address): str,
vol.Required(CONF_PORT, default=port): int,
}
),
errors=errors,
description_placeholders={},
)
class ScreenLogicOptionsFlowHandler(config_entries.OptionsFlow):
"""Handles the options for the ScreenLogic integration."""
def __init__(self, config_entry: config_entries.ConfigEntry):
"""Init the screen logic options flow."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(
title=self.config_entry.title, data=user_input
)
current_interval = self.config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
CONF_SCAN_INTERVAL,
default=current_interval,
): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL))
}
),
description_placeholders={"gateway_name": self.config_entry.title},
)

View file

@ -0,0 +1,7 @@
"""Constants for the ScreenLogic integration."""
DOMAIN = "screenlogic"
DEFAULT_SCAN_INTERVAL = 30
MIN_SCAN_INTERVAL = 10
DISCOVERED_GATEWAYS = "_discovered_gateways"

View file

@ -0,0 +1,11 @@
{
"domain": "screenlogic",
"name": "Pentair ScreenLogic",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/screenlogic",
"requirements": ["screenlogicpy==0.1.2"],
"codeowners": [
"@dieselrabbit"
],
"dhcp": [{"hostname":"pentair: *","macaddress":"00C033*"}]
}

View file

@ -0,0 +1,107 @@
"""Support for a ScreenLogic Sensor."""
import logging
from homeassistant.components.sensor import DEVICE_CLASSES
from . import ScreenlogicEntity
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM")
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up entry."""
entities = []
data = hass.data[DOMAIN][config_entry.entry_id]
coordinator = data["coordinator"]
# Generic sensors
for sensor in data["devices"]["sensor"]:
entities.append(ScreenLogicSensor(coordinator, sensor))
for pump in data["devices"]["pump"]:
for pump_key in PUMP_SENSORS:
entities.append(ScreenLogicPumpSensor(coordinator, pump, pump_key))
async_add_entities(entities, True)
class ScreenLogicSensor(ScreenlogicEntity):
"""Representation of a ScreenLogic sensor entity."""
@property
def name(self):
"""Name of the sensor."""
return f"{self.gateway_name} {self.sensor['name']}"
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self.sensor.get("unit")
@property
def device_class(self):
"""Device class of the sensor."""
device_class = self.sensor.get("hass_type")
if device_class in DEVICE_CLASSES:
return device_class
return None
@property
def state(self):
"""State of the sensor."""
value = self.sensor["value"]
return (value - 1) if "supply" in self._data_key else value
@property
def sensor(self):
"""Shortcut to access the sensor data."""
return self.sensor_data[self._data_key]
@property
def sensor_data(self):
"""Shortcut to access the sensors data."""
return self.coordinator.data["sensors"]
class ScreenLogicPumpSensor(ScreenlogicEntity):
"""Representation of a ScreenLogic pump sensor entity."""
def __init__(self, coordinator, pump, key):
"""Initialize of the pump sensor."""
super().__init__(coordinator, f"{key}_{pump}")
self._pump_id = pump
self._key = key
@property
def name(self):
"""Return the pump sensor name."""
return f"{self.gateway_name} {self.pump_sensor['name']}"
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self.pump_sensor.get("unit")
@property
def device_class(self):
"""Return the device class."""
device_class = self.pump_sensor.get("hass_type")
if device_class in DEVICE_CLASSES:
return device_class
return None
@property
def state(self):
"""State of the pump sensor."""
return self.pump_sensor["value"]
@property
def pump_sensor(self):
"""Shortcut to access the pump sensor data."""
return self.pumps_data[self._pump_id][self._key]
@property
def pumps_data(self):
"""Shortcut to access the pump data."""
return self.coordinator.data["pumps"]

View file

@ -0,0 +1,39 @@
{
"config": {
"flow_title": "ScreenLogic {name}",
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"gateway_entry": {
"title": "ScreenLogic",
"description": "Enter your ScreenLogic Gateway information.",
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]",
"port": "[%key:common::config_flow::data::port%]"
}
},
"gateway_select": {
"title": "ScreenLogic",
"description": "The following ScreenLogic gateways were discovered. Please select one to configure, or choose to manually configure a ScreenLogic gateway.",
"data": {
"selected_gateway": "Gateway"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options":{
"step": {
"init": {
"title": "ScreenLogic",
"description": "Specify settings for {gateway_name}",
"data": {
"scan_interval": "Seconds between scans"
}
}
}
}
}

View file

@ -0,0 +1,63 @@
"""Support for a ScreenLogic 'circuit' switch."""
import logging
from screenlogicpy.const import ON_OFF
from homeassistant.components.switch import SwitchEntity
from . import ScreenlogicEntity
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up entry."""
entities = []
data = hass.data[DOMAIN][config_entry.entry_id]
coordinator = data["coordinator"]
for switch in data["devices"]["switch"]:
entities.append(ScreenLogicSwitch(coordinator, switch))
async_add_entities(entities, True)
class ScreenLogicSwitch(ScreenlogicEntity, SwitchEntity):
"""ScreenLogic switch entity."""
@property
def name(self):
"""Get the name of the switch."""
return f"{self.gateway_name} {self.circuit['name']}"
@property
def is_on(self) -> bool:
"""Get whether the switch is in on state."""
return self.circuit["value"] == 1
async def async_turn_on(self, **kwargs) -> None:
"""Send the ON command."""
return await self._async_set_circuit(ON_OFF.ON)
async def async_turn_off(self, **kwargs) -> None:
"""Send the OFF command."""
return await self._async_set_circuit(ON_OFF.OFF)
async def _async_set_circuit(self, circuit_value) -> None:
if await self.hass.async_add_executor_job(
self.gateway.set_circuit, self._data_key, circuit_value
):
_LOGGER.info("screenlogic turn %s %s", circuit_value, self._data_key)
await self.coordinator.async_request_refresh()
else:
_LOGGER.info("screenlogic turn %s %s error", circuit_value, self._data_key)
@property
def circuit(self):
"""Shortcut to access the circuit."""
return self.circuits_data[self._data_key]
@property
def circuits_data(self):
"""Shortcut to access the circuits data."""
return self.coordinator.data["circuits"]

View file

@ -0,0 +1,39 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect"
},
"flow_title": "ScreenLogic {name}",
"step": {
"gateway_entry": {
"data": {
"ip_address": "IP Address",
"port": "Port"
},
"description": "Enter your ScreenLogic Gateway information.",
"title": "ScreenLogic"
},
"gateway_select": {
"data": {
"selected_gateway": "Gateway"
},
"description": "The following ScreenLogic gateways were discovered. Please select one to configure, or choose to manually configure a ScreenLogic gateway.",
"title": "ScreenLogic"
}
}
},
"options": {
"step": {
"init": {
"data": {
"scan_interval": "Seconds between scans"
},
"description": "Specify settings for {gateway_name}",
"title": "ScreenLogic"
}
}
}
}

View file

@ -0,0 +1,128 @@
"""Support for a ScreenLogic Water Heater."""
import logging
from screenlogicpy.const import HEAT_MODE
from homeassistant.components.water_heater import (
SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE,
WaterHeaterEntity,
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from . import ScreenlogicEntity
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SUPPORTED_FEATURES = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
HEAT_MODE_NAMES = HEAT_MODE.Names
MODE_NAME_TO_MODE_NUM = {
HEAT_MODE_NAMES[num]: num for num in range(len(HEAT_MODE_NAMES))
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up entry."""
entities = []
data = hass.data[DOMAIN][config_entry.entry_id]
coordinator = data["coordinator"]
for body in data["devices"]["water_heater"]:
entities.append(ScreenLogicWaterHeater(coordinator, body))
async_add_entities(entities, True)
class ScreenLogicWaterHeater(ScreenlogicEntity, WaterHeaterEntity):
"""Represents the heating functions for a body of water."""
@property
def name(self) -> str:
"""Name of the water heater."""
ent_name = self.body["heat_status"]["name"]
return f"{self.gateway_name} {ent_name}"
@property
def state(self) -> str:
"""State of the water heater."""
return HEAT_MODE.GetFriendlyName(self.body["heat_status"]["value"])
@property
def min_temp(self) -> float:
"""Minimum allowed temperature."""
return self.body["min_set_point"]["value"]
@property
def max_temp(self) -> float:
"""Maximum allowed temperature."""
return self.body["max_set_point"]["value"]
@property
def current_temperature(self) -> float:
"""Return water temperature."""
return self.body["last_temperature"]["value"]
@property
def target_temperature(self) -> float:
"""Target temperature."""
return self.body["heat_set_point"]["value"]
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
if self.config_data["is_celcius"]["value"] == 1:
return TEMP_CELSIUS
return TEMP_FAHRENHEIT
@property
def current_operation(self) -> str:
"""Return operation."""
return HEAT_MODE_NAMES[self.body["heat_mode"]["value"]]
@property
def operation_list(self):
"""All available operations."""
supported_heat_modes = [HEAT_MODE.OFF]
# Is solar listed as available equipment?
if self.coordinator.data["config"]["equipment_flags"] & 0x1:
supported_heat_modes.extend([HEAT_MODE.SOLAR, HEAT_MODE.SOLAR_PREFERED])
supported_heat_modes.append(HEAT_MODE.HEATER)
return [HEAT_MODE_NAMES[mode_num] for mode_num in supported_heat_modes]
@property
def supported_features(self):
"""Supported features of the water heater."""
return SUPPORTED_FEATURES
async def async_set_temperature(self, **kwargs) -> None:
"""Change the setpoint of the heater."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if await self.hass.async_add_executor_job(
self.gateway.set_heat_temp, int(self._data_key), int(temperature)
):
await self.coordinator.async_request_refresh()
else:
_LOGGER.error("screenlogic set_temperature error")
async def async_set_operation_mode(self, operation_mode) -> None:
"""Set the operation mode."""
mode = MODE_NAME_TO_MODE_NUM[operation_mode]
if await self.hass.async_add_executor_job(
self.gateway.set_heat_mode, int(self._data_key), int(mode)
):
await self.coordinator.async_request_refresh()
else:
_LOGGER.error("screenlogic set_operation_mode error")
@property
def body(self):
"""Shortcut to access body data."""
return self.bodies_data[self._data_key]
@property
def bodies_data(self):
"""Shortcut to access bodies data."""
return self.coordinator.data["bodies"]

View file

@ -195,6 +195,7 @@ FLOWS = [
"rpi_power", "rpi_power",
"ruckus_unleashed", "ruckus_unleashed",
"samsungtv", "samsungtv",
"screenlogic",
"sense", "sense",
"sentry", "sentry",
"sharkiq", "sharkiq",

View file

@ -109,6 +109,11 @@ DHCP = [
"hostname": "irobot-*", "hostname": "irobot-*",
"macaddress": "501479*" "macaddress": "501479*"
}, },
{
"domain": "screenlogic",
"hostname": "pentair: *",
"macaddress": "00C033*"
},
{ {
"domain": "sense", "domain": "sense",
"hostname": "sense-*", "hostname": "sense-*",

View file

@ -2008,6 +2008,9 @@ scapy==2.4.4
# homeassistant.components.deutsche_bahn # homeassistant.components.deutsche_bahn
schiene==0.23 schiene==0.23
# homeassistant.components.screenlogic
screenlogicpy==0.1.2
# homeassistant.components.scsgate # homeassistant.components.scsgate
scsgate==0.1.0 scsgate==0.1.0

View file

@ -1039,6 +1039,9 @@ samsungtvws==1.6.0
# homeassistant.components.dhcp # homeassistant.components.dhcp
scapy==2.4.4 scapy==2.4.4
# homeassistant.components.screenlogic
screenlogicpy==0.1.2
# homeassistant.components.emulated_kasa # homeassistant.components.emulated_kasa
# homeassistant.components.sense # homeassistant.components.sense
sense_energy==0.9.0 sense_energy==0.9.0

View file

@ -0,0 +1 @@
"""Tests for the Screenlogic integration."""

View file

@ -0,0 +1,249 @@
"""Test the Pentair ScreenLogic config flow."""
from unittest.mock import patch
from screenlogicpy import ScreenLogicError
from screenlogicpy.const import (
SL_GATEWAY_IP,
SL_GATEWAY_NAME,
SL_GATEWAY_PORT,
SL_GATEWAY_SUBTYPE,
SL_GATEWAY_TYPE,
)
from homeassistant import config_entries, setup
from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS
from homeassistant.components.screenlogic.config_flow import (
GATEWAY_MANUAL_ENTRY,
GATEWAY_SELECT_KEY,
)
from homeassistant.components.screenlogic.const import (
DEFAULT_SCAN_INTERVAL,
DOMAIN,
MIN_SCAN_INTERVAL,
)
from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL
from tests.common import MockConfigEntry
async def test_flow_discovery(hass):
"""Test the flow works with basic discovery."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.screenlogic.config_flow.discover",
return_value=[
{
SL_GATEWAY_IP: "1.1.1.1",
SL_GATEWAY_PORT: 80,
SL_GATEWAY_TYPE: 12,
SL_GATEWAY_SUBTYPE: 2,
SL_GATEWAY_NAME: "Pentair: 01-01-01",
},
],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "gateway_select"
with patch(
"homeassistant.components.screenlogic.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.screenlogic.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={GATEWAY_SELECT_KEY: "00:c0:33:01:01:01"}
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "Pentair: 01-01-01"
assert result2["data"] == {
CONF_IP_ADDRESS: "1.1.1.1",
CONF_PORT: 80,
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_flow_discover_none(hass):
"""Test when nothing is discovered."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.screenlogic.config_flow.discover",
return_value=[],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "gateway_entry"
async def test_flow_discover_error(hass):
"""Test when discovery errors."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.screenlogic.config_flow.discover",
side_effect=ScreenLogicError("Fake error"),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "gateway_entry"
async def test_dhcp(hass):
"""Test DHCP discovery flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "dhcp"},
data={
HOSTNAME: "Pentair: 01-01-01",
IP_ADDRESS: "1.1.1.1",
},
)
assert result["type"] == "form"
assert result["step_id"] == "gateway_entry"
async def test_form_manual_entry(hass):
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.screenlogic.config_flow.discover",
return_value=[
{
SL_GATEWAY_IP: "1.1.1.1",
SL_GATEWAY_PORT: 80,
SL_GATEWAY_TYPE: 12,
SL_GATEWAY_SUBTYPE: 2,
SL_GATEWAY_NAME: "Pentair: 01-01-01",
},
],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
assert result["step_id"] == "gateway_select"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={GATEWAY_SELECT_KEY: GATEWAY_MANUAL_ENTRY}
)
assert result2["type"] == "form"
assert result2["errors"] == {}
assert result2["step_id"] == "gateway_entry"
with patch(
"homeassistant.components.screenlogic.config_flow.login.create_socket",
return_value=True,
), patch(
"homeassistant.components.screenlogic.config_flow.login.gateway_connect",
return_value="00-C0-33-01-01-01",
):
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_IP_ADDRESS: "1.1.1.1",
CONF_PORT: 80,
},
)
await hass.async_block_till_done()
assert result3["type"] == "create_entry"
assert result3["title"] == "Pentair: 01-01-01"
assert result3["data"] == {
CONF_IP_ADDRESS: "1.1.1.1",
CONF_PORT: 80,
}
async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.screenlogic.config_flow.login.create_socket",
return_value=None,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_IP_ADDRESS: "1.1.1.1",
CONF_PORT: 80,
},
)
assert result2["type"] == "form"
assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"}
async def test_option_flow(hass):
"""Test config flow options."""
entry = MockConfigEntry(domain=DOMAIN, data={}, options=None)
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] == "form"
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={CONF_SCAN_INTERVAL: 15},
)
assert result["type"] == "create_entry"
assert result["data"] == {CONF_SCAN_INTERVAL: 15}
async def test_option_flow_defaults(hass):
"""Test config flow options."""
entry = MockConfigEntry(domain=DOMAIN, data={}, options=None)
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] == "form"
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={}
)
assert result["type"] == "create_entry"
assert result["data"] == {
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
}
async def test_option_flow_input_floor(hass):
"""Test config flow options."""
entry = MockConfigEntry(domain=DOMAIN, data={}, options=None)
entry.add_to_hass(hass)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] == "form"
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_SCAN_INTERVAL: 1}
)
assert result["type"] == "create_entry"
assert result["data"] == {
CONF_SCAN_INTERVAL: MIN_SCAN_INTERVAL,
}