Repository: jabesq/netatmo-api-python

Issue:
Exception during event data update
Hello,
I am facing an issue in home assistant (which is using this module) with update of sensors.

Did some investigation, and below is what is happening

- When does this exception occur : this issue occurs when an update of binary sensor in homeassistant happens on a camera which does not have any event in `['homes'][i]['events']`, note that not all events appear here, only latest 30 events are in this node.
- When does this exception dissapear : as soon as a event is seen in `['homes'][i]['events']` node (new event on the camera, part of latest 30 events of the home)

- Expected behavior : As there is no event linked to the checked sensors (which are linked to outdoormotionDetected, animalDetected, carDetected,. functions), they should return default `False` and not raise exceptions.


`Update for binary_sensor.driveway_outdoor_vehicle fails
Traceback (most recent call last):
  File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/helpers/entity.py", line 221, in async_update_ha_state
    await self.async_device_update()
  File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/helpers/entity.py", line 349, in async_device_update
    await self.hass.async_add_executor_job(self.update)
  File "/usr/lib/python3.5/asyncio/futures.py", line 380, in __iter__
    yield self  # This tells Task to wait for completion.
  File "/usr/lib/python3.5/asyncio/tasks.py", line 304, in _wakeup
    future.result()
  File "/usr/lib/python3.5/asyncio/futures.py", line 293, in result
    raise self._exception
  File "/usr/lib/python3.5/concurrent/futures/thread.py", line 55, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/components/binary_sensor/netatmo.py", line 157, in update
    self._data.update_event()
  File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/util/__init__.py", line 324, in wrapper
    result = method(*args, **kwargs)
  File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/components/netatmo.py", line 119, in update_event
    home=self.home, cameratype=self.camera_type)
  File "/srv/homeassistant/lib/python3.5/site-packages/smart_home/Camera.py", line 256, in updateEvent
    self.outdoor_events[e['camera_id']][e['time']] = e
KeyError: '70:ee:50:26:b5:56'`

Discussion:

