Repository: somm15/PyViCare

Issue:
Read WiFi Signal Strength
Hey,

I've been looking into the code and into the HomeAssistant integration lately. I would be interested in reading gateway specific values with the API and I found `device.wifi`. But this seems not to be in the library code yet. Where could this be implemented? Is there a dedicated Gateway class necessary for it?

Comments:

Patch:
diff --git a/PyViCare/PyViCareDeviceConfig.py b/PyViCare/PyViCareDeviceConfig.py
index 57f546f..0a776a4 100644
--- a/PyViCare/PyViCareDeviceConfig.py
+++ b/PyViCare/PyViCareDeviceConfig.py
@@ -12,6 +12,7 @@ from PyViCare.PyViCarePelletsBoiler import PelletsBoiler
 from PyViCare.PyViCareRadiatorActuator import RadiatorActuator
 from PyViCare.PyViCareRoomSensor import RoomSensor
 from PyViCare.PyViCareElectricalEnergySystem import ElectricalEnergySystem
+from PyViCare.PyViCareGateway import Gateway
 from PyViCare.PyViCareVentilationDevice import VentilationDevice
 
 logger = logging.getLogger('ViCare')
@@ -54,6 +55,9 @@ class PyViCareDeviceConfig:
 
     def asElectricalEnergySystem(self):
         return ElectricalEnergySystem(self.service)
+    
+    def asGateway(self):
+        return Gateway(self.service)
 
     def asVentilation(self):
         return VentilationDevice(self.service)
@@ -84,7 +88,8 @@ class PyViCareDeviceConfig:
             (self.asElectricalEnergySystem, r"E3_TCU10_x07", ["type:tcu"]),
             (self.asElectricalEnergySystem, r"E3_EEBus", ["type:eebus"]),
             (self.asElectricalEnergySystem, r"E3_VitoCharge_03", ["type:energy_storage"]),
-            (self.asVentilation, r"E3_ViAir", ["type:ventilation"])
+            (self.asVentilation, r"E3_ViAir", ["type:ventilation"]),
+            (self.asGateway, r"Heatbox1", ["type:gateway;VitoconnectOpto1"])
         ]
 
         for (creator_method, type_name, roles) in device_types:
diff --git a/PyViCare/PyViCareGateway.py b/PyViCare/PyViCareGateway.py
new file mode 100644
index 0000000..fbdf317
--- /dev/null
+++ b/PyViCare/PyViCareGateway.py
@@ -0,0 +1,9 @@
+from PyViCare.PyViCareDevice import Device
+from PyViCare.PyViCareUtils import handleNotSupported
+
+
+class Gateway(Device):
+
+    @handleNotSupported
+    def getWifiSignalStrength(self):
+        return self.service.getProperty("gateway.wifi")["properties"]["strength"]["value"]
diff --git a/PyViCare/PyViCareService.py b/PyViCare/PyViCareService.py
index 29e7f64..259deee 100644
--- a/PyViCare/PyViCareService.py
+++ b/PyViCare/PyViCareService.py
@@ -27,10 +27,6 @@ def buildSetPropertyUrl(accessor, property_name, action):
     return f'/features/installations/{accessor.id}/gateways/{accessor.serial}/devices/{accessor.device_id}/features/{property_name}/commands/{action}'
 
 
-def buildGetPropertyUrl(accessor, property_name):
-    return f'/features/installations/{accessor.id}/gateways/{accessor.serial}/devices/{accessor.device_id}/features/{property_name}'
-
-
 class ViCareDeviceAccessor:
     def __init__(self, id: int, serial: str, device_id: str) -> None:
         self.id = id
@@ -45,13 +41,21 @@ class ViCareService:
         self.roles = roles
 
     def getProperty(self, property_name: str) -> Any:
-        url = buildGetPropertyUrl(
-            self.accessor, property_name)
+        url = self.buildGetPropertyUrl(property_name)
         return self.oauth_manager.get(url)
+        
+    def buildGetPropertyUrl(self, property_name):
+        if self._isGateway():
+            return f'/features/installations/{self.accessor.id}/gateways/{self.accessor.serial}/features/{property_name}'
+        return f'/features/installations/{self.accessor.id}/gateways/{self.accessor.serial}/devices/{self.accessor.device_id}/features/{property_name}'
+
 
     def hasRoles(self, requested_roles) -> bool:
         return hasRoles(requested_roles, self.roles)
 
+    def _isGateway(self) -> bool:
+        return self.hasRoles(["type:gateway;VitoconnectOpto1"])
+
     def setProperty(self, property_name: str, action: str, data: Any) -> Any:
         url = buildSetPropertyUrl(
             self.accessor, property_name, action)
@@ -61,4 +65,6 @@ class ViCareService:
 
     def fetch_all_features(self) -> Any:
         url = f'/features/installations/{self.accessor.id}/gateways/{self.accessor.serial}/devices/{self.accessor.device_id}/features/'
+        if self._isGateway():
+            url = f'/features/installations/{self.accessor.id}/gateways/{self.accessor.serial}/features/'
         return self.oauth_manager.get(url)


