"""Implements the connection to the ihc controller.""" # pylint: disable=bare-except import base64 import datetime import io import xml.etree.ElementTree as ET import zlib from typing import Any, ClassVar, Literal from ihcsdk.ihcconnection import IHCConnection from ihcsdk.ihcsslconnection import IHCSSLConnection IHCSTATE_READY = "text.ctrl.state.ready" class IHCSoapClient: """Implements a limited set of the soap request for the IHC controller.""" ihcns: ClassVar[dict[str, str]] = { "SOAP-ENV": "http://schemas.xmlsoap.org/soap/envelope/", "ns1": "utcs", "ns2": "utcs.values", "ns3": "utcs.values", } def __init__(self, url: str) -> None: """Initialize the IIHCSoapClient with a url for the controller.""" self.url = url self.username = "" self.password = "" if url.startswith("https://"): self.connection = IHCSSLConnection(url) else: self.connection = IHCConnection(url) def close(self) -> None: """Close the connection.""" self.connection.close() self.connection = None def authenticate(self, username: str, password: str) -> bool: """ Do an Authentricate request. And save the cookie returned to be used on the following requests. Return True if the request was successfull. """ self.username = username self.password = password auth_payload = """ {password} {username} treeview """ payload = auth_payload.format(password=self.password, username=self.username) xdoc = self.connection.soap_action( "/ws/AuthenticationService", "authenticate", payload ) if xdoc is not False: isok = xdoc.find( "./SOAP-ENV:Body/ns1:authenticate2/ns1:loginWasSuccessful", IHCSoapClient.ihcns, ) return isok.text == "true" return False def get_state(self) -> str: """Get the controller state.""" xdoc = self.connection.soap_action("/ws/ControllerService", "getState", "") if xdoc is not False: return xdoc.find( "./SOAP-ENV:Body/ns1:getState1/ns1:state", IHCSoapClient.ihcns ).text return False def wait_for_state_change(self, state: str, waitsec: int) -> str: """Wait for controller state change and return state.""" payload = f""" {state} {waitsec} """ xdoc = self.connection.soap_action( "/ws/ControllerService", "waitForControllerStateChange", payload ) if xdoc is not False: return xdoc.find( "./SOAP-ENV:Body/ns1:waitForControllerStateChange3/ns1:state", IHCSoapClient.ihcns, ).text return False def get_project(self) -> str: """ Get the ihc project in single SOAP action. You should use the get_project_in_segments to get the project in multiple segments. This will stress the IHC controller less. """ xdoc = self.connection.soap_action("/ws/ControllerService", "getIHCProject", "") if xdoc is not False: base64data = xdoc.find( "./SOAP-ENV:Body/ns1:getIHCProject1/ns1:data", IHCSoapClient.ihcns ).text if not base64: return False compresseddata = base64.b64decode(base64data) return zlib.decompress(compresseddata, 16 + zlib.MAX_WBITS).decode( "ISO-8859-1" ) return False def get_project_in_segments(self, info: dict[str, Any] | None = None) -> str: """ Get the ihc project per segments. Param: info .. reuse existing project info. If not provided, the get_project_info() is called internally. """ if info is None: info = self.get_project_info() if info: project_major = info.get("projectMajorRevision", 0) project_minor = info.get("projectMinorRevision", 0) buffer = io.BytesIO() for s in range(self.get_project_number_of_segments()): segment = self.get_project_segment(s, project_major, project_minor) if segment is False: return False buffer.write(segment) return zlib.decompress(buffer.getvalue(), 16 + zlib.MAX_WBITS).decode( "ISO-8859-1" ) return False def get_project_info(self) -> dict[str, Any]: """Return dictionary of project info items.""" xdoc = self.connection.soap_action( "/ws/ControllerService", "getProjectInfo", "" ) if xdoc is not False: info = {} elem = xdoc.find("./SOAP-ENV:Body/ns1:getProjectInfo1", IHCSoapClient.ihcns) if elem is not None: for e in list(elem): name = e.tag.split("}")[-1] info[name] = IHCSoapClient.__get_value(e) return info return False def get_project_number_of_segments(self) -> int: """Return the number of segments needed to fetch the current ihc-project.""" xdoc = self.connection.soap_action( "/ws/ControllerService", "getIHCProjectNumberOfSegments", "" ) if xdoc is not False: return int( xdoc.find( "./SOAP-ENV:Body/ns1:getIHCProjectNumberOfSegments1", IHCSoapClient.ihcns, ).text ) return False def get_project_segment( self, segment: int, project_major: int, project_minor: int ) -> bytes: """ Return a segment of the ihc-project with the given number. Returns null if the segment number increases above the number of segments available. The segments are offset from 0. The project-versions given as parameters are used to indentify the project that should be fetched. That is, to make sure that you suddenly don't get segments belonging to another project. """ payload = f""" {segment} {project_major} {project_minor} """ xdoc = self.connection.soap_action( "/ws/ControllerService", "getIHCProjectSegment", payload ) if xdoc is not False: base64data = xdoc.find( "./SOAP-ENV:Body/ns1:getIHCProjectSegment4/ns1:data", IHCSoapClient.ihcns, ).text if not base64: return False return base64.b64decode(base64data) return False def set_runtime_value_bool(self, resourceid: int, value: bool) -> bool: """Set a boolean runtime value.""" boolvalue = "true" if value else "false" payload = f""" {boolvalue} {resourceid} true """ xdoc = self.connection.soap_action( "/ws/ResourceInteractionService", "setResourceValue", payload ) if xdoc is not False: result = xdoc.find( r"./SOAP-ENV:Body/ns1:setResourceValue2", IHCSoapClient.ihcns ).text return result == "true" return False def set_runtime_value_int(self, resourceid: int, intvalue: int) -> bool: """Set a integer runtime value.""" payload = f""" {intvalue} {resourceid} true """ xdoc = self.connection.soap_action( "/ws/ResourceInteractionService", "setResourceValue", payload ) if xdoc is not False: result = xdoc.find( "./SOAP-ENV:Body/ns1:setResourceValue2", IHCSoapClient.ihcns ).text return result == "true" return False def set_runtime_value_float(self, resourceid: int, floatvalue: float) -> bool: """Set a flot runtime value.""" payload = f""" {floatvalue} {resourceid} true """ xdoc = self.connection.soap_action( "/ws/ResourceInteractionService", "setResourceValue", payload ) if xdoc is not False: result = xdoc.find( "./SOAP-ENV:Body/ns1:setResourceValue2", IHCSoapClient.ihcns ).text return result == "true" return False def set_runtime_value_timer(self, resourceid: int, timer: int) -> bool: """Set a timer runtime value in milliseconds.""" payload = f""" {timer} {resourceid} true """ xdoc = self.connection.soap_action( "/ws/ResourceInteractionService", "setResourceValue", payload ) if xdoc is not False: result = xdoc.find( "./SOAP-ENV:Body/ns1:setResourceValue2", IHCSoapClient.ihcns ).text return result == "true" return False def set_runtime_value_time( self, resourceid: int, hours: int, minutes: int, seconds: int ) -> bool: """Set a time runtime value in hours:minutes:seconds.""" payload = f""" {hours} {minutes} {seconds} {resourceid} true """ xdoc = self.connection.soap_action( "/ws/ResourceInteractionService", "setResourceValue", payload ) if xdoc is not False: result = xdoc.find( "./SOAP-ENV:Body/ns1:setResourceValue2", IHCSoapClient.ihcns ).text return result == "true" return False @staticmethod def _get_time(resource_value: ET.Element) -> datetime.time: hours = int(resource_value.find("./ns2:hours", IHCSoapClient.ihcns).text) minutes = int(resource_value.find("./ns2:minutes", IHCSoapClient.ihcns).text) seconds = int(resource_value.find("./ns2:seconds", IHCSoapClient.ihcns).text) return datetime.time(hours, minutes, seconds) @staticmethod def _get_datetime(resource_value: ET.Element) -> datetime.datetime: year = int(resource_value.find("./ns1:year", IHCSoapClient.ihcns).text) month = int( resource_value.find("./ns1:monthWithJanuaryAsOne", IHCSoapClient.ihcns).text ) day = int(resource_value.find("./ns1:day", IHCSoapClient.ihcns).text) hours = int(resource_value.find("./ns1:hours", IHCSoapClient.ihcns).text) minutes = int(resource_value.find("./ns1:minutes", IHCSoapClient.ihcns).text) seconds = int(resource_value.find("./ns1:seconds", IHCSoapClient.ihcns).text) return datetime.datetime(year, month, day, hours, minutes, seconds) # noqa: DTZ001 @staticmethod def _get_date(resource_value: ET.Element) -> datetime.datetime: year = int(resource_value.find("./ns2:year", IHCSoapClient.ihcns).text) if year == 0: year = datetime.datetime.today().year # noqa: DTZ002 month = int(resource_value.find("./ns2:month", IHCSoapClient.ihcns).text) day = int(resource_value.find("./ns2:day", IHCSoapClient.ihcns).text) return datetime.datetime(year, month, day) # noqa: DTZ001 @staticmethod def __get_value( resource_value: ET.Element, ) -> bool | int | float | str | datetime.datetime | None: """Get a runtime value from the xml base on the type in the xml.""" if resource_value is None: return None valuetype = resource_value.attrib[ "{http://www.w3.org/2001/XMLSchema-instance}type" ].split(":")[1] result = resource_value.text match valuetype: case "WSBooleanValue": result = ( resource_value.find("./ns2:value", IHCSoapClient.ihcns).text == "true" ) case "WSIntegerValue": result = int( resource_value.find("./ns2:integer", IHCSoapClient.ihcns).text ) case "WSFloatingPointValue": result = round( float( resource_value.find( "./ns2:floatingPointValue", IHCSoapClient.ihcns ).text ), 2, ) case "WSEnumValue": result = resource_value.find("./ns2:enumName", IHCSoapClient.ihcns).text case "WSTimerValue": return int( resource_value.find("./ns2:milliseconds", IHCSoapClient.ihcns).text ) case "WSTimeValue": result = IHCSoapClient._get_time(resource_value) case "WSDate": result = IHCSoapClient._get_datetime(resource_value) case "WSDateValue": result = IHCSoapClient._get_date(resource_value) case "int": result = int(resource_value.text) return result def get_runtime_value( self, resourceid: int ) -> bool | int | float | str | datetime.datetime | None: """ Get runtime value of specified resource id. The returned value will be boolean, integer or float Return None if resource cannot be found or on error """ payload = f"""{resourceid} """ xdoc = self.connection.soap_action( "/ws/ResourceInteractionService", "getResourceValue", payload ) if xdoc is False: return None value = xdoc.find( "./SOAP-ENV:Body/ns1:getRuntimeValue2/ns1:value", IHCSoapClient.ihcns ) return IHCSoapClient.__get_value(value) def get_runtime_values( self, resourceids: list[int] ) -> dict[int, Any] | Literal[False]: """ Get runtime values of specified resource ids. Return None if resource cannot be found or on error """ idsarr = "" for ihcid in resourceids: idsarr += f"{ihcid}" payload = '' + idsarr + "" xdoc = self.connection.soap_action( "/ws/ResourceInteractionService", "getResourceValues", payload ) if xdoc is False: return False result = xdoc.findall( "./SOAP-ENV:Body/ns1:getRuntimeValues2/ns1:arrayItem", IHCSoapClient.ihcns ) changes = {} for item in result: ihcid = item.find("ns1:resourceID", IHCSoapClient.ihcns) if ihcid is None: continue resource_value = item.find("./ns1:value", IHCSoapClient.ihcns) item_value = IHCSoapClient.__get_value(resource_value) if item_value is not None: changes[int(ihcid.text)] = item_value return changes def cycle_bool_value(self, resourceid: int) -> bool | None: """ Turn a booelan resource On and back Off. Return None if resource cannot be found or on error """ set_bool = ( "" '' "{value}" "" "" "{id}" "true" "" ) payload = ( '' + set_bool.format(value="true", id=resourceid) + set_bool.format(value="false", id=resourceid) + "" ) xdoc = self.connection.soap_action( "/ws/ResourceInteractionService", "SOAPAction: setResourceValues", payload ) if xdoc is False: return None return True def enable_runtime_notification(self, resourceid: int) -> bool: """Enable notification for specified resource id.""" return self.enable_runtime_notifications([resourceid]) def enable_runtime_notifications(self, resourceids: list[int]) -> bool: """Enable notification for specified resource ids.""" idsarr = "" for ihcid in resourceids: idsarr += f"{ihcid}" payload = f""" {idsarr} """ xdoc = self.connection.soap_action( "/ws/ResourceInteractionService", "enableRuntimeValueNotifications", payload ) return xdoc is not False def wait_for_resource_value_changes( self, wait: int = 10 ) -> dict[int, str] | Literal[False]: """ Long polling for changes. And return a dictionary with resource:value for changes (Only last change) """ change_list = self.wait_for_resource_value_change_list(wait) if change_list is False: return False return dict(change_list) def wait_for_resource_value_change_list( self, wait: int = 10 ) -> list[(int, Any)] | Literal[False]: """ Long polling for changes. And return a resource id dictionary with a list of all changes since last poll. Return a list of tuples with the id,value """ changes = [] payload = f"""{wait} """ xdoc = self.connection.soap_action( "/ws/ResourceInteractionService", "getResourceValue", payload ) if xdoc is False: return False result = xdoc.findall( "./SOAP-ENV:Body/ns1:waitForResourceValueChanges2/ns1:arrayItem", IHCSoapClient.ihcns, ) for item in result: ihcid = item.find("ns1:resourceID", IHCSoapClient.ihcns) if ihcid is None: continue resource_value = item.find("./ns1:value", IHCSoapClient.ihcns) value = IHCSoapClient.__get_value(resource_value) if value is not None: changes.append((int(ihcid.text), value)) return changes def get_user_log(self, language: str = "da") -> str | Literal[False]: """Get the controller state.""" payload = f""" 0 {language} """ xdoc = self.connection.soap_action( "/ws/ConfigurationService", "getUserLog", payload ) if xdoc is not False: base64data = xdoc.find( "./SOAP-ENV:Body/ns1:getUserLog4/ns1:data", IHCSoapClient.ihcns ).text if not base64data: return False return base64.b64decode(base64data).decode("UTF-8") return False def clear_user_log(self) -> None: """Clear the user log in the controller.""" self.connection.soap_action("/ws/ConfigurationService", "clearUserLog", "") def get_system_info(self) -> dict[str, str] | bool: """Get controller system info.""" xdoc = self.connection.soap_action( "/ws/ConfigurationService", "getSystemInfo", "" ) if xdoc is False: return False return { "uptime": IHCSoapClient._extract_sysinfo(xdoc, "uptime"), "realtimeclock": IHCSoapClient._extract_sysinfo(xdoc, "realtimeclock"), "serial_number": IHCSoapClient._extract_sysinfo(xdoc, "serialNumber"), "production_date": IHCSoapClient._extract_sysinfo(xdoc, "productionDate"), "brand": IHCSoapClient._extract_sysinfo(xdoc, "brand"), "version": IHCSoapClient._extract_sysinfo(xdoc, "version"), "hw_revision": IHCSoapClient._extract_sysinfo(xdoc, "hwRevision"), "sw_date": IHCSoapClient._extract_sysinfo(xdoc, "swDate"), "dataline_version": IHCSoapClient._extract_sysinfo(xdoc, "datalineVersion"), "rf_module_software_version": IHCSoapClient._extract_sysinfo( xdoc, "rfModuleSoftwareVersion" ), "rf_module_serial_number": IHCSoapClient._extract_sysinfo( xdoc, "rfModuleSerialNumber" ), "application_is_without_viewer": IHCSoapClient._extract_sysinfo( xdoc, "applicationIsWithoutViewer" ), "sms_modem_software_version": IHCSoapClient._extract_sysinfo( xdoc, "smsModemSoftwareVersion" ), "led_dimmer_software_version": IHCSoapClient._extract_sysinfo( xdoc, "ledDimmerSoftwareVersion" ), } @staticmethod def _extract_sysinfo(xdoc: ET.Element, param: str) -> str: """Extract a parameter from system info.""" element = xdoc.find( f"./SOAP-ENV:Body/ns1:getSystemInfo1/ns1:{param}", IHCSoapClient.ihcns ) if element is None: return None return element.text