Support local Smappee Genius device (#48627)

Co-authored-by: J. Nick Koston <nick@koston.org>
pull/49553/head
bsmappee 2021-04-22 10:12:13 +02:00 committed by GitHub
parent 8c52dfa1c5
commit 8b08134850
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 171 additions and 25 deletions

View File

@ -1,7 +1,7 @@
"""The Smappee integration."""
import asyncio
from pysmappee import Smappee
from pysmappee import Smappee, helper, mqtt
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
@ -75,8 +75,21 @@ async def async_setup(hass: HomeAssistant, config: dict):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Smappee from a zeroconf or config entry."""
if CONF_IP_ADDRESS in entry.data:
smappee_api = api.api.SmappeeLocalApi(ip=entry.data[CONF_IP_ADDRESS])
smappee = Smappee(api=smappee_api, serialnumber=entry.data[CONF_SERIALNUMBER])
if helper.is_smappee_genius(entry.data[CONF_SERIALNUMBER]):
# next generation: local mqtt broker
smappee_mqtt = mqtt.SmappeeLocalMqtt(
serial_number=entry.data[CONF_SERIALNUMBER]
)
await hass.async_add_executor_job(smappee_mqtt.start_and_wait_for_config)
smappee = Smappee(
api=smappee_mqtt, serialnumber=entry.data[CONF_SERIALNUMBER]
)
else:
# legacy devices through local api
smappee_api = api.api.SmappeeLocalApi(ip=entry.data[CONF_IP_ADDRESS])
smappee = Smappee(
api=smappee_api, serialnumber=entry.data[CONF_SERIALNUMBER]
)
await hass.async_add_executor_job(smappee.load_local_service_location)
else:
implementation = (

View File

@ -1,6 +1,7 @@
"""Config flow for Smappee."""
import logging
from pysmappee import helper, mqtt
import voluptuous as vol
from homeassistant import config_entries
@ -41,7 +42,6 @@ class SmappeeFlowHandler(
"""Handle zeroconf discovery."""
if not discovery_info[CONF_HOSTNAME].startswith(SUPPORTED_LOCAL_DEVICES):
# We currently only support Energy and Solar models (legacy)
return self.async_abort(reason="invalid_mdns")
serial_number = (
@ -86,10 +86,18 @@ class SmappeeFlowHandler(
serial_number = self.context.get(CONF_SERIALNUMBER)
# Attempt to make a connection to the local device
smappee_api = api.api.SmappeeLocalApi(ip=ip_address)
logon = await self.hass.async_add_executor_job(smappee_api.logon)
if logon is None:
return self.async_abort(reason="cannot_connect")
if helper.is_smappee_genius(serial_number):
# next generation device, attempt connect to the local mqtt broker
smappee_mqtt = mqtt.SmappeeLocalMqtt(serial_number=serial_number)
connect = await self.hass.async_add_executor_job(smappee_mqtt.start_attempt)
if not connect:
return self.async_abort(reason="cannot_connect")
else:
# legacy devices, without local mqtt broker, try api access
smappee_api = api.api.SmappeeLocalApi(ip=ip_address)
logon = await self.hass.async_add_executor_job(smappee_api.logon)
if logon is None:
return self.async_abort(reason="cannot_connect")
return self.async_create_entry(
title=f"{DOMAIN}{serial_number}",
@ -141,23 +149,35 @@ class SmappeeFlowHandler(
)
# In a LOCAL setup we still need to resolve the host to serial number
ip_address = user_input["host"]
serial_number = None
# Attempt 1: try to use the local api (older generation) to resolve host to serialnumber
smappee_api = api.api.SmappeeLocalApi(ip=ip_address)
logon = await self.hass.async_add_executor_job(smappee_api.logon)
if logon is None:
return self.async_abort(reason="cannot_connect")
if logon is not None:
advanced_config = await self.hass.async_add_executor_job(
smappee_api.load_advanced_config
)
for config_item in advanced_config:
if config_item["key"] == "mdnsHostName":
serial_number = config_item["value"]
else:
# Attempt 2: try to use the local mqtt broker (newer generation) to resolve host to serialnumber
smappee_mqtt = mqtt.SmappeeLocalMqtt()
connect = await self.hass.async_add_executor_job(smappee_mqtt.start_attempt)
if not connect:
return self.async_abort(reason="cannot_connect")
advanced_config = await self.hass.async_add_executor_job(
smappee_api.load_advanced_config
)
serial_number = None
for config_item in advanced_config:
if config_item["key"] == "mdnsHostName":
serial_number = config_item["value"]
serial_number = await self.hass.async_add_executor_job(
smappee_mqtt.start_and_wait_for_config
)
await self.hass.async_add_executor_job(smappee_mqtt.stop)
if serial_number is None:
return self.async_abort(reason="cannot_connect")
if serial_number is None or not serial_number.startswith(
SUPPORTED_LOCAL_DEVICES
):
# We currently only support Energy and Solar models (legacy)
return self.async_abort(reason="invalid_mdns")
serial_number = serial_number.replace("Smappee", "")

View File

@ -14,7 +14,7 @@ ENV_LOCAL = "local"
PLATFORMS = ["binary_sensor", "sensor", "switch"]
SUPPORTED_LOCAL_DEVICES = ("Smappee1", "Smappee2")
SUPPORTED_LOCAL_DEVICES = ("Smappee1", "Smappee2", "Smappee50")
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=20)

View File

@ -4,8 +4,12 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/smappee",
"dependencies": ["http"],
"requirements": ["pysmappee==0.2.17"],
"codeowners": ["@bsmappee"],
"requirements": [
"pysmappee==0.2.24"
],
"codeowners": [
"@bsmappee"
],
"zeroconf": [
{
"type": "_ssh._tcp.local.",
@ -14,6 +18,10 @@
{
"type": "_ssh._tcp.local.",
"name": "smappee2*"
},
{
"type": "_ssh._tcp.local.",
"name": "smappee50*"
}
],
"iot_class": "cloud_polling"

View File

@ -205,6 +205,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
if service_location.has_voltage_values:
for sensor_name, sensor in VOLTAGE_SENSORS.items():
if service_location.phase_type in sensor[5]:
if (
sensor_name.startswith("line_")
and service_location.local_polling
):
continue
entities.append(
SmappeeSensor(
smappee_base=smappee_base,

View File

@ -157,6 +157,10 @@ ZEROCONF = {
{
"domain": "smappee",
"name": "smappee2*"
},
{
"domain": "smappee",
"name": "smappee50*"
}
],
"_touch-able._tcp.local.": [

View File

@ -1714,7 +1714,7 @@ pyskyqhub==0.1.3
pysma==0.4.3
# homeassistant.components.smappee
pysmappee==0.2.17
pysmappee==0.2.24
# homeassistant.components.smartthings
pysmartapp==0.3.3

View File

@ -941,7 +941,7 @@ pysignalclirestapi==0.3.4
pysma==0.4.3
# homeassistant.components.smappee
pysmappee==0.2.17
pysmappee==0.2.24
# homeassistant.components.smartthings
pysmartapp==0.3.3

View File

@ -77,9 +77,69 @@ async def test_show_zeroconf_connection_error_form(hass):
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
async def test_show_zeroconf_connection_error_form_next_generation(hass):
"""Test that the zeroconf confirmation form is served."""
with patch("pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=False):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data={
"host": "1.2.3.4",
"port": 22,
CONF_HOSTNAME: "Smappee5001000212.local.",
"type": "_ssh._tcp.local.",
"name": "Smappee5001000212._ssh._tcp.local.",
"properties": {"_raw": {}},
},
)
assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"}
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "zeroconf_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"host": "1.2.3.4"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "cannot_connect"
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
async def test_connection_error(hass):
"""Test we show user form on Smappee connection error."""
with patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None):
with patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None), patch(
"pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=None
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["step_id"] == "environment"
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"environment": ENV_LOCAL}
)
assert result["step_id"] == ENV_LOCAL
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"host": "1.2.3.4"}
)
assert result["reason"] == "cannot_connect"
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
async def test_user_local_connection_error(hass):
"""Test we show user form on Smappee connection error in local next generation option."""
with patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None), patch(
"pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=True
), patch("pysmappee.mqtt.SmappeeLocalMqtt.start", return_value=True), patch(
"pysmappee.mqtt.SmappeeLocalMqtt.stop", return_value=True
), patch(
"pysmappee.mqtt.SmappeeLocalMqtt.is_config_ready", return_value=None
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
@ -123,7 +183,7 @@ async def test_full_user_wrong_mdns(hass):
"""Test we abort user flow if unsupported mDNS name got resolved."""
with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch(
"pysmappee.api.SmappeeLocalApi.load_advanced_config",
return_value=[{"key": "mdnsHostName", "value": "Smappee5010000001"}],
return_value=[{"key": "mdnsHostName", "value": "Smappee5100000001"}],
), patch(
"pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[]
), patch(
@ -464,3 +524,39 @@ async def test_full_user_local_flow(hass):
entry = hass.config_entries.async_entries(DOMAIN)[0]
assert entry.unique_id == "1006000212"
async def test_full_zeroconf_flow_next_generation(hass):
"""Test the full zeroconf flow."""
with patch(
"pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=True
), patch("pysmappee.mqtt.SmappeeLocalMqtt.start", return_value=None,), patch(
"pysmappee.mqtt.SmappeeLocalMqtt.is_config_ready",
return_value=None,
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data={
"host": "1.2.3.4",
"port": 22,
CONF_HOSTNAME: "Smappee5001000212.local.",
"type": "_ssh._tcp.local.",
"name": "Smappee5001000212._ssh._tcp.local.",
"properties": {"_raw": {}},
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "zeroconf_confirm"
assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"host": "1.2.3.4"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "smappee5001000212"
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
entry = hass.config_entries.async_entries(DOMAIN)[0]
assert entry.unique_id == "5001000212"