Test patch:
diff --git a/tests/response/VitoconnectOpto1.json b/tests/response/VitoconnectOpto1.json
new file mode 100644
index 0000000..db0aa24
--- /dev/null
+++ b/tests/response/VitoconnectOpto1.json
@@ -0,0 +1,65 @@
+{
+    "data": [
+        {
+            "apiVersion": 1,
+            "commands": {},
+            "feature": "gateway.devices",
+            "gatewayId": "################",
+            "isEnabled": true,
+            "isReady": true,
+            "properties": {
+                "devices": {
+                    "type": "DeviceList",
+                    "value": [
+                        {
+                            "fingerprint": "xxx",
+                            "id": "gateway",
+                            "modelId": "Heatbox1",
+                            "modelVersion": "xxx",
+                            "name": "Heatbox 1, Vitoconnect",
+                            "roles": [
+                                "type:gateway;VitoconnectOpto1",
+                                "type:legacy"
+                            ],
+                            "status": "online",
+                            "type": "vitoconnect"
+                        },
+                        {
+                            "fingerprint": "xxx",
+                            "id": "0",
+                            "modelId": "VScotHO1_40",
+                            "modelVersion": "xxx",
+                            "name": "VT 200 (HO1A / HO1B)",
+                            "roles": [
+                                "type:boiler",
+                                "type:legacy",
+                                "type:product;VScotHO1"
+                            ],
+                            "status": "online",
+                            "type": "heating"
+                        }
+                    ]
+                }
+            },
+            "timestamp": "2023-12-25T04:01:00.448Z",
+            "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/features/gateway.devices"
+        },
+        {
+            "apiVersion": 1,
+            "commands": {},
+            "feature": "gateway.wifi",
+            "gatewayId": "################",
+            "isEnabled": true,
+            "isReady": true,
+            "properties": {
+                "strength": {
+                    "type": "number",
+                    "unit": "",
+                    "value": -69
+                }
+            },
+            "timestamp": "2023-12-26T20:44:41.417Z",
+            "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/features/gateway.wifi"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/tests/test_PyViCareDeviceConfig.py b/tests/test_PyViCareDeviceConfig.py
index f0953a5..c95cc3e 100644
--- a/tests/test_PyViCareDeviceConfig.py
+++ b/tests/test_PyViCareDeviceConfig.py
@@ -66,3 +66,14 @@ class PyViCareDeviceConfigTest(unittest.TestCase):
         c = PyViCareDeviceConfig(self.service, "0", "E3_ViAir_300F", "Online")
         device_type = c.asAutoDetectDevice()
         self.assertEqual("VentilationDevice", type(device_type).__name__)
+
+    def test_autoDetect_VitoconnectOpto1_asGateway(self):
+        c = PyViCareDeviceConfig(self.service, "0", "Heatbox1", "Online")
+        device_type = c.asAutoDetectDevice()
+        self.assertEqual("Gateway", type(device_type).__name__)
+
+    def test_autoDetect_RoleGateway_asGateway(self):
+        self.service.hasRoles = has_roles(["type:gateway;VitoconnectOpto1"])
+        c = PyViCareDeviceConfig(self.service, "0", "Unknown", "Online")
+        device_type = c.asAutoDetectDevice()
+        self.assertEqual("Gateway", type(device_type).__name__)
diff --git a/tests/test_PyViCareService.py b/tests/test_PyViCareService.py
index 70adc98..86885bc 100644
--- a/tests/test_PyViCareService.py
+++ b/tests/test_PyViCareService.py
@@ -8,8 +8,8 @@ class PyViCareServiceTest(unittest.TestCase):
 
     def setUp(self):
         self.oauth_mock = Mock()
-        accessor = ViCareDeviceAccessor("[id]", "[serial]", "[device]")
-        self.service = ViCareService(self.oauth_mock, accessor, [])
+        self.accessor = ViCareDeviceAccessor("[id]", "[serial]", "[device]")
+        self.service = ViCareService(self.oauth_mock, self.accessor, [])
 
     def test_getProperty(self):
         self.service.getProperty("someprop")
@@ -25,3 +25,15 @@ class PyViCareServiceTest(unittest.TestCase):
         self.service.setProperty("someprop", "doaction", '{}')
         self.oauth_mock.post.assert_called_once_with(
             '/features/installations/[id]/gateways/[serial]/devices/[device]/features/someprop/commands/doaction', '{}')
+
+    def test_getProperty_gateway(self):
+        self.service = ViCareService(self.oauth_mock, self.accessor, ["type:gateway;VitoconnectOpto1"])
+        self.service.getProperty("someprop")
+        self.oauth_mock.get.assert_called_once_with(
+            '/features/installations/[id]/gateways/[serial]/features/someprop')
+
+    def test_fetch_all_features_gateway(self):
+        self.service = ViCareService(self.oauth_mock, self.accessor, ["type:gateway;VitoconnectOpto1"])
+        self.service.fetch_all_features()
+        self.oauth_mock.get.assert_called_once_with(
+            '/features/installations/[id]/gateways/[serial]/features/')
diff --git a/tests/test_TestForMissingProperties.py b/tests/test_TestForMissingProperties.py
index 7844e55..4fc2af2 100644
--- a/tests/test_TestForMissingProperties.py
+++ b/tests/test_TestForMissingProperties.py
@@ -61,6 +61,9 @@ class TestForMissingProperties(unittest.TestCase):
             # Ignored for now as both are not documented in https://documentation.viessmann.com/static/iot/data-points
             'device.messages.errors.raw',
             'device.productIdentification',
+
+            # gateway
+            'gateway.devices', # not used
         ]
 
         all_features = self.read_all_features()
diff --git a/tests/test_VitoconnectOpto1.py b/tests/test_VitoconnectOpto1.py
new file mode 100644
index 0000000..671c32a
--- /dev/null
+++ b/tests/test_VitoconnectOpto1.py
@@ -0,0 +1,14 @@
+import unittest
+
+from PyViCare.PyViCareGateway import Gateway
+from tests.ViCareServiceMock import ViCareServiceMock
+
+
+class VitoconnectOpto1(unittest.TestCase):
+    def setUp(self):
+        self.service = ViCareServiceMock('response/VitoconnectOpto1.json')
+        self.device = Gateway(self.service)
+
+    def test_getWifiSignalStrength(self):
+        self.assertEqual(
+            self.device.getWifiSignalStrength(), -69)