diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index f803f38b8ea..9c867b7d17f 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -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 = ( diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py index 450874b3f35..caa1bbf58f7 100644 --- a/homeassistant/components/smappee/config_flow.py +++ b/homeassistant/components/smappee/config_flow.py @@ -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", "") diff --git a/homeassistant/components/smappee/const.py b/homeassistant/components/smappee/const.py index fc059509ced..1abfc3a9b02 100644 --- a/homeassistant/components/smappee/const.py +++ b/homeassistant/components/smappee/const.py @@ -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) diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index cf693b8061c..d6e9cc69f6f 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -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" diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 43483dbdb1e..024845a08fc 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -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, diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 03f06fbc4c1..f1485bc6e87 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -157,6 +157,10 @@ ZEROCONF = { { "domain": "smappee", "name": "smappee2*" + }, + { + "domain": "smappee", + "name": "smappee50*" } ], "_touch-able._tcp.local.": [ diff --git a/requirements_all.txt b/requirements_all.txt index 71ab25877f0..f7ae5b4a141 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2209fc39d47..2bdab44dc49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index cba962d3e44..bc9175a3b46 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -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"