From c2556d90ea7629941a834a3fd969e41ba0bdcd1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ville=20Skytt=C3=A4?= <ville.skytta@iki.fi>
Date: Thu, 1 Aug 2019 19:22:04 +0300
Subject: [PATCH] Huawei LTE sensor unique id improvements (#25609)

* Convert sensor setup to async

* Improve sensor unique ids

* Save some indent levels, use f-string formatting

* Require getmac in tests

* Fix RouterData init in tests

* Make discovery_info optional in async_setup_platform signature
---
 .../components/huawei_lte/__init__.py         | 26 ++++++++++++-----
 .../components/huawei_lte/manifest.json       |  1 +
 homeassistant/components/huawei_lte/sensor.py | 28 +++++++++++++++----
 requirements_all.txt                          |  1 +
 requirements_test_all.txt                     |  5 ++++
 script/gen_requirements_all.py                |  1 +
 tests/components/huawei_lte/test_init.py      |  2 +-
 7 files changed, 50 insertions(+), 14 deletions(-)

diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py
index c68231ba0e4..51d0dc5d3a2 100644
--- a/homeassistant/components/huawei_lte/__init__.py
+++ b/homeassistant/components/huawei_lte/__init__.py
@@ -1,12 +1,15 @@
 """Support for Huawei LTE routers."""
 from datetime import timedelta
 from functools import reduce
+from urllib.parse import urlparse
+import ipaddress
 import logging
 import operator
 from typing import Any, Callable
 
 import voluptuous as vol
 import attr
+from getmac import get_mac_address
 from huawei_lte_api.AuthorizedConnection import AuthorizedConnection
 from huawei_lte_api.Client import Client
 from huawei_lte_api.exceptions import ResponseErrorNotSupportedException
@@ -55,6 +58,7 @@ class RouterData:
     """Class for router state."""
 
     client = attr.ib()
+    mac = attr.ib()
     device_information = attr.ib(init=False, factory=dict)
     device_signal = attr.ib(init=False, factory=dict)
     monitoring_traffic_statistics = attr.ib(init=False, factory=dict)
@@ -62,12 +66,6 @@ class RouterData:
 
     _subscriptions = attr.ib(init=False, factory=set)
 
-    def __attrs_post_init__(self) -> None:
-        """Fetch device information once, for serial number in @unique_ids."""
-        self.subscribe("device_information")
-        self._update()
-        self.unsubscribe("device_information")
-
     def __getitem__(self, path: str):
         """
         Get value corresponding to a dotted path.
@@ -148,10 +146,24 @@ def _setup_lte(hass, lte_config) -> None:
     username = lte_config[CONF_USERNAME]
     password = lte_config[CONF_PASSWORD]
 
+    # Get MAC address for use in unique ids. Being able to use something
+    # from the API would be nice, but all of that seems to be available only
+    # through authenticated calls (e.g. device_information.SerialNumber), and
+    # we want this available and the same when unauthenticated too.
+    host = urlparse(url).hostname
+    try:
+        if ipaddress.ip_address(host).version == 6:
+            mode = "ip6"
+        else:
+            mode = "ip"
+    except ValueError:
+        mode = "hostname"
+    mac = get_mac_address(**{mode: host})
+
     connection = AuthorizedConnection(url, username=username, password=password)
     client = Client(connection)
 
-    data = RouterData(client)
+    data = RouterData(client, mac)
     hass.data[DATA_KEY].data[url] = data
 
     def cleanup(event):
diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json
index bfdc6f167aa..f31ff74c055 100644
--- a/homeassistant/components/huawei_lte/manifest.json
+++ b/homeassistant/components/huawei_lte/manifest.json
@@ -3,6 +3,7 @@
   "name": "Huawei lte",
   "documentation": "https://www.home-assistant.io/components/huawei_lte",
   "requirements": [
+    "getmac==0.8.1",
     "huawei-lte-api==1.2.0"
   ],
   "dependencies": [],
diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py
index 02ccff82c52..e72bd3aa438 100644
--- a/homeassistant/components/huawei_lte/sensor.py
+++ b/homeassistant/components/huawei_lte/sensor.py
@@ -11,6 +11,7 @@ from homeassistant.components.sensor import (
     PLATFORM_SCHEMA,
     DEVICE_CLASS_SIGNAL_STRENGTH,
 )
+from homeassistant.helpers import entity_registry
 from homeassistant.helpers.entity import Entity
 import homeassistant.helpers.config_validation as cv
 
@@ -101,7 +102,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
 )
 
 
-def setup_platform(hass, config, add_entities, discovery_info):
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
     """Set up Huawei LTE sensor devices."""
     data = hass.data[DATA_KEY].get_data(config)
     sensors = []
@@ -111,7 +112,24 @@ def setup_platform(hass, config, add_entities, discovery_info):
         data.subscribe(path)
         sensors.append(HuaweiLteSensor(data, path, SENSOR_META.get(path, {})))
 
-    add_entities(sensors, True)
+    # Pre-0.97 unique id migration. Old ones used the device serial number
+    # (see comments in HuaweiLteData._setup_lte for more info), as well as
+    # had a bug that joined the path str with periods, not the path components,
+    # resulting e.g. *_device_signal.sinr to end up as
+    # *_d.e.v.i.c.e._.s.i.g.n.a.l...s.i.n.r
+    entreg = await entity_registry.async_get_registry(hass)
+    for entid, ent in entreg.entities.items():
+        if ent.platform != "huawei_lte":
+            continue
+        for sensor in sensors:
+            oldsuf = ".".join(sensor.path)
+            if ent.unique_id.endswith(f"_{oldsuf}"):
+                entreg.async_update_entity(entid, new_unique_id=sensor.unique_id)
+                _LOGGER.debug(
+                    "Updated entity %s unique id to %s", entid, sensor.unique_id
+                )
+
+    async_add_entities(sensors, True)
 
 
 def format_default(value):
@@ -134,7 +152,7 @@ class HuaweiLteSensor(Entity):
     """Huawei LTE sensor entity."""
 
     data = attr.ib(type=RouterData)
-    path = attr.ib(type=list)
+    path = attr.ib(type=str)
     meta = attr.ib(type=dict)
 
     _state = attr.ib(init=False, default=STATE_UNKNOWN)
@@ -143,9 +161,7 @@ class HuaweiLteSensor(Entity):
     @property
     def unique_id(self) -> str:
         """Return unique ID for sensor."""
-        return "{}_{}".format(
-            self.data["device_information.SerialNumber"], ".".join(self.path)
-        )
+        return "{}-{}".format(self.data.mac, self.path)
 
     @property
     def name(self) -> str:
diff --git a/requirements_all.txt b/requirements_all.txt
index 5d365c9c732..ff2d33f5d77 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -537,6 +537,7 @@ georss_ign_sismologia_client==0.2
 georss_qld_bushfire_alert_client==0.3
 
 # homeassistant.components.braviatv
+# homeassistant.components.huawei_lte
 # homeassistant.components.nmap_tracker
 getmac==0.8.1
 
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 1feeab8f32d..c2139f7f4e0 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -147,6 +147,11 @@ georss_ign_sismologia_client==0.2
 # homeassistant.components.qld_bushfire
 georss_qld_bushfire_alert_client==0.3
 
+# homeassistant.components.braviatv
+# homeassistant.components.huawei_lte
+# homeassistant.components.nmap_tracker
+getmac==0.8.1
+
 # homeassistant.components.google
 google-api-python-client==1.6.4
 
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index bc3a2133607..edf74b93793 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -73,6 +73,7 @@ TEST_REQUIREMENTS = (
     "georss_generic_client",
     "georss_ign_sismologia_client",
     "georss_qld_bushfire_alert_client",
+    "getmac",
     "google-api-python-client",
     "gTTS-token",
     "ha-ffmpeg",
diff --git a/tests/components/huawei_lte/test_init.py b/tests/components/huawei_lte/test_init.py
index 46cec10963a..70a00b02b4e 100644
--- a/tests/components/huawei_lte/test_init.py
+++ b/tests/components/huawei_lte/test_init.py
@@ -9,7 +9,7 @@ from homeassistant.components import huawei_lte
 @pytest.fixture(autouse=True)
 def routerdata():
     """Set up a router data for testing."""
-    rd = huawei_lte.RouterData(Mock())
+    rd = huawei_lte.RouterData(Mock(), "de:ad:be:ef:00:00")
     rd.device_information = {"SoftwareVersion": "1.0", "nested": {"foo": "bar"}}
     return rd
 
-- 
GitLab