Repository: frenck/python-demetriek (language: python)

Issue:
Strictness of `Wifi` model doesn't match in-the-field device payload
(Posting a companion issue to https://github.com/home-assistant/core/issues/78605, since the underlying problem is here.)

# Problem/Motivation

https://github.com/home-assistant/core/issues/78605

## Expected behavior

I expect the integration to load. 😉 

## Actual behavior

The integration didn't load.

## Steps to reproduce

https://github.com/home-assistant/core/issues/78605

## Proposed changes

The strictness of the `Wifi` model needs to be relaxed. My device returns this payload:

```json
{
  "audio": {
    "volume": 100,
    "volume_limit": {
      "max": 100,
      "min": 0
    },
    "volume_range": {
      "max": 100,
      "min": 0
    }
  },
  "bluetooth": {
    "active": false,
    "address": "24:18:C6:19:6C:55",
    "available": true,
    "discoverable": true,
    "low_energy": {
      "active": true,
      "advertising": true,
      "connectable": true
    },
    "name": "LM2207",
    "pairable": true
  },
  "display": {
    "brightness": 100,
    "brightness_limit": {
      "max": 100,
      "min": 2
    },
    "brightness_mode": "auto",
    "brightness_range": {
      "max": 100,
      "min": 0
    },
    "height": 8,
    "screensaver": {
      "enabled": true,
      "modes": {
        "time_based": {
          "enabled": false
        },
        "when_dark": {
          "enabled": true
        }
      },
      "widget": "08b8eac21074f8f7e5a29f2855ba8060"
    },
    "type": "mixed",
    "width": 37
  },
  "id": "57880",
  "mode": "manual",
  "model": "LM 37X8",
  "name": "Basement Desk",
  "os_version": "2.2.4",
  "serial_number": "SA110405220700W00BS9",
  "wifi": {
    "active": true,
    "address": "24:18:C6:19:04:59",
    "available": true,
    "essid": "Phil?_IoT",
    "ip": "172.16.20.247",
    "mode": "dhcp",
    "netmask": "255.255.255.0"
  }
}
```

The wifi data:

```json
{
    "active": true,
    "address": "24:18:C6:19:04:59",
    "available": true,
    "essid": "Phil?_IoT",
    "ip": "172.16.20.247",
    "mode": "dhcp",
    "netmask": "255.255.255.0"
}
```

...cannot successfully be parsed by the current `Wifi` model for at least a few fields (`encryption`, `essid`, `rssi`, etc.):

https://github.com/frenck/python-demetriek/blob/b492f9d0edcd43624ed1066554dfdf49cb388344/src/demetriek/models.py#L62-L73



Discussion / hints:
I see that the `pydantic` model was modeled off of [their official API docs](https://lametric-documentation.readthedocs.io/en/latest/reference-docs/device-wifi.html#response):

![CleanShot 2022-09-16 at 17 14 03](https://user-images.githubusercontent.com/47216/190829397-0a842ee0-1785-4425-9287-5724f8d619ee.png)

Might be worth a reach-out to them to determine whether my payload is an anomaly, or whether the docs are incorrect/outdated.

Fix patch:
diff --git a/src/demetriek/models.py b/src/demetriek/models.py
index 28a12129..d1ec75ab 100644
--- a/src/demetriek/models.py
+++ b/src/demetriek/models.py
@@ -65,12 +65,12 @@ class Wifi(BaseModel):
     active: bool
     mac: str
     available: bool
-    encryption: str
+    encryption: Optional[str] = None
     ssid: str
     ip: IPv4Address
     mode: WifiMode
     netmask: str
-    rssi: int
+    rssi: Optional[int] = None
 
 
 class Device(BaseModel):


Test patch:
diff --git a/tests/fixtures/device2.json b/tests/fixtures/device2.json
new file mode 100644
index 00000000..244af341
--- /dev/null
+++ b/tests/fixtures/device2.json
@@ -0,0 +1,70 @@
+{
+  "audio": {
+    "volume": 100,
+    "volume_limit": {
+      "max": 100,
+      "min": 0
+    },
+    "volume_range": {
+      "max": 100,
+      "min": 0
+    }
+  },
+  "bluetooth": {
+    "active": false,
+    "address": "AA:BB:CC:DD:EE:FF",
+    "available": true,
+    "discoverable": true,
+    "low_energy": {
+      "active": true,
+      "advertising": true,
+      "connectable": true
+    },
+    "name": "LM1234",
+    "pairable": true
+  },
+  "display": {
+    "brightness": 100,
+    "brightness_limit": {
+      "max": 100,
+      "min": 2
+    },
+    "brightness_mode": "auto",
+    "brightness_range": {
+      "max": 100,
+      "min": 0
+    },
+    "height": 8,
+    "screensaver": {
+      "enabled": false,
+      "modes": {
+        "time_based": {
+          "enabled": true,
+          "local_start_time": "01:00:39",
+          "start_time": "00:00:39"
+        },
+        "when_dark": {
+          "enabled": false
+        }
+      },
+      "widget": "08b8eac21074f8f7e5a29f2855ba8060"
+    },
+    "type": "mixed",
+    "width": 37
+  },
+  "id": "12345",
+  "mode": "auto",
+  "model": "LM 37X8",
+  "name": "Frenck's LaMetric",
+  "os_version": "2.2.2",
+  "serial_number": "SA110405124500W00BS9",
+  "wifi": {
+    "active": true,
+    "address": "AA:BB:CC:DD:EE:FF",
+    "available": true,
+    "essid": "Frenck",
+    "ip": "192.168.1.11",
+    "mode": "dhcp",
+    "netmask": "255.255.255.0"
+  }
+}
diff --git a/tests/fixtures/wifi2.json b/tests/fixtures/wifi2.json
new file mode 100644
index 00000000..85bf5f33
--- /dev/null
+++ b/tests/fixtures/wifi2.json
@@ -0,0 +1,10 @@
+{
+  "active": true,
+  "available": true,
+  "ipv4": "192.168.1.2",
+  "mac": "AA:BB:CC:DD:EE:FF",
+  "mode": "dhcp",
+  "netmask": "255.255.255.0",
+  "signal_strength": null,
+  "ssid": "AllYourBaseAreBelongToUs"
+}
diff --git a/tests/test_device.py b/tests/test_device.py
index 90679b7b..730913ce 100644
--- a/tests/test_device.py
+++ b/tests/test_device.py
@@ -62,3 +62,55 @@ async def test_get_device(aresponses: ResponsesMockServer) -> None:
     assert device.wifi.mode is WifiMode.DHCP
     assert device.wifi.netmask == "255.255.255.0"
     assert device.wifi.rssi == 21
+
+
+@pytest.mark.asyncio
+async def test_get_device2(aresponses: ResponsesMockServer) -> None:
+    """Test getting device information."""
+    aresponses.add(
+        "127.0.0.2:4343",
+        "/api/v2/device",
+        "GET",
+        aresponses.Response(
+            status=200,
+            headers={"Content-Type": "application/json"},
+            text=load_fixture("device2.json"),
+        ),
+    )
+    async with aiohttp.ClientSession() as session:
+        demetriek = LaMetricDevice(host="127.0.0.2", api_key="abc", session=session)
+        device = await demetriek.device()
+
+    assert device
+    assert device.device_id == "12345"
+    assert device.name == "Frenck's LaMetric"
+    assert device.os_version == "2.2.2"
+    assert device.mode is DeviceMode.AUTO
+    assert device.model == "LM 37X8"
+    assert device.audio.volume == 100
+    assert device.audio.volume_range
+    assert device.audio.volume_range.range_min == 0
+    assert device.audio.volume_range.range_max == 100
+    assert device.audio.volume_limit
+    assert device.audio.volume_limit.range_min == 0
+    assert device.audio.volume_limit.range_max == 100
+    assert device.bluetooth.available is True
+    assert device.bluetooth.name == "LM1234"
+    assert device.bluetooth.active is False
+    assert device.bluetooth.discoverable is True
+    assert device.bluetooth.pairable is True
+    assert device.bluetooth.address == "AA:BB:CC:DD:EE:FF"
+    assert device.display.brightness == 100
+    assert device.display.brightness_mode is BrightnessMode.AUTO
+    assert device.display.width == 37
+    assert device.display.height == 8
+    assert device.display.display_type is DisplayType.MIXED
+    assert device.wifi.active is True
+    assert device.wifi.available is True
+    assert device.wifi.mac == "AA:BB:CC:DD:EE:FF"
+    assert device.wifi.encryption is None
+    assert device.wifi.ssid == "Frenck"
+    assert device.wifi.ip == IPv4Address("192.168.1.11")
+    assert device.wifi.mode is WifiMode.DHCP
+    assert device.wifi.netmask == "255.255.255.0"
+    assert device.wifi.rssi is None
diff --git a/tests/test_wifi.py b/tests/test_wifi.py
index 0e2b3265..7f02a2aa 100644
--- a/tests/test_wifi.py
+++ b/tests/test_wifi.py
@@ -13,7 +13,7 @@
 
 
 @pytest.mark.asyncio
-async def test_get_audio(aresponses: ResponsesMockServer) -> None:
+async def test_get_wifi(aresponses: ResponsesMockServer) -> None:
     """Test getting audio information."""
     aresponses.add(
         "127.0.0.2:4343",
@@ -39,3 +39,32 @@ async def test_get_audio(aresponses: ResponsesMockServer) -> None:
     assert wifi.netmask == "255.255.255.0"
     assert wifi.ssid == "AllYourBaseAreBelongToUs"
     assert wifi.rssi == 42
+
+
+@pytest.mark.asyncio
+async def test_get_wifi2(aresponses: ResponsesMockServer) -> None:
+    """Test getting audio information."""
+    aresponses.add(
+        "127.0.0.2:4343",
+        "/api/v2/device/wifi",
+        "GET",
+        aresponses.Response(
+            status=200,
+            headers={"Content-Type": "application/json"},
+            text=load_fixture("wifi2.json"),
+        ),
+    )
+    async with aiohttp.ClientSession() as session:
+        demetriek = LaMetricDevice(host="127.0.0.2", api_key="abc", session=session)
+        wifi = await demetriek.wifi()
+
+    assert wifi
+    assert wifi.active is True
+    assert wifi.mac == "AA:BB:CC:DD:EE:FF"
+    assert wifi.available is True
+    assert wifi.encryption is None
+    assert wifi.ip == IPv4Address("192.168.1.2")
+    assert wifi.mode == WifiMode.DHCP
+    assert wifi.netmask == "255.255.255.0"
+    assert wifi.ssid == "AllYourBaseAreBelongToUs"
+    assert wifi.rssi is None