Support local Smappee Genius device (#48627)
Co-authored-by: J. Nick Koston <nick@koston.org>pull/49553/head
parent
8c52dfa1c5
commit
8b08134850
|
@ -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 = (
|
||||
|
|
|
@ -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", "")
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -157,6 +157,10 @@ ZEROCONF = {
|
|||
{
|
||||
"domain": "smappee",
|
||||
"name": "smappee2*"
|
||||
},
|
||||
{
|
||||
"domain": "smappee",
|
||||
"name": "smappee50*"
|
||||
}
|
||||
],
|
||||
"_touch-able._tcp.local.": [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue