From 6f7b1f675ac0d530dbf11f997e718e0e555c6ee7 Mon Sep 17 00:00:00 2001 From: Alessandro Manighetti Date: Thu, 9 Nov 2023 00:01:18 +0100 Subject: [PATCH 1/3] feat: add query.OUTPUTS to collect outputs status --- src/elmo/api/client.py | 17 ++++++++++--- src/elmo/api/router.py | 4 +++ src/elmo/query.py | 1 + tests/test_client.py | 55 +++++++++++++++++++++++++++++++++++++++--- 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/elmo/api/client.py b/src/elmo/api/client.py index 305e22e..8d862d9 100644 --- a/src/elmo/api/client.py +++ b/src/elmo/api/client.py @@ -115,12 +115,15 @@ def poll(self, ids): { "areas": False, "inputs": True, + "outputs": False, + "statusAdv": False, } """ payload = { "sessionId": self._session_id, "Areas": ids[q.SECTORS], "Inputs": ids[q.INPUTS], + "Outputs": ids[q.OUTPUTS], "StatusAdv": ids[q.ALERTS], "CanElevate": "1", "ConnectionStatus": "1", @@ -133,9 +136,10 @@ def poll(self, ids): state = response.json() try: update = { - "has_changes": state["Areas"] or state["Inputs"] or state["StatusAdv"], + "has_changes": state["Areas"] or state["Inputs"] or state["Outputs"] or state["StatusAdv"], "areas": state["Areas"], "inputs": state["Inputs"], + "outputs": state["Outputs"], "statusadv": state["StatusAdv"], } except KeyError as err: @@ -499,6 +503,7 @@ def query(self, query): sectors = client.query(query.SECTORS).get("sectors") inputs = client.query(query.INPUTS).get("inputs") + outputs = client.query(query.OUTPUTS).get("outputs") Raises: QueryNotValid: if the query is not recognized. @@ -530,6 +535,7 @@ def query(self, query): `last_id`: is the last ID of the query, used to retrieve new state changes `sectors`: is the key you use to retrieve sectors if that was the query `inputs`: is the key you use to retrieve inputs if that was the query + 'outputs`: is the key you use to retrieve outputs if that was the query """ # Query detection if query == q.SECTORS: @@ -542,6 +548,11 @@ def query(self, query): key_group = "inputs" endpoint = self._router.inputs _LOGGER.debug("Client | Querying inputs") + elif query == q.OUTPUTS: + status = "Active" + key_group = "outputs" + endpoint = self._router.outputs + _LOGGER.debug("Client | Querying outputs") elif query == q.ALERTS: endpoint = self._router.status _LOGGER.debug("Client | Querying alerts") @@ -552,7 +563,7 @@ def query(self, query): response = self._session.post(endpoint, data={"sessionId": self._session_id}) response.raise_for_status() - if query in [q.SECTORS, q.INPUTS]: + if query in [q.SECTORS, q.INPUTS, q.OUTPUTS]: # Retrieve description or use the cache descriptions = self._get_descriptions() @@ -572,7 +583,7 @@ def query(self, query): if entry["InUse"]: # Address potential data inconsistency between cloud data and main unit. # In some installations, they may be out of sync, resulting in the cloud - # providing a sector/input that doesn't actually exist in the main unit. + # providing a sector/input/output that doesn't actually exist in the main unit. # To handle this, we default the name to "Unknown" if its description # isn't found in the cloud data to prevent KeyError. name = descriptions[query].get(entry["Index"], "Unknown") diff --git a/src/elmo/api/router.py b/src/elmo/api/router.py index 5edab8c..a517d69 100644 --- a/src/elmo/api/router.py +++ b/src/elmo/api/router.py @@ -54,3 +54,7 @@ def sectors(self): @property def inputs(self): return "{}/api/inputs".format(self._base_url) + + @property + def outputs(self): + return "{}/api/outputs".format(self._base_url) diff --git a/src/elmo/query.py b/src/elmo/query.py index 8625fdf..c7b0d52 100644 --- a/src/elmo/query.py +++ b/src/elmo/query.py @@ -1,3 +1,4 @@ SECTORS = 9 INPUTS = 10 ALERTS = 11 +OUTPUTS = 12 diff --git a/tests/test_client.py b/tests/test_client.py index 11821fa..bce867d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -273,21 +273,24 @@ def test_client_poll(server): ids = { query.SECTORS: 42, query.INPUTS: 4242, + query.OUTPUTS: 424, query.ALERTS: 424242, } # Test state = client.poll(ids) - assert len(state.keys()) == 4 + assert len(state.keys()) == 5 # Check response assert state["has_changes"] is False assert state["inputs"] is False assert state["areas"] is False + assert state["outputs"] is False assert state["statusadv"] is False # Check request body = server.calls[0].request.body.split("&") assert "sessionId=test" in body assert "Areas=42" in body assert "Inputs=4242" in body + assert "Outputs=424" in body assert "CanElevate=1" in body assert "ConnectionStatus=1" in body @@ -303,7 +306,7 @@ def test_client_poll_with_changes(server): "Areas": true, "Events": false, "Inputs": true, - "Outputs": false, + "Outputs": true, "Anomalies": false, "ReadStringsInProgress": false, "ReadStringPercentage": 0, @@ -322,14 +325,16 @@ def test_client_poll_with_changes(server): ids = { query.SECTORS: 42, query.INPUTS: 4242, + query.OUTPUTS: 424, query.ALERTS: 424242, } # Test state = client.poll(ids) - assert len(state.keys()) == 4 + assert len(state.keys()) == 5 assert state["has_changes"] is True assert state["inputs"] is True assert state["areas"] is True + assert state["outputs"] is True assert state["statusadv"] is True @@ -363,11 +368,12 @@ def test_client_poll_ignore_has_changes(server): ids = { query.SECTORS: 42, query.INPUTS: 4242, + query.OUTPUTS: 424, query.ALERTS: 424242, } # Test state = client.poll(ids) - assert len(state.keys()) == 4 + assert len(state.keys()) == 5 assert state["has_changes"] is False @@ -384,6 +390,7 @@ def test_client_poll_unknown_error(server): ids = { query.SECTORS: 42, query.INPUTS: 4242, + query.OUTPUTS: 424, query.ALERTS: 424242, } # Test @@ -423,6 +430,7 @@ def test_areas_missing(self, server): ids = { query.SECTORS: 42, query.INPUTS: 4242, + query.OUTPUTS: 424, query.ALERTS: 424242, } # Test @@ -459,6 +467,44 @@ def test_inputs_missing(self, server): ids = { query.SECTORS: 42, query.INPUTS: 4242, + query.OUTPUTS: 424, + query.ALERTS: 424242, + } + # Test + with pytest.raises(ParseError): + client.poll(ids) + + def test_outputs_missing(self, server): + """Should raise a ParseError if the response is different from what is expected. + In this case `Outputs` is missing from the response.""" + html = """ + { + "ConnectionStatus": false, + "CanElevate": false, + "LoggedIn": false, + "LoginInProgress": false, + "Areas": false, + "Events": true, + "Inputs": false, + "Anomalies": false, + "ReadStringsInProgress": false, + "ReadStringPercentage": 0, + "Strings": 0, + "ManagedAccounts": false, + "Temperature": false, + "StatusAdv": false, + "Images": false, + "AdditionalInfoSupported": true, + "HasChanges": false + } + """ + server.add(responses.POST, "https://example.com/api/updates", body=html, status=200) + client = ElmoClient(base_url="https://example.com", domain="domain") + client._session_id = "test" + ids = { + query.SECTORS: 42, + query.INPUTS: 4242, + query.OUTPUTS: 424, query.ALERTS: 424242, } # Test @@ -495,6 +541,7 @@ def test_statusadv_missing(self, server): ids = { query.SECTORS: 42, query.INPUTS: 4242, + query.OUTPUTS: 424, query.ALERTS: 424242, } # Test From 433accebcc7a0bcdefd56eba9e6be0c395401ec7 Mon Sep 17 00:00:00 2001 From: Alessandro Manighetti Date: Thu, 9 Nov 2023 11:36:01 +0100 Subject: [PATCH 2/3] feat: add query.OUTPUTS to collect outputs status --- tests/conftest.py | 1 + tests/fixtures/responses.py | 78 +++++++++++++++++++++++++++++++ tests/test_client.py | 93 +++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 6607947..62f0a65 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,6 +29,7 @@ def client(): server.add(responses.POST, "https://example.com/api/strings", body=r.STRINGS, status=200) server.add(responses.POST, "https://example.com/api/areas", body=r.AREAS, status=200) server.add(responses.POST, "https://example.com/api/inputs", body=r.INPUTS, status=200) + server.add(responses.POST, "https://example.com/api/outputs", body=r.OUTPUTS, status=200) yield client diff --git a/tests/fixtures/responses.py b/tests/fixtures/responses.py index 24878ee..59883cb 100644 --- a/tests/fixtures/responses.py +++ b/tests/fixtures/responses.py @@ -134,6 +134,38 @@ def test_client_get_sectors_status(server): "Description": "Outdoor Sensor 3", "Created": "/Date(1546004147493+0100)/", "Version": "AAAAAAAAgRw=" + }, + { + "AccountId": 1, + "Class": 12, + "Index": 0, + "Description": "Output 1", + "Created": "/Date(1546004147493+0100)/", + "Version": "AAAAAAAAgRw=" + }, + { + "AccountId": 1, + "Class": 12, + "Index": 1, + "Description": "Output 2", + "Created": "/Date(1546004147493+0100)/", + "Version": "AAAAAAAAgRw=" + }, + { + "AccountId": 1, + "Class": 12, + "Index": 2, + "Description": "Output 3", + "Created": "/Date(1546004147493+0100)/", + "Version": "AAAAAAAAgRw=" + }, + { + "AccountId": 3, + "Class": 12, + "Index": 3, + "Description": "Output 4", + "Created": "/Date(1546004147493+0100)/", + "Version": "AAAAAAAAgRw=" } ]""" AREAS = """[ @@ -240,3 +272,49 @@ def test_client_get_sectors_status(server): "InProgress": false } ]""" +OUTPUTS = """[ + { + "Active": true, + "InUse": true, + "DoNotRequireAuthentication": true, + "ControlDeniedToUsers": false, + "Id": 400258, + "Index": 0, + "Element": 1, + "CommandId": 0, + "InProgress": false + }, + { + "Active": false, + "InUse": true, + "DoNotRequireAuthentication": false, + "ControlDeniedToUsers": false, + "Id": 400259, + "Index": 1, + "Element": 2, + "CommandId": 0, + "InProgress": false + }, + { + "Active": false, + "InUse": true, + "DoNotRequireAuthentication": false, + "ControlDeniedToUsers": false, + "Id": 400260, + "Index": 2, + "Element": 3, + "CommandId": 0, + "InProgress": false + }, + { + "Active": false, + "InUse": false, + "DoNotRequireAuthentication": false, + "ControlDeniedToUsers": false, + "Id": 400261, + "Index": 3, + "Element": 4, + "CommandId": 0, + "InProgress": false + } +]""" diff --git a/tests/test_client.py b/tests/test_client.py index bce867d..4c97483 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1566,6 +1566,99 @@ def test_client_get_inputs_status(server, mocker): } +def test_client_get_outputs_status(server, mocker): + """Should query a Elmo system to retrieve inputs status.""" + html = """[ + { + "Active": true, + "InUse": true, + "DoNotRequireAuthentication": true, + "ControlDeniedToUsers": false, + "Id": 400258, + "Index": 0, + "Element": 1, + "CommandId": 0, + "InProgress": false + }, + { + "Active": false, + "InUse": true, + "DoNotRequireAuthentication": false, + "ControlDeniedToUsers": false, + "Id": 400259, + "Index": 1, + "Element": 2, + "CommandId": 0, + "InProgress": false + }, + { + "Active": false, + "InUse": true, + "DoNotRequireAuthentication": false, + "ControlDeniedToUsers": false, + "Id": 400260, + "Index": 2, + "Element": 3, + "CommandId": 0, + "InProgress": false + }, + { + "Active": false, + "InUse": false, + "DoNotRequireAuthentication": false, + "ControlDeniedToUsers": false, + "Id": 400261, + "Index": 3, + "Element": 4, + "CommandId": 0, + "InProgress": false + } + ]""" + # query() depends on _get_descriptions() + server.add(responses.POST, "https://example.com/api/outputs", body=html, status=200) + client = ElmoClient(base_url="https://example.com", domain="domain") + client._session_id = "test" + mocker.patch.object(client, "_get_descriptions") + client._get_descriptions.return_value = { + 12: {0: "Output 1", 1: "Output 2", 2: "Output 3", 3: "Output 4"}, + } + # Test + outputs = client.query(query.OUTPUTS) + # Expected output + assert client._get_descriptions.called is True + assert len(server.calls) == 1 + + assert outputs == { + "last_id": 400261, + "outputs": { + 0: { + "element": 1, + "id": 400258, + "index": 0, + "status": True, + "excluded": False, + "name": "Output 1", + }, + 1: { + "element": 2, + "id": 400259, + "index": 1, + "status": False, + "excluded": False, + "name": "Output 2", + }, + 2: { + "element": 3, + "id": 400260, + "status": False, + "index": 2, + "excluded": False, + "name": "Output 3", + }, + }, + } + + def test_client_get_sectors_missing_area(server, mocker): """Should set an Unknown `sector` name if the description is missing. Regression test for: https://github.com/palazzem/econnect-python/issues/91""" From 06c65dc44d357e2f2e358c2c78f0561f08919c10 Mon Sep 17 00:00:00 2001 From: Alessandro Manighetti Date: Thu, 9 Nov 2023 11:41:31 +0100 Subject: [PATCH 3/3] feat: add query.OUTPUTS to collect outputs status --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 4c97483..90e8563 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1567,7 +1567,7 @@ def test_client_get_inputs_status(server, mocker): def test_client_get_outputs_status(server, mocker): - """Should query a Elmo system to retrieve inputs status.""" + """Should query a Elmo system to retrieve outputs status.""" html = """[ { "Active": true,