Patch:
diff --git a/fixtures/camera_home_data_disconnected.json b/fixtures/camera_home_data_disconnected.json
new file mode 100644
index 0000000..c0ee0d8
--- /dev/null
+++ b/fixtures/camera_home_data_disconnected.json
@@ -0,0 +1,182 @@
+{
+    "body": {
+        "homes": [
+            {
+                "id": "91763b24c43d3e344f424e8b",
+                "name": "MYHOME",
+                "persons": [
+                    {
+                        "id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+                        "last_seen": 1557071156,
+                        "out_of_sight": true,
+                        "face": {
+                            "id": "d74fad765b9100ef480720a9",
+                            "version": 1,
+                            "key": "a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7",
+                            "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7"
+                        },
+                        "pseudo": "John Doe"
+                    },
+                    {
+                        "id": "91827375-7e04-5298-83ae-a0cb8372dff2",
+                        "last_seen": 1560600726,
+                        "out_of_sight": true,
+                        "face": {
+                            "id": "d74fad765b9100ef480720a9",
+                            "version": 3,
+                            "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72",
+                            "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72"
+                        },
+                        "pseudo": "Jane Doe"
+                    },
+                    {
+                        "id": "91827376-7e04-5298-83af-a0cb8372dff3",
+                        "last_seen": 1560626666,
+                        "out_of_sight": false,
+                        "face": {
+                            "id": "d74fad765b9100ef480720a9",
+                            "version": 1,
+                            "key": "a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8",
+                            "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8"
+                        },
+                        "pseudo": "Richard Doe"
+                    },
+                    {
+                        "id": "91827376-7e04-5298-83af-a0cb8372dff4",
+                        "last_seen": 1560621666,
+                        "out_of_sight": true,
+                        "face": {
+                            "id": "d0ef44fad765b980720710a9",
+                            "version": 1,
+                            "key": "ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928",
+                            "url": "https://netatmocameraimage.blob.core.windows.net/production/d0ef44fad765b980720710a9ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928"
+                        }
+                    }
+                ],
+                "place": {
+                    "city": "Frankfurt",
+                    "country": "DE",
+                    "timezone": "Europe/Berlin"
+                },
+                "cameras": [
+                    {
+                        "id": "12:34:56:00:f1:62",
+                        "type": "NACamera",
+                        "status": "disconnected",
+                        "sd_status": "on",
+                        "alim_status": "on",
+                        "name": "Hall",
+                        "use_pin_code": false,
+                        "last_setup": 1544828430
+                    }
+                ],
+                "smokedetectors": [],
+                "events": [
+                    {
+                        "id": "a1b2c3d4e5f6abcdef123456",
+                        "type": "person",
+                        "time": 1560604700,
+                        "camera_id": "12:34:56:00:f1:62",
+                        "device_id": "12:34:56:00:f1:62",
+                        "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+                        "video_status": "deleted",
+                        "is_arrival": false,
+                        "message": "<b>John Doe</b> gesehen"
+                    },
+                    {
+                        "id": "a1b2c3d4e5f6abcdef123457",
+                        "type": "person_away",
+                        "time": 1560602400,
+                        "camera_id": "12:34:56:00:f1:62",
+                        "device_id": "12:34:56:00:f1:62",
+                        "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+                        "message": "<b>John Doe</b> hat das Haus verlassen",
+                        "sub_message": "John Doe gilt als abwesend, da das mit diesem Profil verbundene Telefon den Bereich des Hauses verlassen hat."
+                    },
+                    {
+                        "id": "a1b2c3d4e5f6abcdef123458",
+                        "type": "person",
+                        "time": 1560601200,
+                        "camera_id": "12:34:56:00:f1:62",
+                        "device_id": "12:34:56:00:f1:62",
+                        "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+                        "video_status": "deleted",
+                        "is_arrival": false,
+                        "message": "<b>John Doe</b> gesehen"
+                    },
+                    {
+                        "id": "a1b2c3d4e5f6abcdef123459",
+                        "type": "person",
+                        "time": 1560600100,
+                        "camera_id": "12:34:56:00:f1:62",
+                        "device_id": "12:34:56:00:f1:62",
+                        "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2",
+                        "snapshot": {
+                            "id": "d74fad765b9100ef480720a9",
+                            "version": 1,
+                            "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72",
+                            "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72"
+                        },
+                        "video_id": "12345678-36bc-4b9a-9762-5194e707ed51",
+                        "video_status": "available",
+                        "is_arrival": false,
+                        "message": "<b>Jane Doe</b> gesehen"
+                    },
+                    {
+                        "id": "a1b2c3d4e5f6abcdef12345a",
+                        "type": "person",
+                        "time": 1560603600,
+                        "camera_id": "12:34:56:00:f1:62",
+                        "device_id": "12:34:56:00:f1:62",
+                        "person_id": "91827375-7e04-5298-83ae-a0cb8372dff3",
+                        "snapshot": {
+                            "id": "532dde8d17554c022ab071b8",
+                            "version": 1,
+                            "key": "9fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28",
+                            "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b89fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28"
+                        },
+                        "video_id": "12345678-1234-46cb-ad8f-23d893874099",
+                        "video_status": "available",
+                        "is_arrival": false,
+                        "message": "<b>Bewegung</b> erkannt"
+                    },
+                    {
+                        "id": "a1b2c3d4e5f6abcdef12345b",
+                        "type": "movement",
+                        "time": 1560506200,
+                        "camera_id": "12:34:56:00:f1:62",
+                        "device_id": "12:34:56:00:f1:62",
+                        "category": "human",
+                        "snapshot": {
+                            "id": "532dde8d17554c022ab071b9",
+                            "version": 1,
+                            "key": "8fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28",
+                            "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b98fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28"
+                        },
+                        "vignette": {
+                            "id": "5dc021b5dea854bd2321707a",
+                            "version": 1,
+                            "key": "58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944",
+                            "url": "https://netatmocameraimage.blob.core.windows.net/production/5dc021b5dea854bd2321707a58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944"
+                        },
+                        "video_id": "12345678-1234-46cb-ad8f-23d89387409a",
+                        "video_status": "available",
+                        "message": "<b>Bewegung</b> erkannt"
+                    }
+                ]
+            }
+        ],
+        "user": {
+            "reg_locale": "de-DE",
+            "lang": "de-DE",
+            "country": "DE",
+            "mail": "john@doe.com"
+        },
+        "global_info": {
+            "show_tags": true
+        }
+    },
+    "status": "ok",
+    "time_exec": 0.03621506690979,
+    "time_server": 1560626960
+}
\ No newline at end of file
diff --git a/fixtures/home_status_error_and_data.json b/fixtures/home_status_error_and_data.json
new file mode 100644
index 0000000..49228b4
--- /dev/null
+++ b/fixtures/home_status_error_and_data.json
@@ -0,0 +1,109 @@
+{
+    "status": "ok",
+    "time_server": 1559292039,
+    "body": {
+        "home": {
+            "modules": [
+                {
+                    "id": "12:34:56:00:fa:d0",
+                    "type": "NAPlug",
+                    "firmware_revision": 174,
+                    "rf_strength": 107,
+                    "wifi_strength": 42
+                },
+                {
+                    "id": "12:34:56:00:01:ae",
+                    "reachable": true,
+                    "type": "NATherm1",
+                    "firmware_revision": 65,
+                    "rf_strength": 58,
+                    "battery_level": 3793,
+                    "boiler_valve_comfort_boost": false,
+                    "boiler_status": false,
+                    "anticipating": false,
+                    "bridge": "12:34:56:00:fa:d0",
+                    "battery_state": "high"
+                },
+                {
+                    "id": "12:34:56:03:a5:54",
+                    "reachable": true,
+                    "type": "NRV",
+                    "firmware_revision": 79,
+                    "rf_strength": 51,
+                    "battery_level": 3025,
+                    "bridge": "12:34:56:00:fa:d0",
+                    "battery_state": "full"
+                },
+                {
+                    "id": "12:34:56:03:a0:ac",
+                    "reachable": true,
+                    "type": "NRV",
+                    "firmware_revision": 79,
+                    "rf_strength": 59,
+                    "battery_level": 3029,
+                    "bridge": "12:34:56:00:fa:d0",
+                    "battery_state": "full"
+                }
+            ],
+            "rooms": [
+                {
+                    "id": "2746182631",
+                    "reachable": true,
+                    "therm_measured_temperature": 19.8,
+                    "therm_setpoint_temperature": 12,
+                    "therm_setpoint_mode": "away",
+                    "therm_setpoint_start_time": 1559229567,
+                    "therm_setpoint_end_time": 0
+                },
+                {
+                    "id": "2940411577",
+                    "reachable": true,
+                    "therm_measured_temperature": 27,
+                    "heating_power_request": 0,
+                    "therm_setpoint_temperature": 7,
+                    "therm_setpoint_mode": "hg",
+                    "therm_setpoint_start_time": 0,
+                    "therm_setpoint_end_time": 0,
+                    "anticipating": false,
+                    "open_window": false
+                },
+                {
+                    "id": "2833524037",
+                    "reachable": true,
+                    "therm_measured_temperature": 24.5,
+                    "heating_power_request": 0,
+                    "therm_setpoint_temperature": 7,
+                    "therm_setpoint_mode": "hg",
+                    "therm_setpoint_start_time": 0,
+                    "therm_setpoint_end_time": 0,
+                    "anticipating": false,
+                    "open_window": false
+                }
+            ],
+            "id": "91763b24c43d3e344f424e8b",
+            "persons": [
+                {
+                    "id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+                    "last_seen": 1557071156,
+                    "out_of_sight": true
+                },
+                {
+                    "id": "91827375-7e04-5298-83ae-a0cb8372dff2",
+                    "last_seen": 1559282761,
+                    "out_of_sight": false
+                },
+                {
+                    "id": "91827376-7e04-5298-83af-a0cb8372dff3",
+                    "last_seen": 1559224132,
+                    "out_of_sight": true
+                }
+            ]
+        },
+        "errors": [
+            {
+                "code": 6,
+                "id": "12:34:56:00:f1:62"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/smart_home/Camera.py b/smart_home/Camera.py
index c489c0c..fe77d59 100644
--- a/smart_home/Camera.py
+++ b/smart_home/Camera.py
@@ -170,17 +170,13 @@ class CameraData:
             vpn_url = camera_data.get("vpn_url")
             if camera_data.get("is_local"):
                 try:
-                    resp = postRequest(
-                        "{0}/command/ping".format(vpn_url), {}
-                    )
+                    resp = postRequest("{0}/command/ping".format(vpn_url), {})
                     temp_local_url = resp["local_url"]
                 except URLError:
                     return None, None
 
                 try:
-                    resp = postRequest(
-                        "{0}/command/ping".format(temp_local_url), {}
-                    )
+                    resp = postRequest("{0}/command/ping".format(temp_local_url), {})
                     if temp_local_url == resp["local_url"]:
                         local_url = temp_local_url
                 except URLError:
@@ -264,8 +260,12 @@ class CameraData:
         eventList = resp["body"]["events_list"]
         for e in eventList:
             if e["type"] == "outdoor":
+                if e["camera_id"] not in self.outdoor_events:
+                    self.outdoor_events[e["camera_id"]] = {}
                 self.outdoor_events[e["camera_id"]][e["time"]] = e
             elif e["type"] != "outdoor":
+                if e["camera_id"] not in self.events:
+                    self.events[e["camera_id"]] = {}
                 self.events[e["camera_id"]][e["time"]] = e
         for camera in self.events:
             self.lastEvent[camera] = self.events[camera][
diff --git a/smart_home/WeatherStation.py b/smart_home/WeatherStation.py
index 16d0f1e..f0057c4 100644
--- a/smart_home/WeatherStation.py
+++ b/smart_home/WeatherStation.py
@@ -46,10 +46,15 @@ class WeatherStationData:
         self.default_station = list(self.stations.values())[0]["station_name"]
 
     def modulesNamesList(self, station=None):
-        res = set([m["module_name"] for m in self.modules.values()])
         if station:
-            res.add(self.stationByName(station)["module_name"])
+            res = set()
+            s = self.stationByName(station)
+            if s is not None:
+                res.add(s["module_name"])
+                for m in s["modules"]:
+                    res.add(m["module_name"])
         else:
+            res = set([m["module_name"] for m in self.modules.values()])
             for id, station in self.stations.items():
                 res.add(station["module_name"])
         return list(res)
@@ -116,31 +121,43 @@ class WeatherStationData:
         return conditions
 
     def lastData(self, station=None, exclude=0):
-        s = self.stationByName(station)
+        if station is not None:
+            stations = [station]
+        else:
+            stations = [s["station_name"] for s in list(self.stations.values())]
         # Breaking change from Netatmo : dashboard_data no longer available if station lost
-        if not s or "dashboard_data" not in s:
-            return None
         lastD = {}
-        # Define oldest acceptable sensor measure event
-        limit = (time.time() - exclude) if exclude else 0
-        ds = s["dashboard_data"]
-        if "module_name" in s and ds["time_utc"] > limit:
-            lastD[s["module_name"]] = ds.copy()
-            lastD[s["module_name"]]["When"] = lastD[s["module_name"]].pop("time_utc")
-            lastD[s["module_name"]]["wifi_status"] = s["wifi_status"]
-        for module in s["modules"]:
-            if "dashboard_data" not in module or "module_name" not in module:
-                continue
-            ds = module["dashboard_data"]
-            if "time_utc" in ds and ds["time_utc"] > limit:
-                lastD[module["module_name"]] = ds.copy()
-                lastD[module["module_name"]]["When"] = lastD[module["module_name"]].pop(
+        for st in stations:
+            s = self.stationByName(st)
+            if not s or "dashboard_data" not in s:
+                return None
+            # Define oldest acceptable sensor measure event
+            limit = (time.time() - exclude) if exclude else 0
+            ds = s["dashboard_data"]
+            if "module_name" in s and ds["time_utc"] > limit:
+                lastD[s["module_name"]] = ds.copy()
+                lastD[s["module_name"]]["When"] = lastD[s["module_name"]].pop(
                     "time_utc"
                 )
-                # For potential use, add battery and radio coverage information to module data if present
-                for i in ("rf_status", "battery_vp", "battery_percent", "wifi_status"):
-                    if i in module:
-                        lastD[module["module_name"]][i] = module[i]
+                lastD[s["module_name"]]["wifi_status"] = s["wifi_status"]
+            for module in s["modules"]:
+                if "dashboard_data" not in module or "module_name" not in module:
+                    continue
+                ds = module["dashboard_data"]
+                if "time_utc" in ds and ds["time_utc"] > limit:
+                    lastD[module["module_name"]] = ds.copy()
+                    lastD[module["module_name"]]["When"] = lastD[
+                        module["module_name"]
+                    ].pop("time_utc")
+                    # For potential use, add battery and radio coverage information to module data if present
+                    for i in (
+                        "rf_status",
+                        "battery_vp",
+                        "battery_percent",
+                        "wifi_status",
+                    ):
+                        if i in module:
+                            lastD[module["module_name"]][i] = module[i]
         return lastD
 
     def checkNotUpdated(self, station=None, delay=3600):


Test patch:
diff --git a/tests/test_smart_home_homecoach.py b/tests/test_smart_home_homecoach.py
index 7166e9c..5b7a737 100644
--- a/tests/test_smart_home_homecoach.py
+++ b/tests/test_smart_home_homecoach.py
@@ -10,7 +10,7 @@ def test_HomeCoachData(homeCoachData):
     "station, expected",
     [
         (None, ["Bedroom", "Kitchen", "Livingroom"]),
-        ("Bedroom", ["Bedroom", "Kitchen", "Livingroom"]),
+        ("Bedroom", ["Bedroom"]),
         pytest.param(
             "NoValidStation",
             None,