├── MANIFEST.in ├── create_dist.sh ├── .gitignore ├── boschshcpy ├── room.py ├── scenario.py ├── exceptions.py ├── __init__.py ├── userdefinedstate.py ├── generate_cert.py ├── message.py ├── tls_ca_chain.pem ├── emma.py ├── device_service.py ├── rawscan.py ├── device.py ├── domain_impl.py ├── register_client.py ├── information.py ├── api.py ├── device_helper.py ├── session.py ├── services_impl.py └── models_impl.py ├── examples ├── apitest.py ├── discover.py ├── messages.py ├── apisummary.py ├── pollingtest.py └── register_client.py ├── LICENSE ├── setup.py └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include boschshcpy/tls_ca_chain.pem 2 | -------------------------------------------------------------------------------- /create_dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | source venv/bin/activate 4 | rm -rf dist/* 5 | rm -rf build/* 6 | python3 setup.py sdist bdist_wheel 7 | python3 -m twine upload dist/* 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | build/ 3 | dist/ 4 | venv/ 5 | boschshcpy.egg-info/ 6 | boschshcpy_dev.egg-info/ 7 | examples/keystore 8 | __pycache__ 9 | *.pyc 10 | .project 11 | .pydevproject 12 | .vscode/settings.json 13 | .env 14 | *.pem 15 | *.json 16 | -------------------------------------------------------------------------------- /boschshcpy/room.py: -------------------------------------------------------------------------------- 1 | class SHCRoom: 2 | def __init__(self, api, raw_room): 3 | self._api = api 4 | self._raw_room = raw_room 5 | 6 | @property 7 | def id(self): 8 | return self._raw_room["id"] 9 | 10 | @property 11 | def icon_id(self): 12 | return self._raw_room["iconId"] 13 | 14 | @property 15 | def name(self): 16 | return self._raw_room["name"] 17 | 18 | def summary(self): 19 | print(f"Room: {self.id}") 20 | print(f" Name : {self.name}") 21 | print(f" Icon Id: {self.icon_id}") 22 | -------------------------------------------------------------------------------- /boschshcpy/scenario.py: -------------------------------------------------------------------------------- 1 | from .api import SHCAPI 2 | 3 | 4 | class SHCScenario: 5 | def __init__(self, api: SHCAPI, raw_scenario): 6 | self._api = api 7 | self._raw_scenario = raw_scenario 8 | 9 | @property 10 | def id(self): 11 | return self._raw_scenario["id"] 12 | 13 | @property 14 | def icon_id(self): 15 | return self._raw_scenario["iconId"] 16 | 17 | @property 18 | def name(self): 19 | return self._raw_scenario["name"] 20 | 21 | def trigger(self): 22 | return self._api._post_api_or_fail( 23 | f"{self._api._api_root}/scenarios/{self.id}/triggers", "" 24 | ) 25 | 26 | def summary(self): 27 | print(f"scenario: {self.id}") 28 | print(f" Name : {self.name}") 29 | print(f" Icon Id: {self.icon_id}") 30 | -------------------------------------------------------------------------------- /boschshcpy/exceptions.py: -------------------------------------------------------------------------------- 1 | class JSONRPCError(Exception): 2 | """Error to indicate a JSON RPC problem.""" 3 | 4 | def __init__(self, code, message): 5 | super().__init__() 6 | self._code = code 7 | self._message = message 8 | 9 | @property 10 | def code(self): 11 | return self._code 12 | 13 | @property 14 | def message(self): 15 | return self._message 16 | 17 | def __str__(self): 18 | return f"JSONRPCError (code: {self.code}, message: {self.message})" 19 | 20 | class SHCException(Exception): 21 | """Generic SHC exception.""" 22 | def __init__(self, message): 23 | super().__init__() 24 | self._message = message 25 | 26 | @property 27 | def message(self): 28 | return self._message 29 | 30 | def __str__(self): 31 | return f"SHC Error (message: {self.message})" 32 | 33 | 34 | class SHCConnectionError(Exception): 35 | """Error to indicate a connection problem.""" 36 | 37 | 38 | class SHCAuthenticationError(Exception): 39 | """Error to indicate an authentication problem.""" 40 | 41 | 42 | class SHCRegistrationError(SHCException): 43 | """Error to indicate an error during client registration.""" 44 | 45 | class SHCSessionError(SHCException): 46 | """Error to indicate a session problem.""" 47 | -------------------------------------------------------------------------------- /boschshcpy/__init__.py: -------------------------------------------------------------------------------- 1 | from .device import SHCDevice 2 | from .device_helper import ( 3 | SHCBatteryDevice, 4 | SHCCamera360, 5 | SHCCameraEyes, 6 | SHCClimateControl, 7 | SHCDeviceHelper, 8 | SHCLight, 9 | SHCMotionDetector, 10 | SHCShutterContact, 11 | SHCShutterContact2, 12 | SHCShutterContact2Plus, 13 | SHCShutterControl, 14 | SHCMicromoduleBlinds, 15 | SHCMicromoduleDimmer, 16 | SHCMicromoduleShutterControl, 17 | SHCMicromoduleRelay, 18 | SHCLightControl, 19 | SHCLightSwitch, 20 | SHCLightSwitchBSM, 21 | SHCPresenceSimulationSystem, 22 | SHCSmartPlug, 23 | SHCSmartPlugCompact, 24 | SHCSmokeDetector, 25 | SHCSmokeDetectionSystem, 26 | SHCThermostat, 27 | SHCRoomThermostat2, 28 | SHCTwinguard, 29 | SHCUniversalSwitch, 30 | SHCWallThermostat, 31 | SHCWaterLeakageSensor, 32 | ) 33 | from .device_service import SHCDeviceService 34 | from .domain_impl import SHCIntrusionSystem 35 | from .exceptions import SHCAuthenticationError, SHCConnectionError, SHCRegistrationError 36 | from .information import SHCInformation 37 | from .register_client import SHCRegisterClient 38 | from .scenario import SHCScenario 39 | from .session import SHCSession 40 | from .userdefinedstate import SHCUserDefinedState 41 | from .message import SHCMessage 42 | from .emma import SHCEmma 43 | -------------------------------------------------------------------------------- /examples/apitest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Use this script to register a new client connection to Bosch Smart Home products 4 | # See https://github.com/BoschSmartHome/bosch-shc-api-docs 5 | # Before executing the script to register a new client, the button on the controller has to be pressed until the LED begins flashing. 6 | 7 | import os, sys 8 | import time 9 | 10 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 11 | 12 | import boschshcpy 13 | 14 | def api_test(): 15 | session = boschshcpy.SHCSession(args.ip_address, args.access_cert, args.access_key, False) 16 | session.information.summary() 17 | 18 | if __name__ == "__main__": 19 | import argparse, sys 20 | 21 | parser = argparse.ArgumentParser() 22 | parser.add_argument("-ac", "--access_cert", 23 | help="Path to access certificate.", 24 | default="keystore/boschshc-cert.pem") 25 | parser.add_argument("-ak", "--access_key", 26 | help="Path to access key.", 27 | default="keystore/boschshc-key.pem") 28 | parser.add_argument("-ip", "--ip_address", 29 | help="IP of the smart home controller.") 30 | args = parser.parse_args() 31 | 32 | if len(sys.argv) == 1: 33 | parser.print_help() 34 | sys.exit() 35 | 36 | api_test() 37 | -------------------------------------------------------------------------------- /examples/discover.py: -------------------------------------------------------------------------------- 1 | from zeroconf import ServiceBrowser, Zeroconf, ServiceInfo, IPVersion 2 | 3 | 4 | class MyListener: 5 | def filter(self, info: ServiceInfo): 6 | if "Bosch SHC" in info.name: 7 | print(f"SHC Device found!") 8 | print(f"Name: {info.get_name()}") 9 | print(f"IP: {info.parsed_addresses(IPVersion.V4Only)}") 10 | for host in info.parsed_addresses(IPVersion.V4Only): 11 | if host.startswith("169."): 12 | continue 13 | print(f"Found host {host}") 14 | server_epos = info.server.find(".local.") 15 | if server_epos > -1: 16 | print(f"server: {info.server[:server_epos]}") 17 | 18 | # print("Service %s added, service info: %s" % (name, info)) 19 | 20 | def update_service(self, arg0, arg1, arg2): 21 | return 22 | 23 | def remove_service(self, zeroconf, type, name): 24 | print("Service %s removed" % (name,)) 25 | 26 | def add_service(self, zeroconf, type, name): 27 | info = zeroconf.get_service_info(type, name) 28 | self.filter(info) 29 | 30 | 31 | zeroconf = Zeroconf() 32 | listener = MyListener() 33 | browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener) 34 | try: 35 | input("Press enter to exit...\n\n") 36 | finally: 37 | browser.cancel() 38 | zeroconf.close() 39 | -------------------------------------------------------------------------------- /boschshcpy/userdefinedstate.py: -------------------------------------------------------------------------------- 1 | from .api import SHCAPI 2 | from .information import SHCInformation 3 | from .exceptions import SHCException 4 | 5 | 6 | class SHCUserDefinedState: 7 | def __init__(self, api: SHCAPI, info: SHCInformation, raw_state): 8 | self._api = api 9 | self._info = info 10 | self._raw_state = raw_state 11 | 12 | @property 13 | def id(self): 14 | return self._raw_state["id"] 15 | 16 | @property 17 | def root_device_id(self): 18 | return self._info.macAddress 19 | 20 | @property 21 | def name(self): 22 | return self._raw_state["name"] 23 | 24 | @property 25 | def deleted(self): 26 | return self._raw_state["deleted"] 27 | 28 | @property 29 | def state(self): 30 | return self._raw_state["state"] 31 | 32 | @state.setter 33 | def state(self, state: bool): 34 | return self._api._put_api_or_fail( 35 | f"{self._api._api_root}/userdefinedstates/{self.id}/state", state 36 | ) 37 | 38 | def update_raw_information(self, raw_state): 39 | if self._raw_state["id"] != raw_state["id"]: 40 | raise SHCException("Error due to mismatching ids!") 41 | self._raw_state = raw_state 42 | 43 | def summary(self): 44 | print(f"userdefinedstate: {self.id}") 45 | print(f" Name : {self.name}") 46 | print(f" State : {self.state}") 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Clemens-Alexander Brust, Thomas Schamm 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /examples/messages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Use this script to see messages from api 4 | 5 | import os, sys 6 | import time 7 | import logging 8 | from datetime import datetime 9 | 10 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 11 | 12 | import boschshcpy 13 | 14 | logger = logging.getLogger("boschshcpy") 15 | 16 | def api_test(): 17 | session = boschshcpy.SHCSession(args.ip_address, args.access_cert, args.access_key, False) 18 | #session.information.summary() 19 | logger.debug("getting messages") 20 | for _message in session.messages: 21 | print(f"Timestamp of message: {datetime.fromtimestamp(_message.timestamp/1000.0)}") 22 | _message.summary() 23 | 24 | 25 | if __name__ == "__main__": 26 | import argparse, sys 27 | 28 | logging.basicConfig(level=logging.DEBUG) 29 | 30 | parser = argparse.ArgumentParser() 31 | parser.add_argument("-ac", "--access_cert", 32 | help="Path to access certificate.", 33 | default="keystore/boschshc-cert.pem") 34 | parser.add_argument("-ak", "--access_key", 35 | help="Path to access key.", 36 | default="keystore/boschshc-key.pem") 37 | parser.add_argument("-ip", "--ip_address", 38 | help="IP of the smart home controller.") 39 | args = parser.parse_args() 40 | 41 | if len(sys.argv) == 1: 42 | parser.print_help() 43 | sys.exit() 44 | 45 | api_test() 46 | -------------------------------------------------------------------------------- /boschshcpy/generate_cert.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from cryptography import x509 4 | from cryptography.hazmat.backends import default_backend 5 | from cryptography.hazmat.primitives import hashes, serialization 6 | from cryptography.hazmat.primitives.asymmetric import rsa 7 | from cryptography.x509.oid import NameOID 8 | 9 | 10 | def generate_certificate(client_id, orgname) -> x509.Certificate: 11 | key = rsa.generate_private_key( 12 | public_exponent=65537, key_size=2048, backend=default_backend() 13 | ) 14 | 15 | name = x509.Name( 16 | [ 17 | x509.NameAttribute(NameOID.COMMON_NAME, client_id), 18 | x509.NameAttribute(NameOID.ORGANIZATION_NAME, orgname), 19 | ] 20 | ) 21 | 22 | utc_now = datetime.utcnow() 23 | cert = ( 24 | x509.CertificateBuilder() 25 | .serial_number(1000) 26 | .issuer_name(name) 27 | .subject_name(name) 28 | .public_key(key.public_key()) 29 | .not_valid_before(utc_now) 30 | .not_valid_after(utc_now + timedelta(days=10 * 365)) 31 | .add_extension(x509.BasicConstraints(ca=True, path_length=None), True) 32 | .sign(key, hashes.SHA256(), default_backend()) 33 | ) 34 | cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM) 35 | 36 | key_pem = key.private_bytes( 37 | encoding=serialization.Encoding.PEM, 38 | format=serialization.PrivateFormat.TraditionalOpenSSL, 39 | encryption_algorithm=serialization.NoEncryption(), 40 | ) 41 | 42 | return cert_pem, key_pem 43 | -------------------------------------------------------------------------------- /examples/apisummary.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import zeroconf 5 | from zeroconf import Zeroconf 6 | 7 | sys.path.append(os.path.join(os.path.dirname(__file__), "..")) 8 | 9 | from boschshcpy import ( 10 | SHCSession, 11 | SHCDeviceHelper, 12 | SHCThermostat, 13 | SHCBatteryDevice, 14 | SHCShutterContact, 15 | SHCShutterContact2, 16 | ) 17 | 18 | # Create session with additional zeroconf info 19 | zeroconf = Zeroconf() 20 | session = SHCSession( 21 | controller_ip="192.168.1.6", 22 | certificate="../keystore/dev-cert.pem", 23 | key="../keystore/dev-key.pem", 24 | zeroconf=zeroconf, 25 | ) 26 | zeroconf.close() 27 | 28 | for device in session.devices: 29 | device.summary() 30 | 31 | for room in session.rooms: 32 | room.summary() 33 | 34 | for scenario in session.scenarios: 35 | scenario.summary() 36 | 37 | for state in session.userdefinedstates: 38 | state.summary() 39 | 40 | session.intrusion_system.summary() 41 | 42 | session.information.summary() 43 | 44 | res = session.rawscan( 45 | command="device_service", 46 | device_id="hdm:ZigBee:000d6f000b856cb6", 47 | service_id="MultiLevelSensor", 48 | ) 49 | 50 | device = next(iter(session.device_helper.shutter_contacts)) 51 | print(f"isinstance SHCShutterContact2: {isinstance(device, SHCShutterContact2)}") 52 | print(f"isinstance SHCShutterContact: {isinstance(device, SHCShutterContact)}") 53 | print(f"isinstance SHCBatteryDevice: {isinstance(device, SHCBatteryDevice)}") 54 | 55 | print(res) 56 | res = session.rawscan(command="intrusion_detection") 57 | print(res) 58 | -------------------------------------------------------------------------------- /boschshcpy/message.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import logging 3 | 4 | logger = logging.getLogger("boschshcpy") 5 | 6 | 7 | class SHCMessage: 8 | def __init__(self, api, raw_message): 9 | self.api = api 10 | self._raw_message = raw_message 11 | 12 | class MessageCode: 13 | def __init__(self, message_code): 14 | self._message_code = message_code 15 | 16 | @property 17 | def name(self): 18 | return self._message_code["name"] 19 | 20 | @property 21 | def category(self): 22 | return self._message_code["category"] 23 | 24 | @property 25 | def id(self): 26 | return self._raw_message["id"] 27 | 28 | @property 29 | def message_code(self) -> MessageCode: 30 | return self._raw_message["messageCode"] 31 | 32 | @property 33 | def source_type(self): 34 | return self._raw_message["sourceType"] 35 | 36 | @property 37 | def timestamp(self): 38 | return self._raw_message["timestamp"] 39 | 40 | @property 41 | def flags(self): 42 | return self._raw_message["flags"] 43 | 44 | @property 45 | def arguments(self): 46 | return self._raw_message["arguments"] 47 | 48 | def summary(self): 49 | print(f"Message : {self.id}") 50 | print(f" Source : {self.source_type}") 51 | print(f" Timestamp : {self.timestamp}") 52 | print(f" MessageCode: {self.message_code}") 53 | if self.flags: 54 | _flags_string = "; ".join(self.flags) 55 | print(f" Flags : {_flags_string}") 56 | print(f" Arguments : {self.arguments}") 57 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | import pathlib 3 | 4 | here = pathlib.Path(__file__).parent.resolve() 5 | 6 | # Get the long description from the README file 7 | long_description = (here / "README.md").read_text(encoding="utf-8") 8 | 9 | setup( 10 | name="boschshcpy", 11 | version="0.2.107", 12 | url="https://github.com/tschamm/boschshcpy", 13 | author="Clemens-Alexander Brust, Thomas Schamm", 14 | author_email="cabrust@pm.me, thomas@tschamm.de", 15 | description="Bosch Smart Home Controller API Python Library", 16 | keywords="boschshcpy", 17 | license="bsd-3-clause", 18 | long_description=long_description, 19 | long_description_content_type="text/markdown", 20 | packages=find_packages(), 21 | entry_points={ 22 | "console_scripts": [ 23 | "boschshc_rawscan=boschshcpy.rawscan:main", 24 | "boschshc_registerclient=boschshcpy.register_client:main", 25 | ], 26 | }, 27 | package_data={"": ["tls_ca_chain.pem"]}, 28 | classifiers=[ 29 | "Intended Audience :: Developers", 30 | "Operating System :: OS Independent", 31 | "Programming Language :: Python", 32 | "Programming Language :: Python :: 3", 33 | "License :: OSI Approved :: BSD License", 34 | ], 35 | python_requires=">=3.10, <4", 36 | install_requires=[ 37 | "cryptography>=3.3.2", 38 | "getmac>=0.8.2,<1", 39 | "requests>=2.22", 40 | "zeroconf>=0.28.0", 41 | ], 42 | project_urls={ 43 | "Bug Reports": "https://github.com/tschamm/boschshcpy/issues", 44 | "Source": "https://github.com/tschamm/boschshcpy", 45 | }, 46 | include_package_data=True, 47 | ) 48 | -------------------------------------------------------------------------------- /examples/pollingtest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys, time 3 | import zeroconf 4 | import logging 5 | from zeroconf import Zeroconf 6 | 7 | sys.path.append(os.path.join(os.path.dirname(__file__), "..")) 8 | 9 | from boschshcpy import SHCSession, SHCDevice 10 | 11 | logging.basicConfig(level=logging.DEBUG) 12 | 13 | 14 | def callback(): 15 | print("Notification of device update") 16 | 17 | 18 | def setup_session(): 19 | # Create a BoschSHC client with the specified ACCESS_CERT and ACCESS_KEY. 20 | zeroconf = Zeroconf() 21 | session = SHCSession( 22 | controller_ip="192.168.1.6", 23 | certificate="../keystore/dev-cert.pem", 24 | key="../keystore/dev-key.pem", 25 | zeroconf=zeroconf, 26 | ) 27 | zeroconf.close() 28 | 29 | shc_info = session.information 30 | print(" version : %s" % shc_info.version) 31 | print(" updateState : %s" % shc_info.updateState) 32 | 33 | print("Accessing all devices...") 34 | 35 | smart_plugs = session.device_helper.smart_plugs 36 | for item in smart_plugs: 37 | for service in item.device_services: 38 | service.subscribe_callback(item.id, callback) 39 | 40 | shutter_controls: SHCDevice = session.device_helper.shutter_controls 41 | for item in shutter_controls: 42 | for service in item.device_services: 43 | service.subscribe_callback(item.id, callback) 44 | 45 | return session 46 | 47 | 48 | def main(): 49 | exit = False 50 | session = setup_session() 51 | duration = 0 52 | while not exit: 53 | userInput = input("Enter seconds for polling duration (or nothing to exit) ") 54 | 55 | if userInput.isdigit(): 56 | duration = int(userInput) 57 | print("Starting polling for {} seconds".format(duration)) 58 | session.start_polling() 59 | time.sleep(duration) 60 | if session.emma: 61 | session.emma.summary() 62 | session.stop_polling() 63 | print("Stopping polling") 64 | else: 65 | exit = True 66 | print("OK Bye, bye.") 67 | 68 | 69 | if __name__ == "__main__": 70 | main() 71 | -------------------------------------------------------------------------------- /boschshcpy/tls_ca_chain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFujCCA6KgAwIBAgIUIbQ+BIVcGVD29UIe+Sv6/+Qy/OUwDQYJKoZIhvcNAQEL 3 | BQAwYzELMAkGA1UEBhMCREUxITAfBgNVBAoMGEJvc2NoIFRoZXJtb3RlY2huaWsg 4 | R21iSDExMC8GA1UEAwwoU21hcnQgSG9tZSBDb250cm9sbGVyIFByb2R1Y3RpdmUg 5 | Um9vdCBDQTAeFw0xNTA4MTgwNzIwMTNaFw0zNTA4MTQwNzIwMTNaMGMxCzAJBgNV 6 | BAYTAkRFMSEwHwYDVQQKDBhCb3NjaCBUaGVybW90ZWNobmlrIEdtYkgxMTAvBgNV 7 | BAMMKFNtYXJ0IEhvbWUgQ29udHJvbGxlciBQcm9kdWN0aXZlIFJvb3QgQ0EwggIi 8 | MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCcFmt1vu85lfXMl66Ix32tmEbc 9 | n4bt6Oa6QIiT6zJIR2DsE85c42H8XogATWiqfp3FTbmfIIijfoj9JL6uyFkw0yrT 10 | qfttw9KD8DRIV973F1UyAP8wPxpdt2QPJCBMmqymC6h2oT7eS6hRIMbY3SFLa5lO 11 | 4EQ10uflZnY9Yv7kTzeuEw1qWqd8kHhfDBq3k2N90oopt47ghDQ/qUmne19xp0jQ 12 | fXFA6hfudNcU9vuZ6hvObm25++ySmRKvtuY+O/CmLVnUJngpKQWJCnYOv3/Z5StZ 13 | 5aVvLR028ozc1oqdL8fVeaJX8xIdBsSjB+gOaauEYodJzVfeLdXVb8R4CqVighci 14 | EUuwZVhzdtA5qs2O9jLJv6JFiD+uuRn8Ip1uYiajYqkRzR2egKWFfhZvV6Yk2zuw 15 | s8FUtagtYRwKCp+F+f+PCryLcBcnyc7iVm0Xo7kQAjzoDql4vmXQybmP6kU9qzmD 16 | xEG02s6FHVn1X1X4htXc/+Wh0/0850T+Up2HeN+ZN92BubI8yM62mecvfx08vSb1 17 | 5AviYkQQE37KzGeKYYbciEMeVu5sLx/lN6YIcyHY5kTUsU7SCzw7vTTsNjTzuzYa 18 | l2fudHS8lOHaAwvZP//14cM+N9beQqLzxS7jdmFQxtToyzdbgL1OekO58fiqti4W 19 | d88bnmMBZsl3bR9b5QIDAQABo2YwZDASBgNVHRMBAf8ECDAGAQH/AgECMB0GA1Ud 20 | DgQWBBThUGsROMNnqMhPn+qFxk8R9VdWPjAfBgNVHSMEGDAWgBThUGsROMNnqMhP 21 | n+qFxk8R9VdWPjAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAEp2 22 | bQei/KQGrnsnqugeseDVKNTOFp5o0xYz8gXEWzRCuAIo/sYKcFWziquajJWuCt/9 23 | CexNFWkYtV95EbunyE+wijQcnOehGSZ2gWnZiQU2fu1Y4aA5g3LlB61ljnbhX4SE 24 | tLs31iTdjPFcWMx+rsS3+qfuOiOqQbliTykG+p/ULVLLPDCmzL/MHg3w5AiGB8k5 25 | i1npzDKJKpLFGFWEnECYKhPi93rLfdgmOEFalIoFB96/upm6bfOWbNvsdIspFVGe 26 | 3zSjWUvveHe9mm+VTq9aldwy/J0/81oFF7C5CmlB31sDwfY+qF5/mHKfPbrnWTIi 27 | QAiZJxXrbmeWX9JVutRbokP1UTX63ghH+BNab/E1D020JVkimMf2Vg1/5WR2gdkN 28 | S4j+f//uVKuCr7bPGWzcADeURlyCmW/O2CNfln+T/0YFg2lET9PAEDkZ7Js3I/4f 29 | +Dy58LwjdQYI3Z6qKA9h0Cfgy6KOA8Omyw3QmdTAAd0EgABQ/vxNVL3Q4Oh8Eiff 30 | ZVrpFWLgMxeRckHTMqG9SfGBdZQCO7XPz7mb/8Da6prEfw4VKvdh9llvatWeB1V1 31 | vqixwFVuHIWKxIiR8GXZEjIQXBmeuzdgIceYcw12HYHLUifFozaNtjxMcPcIALKz 32 | GrR4oS2tFVZCjwF4vPAt15fsbEx/F/NfaO6SAFz8 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /examples/register_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Use this script to register a new client connection to Bosch Smart Home products 4 | # See https://github.com/BoschSmartHome/bosch-shc-api-docs 5 | # Before executing the script to register a new client, the button on the controller has to be pressed until the LED begins flashing. 6 | 7 | import os, sys 8 | import time 9 | import logging 10 | 11 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 12 | 13 | from boschshcpy.register_client import SHCRegisterClient 14 | import boschshcpy 15 | 16 | def registering(): 17 | # Create a BoschSHC client with the specified ACCESS_CERT and ACCESS_KEY. 18 | helper = SHCRegisterClient(args.ip_address, args.password) 19 | #result = helper.register(args.id, args.name, args.access_cert) 20 | result = helper.register(args.id, args.name) 21 | 22 | if result != None: 23 | print('successful registered new device with token {}'.format(result["token"])) 24 | print(f"Cert: {result['cert']}") 25 | print(f"Key: {result['key']}") 26 | else: 27 | print('No valid token received. Did you press client registration button on smart home controller?') 28 | sys.exit() 29 | 30 | if __name__ == "__main__": 31 | import argparse, sys 32 | 33 | logging.basicConfig(level=logging.DEBUG) 34 | 35 | parser = argparse.ArgumentParser() 36 | parser.add_argument("-pw", "--password", 37 | help="systempassword was set-up initially in the SHC setup process.") 38 | parser.add_argument("-ac", "--access_cert", 39 | help="Path to access certificat.", 40 | default=None) 41 | parser.add_argument("-n", "--name", 42 | help="Name of the new client user.", 43 | default="SHC Api Test") 44 | parser.add_argument("-id", "--id", 45 | help="ID of the new client user.", 46 | default="shc_api_test") 47 | parser.add_argument("-ip", "--ip_address", 48 | help="IP of the smart home controller.") 49 | args = parser.parse_args() 50 | 51 | if len(sys.argv) == 1: 52 | parser.print_help() 53 | sys.exit() 54 | 55 | registering() 56 | -------------------------------------------------------------------------------- /boschshcpy/emma.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .device import SHCDevice 3 | from .exceptions import SHCException 4 | from .information import SHCInformation 5 | 6 | logger = logging.getLogger("boschshcpy") 7 | 8 | 9 | class SHCEmma(SHCDevice): 10 | 11 | def __init__(self, api, shc_info: SHCInformation = None, raw_result=None): 12 | _emma_raw_device = { 13 | "rootDeviceId": shc_info.macAddress if shc_info else "", 14 | "id": "com.bosch.tt.emma.applink", 15 | "manufacturer": "BOSCH", 16 | "roomId": "", 17 | "deviceModel": "EMMA", 18 | "serial": ( 19 | shc_info.macAddress + "_" + "com.bosch.tt.emma.applink" 20 | if shc_info 21 | else "" 22 | ), 23 | "name": "EMMA", 24 | "status": ( 25 | "AVAILABLE" 26 | if raw_result 27 | else "UNAVAILABLE" if shc_info else "UNDEFINED" 28 | ), 29 | "deviceServiceIds": [], 30 | } 31 | 32 | if not raw_result: 33 | raw_result = { 34 | "version": "", 35 | "localizedTitles": {"en": ""}, 36 | "localizedInformation": {"en": "0 W"}, 37 | } 38 | 39 | super().__init__(api=api, raw_device=_emma_raw_device, raw_device_services=None) 40 | self.api = api 41 | self._shc_info = shc_info 42 | self._raw_result = raw_result 43 | 44 | @property 45 | def version(self) -> str: 46 | return self._raw_result["version"] 47 | 48 | @property 49 | def localizedTitles(self) -> str: 50 | return self._raw_result["localizedTitles"]["en"] 51 | 52 | @property 53 | def localizedSubtitles(self) -> str: 54 | return self._raw_result["localizedSubTitles"]["en"] 55 | 56 | @property 57 | def localizedInformation(self) -> str: 58 | return self._raw_result["localizedInformation"]["en"] 59 | 60 | @property 61 | def value(self) -> int | None: 62 | try: 63 | value = int(self.localizedInformation.split(" W")[0]) 64 | sign = -1.0 if self.localizedSubtitles == "Grid Supply" else 1.0 65 | return sign * value 66 | except ValueError: 67 | return None 68 | else: 69 | return None 70 | 71 | def update_emma_data(self, raw_result): 72 | if self._shc_info is None: 73 | raise SHCException("Error due to missing initialization!") 74 | 75 | self._raw_result = raw_result 76 | self._raw_device["status"] = "AVAILABLE" 77 | 78 | for callback in self._callbacks: 79 | self._callbacks[callback]() 80 | 81 | def summary(self): 82 | super().summary() 83 | print(f"EMMA : {self.id}") 84 | print(f" Name : {self.name}") 85 | print(f" Version : {self.version}") 86 | print(f" Title : {self.localizedTitles}") 87 | print(f" Subtitle : {self.localizedSubtitles}") 88 | print(f" Info : {self.value}") 89 | -------------------------------------------------------------------------------- /boschshcpy/device_service.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from .api import SHCAPI 4 | 5 | 6 | class SHCDeviceService: 7 | def __init__(self, api: SHCAPI, raw_device_service): 8 | self._api = api 9 | self._raw_device_service = raw_device_service 10 | self._raw_state = ( 11 | self._raw_device_service["state"] 12 | if "state" in self._raw_device_service 13 | else {} 14 | ) 15 | self._last_update = None 16 | 17 | self._callbacks = {} 18 | self._event_callbacks = {} 19 | 20 | @property 21 | def id(self): 22 | return self._raw_device_service["id"] 23 | 24 | @property 25 | def device_id(self): 26 | return self._raw_device_service["deviceId"] 27 | 28 | @property 29 | def state(self): 30 | return self._raw_state 31 | 32 | @property 33 | def path(self): 34 | return self._raw_device_service["path"] 35 | 36 | def subscribe_callback(self, entity, callback): 37 | self._callbacks[entity] = callback 38 | 39 | def unsubscribe_callback(self, entity): 40 | self._callbacks.pop(entity, None) 41 | 42 | def register_event(self, event, callback): 43 | self._event_callbacks[event] = callback 44 | 45 | def summary(self): 46 | print(f" Device Service: {self.id}") 47 | print(f" State: {self.state}") 48 | print(f" Path: {self.path}") 49 | 50 | def put_state(self, key_value_pairs): 51 | self._api.put_device_service_state( 52 | self.device_id.replace("#", "%23"), 53 | self.id, 54 | {"@type": self.state["@type"], **key_value_pairs}, 55 | ) 56 | 57 | def put_state_element(self, key, value): 58 | self.put_state({key: value}) 59 | 60 | def short_poll(self): 61 | if self._last_update is None or ( 62 | datetime.utcnow() - self._last_update 63 | ) > timedelta(seconds=1): 64 | self._raw_device_service = self._api.get_device_service( 65 | self.device_id, self.id 66 | ) 67 | self._last_update = datetime.utcnow() 68 | self._raw_state = ( 69 | self._raw_device_service["state"] 70 | if "state" in self._raw_device_service 71 | else {} 72 | ) 73 | 74 | def process_long_polling_poll_result(self, raw_result): 75 | assert raw_result["@type"] == "DeviceServiceData" 76 | self._raw_device_service = raw_result # Update device service data 77 | 78 | if "state" in self._raw_device_service: 79 | if self.state: 80 | assert raw_result["state"]["@type"] == self.state["@type"] 81 | self._raw_state = raw_result["state"] # Update state 82 | 83 | for callback in self._callbacks: 84 | self._callbacks[callback]() 85 | 86 | self._process_events(raw_result) 87 | 88 | def _process_events(self, raw_result): 89 | if raw_result["id"] == "Keypad": 90 | if raw_result["state"]["keyName"] in self._event_callbacks: 91 | self._event_callbacks[raw_result["state"]["keyName"]]() 92 | if raw_result["id"] == "LatestMotion": 93 | if raw_result["deviceId"] in self._event_callbacks: 94 | self._event_callbacks[raw_result["deviceId"]]() 95 | -------------------------------------------------------------------------------- /boschshcpy/rawscan.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os, sys 4 | 5 | from boschshcpy import SHCSession, SHCDeviceHelper, SHCAuthenticationError 6 | 7 | logger = logging.getLogger("boschshcpy") 8 | 9 | #### Main Program #### 10 | 11 | 12 | def main(): 13 | import argparse, sys 14 | 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument( 17 | "--certificate", 18 | "-cert", 19 | help="Certificate of a registered client.", 20 | ) 21 | parser.add_argument( 22 | "--key", 23 | "-key", 24 | help="Key of a registered client.", 25 | ) 26 | parser.add_argument("--ip_address", "-ip", help="IP of the smart home controller.") 27 | 28 | subparsers = parser.add_subparsers(dest="subcommand") 29 | subparsers.required = True 30 | 31 | parser_device = subparsers.add_parser("devices") 32 | parser_device = subparsers.add_parser("services") 33 | parser_device = subparsers.add_parser("userdefinedstates") 34 | parser_device = subparsers.add_parser("scenarios") 35 | parser_device = subparsers.add_parser("rooms") 36 | parser_device = subparsers.add_parser("info") 37 | parser_device = subparsers.add_parser("information") 38 | parser_device = subparsers.add_parser("public_information") 39 | parser_device = subparsers.add_parser("intrusion_detection") 40 | 41 | parser_service = subparsers.add_parser("device") 42 | parser_service.add_argument("device_id", help="Specify the device id.") 43 | 44 | parser_device = subparsers.add_parser("device_services") 45 | parser_device.add_argument("device_id", help="Specify the device id.") 46 | 47 | parser_service = subparsers.add_parser("device_service") 48 | parser_service.add_argument("device_id", help="Specify the device id.") 49 | parser_service.add_argument("service_id", help="Specify the service id.") 50 | 51 | args = parser.parse_args() 52 | 53 | if len(sys.argv) == 1: 54 | parser.print_help() 55 | sys.exit() 56 | 57 | # Create a BoschSHC client with the specified ACCESS_CERT and ACCESS_KEY. 58 | try: 59 | session = SHCSession( 60 | controller_ip=args.ip_address, 61 | certificate=args.certificate, 62 | key=args.key, 63 | ) 64 | except SHCAuthenticationError as e: 65 | print(e) 66 | sys.exit() 67 | 68 | match (args.subcommand): 69 | case "devices": 70 | print(json.dumps(session.api.get_devices(), indent=4)) 71 | 72 | case "services": 73 | print(json.dumps(session.api.get_services(), indent=4)) 74 | 75 | case "userdefinedstates": 76 | print(json.dumps(session.api.get_userdefinedstates(), indent=4)) 77 | 78 | case "rooms": 79 | print(json.dumps(session.api.get_rooms(), indent=4)) 80 | 81 | case "scenarios": 82 | print(json.dumps(session.api.get_scenarios(), indent=4)) 83 | 84 | case "device": 85 | print(json.dumps(session.api.get_device(args.device_id), indent=4)) 86 | 87 | case "device_services": 88 | print(json.dumps(session.api.get_device_services(args.device_id), indent=4)) 89 | 90 | case "device_service": 91 | print( 92 | json.dumps( 93 | session.api.get_device_service(args.device_id, args.service_id), 94 | indent=4, 95 | ) 96 | ) 97 | 98 | case "info" | "information": 99 | print(json.dumps(session.api.get_information(), indent=4)) 100 | 101 | case "public_information": 102 | print(json.dumps(session.api.get_public_information(), indent=4)) 103 | 104 | case "intrusion_detection": 105 | print(json.dumps(session.api.get_domain_intrusion_detection(), indent=4)) 106 | 107 | case _: 108 | print("Please select a valid mode.") 109 | 110 | sys.exit() 111 | 112 | 113 | if __name__ == "__main__": 114 | try: 115 | main() 116 | except KeyboardInterrupt: 117 | try: 118 | sys.exit(0) 119 | except SystemExit: 120 | os._exit(0) 121 | -------------------------------------------------------------------------------- /boschshcpy/device.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import typing 3 | 4 | from .device_service import SHCDeviceService 5 | from .exceptions import SHCException 6 | from .services_impl import SUPPORTED_DEVICE_SERVICE_IDS, build 7 | 8 | logger = logging.getLogger("boschshcpy") 9 | 10 | 11 | class SHCDevice: 12 | def __init__(self, api, raw_device, raw_device_services): 13 | self._api = api 14 | self._raw_device = raw_device 15 | 16 | self._callbacks = {} 17 | self._device_services_by_id = {} 18 | if not raw_device_services: 19 | raw_device_services = self._enumerate_services() 20 | 21 | self._init_services(raw_device_services) 22 | 23 | def _enumerate_services(self): 24 | raw_device_services = [] 25 | for device_service_id in self._raw_device["deviceServiceIds"]: 26 | if device_service_id not in SUPPORTED_DEVICE_SERVICE_IDS: 27 | continue 28 | 29 | raw_device_service_data = self._api.get_device_service( 30 | self.id, device_service_id 31 | ) 32 | raw_device_services.append(raw_device_service_data) 33 | return raw_device_services 34 | 35 | def _init_services(self, raw_device_services): 36 | for raw_device_service_data in raw_device_services: 37 | device_service = build(self._api, raw_device_service_data) 38 | self._device_services_by_id[raw_device_service_data["id"]] = device_service 39 | 40 | @property 41 | def root_device_id(self): 42 | return self._raw_device["rootDeviceId"] 43 | 44 | @property 45 | def id(self): 46 | return self._raw_device["id"] 47 | 48 | @property 49 | def manufacturer(self): 50 | return self._raw_device["manufacturer"] 51 | 52 | @property 53 | def room_id(self): 54 | return self._raw_device["roomId"] if "roomId" in self._raw_device else None 55 | 56 | @property 57 | def device_model(self): 58 | return self._raw_device["deviceModel"] 59 | 60 | @property 61 | def serial(self): 62 | return self._raw_device["serial"] if "serial" in self._raw_device else None 63 | 64 | @property 65 | def profile(self): 66 | return self._raw_device["profile"] if "profile" in self._raw_device else None 67 | 68 | @property 69 | def name(self): 70 | return self._raw_device["name"] 71 | 72 | @property 73 | def status(self): 74 | return self._raw_device["status"] 75 | 76 | @property 77 | def deleted(self): 78 | return ( 79 | True 80 | if "deleted" in self._raw_device and self._raw_device["deleted"] == True 81 | else False 82 | ) 83 | 84 | @property 85 | def child_device_ids(self): 86 | return ( 87 | self._raw_device["childDeviceIds"] 88 | if "childDeviceIds" in self._raw_device 89 | else None 90 | ) 91 | 92 | @property 93 | def parent_device_id(self): 94 | return ( 95 | self._raw_device["parentDeviceId"] 96 | if "parentDeviceId" in self._raw_device 97 | else None 98 | ) 99 | 100 | @property 101 | def device_services(self) -> typing.Sequence[SHCDeviceService]: 102 | return list(self._device_services_by_id.values()) 103 | 104 | @property 105 | def device_service_ids(self) -> typing.Set[str]: 106 | return set(self._device_services_by_id.keys()) 107 | 108 | def subscribe_callback(self, entity, callback): 109 | self._callbacks[entity] = callback 110 | 111 | def unsubscribe_callback(self, entity): 112 | self._callbacks.pop(entity, None) 113 | 114 | def update_raw_information(self, raw_device): 115 | if self._raw_device["id"] != raw_device["id"]: 116 | raise SHCException("Error due to mismatching device ids!") 117 | self._raw_device = raw_device 118 | 119 | for callback in self._callbacks: 120 | self._callbacks[callback]() 121 | 122 | def device_service(self, device_service_id): 123 | return ( 124 | self._device_services_by_id[device_service_id] 125 | if device_service_id in self._device_services_by_id 126 | else None 127 | ) 128 | 129 | def update(self): 130 | for service in self.device_services: 131 | service.short_poll() 132 | 133 | def summary(self): 134 | print(f"Device: {self.id}") 135 | print(f" Name : {self.name}") 136 | print(f" Manufacturer : {self.manufacturer}") 137 | print(f" Model : {self.device_model}") 138 | print(f" Room : {self.room_id}") 139 | print(f" Serial : {self.serial}") 140 | print(f" Profile : {self.profile}") 141 | print(f" Status : {self.status}") 142 | print(f" ParentDevice : {self.parent_device_id}") 143 | print(f" ChildDevices : {self.child_device_ids}") 144 | print(f" DeviceServices: {self._raw_device['deviceServiceIds']}") 145 | for device_service in self.device_services: 146 | device_service.summary() 147 | 148 | def process_long_polling_poll_result(self, raw_result): 149 | assert raw_result["@type"] == "DeviceServiceData" 150 | device_service_id = raw_result["id"] 151 | if device_service_id in self._device_services_by_id.keys(): 152 | device_service: SHCDeviceService = self._device_services_by_id[ 153 | device_service_id 154 | ] 155 | device_service.process_long_polling_poll_result(raw_result) 156 | else: 157 | logger.debug( 158 | f"Skipping polling result with unknown device service id {device_service_id}." 159 | ) 160 | -------------------------------------------------------------------------------- /boschshcpy/domain_impl.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from .api import SHCAPI 4 | 5 | 6 | class SHCIntrusionSystem: 7 | DOMAIN_STATES = { 8 | "armingState", 9 | "alarmState", 10 | "securityGapState", 11 | "activeConfigurationProfile", 12 | "systemAvailability", 13 | } 14 | 15 | class ArmingState(Enum): 16 | SYSTEM_DISARMED = "SYSTEM_DISARMED" 17 | SYSTEM_ARMED = "SYSTEM_ARMED" 18 | SYSTEM_ARMING = "SYSTEM_ARMING" 19 | 20 | class AlarmState(Enum): 21 | ALARM_OFF = "ALARM_OFF" 22 | ALARM_ON = "ALARM_ON" 23 | ALARM_MUTED = "ALARM_MUTED" 24 | PRE_ALARM = "PRE_ALARM" 25 | 26 | class Profile(Enum): 27 | FULL_PROTECTION = 0 28 | PARTIAL_PROTECTION = 1 29 | CUSTOM_PROTECTION = 2 30 | 31 | def __init__(self, api: SHCAPI, raw_domain_state, root_device_id): 32 | self._api = api 33 | self._raw_system_availability = raw_domain_state["systemAvailability"] 34 | self._raw_arming_state = raw_domain_state["armingState"] 35 | self._raw_alarm_state = raw_domain_state["alarmState"] 36 | self._raw_active_configuration_profile = raw_domain_state[ 37 | "activeConfigurationProfile" 38 | ] 39 | self._raw_security_gap_state = raw_domain_state["securityGapState"] 40 | self._root_device_id = root_device_id 41 | 42 | self._callbacks = {} 43 | 44 | @property 45 | def id(self): 46 | return "/intrusion" 47 | 48 | @property 49 | def manufacturer(self): 50 | return "BOSCH" 51 | 52 | @property 53 | def name(self): 54 | return "Intrusion Detection System" 55 | 56 | @property 57 | def root_device_id(self): 58 | return self._root_device_id 59 | 60 | @property 61 | def device_model(self): 62 | return "IDS" 63 | 64 | @property 65 | def deleted(self): 66 | return False 67 | 68 | @property 69 | def system_availability(self) -> bool: 70 | return self._raw_system_availability["available"] 71 | 72 | @property 73 | def arming_state(self) -> ArmingState: 74 | return self.ArmingState(self._raw_arming_state["state"]) 75 | 76 | @property 77 | def remaining_time_until_armed(self) -> int: 78 | if self.arming_state == self.ArmingState.SYSTEM_ARMING: 79 | return self._raw_arming_state["remainingTimeUntilArmed"] 80 | return 0 81 | 82 | @property 83 | def alarm_state(self) -> AlarmState: 84 | return self.AlarmState(self._raw_alarm_state["value"]) 85 | 86 | @property 87 | def alarm_state_incidents(self): 88 | return self._raw_alarm_state["incidents"] 89 | 90 | @property 91 | def active_configuration_profile(self) -> Profile: 92 | return self.Profile(int(self._raw_active_configuration_profile["profileId"])) 93 | 94 | @property 95 | def security_gaps(self): 96 | return self._raw_security_gap_state["securityGaps"] 97 | 98 | def subscribe_callback(self, entity, callback): 99 | self._callbacks[entity] = callback 100 | 101 | def unsubscribe_callback(self, entity): 102 | self._callbacks.pop(entity, None) 103 | 104 | def summary(self): 105 | print(f" Domain: {self.id}") 106 | print(f" System Availability: {self.system_availability}") 107 | print(f" Arming State: {self.arming_state}") 108 | print(f" Alarm State: {self.alarm_state}") 109 | 110 | def arm(self): 111 | result = self._api.post_domain_action("intrusion/actions/arm") 112 | 113 | def arm_full_protection(self): 114 | data = {"@type": "armRequest", "profileId": "0"} 115 | result = self._api.post_domain_action("intrusion/actions/arm", data) 116 | 117 | def arm_partial_protection(self): 118 | data = {"@type": "armRequest", "profileId": "1"} 119 | result = self._api.post_domain_action("intrusion/actions/arm", data) 120 | 121 | def arm_individual_protection(self): 122 | data = {"@type": "armRequest", "profileId": "2"} 123 | self._api.post_domain_action("intrusion/actions/arm", data) 124 | 125 | def disarm(self): 126 | self._api.post_domain_action("intrusion/actions/disarm") 127 | 128 | def mute(self): 129 | self._api.post_domain_action("intrusion/actions/mute") 130 | 131 | def short_poll(self): 132 | raw_domain_state = self._api.get_domain_intrusion_detection() 133 | self._raw_system_availability = raw_domain_state["systemAvailability"] 134 | self._raw_arming_state = raw_domain_state["armingState"] 135 | self._raw_alarm_state = raw_domain_state["alarmState"] 136 | self._raw_active_configuration_profile = raw_domain_state[ 137 | "activeConfigurationProfile" 138 | ] 139 | self._raw_security_gap_state = raw_domain_state["securityGapState"] 140 | 141 | def process_long_polling_poll_result(self, raw_result): 142 | if raw_result["@type"] == "armingState": 143 | self._raw_arming_state = raw_result 144 | if raw_result["@type"] == "alarmState": 145 | self._raw_alarm_state = raw_result 146 | if raw_result["@type"] == "systemAvailability": 147 | self._raw_system_availability = raw_result 148 | if raw_result["@type"] == "activeConfigurationProfile": 149 | self._raw_active_configuration_profile = raw_result 150 | if raw_result["@type"] == "securityGapState": 151 | self._raw_security_gap_state = raw_result 152 | 153 | for callback in self._callbacks: 154 | self._callbacks[callback]() 155 | 156 | 157 | MODEL_MAPPING = {"IDS": SHCIntrusionSystem} 158 | 159 | SUPPORTED_DOMAINS = MODEL_MAPPING.keys() 160 | 161 | 162 | def build(api, domain_model, raw_domain_state): 163 | assert domain_model in SUPPORTED_DOMAINS, "Domain model is supported" 164 | return MODEL_MAPPING[domain_model](api=api, raw_domain_state=raw_domain_state) 165 | -------------------------------------------------------------------------------- /boschshcpy/register_client.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | import os.path 5 | import sys 6 | 7 | import requests 8 | from importlib.resources import files 9 | from requests.adapters import HTTPAdapter 10 | from requests.packages.urllib3.poolmanager import PoolManager 11 | 12 | from .exceptions import SHCRegistrationError, SHCSessionError 13 | from .generate_cert import generate_certificate 14 | 15 | logger = logging.getLogger("boschshcpy") 16 | 17 | 18 | class HostNameIgnoringAdapter(HTTPAdapter): 19 | def init_poolmanager(self, connections, maxsize, block=False): 20 | self.poolmanager = PoolManager( 21 | num_pools=connections, maxsize=maxsize, block=block, assert_hostname=False 22 | ) 23 | 24 | 25 | class SHCRegisterClient: 26 | """Press and hold the button at the front of the SHC until the lights are flashing before you POST the command. When the SHC is not in pairing mode, there will be a connection error.""" 27 | 28 | def __init__(self, controller_ip: str, password: str): 29 | """Initializes with IP address and access credentials.""" 30 | self._controller_ip = controller_ip 31 | self._url = f"https://{self._controller_ip}:8443/smarthome/clients" 32 | 33 | # Settings for API call 34 | password_base64 = base64.b64encode(password.encode("utf-8")) 35 | self._requests_session = requests.Session() 36 | self._requests_session.mount("https://", HostNameIgnoringAdapter()) 37 | self._requests_session.headers.update( 38 | { 39 | "Content-Type": "application/json", 40 | "Systempassword": password_base64.decode("utf-8"), 41 | } 42 | ) 43 | self._requests_session.verify = files('boschshcpy').joinpath('tls_ca_chain.pem') 44 | 45 | import urllib3 46 | 47 | urllib3.disable_warnings() 48 | 49 | def _post_api_or_fail(self, body, timeout=30): 50 | try: 51 | result = self._requests_session.post( 52 | self._url, data=json.dumps(body), timeout=timeout 53 | ) 54 | if not result.ok: 55 | self._process_nok_result(result) 56 | if len(result.content) > 0: 57 | return json.loads(result.content) 58 | else: 59 | return {} 60 | except requests.exceptions.SSLError as e: 61 | raise SHCRegistrationError( 62 | f"SHC probably not in pairing mode! Please press the Bosch Smart Home Controller button until LED starts flashing.\n(SSL Error: {e})." 63 | ) 64 | 65 | def _process_nok_result(self, result): 66 | raise SHCRegistrationError( 67 | f"API call returned non-OK result (code {result.status_code})!: {result.content}... Please check your password?" 68 | ) 69 | 70 | def register(self, client_id, name): 71 | cert, key = generate_certificate(client_id, name) 72 | cert_str = ( 73 | cert.decode("utf-8") 74 | .replace("\n", "") 75 | .replace("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\r") 76 | .replace("-----END CERTIFICATE-----", "\r-----END CERTIFICATE-----") 77 | ) 78 | 79 | data = { 80 | "@type": "client", 81 | "id": client_id, 82 | "name": f"oss_{name}_Binding", 83 | "primaryRole": "ROLE_RESTRICTED_CLIENT", 84 | "certificate": cert_str, 85 | } 86 | logger.debug( 87 | f"Registering new client with id {data['id']} and name {data['name']}." 88 | ) 89 | 90 | result = self._post_api_or_fail(data) 91 | return ( 92 | {"token": result["token"], "cert": cert, "key": key} 93 | if "token" in result 94 | else None 95 | ) 96 | 97 | 98 | def write_tls_asset(filename: str, asset: bytes) -> None: 99 | """Write the tls assets to disk.""" 100 | with open(filename, "w", encoding="utf8") as file_handle: 101 | file_handle.write(asset.decode("utf-8")) 102 | 103 | 104 | def main(): 105 | import argparse, sys 106 | 107 | logging.basicConfig(level=logging.DEBUG) 108 | 109 | parser = argparse.ArgumentParser() 110 | parser.add_argument( 111 | "-pw", 112 | "--password", 113 | help="system password which was set-up initially in the SHC setup process.", 114 | ) 115 | parser.add_argument( 116 | "-n", 117 | "--name", 118 | help="Name of the new client user (default: BOSCHSHCPY_Client)", 119 | default="BOSCHSHCPY_Client", 120 | ) 121 | parser.add_argument( 122 | "-id", 123 | "--id", 124 | help="ID of the new client user (default: boschshcpy_client)", 125 | default="boschshcpy_client", 126 | ) 127 | parser.add_argument("-ip", "--ip_address", help="IP of the smart home controller.") 128 | args = parser.parse_args() 129 | 130 | if len(sys.argv) == 1: 131 | parser.print_help() 132 | sys.exit() 133 | 134 | # Create a BoschSHC client with the specified ACCESS_CERT and ACCESS_KEY. 135 | helper = SHCRegisterClient(args.ip_address, args.password) 136 | result = None 137 | try: 138 | result = helper.register(args.id, args.name) 139 | except SHCRegistrationError as e: 140 | print(e) 141 | 142 | if result != None: 143 | print("successful registered new device with token {}".format(result["token"])) 144 | print(f"Cert: {result['cert']}") 145 | print(f"Key: {result['key']}") 146 | 147 | hostname = result["token"].split(":", 1)[1] 148 | print( 149 | f"Create new certificate key pair: {'oss_' + args.id + '_' + hostname + '_cert.pem'} {'oss_' + args.id + '_' + hostname + '_key.pem'}" 150 | ) 151 | write_tls_asset("oss_" + args.id + "_" + hostname + "_cert.pem", result["cert"]) 152 | write_tls_asset("oss_" + args.id + "_" + hostname + "_key.pem", result["key"]) 153 | 154 | else: 155 | print( 156 | "No valid token received. Did you press client registration button on smart home controller?" 157 | ) 158 | sys.exit() 159 | 160 | 161 | if __name__ == "__main__": 162 | try: 163 | main() 164 | except KeyboardInterrupt: 165 | try: 166 | sys.exit(0) 167 | except SystemExit: 168 | os._exit(0) 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bosch Smart Home Controller API Python Library 2 | 3 | This library implements the local communication REST API for the Bosch Smart Home Controller system. It supports both long and short polling. The API documentation is available [here](https://github.com/BoschSmartHome/bosch-shc-api-docs). 4 | 5 | The following device services are implemented: 6 | 7 | * ```TemperatureLevel``` 8 | * ```HumidityLevel``` 9 | * ```RoomClimateControl``` 10 | * ```ShutterContact``` 11 | * ```ValveTappet``` 12 | * ```PowerSwitch``` 13 | * ```PowerMeter``` 14 | * ```Routing``` 15 | * ```PowerSwitchProgram``` 16 | * ```PresenceSimulationConfiguration``` 17 | * ```BinarySwitch``` 18 | * ```SmokeDetectorCheck``` 19 | * ```Alarm``` 20 | * ```ShutterControl``` 21 | * ```CameraLight``` 22 | * ```PrivacyMode``` 23 | * ```CameraNotification``` 24 | * ```IntrusionDetectionControl``` 25 | * ```Keypad``` 26 | * ```LatestMotion``` 27 | * ```AirQualityLevel``` 28 | * ```SurveillanceAlarm``` 29 | * ```BatteryLevel``` 30 | * ```Thermostat``` 31 | * ```WaterLeakageSensor``` 32 | * ```WaterLeakageSensorTilt``` 33 | * and more 34 | 35 | The following device models are implemented, using the above services: 36 | 37 | * ```ShutterContact```, ```ShutterContact2``` 38 | * ```ShutterControl```, ```Micromodule Shutter``` 39 | * ```SmartPlug``` 40 | * ```SmartPlugCompact``` 41 | * ```LightControl```, ```Micromodule Light Control```, ```Micromodule Light Attached```, ```Micromodule Relay``` 42 | * ```SmokeDetector``` 43 | * ```CameraEyes```, ```Camera360``` 44 | * ```IntrusionDetectionSystem``` 45 | * ```RoomClimateControl``` 46 | * ```Thermostat```, ```Thermostat2``` 47 | * ```WallThermostat``` 48 | * ```UniversalSwitch``` 49 | * ```MotionDetector``` 50 | * ```PresenceSimulationSystem``` 51 | * ```Twinguard``` 52 | * ```WaterLeakageSensor``` 53 | 54 | ## Command line access to SHC 55 | 1. Install a `python` (>=3.10) environment on your computer. 56 | 2. Install latest version of `boschshcpy`, you should have at least `version>=0.2.45`. 57 | ```bash 58 | pip install boschshcpy 59 | ``` 60 | 61 | ### Registering a new client 62 | 63 | To register a new client, use the script `boschshc_registerclient`: 64 | ```bash 65 | boschshc_registerclient -ip _your_shc_ip_ -pw _your_shc_password_ 66 | ``` 67 | 68 | This will register your client and will write the associated certificate/key pair into your working directory. See also [Usage Guide](#usage-guide) 69 | 70 | ### Rawscans 71 | 72 | To make a rawscan of your devices, use the script `boschshc_rawscan` 73 | 74 | #### Make a rawscan of the public information 75 | ```bash 76 | boschshc_rawscan -ip _your_shc_ip_ -cert _your_shc_cert_file_ -key _your_shc_key_file_ public_information 77 | ``` 78 | 79 | #### Make a rawscan of all devices 80 | ```bash 81 | boschshc_rawscan -ip _your_shc_ip_ -cert _your_shc_cert_file_ -key _your_shc_key_file_ devices 82 | ``` 83 | 84 | #### Make a rawscan of a single device with a known `device_id` 85 | ```bash 86 | boschshc_rawscan -ip _your_shc_ip_ -cert _your_shc_cert_file_ -key _your_shc_key_file_ device _your_device_id_ 87 | ``` 88 | 89 | An exemplary output looks as follows: 90 | ```bash 91 | { 92 | "@type": "device", 93 | "rootDeviceId": "xx-xx-xx-xx-xx-xx", 94 | "id": "hdm:HomeMaticIP:30xxx", 95 | "deviceServiceIds": [ 96 | "Thermostat", 97 | "BatteryLevel", 98 | "ValveTappet", 99 | "SilentMode", 100 | "TemperatureLevel", 101 | "Linking", 102 | "TemperatureOffset" 103 | ], 104 | "manufacturer": "BOSCH", 105 | "roomId": "hz_8", 106 | "deviceModel": "TRV", 107 | "serial": "30xxx", 108 | "profile": "GENERIC", 109 | "name": "Test Thermostat", 110 | "status": "AVAILABLE", 111 | "parentDeviceId": "roomClimateControl_hz_8", 112 | "childDeviceIds": [] 113 | }, 114 | ``` 115 | 116 | #### Make a rawscan of the associated services of a device 117 | ```bash 118 | boschshc_rawscan -ip _your_shc_ip_ -cert _your_shc_cert_file_ -key _your_shc_key_file_ device_services _your_device_id_ 119 | ``` 120 | 121 | The exemplary output will look as follows: 122 | ```bash 123 | [ 124 | { 125 | "@type": "DeviceServiceData", 126 | "id": "BatteryLevel", 127 | "deviceId": "hdm:HomeMaticIP:30xxx", 128 | "path": "/devices/hdm:HomeMaticIP:30xxx/services/BatteryLevel" 129 | }, 130 | { 131 | "@type": "DeviceServiceData", 132 | "id": "Thermostat", 133 | "deviceId": "hdm:HomeMaticIP:30xxx", 134 | "state": { 135 | "@type": "childLockState", 136 | "childLock": "OFF" 137 | }, 138 | "path": "/devices/hdm:HomeMaticIP:30xxx/services/Thermostat" 139 | }, 140 | ... 141 | ] 142 | ``` 143 | 144 | #### Make a rawscan of the a service of a device, where the `device_id` as well as the `service_id` are known 145 | ```bash 146 | boschshc_rawscan -ip _your_shc_ip_ -cert _your_shc_cert_file_ -key _your_shc_key_file_ device_service _your_device_id_ _your_service_id 147 | ``` 148 | 149 | #### Make a rawscan of the all scenarios 150 | ```bash 151 | boschshc_rawscan -ip _your_shc_ip_ -cert _your_shc_cert_file_ -key _your_shc_key_file_ scenarios 152 | ``` 153 | 154 | #### Make a rawscan of the all rooms 155 | ```bash 156 | boschshc_rawscan -ip _your_shc_ip_ -cert _your_shc_cert_file_ -key _your_shc_key_file_ rooms 157 | ``` 158 | 159 | ## Example code to use the `boschshcpy` library 160 | 161 | ```python 162 | import boschshcpy 163 | 164 | # Create session 165 | session = boschshcpy.SHCSession(controller_ip="192.168.25.51", certificate='cert.pem', key='key.pem') 166 | session.information.summary() 167 | 168 | device = session.device('roomClimateControl_hz_5') 169 | service = device.device_service('TemperatureLevel') 170 | print(service.temperature) 171 | 172 | # Update this service's state 173 | service.short_poll() 174 | 175 | # Start long polling thread in background 176 | session.start_polling() 177 | 178 | # Do work here 179 | ... 180 | 181 | # Stop polling 182 | session.stop_polling() 183 | 184 | # Trigger intrusion detection system 185 | intrusion_control = session.intrusion_system 186 | intrusion_control.arm() 187 | 188 | # Get rawscan of devices 189 | scan_result = session.rawscan(command="devices") 190 | ``` 191 | 192 | ## Usage guide 193 | 194 | Before accessing the Bosch Smart Home Controller, a client must be registered on the controller. For this a valid cert/key pair must be provided to the controller. To start the client registration, press and hold the button on the controller until the led starts flashing. More information [here](https://github.com/BoschSmartHome/bosch-shc-api-docs/tree/master/postman#register-a-new-client-to-the-bosch-smart-home-controller). 195 | -------------------------------------------------------------------------------- /boschshcpy/information.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import socket 3 | import time 4 | from enum import Enum 5 | 6 | from getmac import get_mac_address 7 | from zeroconf import Error as ZeroconfError 8 | from zeroconf import ( 9 | IPVersion, 10 | ServiceBrowser, 11 | ServiceInfo, 12 | ServiceStateChange, 13 | current_time_millis, 14 | ) 15 | 16 | from boschshcpy.exceptions import ( 17 | SHCAuthenticationError, 18 | SHCConnectionError, 19 | ) 20 | 21 | logger = logging.getLogger("boschshcpy") 22 | 23 | 24 | class SHCListener: 25 | """SHC Listener for Zeroconf browser updates.""" 26 | 27 | def __init__(self, zeroconf, callback) -> None: 28 | """Initialize SHC Listener.""" 29 | self.shc_services = {} 30 | self.waiting = True 31 | 32 | browser = ServiceBrowser( 33 | zeroconf, "_http._tcp.local.", handlers=[self.service_update] 34 | ) 35 | current_millis = current_time_millis() 36 | while ( 37 | self.waiting and current_time_millis() - current_millis < 10000 38 | ): # Give zeroconf some time to respond 39 | time.sleep(0.1) 40 | callback(self.shc_services) 41 | browser.cancel() 42 | 43 | def service_update(self, zeroconf, service_type, name, state_change): 44 | """Service state changed.""" 45 | 46 | if state_change != ServiceStateChange.Added: 47 | return 48 | 49 | try: 50 | service_info = zeroconf.get_service_info(service_type, name) 51 | except ZeroconfError: 52 | logger.exception("Failed to get info for device %s", name) 53 | return 54 | 55 | if service_info is not None: 56 | if service_info.name.startswith("Bosch SHC"): 57 | self.waiting = False 58 | self.shc_services[name] = service_info 59 | 60 | return 61 | 62 | 63 | class SHCInformation: 64 | class UpdateState(Enum): 65 | NO_UPDATE_AVAILABLE = "NO_UPDATE_AVAILABLE" 66 | DOWNLOADING = "DOWNLOADING" 67 | UPDATE_IN_PROGRESS = "UPDATE_IN_PROGRESS" 68 | UPDATE_AVAILABLE = "UPDATE_AVAILABLE" 69 | 70 | def __init__(self, api, authenticate=True, zeroconf=None): 71 | self._api = api 72 | self._unique_id = None 73 | self._name = None 74 | 75 | self._pub_info = self._api.get_public_information() 76 | if self._pub_info == None: 77 | raise SHCConnectionError 78 | 79 | if authenticate: 80 | if self._api.get_information() == None: 81 | raise SHCAuthenticationError 82 | 83 | self.get_unique_id(zeroconf) 84 | 85 | @property 86 | def version(self): 87 | return self._pub_info["softwareUpdateState"]["swInstalledVersion"] 88 | 89 | @property 90 | def updateState(self) -> UpdateState: 91 | return self.UpdateState(self._pub_info["softwareUpdateState"]["swUpdateState"]) 92 | 93 | @property 94 | def shcIpAddress(self): 95 | """Get the ip address from public information.""" 96 | return ( 97 | self._pub_info["shcIpAddress"] if "shcIpAddress" in self._pub_info else None 98 | ) 99 | 100 | @property 101 | def macAddress(self): 102 | """Get the mac address from public information.""" 103 | return self._pub_info["macAddress"] if "macAddress" in self._pub_info else None 104 | 105 | @property 106 | def name(self): 107 | return self._name 108 | 109 | @property 110 | def unique_id(self): 111 | return self._unique_id 112 | 113 | def filter(self, service_info: ServiceInfo): 114 | mac_address = None 115 | name = None 116 | 117 | try: 118 | host_ip = socket.gethostbyname(self.shcIpAddress) 119 | except Exception as e: 120 | host_ip = None 121 | 122 | for info in service_info.values(): 123 | if "Bosch SHC" in info.name: 124 | if ( 125 | host_ip in info.parsed_addresses(IPVersion.V4Only) 126 | or host_ip is None 127 | ): 128 | mac_address = info.name[ 129 | info.name.find("[") + 1 : info.name.find("]") 130 | ] 131 | server_pos = info.server.find(".local.") 132 | if server_pos > -1: 133 | name = info.server[:server_pos] 134 | if mac_address is None or name is None: 135 | return 136 | self._unique_id = format_mac(mac_address) 137 | self._name = name 138 | 139 | def get_unique_id(self, zeroconf): 140 | if zeroconf is not None: 141 | self._listener = SHCListener(zeroconf, self.filter) 142 | if self._unique_id is not None: 143 | logger.debug( 144 | "Obtain unique_id for '%s' via zeroconf: '%s'", 145 | self._name, 146 | self._unique_id, 147 | ) 148 | return 149 | 150 | if self.macAddress is not None: 151 | self._unique_id = self.macAddress 152 | self._name = ( 153 | self.shcIpAddress if self.shcIpAddress is not None else self.macAddress 154 | ) 155 | logger.debug( 156 | "Obtain unique_id for '%s' via public information: '%s'", 157 | self._name, 158 | self._unique_id, 159 | ) 160 | elif self.shcIpAddress is not None: 161 | host = self.shcIpAddress 162 | try: 163 | mac_address = get_mac_address(ip=host) 164 | if not mac_address: 165 | mac_address = get_mac_address(hostname=host) 166 | except Exception as err: # pylint: disable=broad-except 167 | logger.exception("Unable to get mac address: %s", err) 168 | 169 | if mac_address is not None: 170 | self._unique_id = format_mac(mac_address) 171 | logger.debug( 172 | "Obtain unique_id for '%s' via host IP: '%s'", 173 | self._name, 174 | self._unique_id, 175 | ) 176 | else: 177 | self._unique_id = host 178 | logger.warning( 179 | "Cannot obtain unique id, using IP address '%s' instead. Please make sure the IP stays the same!", 180 | host, 181 | ) 182 | self._name = host 183 | else: 184 | raise SHCConnectionError 185 | 186 | def summary(self): 187 | print(f"Information:") 188 | print(f" shcIpAddress : {self.shcIpAddress}") 189 | print(f" macAddress : {self.macAddress}") 190 | print(f" SW-Version : {self.version}") 191 | print(f" updateState : {self.updateState.name}") 192 | print(f" uniqueId : {self.unique_id}") 193 | print(f" name : {self.name}") 194 | 195 | 196 | def format_mac(mac: str) -> str: 197 | """ 198 | Format the mac address string. 199 | Helper function from homeassistant.helpers.device_registry.py 200 | """ 201 | to_test = mac 202 | 203 | if len(to_test) == 17 and to_test.count("-") == 5: 204 | return to_test.lower() 205 | 206 | if len(to_test) == 17 and to_test.count(":") == 5: 207 | to_test = to_test.replace(":", "") 208 | elif len(to_test) == 14 and to_test.count(".") == 2: 209 | to_test = to_test.replace(".", "") 210 | 211 | if len(to_test) == 12: 212 | # no - included 213 | return "-".join(to_test.lower()[i : i + 2] for i in range(0, 12, 2)) 214 | 215 | # Not sure how formatted, return original 216 | return mac 217 | -------------------------------------------------------------------------------- /boschshcpy/api.py: -------------------------------------------------------------------------------- 1 | import importlib.resources 2 | import json 3 | import logging 4 | 5 | import requests 6 | from requests.adapters import HTTPAdapter 7 | from requests.packages.urllib3.poolmanager import PoolManager 8 | 9 | from .exceptions import SHCSessionError 10 | 11 | logger = logging.getLogger("boschshcpy") 12 | 13 | 14 | class JSONRPCError(Exception): 15 | def __init__(self, code, message): 16 | super().__init__() 17 | self._code = code 18 | self._message = message 19 | 20 | @property 21 | def code(self): 22 | return self._code 23 | 24 | @property 25 | def message(self): 26 | return self._message 27 | 28 | def __str__(self): 29 | return f"JSONRPCError (code: {self.code}, message: {self.message})" 30 | 31 | 32 | class HostNameIgnoringAdapter(HTTPAdapter): 33 | def init_poolmanager(self, connections, maxsize, block=False): 34 | self.poolmanager = PoolManager( 35 | num_pools=connections, maxsize=maxsize, block=block, assert_hostname=False 36 | ) 37 | 38 | 39 | class SHCAPI: 40 | def __init__(self, controller_ip: str, certificate, key): 41 | self._certificate = certificate 42 | self._key = key 43 | self._controller_ip = controller_ip 44 | self._api_root = f"https://{self._controller_ip}:8444/smarthome" 45 | self._public_root = f"https://{self._controller_ip}:8446/smarthome/public" 46 | self._rpc_root = f"https://{self._controller_ip}:8444/remote/json-rpc" 47 | 48 | # Settings for all API calls 49 | self._requests_session = requests.Session() 50 | self._requests_session.mount("https://", HostNameIgnoringAdapter()) 51 | self._requests_session.cert = (self._certificate, self._key) 52 | self._requests_session.headers.update( 53 | {"api-version": "3.2", "Content-Type": "application/json"} 54 | ) 55 | self._requests_session.verify = str( 56 | importlib.resources.files("boschshcpy") / "tls_ca_chain.pem" 57 | ) 58 | 59 | import urllib3 60 | 61 | urllib3.disable_warnings() 62 | 63 | @property 64 | def controller_ip(self): 65 | return self._controller_ip 66 | 67 | def _get_api_result_or_fail( 68 | self, 69 | api_url, 70 | expected_type=None, 71 | expected_element_type=None, 72 | headers=None, 73 | timeout=30, 74 | ): 75 | try: 76 | result = self._requests_session.get( 77 | api_url, headers=headers, timeout=timeout 78 | ) 79 | if not result.ok: 80 | self._process_nok_result(result) 81 | 82 | else: 83 | if len(result.content) > 0: 84 | result = json.loads(result.content) 85 | if expected_type is not None: 86 | assert result["@type"] == expected_type 87 | if expected_element_type is not None: 88 | for result_ in result: 89 | assert result_["@type"] == expected_element_type 90 | 91 | return result 92 | else: 93 | return {} 94 | except requests.exceptions.SSLError as e: 95 | raise Exception(f"API call returned SSLError: {e}.") 96 | 97 | def _put_api_or_fail(self, api_url, body, timeout=30): 98 | result = self._requests_session.put( 99 | api_url, data=json.dumps(body), timeout=timeout 100 | ) 101 | if not result.ok: 102 | self._process_nok_result(result) 103 | if len(result.content) > 0: 104 | return json.loads(result.content) 105 | else: 106 | return {} 107 | 108 | def _post_api_or_fail(self, api_url, body, timeout=30): 109 | result = self._requests_session.post( 110 | api_url, data=json.dumps(body), timeout=timeout 111 | ) 112 | if not result.ok: 113 | self._process_nok_result(result) 114 | if len(result.content) > 0: 115 | return json.loads(result.content) 116 | else: 117 | return {} 118 | 119 | def _process_nok_result(self, result): 120 | logging.error(f"Body: {result.request.body}") 121 | logging.error(f"Headers: {result.request.headers}") 122 | logging.error(f"URL: {result.request.url}") 123 | raise SHCSessionError( 124 | f"API call returned non-OK result (code {result.status_code})!: {result.content}" 125 | ) 126 | 127 | # API calls here 128 | def get_information(self): 129 | api_url = f"{self._api_root}/information" 130 | try: 131 | result = self._get_api_result_or_fail(api_url) 132 | except Exception as e: 133 | logging.error(f"Failed to get information from SHC controller: {e}") 134 | return None 135 | return result 136 | 137 | def get_public_information(self): 138 | api_url = f"{self._public_root}/information" 139 | try: 140 | result = self._get_api_result_or_fail(api_url, headers={}) 141 | except Exception as e: 142 | logging.error(f"Failed to get public information from SHC controller: {e}") 143 | return None 144 | return result 145 | 146 | def get_rooms(self): 147 | api_url = f"{self._api_root}/rooms" 148 | return self._get_api_result_or_fail(api_url, expected_element_type="room") 149 | 150 | def get_scenarios(self): 151 | api_url = f"{self._api_root}/scenarios" 152 | return self._get_api_result_or_fail(api_url, expected_element_type="scenario") 153 | 154 | def get_userdefinedstates(self): 155 | api_url = f"{self._api_root}/userdefinedstates" 156 | return self._get_api_result_or_fail( 157 | api_url, expected_element_type="userDefinedState" 158 | ) 159 | 160 | def get_messages(self): 161 | api_url = f"{self._api_root}/messages" 162 | return self._get_api_result_or_fail( 163 | api_url, expected_element_type="message" 164 | ) 165 | 166 | def get_devices(self): 167 | api_url = f"{self._api_root}/devices" 168 | return self._get_api_result_or_fail(api_url, expected_element_type="device") 169 | 170 | def get_device(self, device_id): 171 | api_url = f"{self._api_root}/devices/{device_id}" 172 | return self._get_api_result_or_fail(api_url, expected_type="device") 173 | 174 | def get_services(self): 175 | api_url = f"{self._api_root}/services" 176 | return self._get_api_result_or_fail( 177 | api_url, expected_element_type="DeviceServiceData" 178 | ) 179 | 180 | def get_device_services(self, device_id): 181 | api_url = f"{self._api_root}/devices/{device_id}/services" 182 | return self._get_api_result_or_fail(api_url) 183 | 184 | def get_device_service(self, device_id, service_id): 185 | api_url = f"{self._api_root}/devices/{device_id}/services/{service_id}" 186 | return self._get_api_result_or_fail(api_url, expected_type="DeviceServiceData") 187 | 188 | def put_device_service_state(self, device_id, service_id, state_update): 189 | api_url = f"{self._api_root}/devices/{device_id}/services/{service_id}/state" 190 | self._put_api_or_fail(api_url, state_update) 191 | 192 | def put_shading_shutters_stop(self, device_id): 193 | api_url = f"{self._api_root}/shading/shutters/{device_id}/stop" 194 | self._put_api_or_fail(api_url, body=None) 195 | 196 | def get_domain_intrusion_detection(self): 197 | api_url = f"{self._api_root}/intrusion/states/system" 198 | return self._get_api_result_or_fail(api_url, expected_type="systemState") 199 | 200 | def post_domain_action(self, path, data=None): 201 | api_url = f"{self._api_root}/{path}" 202 | self._post_api_or_fail(api_url, body=data) 203 | 204 | def long_polling_subscribe(self): 205 | data = [ 206 | { 207 | "jsonrpc": "2.0", 208 | "method": "RE/subscribe", 209 | "params": ["com/bosch/sh/remote/*", None], 210 | } 211 | ] 212 | result = self._post_api_or_fail(self._rpc_root, data) 213 | assert result[0]["jsonrpc"] == "2.0" 214 | if "error" in result[0].keys(): 215 | raise JSONRPCError( 216 | result[0]["error"]["code"], result[0]["error"]["message"] 217 | ) 218 | else: 219 | return result[0]["result"] 220 | 221 | def long_polling_poll(self, poll_id, wait_seconds=30): 222 | data = [ 223 | { 224 | "jsonrpc": "2.0", 225 | "method": "RE/longPoll", 226 | "params": [poll_id, wait_seconds], 227 | } 228 | ] 229 | result = self._post_api_or_fail(self._rpc_root, data, wait_seconds + 5) 230 | assert result[0]["jsonrpc"] == "2.0" 231 | if "error" in result[0].keys(): 232 | raise JSONRPCError( 233 | result[0]["error"]["code"], result[0]["error"]["message"] 234 | ) 235 | else: 236 | return result[0]["result"] 237 | 238 | def long_polling_unsubscribe(self, poll_id): 239 | data = [{"jsonrpc": "2.0", "method": "RE/unsubscribe", "params": [poll_id]}] 240 | result = self._post_api_or_fail(self._rpc_root, data) 241 | assert result[0]["jsonrpc"] == "2.0" 242 | if "error" in result[0].keys(): 243 | raise JSONRPCError( 244 | result[0]["error"]["code"], result[0]["error"]["message"] 245 | ) 246 | else: 247 | return result[0]["result"] 248 | -------------------------------------------------------------------------------- /boschshcpy/device_helper.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import typing 5 | 6 | from .api import SHCAPI 7 | from .device import SHCDevice 8 | from .models_impl import ( 9 | SUPPORTED_MODELS, 10 | SHCBatteryDevice, 11 | SHCCamera360, 12 | SHCCameraEyes, 13 | SHCClimateControl, 14 | SHCHeatingCircuit, 15 | SHCLight, 16 | SHCMotionDetector, 17 | SHCShutterContact, 18 | SHCShutterContact2, 19 | SHCShutterContact2Plus, 20 | SHCShutterControl, 21 | SHCMicromoduleBlinds, 22 | SHCMicromoduleRelay, 23 | SHCMicromoduleShutterControl, 24 | SHCLightControl, 25 | SHCLightSwitch, 26 | SHCLightSwitchBSM, 27 | SHCPresenceSimulationSystem, 28 | SHCSmartPlug, 29 | SHCSmartPlugCompact, 30 | SHCSmokeDetector, 31 | SHCSmokeDetectionSystem, 32 | SHCThermostat, 33 | SHCRoomThermostat2, 34 | SHCTwinguard, 35 | SHCUniversalSwitch, 36 | SHCWallThermostat, 37 | SHCWaterLeakageSensor, 38 | SHCMicromoduleDimmer, 39 | build, 40 | ) 41 | 42 | logger = logging.getLogger("boschshcpy") 43 | 44 | 45 | class SHCDeviceHelper: 46 | def __init__(self, api: SHCAPI) -> None: 47 | self._api = api 48 | self._devices_by_model: dict[str, dict[str, SHCDevice]] = {} 49 | for model in SUPPORTED_MODELS: 50 | self._devices_by_model[model] = {} 51 | 52 | def device_init(self, raw_device, device_services) -> SHCDevice: 53 | device_id = raw_device["id"] 54 | device_model = raw_device["deviceModel"] 55 | device = [] 56 | if device_model in SUPPORTED_MODELS: 57 | device = build( 58 | api=self._api, 59 | raw_device=raw_device, 60 | raw_device_services=device_services, 61 | ) 62 | self._devices_by_model[device_model][device_id] = device 63 | else: 64 | device = SHCDevice( 65 | api=self._api, 66 | raw_device=raw_device, 67 | raw_device_services=device_services, 68 | ) 69 | 70 | return device 71 | 72 | @property 73 | def shutter_contacts(self) -> typing.Sequence[SHCShutterContact]: 74 | devices = [] 75 | if "SWD" in SUPPORTED_MODELS: 76 | devices.extend(self._devices_by_model["SWD"].values()) 77 | return devices 78 | 79 | @property 80 | def shutter_contacts2(self) -> typing.Sequence[SHCShutterContact2]: 81 | devices = [] 82 | if "SWD2" in SUPPORTED_MODELS: 83 | devices.extend(self._devices_by_model["SWD2"].values()) 84 | if "SWD2_PLUS" in SUPPORTED_MODELS: 85 | devices.extend(self._devices_by_model["SWD2_PLUS"].values()) 86 | if "SWD2_DUAL" in SUPPORTED_MODELS: 87 | devices.extend(self._devices_by_model["SWD2_DUAL"].values()) 88 | return devices 89 | 90 | @property 91 | def shutter_controls(self) -> typing.Sequence[SHCShutterControl]: 92 | devices = [] 93 | if "BBL" in SUPPORTED_MODELS: 94 | devices.extend(self._devices_by_model["BBL"].values()) 95 | return devices 96 | 97 | @property 98 | def micromodule_shutter_controls( 99 | self, 100 | ) -> typing.Sequence[SHCMicromoduleShutterControl]: 101 | devices = [] 102 | if "MICROMODULE_SHUTTER" in SUPPORTED_MODELS: 103 | devices.extend(self._devices_by_model["MICROMODULE_SHUTTER"].values()) 104 | if "MICROMODULE_AWNING" in SUPPORTED_MODELS: 105 | devices.extend(self._devices_by_model["MICROMODULE_AWNING"].values()) 106 | return devices 107 | 108 | @property 109 | def micromodule_blinds( 110 | self, 111 | ) -> typing.Sequence[SHCMicromoduleBlinds]: 112 | devices = [] 113 | if "MICROMODULE_BLINDS" in SUPPORTED_MODELS: 114 | devices.extend(self._devices_by_model["MICROMODULE_BLINDS"].values()) 115 | return devices 116 | 117 | @property 118 | def micromodule_relays( 119 | self, 120 | ) -> typing.Sequence[SHCMicromoduleRelay]: 121 | devices = [] 122 | if "MICROMODULE_RELAY" in SUPPORTED_MODELS: 123 | for relay in self._devices_by_model["MICROMODULE_RELAY"].values(): 124 | if relay.relay_type == SHCMicromoduleRelay.RelayType.SWITCH: 125 | devices.extend([relay]) 126 | return devices 127 | 128 | @property 129 | def micromodule_impulse_relays( 130 | self, 131 | ) -> typing.Sequence[SHCMicromoduleRelay]: 132 | devices = [] 133 | if "MICROMODULE_RELAY" in SUPPORTED_MODELS: 134 | for relay in self._devices_by_model["MICROMODULE_RELAY"].values(): 135 | if relay.relay_type == SHCMicromoduleRelay.RelayType.BUTTON: 136 | devices.extend([relay]) 137 | return devices 138 | 139 | @property 140 | def light_switches_bsm(self) -> typing.Sequence[SHCLightSwitchBSM]: 141 | devices = [] 142 | if "BSM" in SUPPORTED_MODELS: 143 | devices.extend(self._devices_by_model["BSM"].values()) 144 | return devices 145 | 146 | @property 147 | def micromodule_light_attached(self) -> typing.Sequence[SHCLightSwitch]: 148 | devices = [] 149 | if "MICROMODULE_LIGHT_ATTACHED" in SUPPORTED_MODELS: 150 | devices.extend( 151 | self._devices_by_model["MICROMODULE_LIGHT_ATTACHED"].values() 152 | ) 153 | return devices 154 | 155 | @property 156 | def micromodule_light_controls(self) -> typing.Sequence[SHCLightControl]: 157 | devices = [] 158 | if "MICROMODULE_LIGHT_CONTROL" in SUPPORTED_MODELS: 159 | devices.extend(self._devices_by_model["MICROMODULE_LIGHT_CONTROL"].values()) 160 | return devices 161 | 162 | @property 163 | def smart_plugs(self) -> typing.Sequence[SHCSmartPlug]: 164 | if "PSM" not in SUPPORTED_MODELS: 165 | return [] 166 | return list(self._devices_by_model["PSM"].values()) 167 | 168 | @property 169 | def smart_plugs_compact(self) -> typing.Sequence[SHCSmartPlugCompact]: 170 | devices = [] 171 | if "PLUG_COMPACT" in SUPPORTED_MODELS: 172 | devices.extend(self._devices_by_model["PLUG_COMPACT"].values()) 173 | if "PLUG_COMPACT_DUAL" in SUPPORTED_MODELS: 174 | devices.extend(self._devices_by_model["PLUG_COMPACT_DUAL"].values()) 175 | return devices 176 | 177 | @property 178 | def smoke_detectors(self) -> typing.Sequence[SHCSmokeDetector]: 179 | devices = [] 180 | if "SD" in SUPPORTED_MODELS: 181 | devices.extend(self._devices_by_model["SD"].values()) 182 | if "SMOKE_DETECTOR2" in SUPPORTED_MODELS: 183 | devices.extend(self._devices_by_model["SMOKE_DETECTOR2"].values()) 184 | return devices 185 | 186 | @property 187 | def climate_controls(self) -> typing.Sequence[SHCClimateControl]: 188 | if "ROOM_CLIMATE_CONTROL" not in SUPPORTED_MODELS: 189 | return [] 190 | return list(self._devices_by_model["ROOM_CLIMATE_CONTROL"].values()) 191 | 192 | @property 193 | def thermostats(self) -> typing.Sequence[SHCThermostat]: 194 | devices = [] 195 | if "TRV" in SUPPORTED_MODELS: 196 | devices.extend(self._devices_by_model["TRV"].values()) 197 | if "TRV_GEN2" in SUPPORTED_MODELS: 198 | devices.extend(self._devices_by_model["TRV_GEN2"].values()) 199 | if "TRV_GEN2_DUAL" in SUPPORTED_MODELS: 200 | devices.extend(self._devices_by_model["TRV_GEN2_DUAL"].values()) 201 | return devices 202 | 203 | @property 204 | def wallthermostats(self) -> typing.Sequence[SHCWallThermostat]: 205 | devices = [] 206 | if "THB" in SUPPORTED_MODELS: 207 | devices.extend(self._devices_by_model["THB"].values()) 208 | if "BWTH" in SUPPORTED_MODELS: 209 | devices.extend(self._devices_by_model["BWTH"].values()) 210 | if "BWTH24" in SUPPORTED_MODELS: 211 | devices.extend(self._devices_by_model["BWTH24"].values()) 212 | return devices 213 | 214 | @property 215 | def roomthermostats(self) -> typing.Sequence[SHCRoomThermostat2]: 216 | devices = [] 217 | if "RTH2_BAT" in SUPPORTED_MODELS: 218 | devices.extend(self._devices_by_model["RTH2_BAT"].values()) 219 | if "RTH2_230" in SUPPORTED_MODELS: 220 | devices.extend(self._devices_by_model["RTH2_230"].values()) 221 | return devices 222 | 223 | @property 224 | def motion_detectors(self) -> typing.Sequence[SHCMotionDetector]: 225 | if "MD" not in SUPPORTED_MODELS: 226 | return [] 227 | return list(self._devices_by_model["MD"].values()) 228 | 229 | @property 230 | def twinguards(self) -> typing.Sequence[SHCTwinguard]: 231 | if "TWINGUARD" not in SUPPORTED_MODELS: 232 | return [] 233 | return list(self._devices_by_model["TWINGUARD"].values()) 234 | 235 | @property 236 | def universal_switches(self) -> typing.Sequence[SHCUniversalSwitch]: 237 | devices = [] 238 | if "WRC2" in SUPPORTED_MODELS: 239 | devices.extend(self._devices_by_model["WRC2"].values()) 240 | if "SWITCH2" in SUPPORTED_MODELS: 241 | devices.extend(self._devices_by_model["SWITCH2"].values()) 242 | return devices 243 | 244 | @property 245 | def camera_eyes(self) -> typing.Sequence[SHCCameraEyes]: 246 | if "CAMERA_EYES" not in SUPPORTED_MODELS: 247 | return [] 248 | return list(self._devices_by_model["CAMERA_EYES"].values()) 249 | 250 | @property 251 | def camera_360(self) -> typing.Sequence[SHCCamera360]: 252 | if "CAMERA_360" not in SUPPORTED_MODELS: 253 | return [] 254 | return list(self._devices_by_model["CAMERA_360"].values()) 255 | 256 | @property 257 | def ledvance_lights(self) -> typing.Sequence[SHCLight]: 258 | if "LEDVANCE_LIGHT" not in SUPPORTED_MODELS: 259 | return [] 260 | return list(self._devices_by_model["LEDVANCE_LIGHT"].values()) 261 | 262 | @property 263 | def hue_lights(self) -> typing.Sequence[SHCLight]: 264 | if "HUE_LIGHT" not in SUPPORTED_MODELS: 265 | return [] 266 | return list(self._devices_by_model["HUE_LIGHT"].values()) 267 | 268 | @property 269 | def water_leakage_detectors(self) -> typing.Sequence[SHCWaterLeakageSensor]: 270 | if "WLS" not in SUPPORTED_MODELS: 271 | return [] 272 | return list(self._devices_by_model["WLS"].values()) 273 | 274 | @property 275 | def presence_simulation_system( 276 | self, 277 | ) -> SHCPresenceSimulationSystem: 278 | if "PRESENCE_SIMULATION_SERVICE" not in SUPPORTED_MODELS: 279 | return None 280 | return ( 281 | self._devices_by_model["PRESENCE_SIMULATION_SERVICE"][ 282 | "presenceSimulationService" 283 | ] 284 | if "presenceSimulationService" 285 | in self._devices_by_model["PRESENCE_SIMULATION_SERVICE"] 286 | else None 287 | ) 288 | 289 | @property 290 | def smoke_detection_system(self) -> SHCSmokeDetectionSystem: 291 | if "SMOKE_DETECTION_SYSTEM" not in SUPPORTED_MODELS: 292 | return None 293 | return ( 294 | self._devices_by_model["SMOKE_DETECTION_SYSTEM"]["smokeDetectionSystem"] 295 | if "smokeDetectionSystem" 296 | in self._devices_by_model["SMOKE_DETECTION_SYSTEM"] 297 | else None 298 | ) 299 | 300 | @property 301 | def heating_circuits(self) -> typing.Sequence[SHCHeatingCircuit]: 302 | if "HEATING_CIRCUIT" not in SUPPORTED_MODELS: 303 | return [] 304 | return list(self._devices_by_model["HEATING_CIRCUIT"].values()) 305 | 306 | @property 307 | def micromodule_dimmers(self) -> typing.Sequence[SHCMicromoduleDimmer]: 308 | if "MICROMODULE_DIMMER" not in SUPPORTED_MODELS: 309 | return [] 310 | return list(self._devices_by_model["MICROMODULE_DIMMER"].values()) 311 | -------------------------------------------------------------------------------- /boschshcpy/session.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import threading 4 | import time 5 | import typing 6 | from collections import defaultdict 7 | from collections.abc import Callable 8 | 9 | from .api import SHCAPI, JSONRPCError 10 | from .device import SHCDevice 11 | from .device_helper import SHCDeviceHelper 12 | from .domain_impl import SHCIntrusionSystem 13 | from .exceptions import SHCAuthenticationError, SHCSessionError 14 | from .information import SHCInformation 15 | from .room import SHCRoom 16 | from .scenario import SHCScenario 17 | from .message import SHCMessage 18 | from .emma import SHCEmma 19 | from .userdefinedstate import SHCUserDefinedState 20 | from .services_impl import SUPPORTED_DEVICE_SERVICE_IDS 21 | 22 | logger = logging.getLogger("boschshcpy") 23 | 24 | 25 | class SHCSession: 26 | def __init__(self, controller_ip: str, certificate, key, lazy=False, zeroconf=None): 27 | # API 28 | self._api = SHCAPI( 29 | controller_ip=controller_ip, certificate=certificate, key=key 30 | ) 31 | self._device_helper = SHCDeviceHelper(self._api) 32 | 33 | # Subscription status 34 | self._poll_id = None 35 | 36 | # SHC Information 37 | self._shc_information = None 38 | self._zeroconf = zeroconf 39 | 40 | # All devices 41 | self._rooms_by_id = {} 42 | self._scenarios_by_id = {} 43 | self._devices_by_id = {} 44 | self._services_by_device_id = defaultdict(list) 45 | self._domains_by_id = {} 46 | self._messages_by_id = {} 47 | self._userdefinedstates_by_id = {} 48 | self._subscribers = [] 49 | self._emma: SHCEmma = SHCEmma(self._api) 50 | 51 | if not lazy: 52 | self._enumerate_all() 53 | 54 | self._polling_thread = None 55 | self._stop_polling_thread = False 56 | 57 | # Stop polling function 58 | self.reset_connection_listener = None 59 | 60 | self._scenario_callbacks = {} 61 | self._userdefinedstate_callbacks = defaultdict(list) 62 | 63 | def _enumerate_all(self): 64 | self.authenticate() 65 | self._enumerate_services() 66 | self._enumerate_devices() 67 | self._enumerate_rooms() 68 | self._enumerate_scenarios() 69 | self._enumerate_messages() 70 | self._enumerate_userdefinedstates() 71 | self._initialize_domains() 72 | self._initialize_emma() 73 | 74 | def _add_device(self, raw_device, update_services=False) -> SHCDevice: 75 | device_id = raw_device["id"] 76 | 77 | if update_services: 78 | self._services_by_device_id.pop(device_id, None) 79 | raw_services = self._api.get_device_services(device_id) 80 | for service in raw_services: 81 | if service["id"] not in SUPPORTED_DEVICE_SERVICE_IDS: 82 | continue 83 | device_id = service["deviceId"] 84 | self._services_by_device_id[device_id].append(service) 85 | 86 | if not self._services_by_device_id[device_id]: 87 | logger.debug( 88 | f"Skipping device id {device_id} which has no services that are supported by this library" 89 | ) 90 | return None 91 | 92 | device = self._device_helper.device_init( 93 | raw_device, self._services_by_device_id[device_id] 94 | ) 95 | self._devices_by_id[device_id] = device 96 | return device 97 | 98 | def _update_device(self, raw_device): 99 | device_id = raw_device["id"] 100 | self._devices_by_id[device_id].update_raw_information(raw_device) 101 | 102 | def _enumerate_services(self): 103 | raw_services = self._api.get_services() 104 | for service in raw_services: 105 | if service["id"] not in SUPPORTED_DEVICE_SERVICE_IDS: 106 | continue 107 | device_id = service["deviceId"] 108 | self._services_by_device_id[device_id].append(service) 109 | 110 | def _enumerate_devices(self): 111 | raw_devices = self._api.get_devices() 112 | 113 | for raw_device in raw_devices: 114 | self._add_device(raw_device) 115 | 116 | def _enumerate_rooms(self): 117 | raw_rooms = self._api.get_rooms() 118 | for raw_room in raw_rooms: 119 | room_id = raw_room["id"] 120 | room = SHCRoom(api=self._api, raw_room=raw_room) 121 | self._rooms_by_id[room_id] = room 122 | 123 | def _enumerate_scenarios(self): 124 | raw_scenarios = self._api.get_scenarios() 125 | for raw_scenario in raw_scenarios: 126 | scenario_id = raw_scenario["id"] 127 | scenario = SHCScenario(api=self._api, raw_scenario=raw_scenario) 128 | self._scenarios_by_id[scenario_id] = scenario 129 | 130 | def _enumerate_messages(self): 131 | raw_messages = self._api.get_messages() 132 | for raw_message in raw_messages: 133 | message_id = raw_message["id"] 134 | message = SHCMessage(api=self._api, raw_message=raw_message) 135 | self._messages_by_id[message_id] = message 136 | 137 | def _enumerate_userdefinedstates(self): 138 | raw_states = self._api.get_userdefinedstates() 139 | for raw_state in raw_states: 140 | userdefinedstate_id = raw_state["id"] 141 | userdefinedstate = SHCUserDefinedState( 142 | api=self._api, info=self.information, raw_state=raw_state 143 | ) 144 | self._userdefinedstates_by_id[userdefinedstate_id] = userdefinedstate 145 | 146 | def _initialize_domains(self): 147 | self._domains_by_id["IDS"] = SHCIntrusionSystem( 148 | self._api, 149 | self._api.get_domain_intrusion_detection(), 150 | self.information.macAddress, 151 | ) 152 | 153 | def _initialize_emma(self): 154 | self._emma = SHCEmma(self._api, self._shc_information, None) 155 | 156 | def _long_poll(self, wait_seconds=10): 157 | if self._poll_id is None: 158 | self._poll_id = self.api.long_polling_subscribe() 159 | logger.debug(f"Subscribed for long poll. Poll id: {self._poll_id}") 160 | try: 161 | raw_results = self.api.long_polling_poll(self._poll_id, wait_seconds) 162 | for raw_result in raw_results: 163 | self._process_long_polling_poll_result(raw_result) 164 | 165 | return True 166 | except JSONRPCError as json_rpc_error: 167 | if json_rpc_error.code == -32001: 168 | self._poll_id = None 169 | logger.warning( 170 | f"SHC claims unknown poll id. Invalidating poll id and trying resubscribe next time..." 171 | ) 172 | return False 173 | else: 174 | raise json_rpc_error 175 | 176 | def _maybe_unsubscribe(self): 177 | if self._poll_id is not None: 178 | self.api.long_polling_unsubscribe(self._poll_id) 179 | logger.debug(f"Unsubscribed from long poll w/ poll id {self._poll_id}") 180 | self._poll_id = None 181 | 182 | def _process_long_polling_poll_result(self, raw_result): 183 | logger.debug(f"Long poll: {raw_result}") 184 | if raw_result["@type"] == "DeviceServiceData": 185 | device_id = raw_result["deviceId"] 186 | if device_id in self._devices_by_id.keys(): 187 | device = self._devices_by_id[device_id] 188 | device.process_long_polling_poll_result(raw_result) 189 | else: 190 | logger.debug( 191 | f"Skipping polling result with unknown device id {device_id}." 192 | ) 193 | return 194 | if raw_result["@type"] == "message": 195 | assert "arguments" in raw_result 196 | if "deviceServiceDataModel" in raw_result["arguments"]: 197 | raw_data_model = json.loads( 198 | raw_result["arguments"]["deviceServiceDataModel"] 199 | ) 200 | self._process_long_polling_poll_result(raw_data_model) 201 | else: 202 | # callback is missing when receiving new message 203 | message_id = raw_result["id"] 204 | message = SHCMessage(api=self._api, raw_message=raw_result) 205 | self._messages_by_id[message_id] = message 206 | return 207 | if raw_result["@type"] == "scenarioTriggered": 208 | if raw_result["id"] in self._scenario_callbacks: 209 | self._scenario_callbacks[raw_result["id"]](raw_result) 210 | if ( 211 | "shc" in self._scenario_callbacks 212 | ): # deprecated for providing bosch_shc.event trigger callbacks 213 | self._scenario_callbacks["shc"](raw_result) 214 | return 215 | if raw_result["@type"] == "device": 216 | device_id = raw_result["id"] 217 | if device_id in self._devices_by_id.keys(): 218 | self._update_device(raw_result) 219 | if ( 220 | "deleted" in raw_result and raw_result["deleted"] == True 221 | ): # Device deleted 222 | logger.debug("Deleting device with id %s", device_id) 223 | self._services_by_device_id.pop(device_id, None) 224 | self._devices_by_id.pop(device_id, None) 225 | else: # New device registered 226 | logger.debug("Found new device with id %s", device_id) 227 | device = self._add_device(raw_result, update_services=True) 228 | for instance, callback in self._subscribers: 229 | if isinstance(device, instance): 230 | callback(device) 231 | return 232 | if raw_result["@type"] in SHCIntrusionSystem.DOMAIN_STATES: 233 | if self.intrusion_system is not None: 234 | self.intrusion_system.process_long_polling_poll_result(raw_result) 235 | return 236 | if raw_result["@type"] == "userDefinedState": 237 | state_id = raw_result["id"] 238 | if state_id in self._userdefinedstates_by_id: 239 | self._userdefinedstates_by_id[state_id].update_raw_information( 240 | raw_result 241 | ) 242 | else: 243 | userdefinedstate = SHCUserDefinedState( 244 | api=self._api, info=self.information, raw_state=raw_result 245 | ) 246 | self._userdefinedstates_by_id[state_id] = userdefinedstate 247 | for instance, callback in self._subscribers: 248 | if isinstance(userdefinedstate, instance): 249 | callback(userdefinedstate) 250 | if state_id in self._userdefinedstate_callbacks: 251 | for callback in self._userdefinedstate_callbacks[state_id]: 252 | callback() 253 | return 254 | if raw_result["@type"] == "link": 255 | link_id = raw_result["id"] 256 | if link_id == "com.bosch.tt.emma.applink": 257 | self._emma.update_emma_data(raw_result) 258 | return 259 | 260 | def start_polling(self): 261 | if self._polling_thread is None: 262 | 263 | def polling_thread_main(): 264 | while not self._stop_polling_thread: 265 | try: 266 | if not self._long_poll(): 267 | logger.warning( 268 | "_long_poll returned False. Waiting 1 second." 269 | ) 270 | time.sleep(1.0) 271 | except RuntimeError as err: 272 | self._stop_polling_thread = True 273 | logger.info( 274 | "Stopping polling thread after expected runtime error." 275 | ) 276 | logger.info(f"Error description: {err}. {err.args}") 277 | logger.info(f"Attempting unsubscribe...") 278 | try: 279 | self._maybe_unsubscribe() 280 | except Exception as ex: 281 | logger.info(f"Unsubscribe not successful: {ex}") 282 | 283 | except Exception as ex: 284 | logger.error( 285 | f"Error in polling thread: {ex}. Waiting 15 seconds." 286 | ) 287 | time.sleep(15.0) 288 | 289 | self._polling_thread = threading.Thread( 290 | target=polling_thread_main, name="SHCPollingThread" 291 | ) 292 | self._polling_thread.start() 293 | 294 | else: 295 | raise SHCSessionError("Already polling!") 296 | 297 | def stop_polling(self): 298 | if self._polling_thread is not None: 299 | logger.debug(f"Unsubscribing from long poll") 300 | self._stop_polling_thread = True 301 | self._polling_thread.join() 302 | 303 | self._maybe_unsubscribe() 304 | self._polling_thread = None 305 | self._poll_id = None 306 | else: 307 | raise SHCSessionError("Not polling!") 308 | 309 | def subscribe(self, callback_tuple) -> Callable: 310 | self._subscribers.append(callback_tuple) 311 | 312 | def subscribe_scenario_callback(self, scenario_id, callback) -> Callable: 313 | self._scenario_callbacks[scenario_id] = callback 314 | 315 | def unsubscribe_scenario_callback(self, scenario_id) -> Callable: 316 | self._scenario_callbacks.pop(scenario_id, None) 317 | 318 | def subscribe_userdefinedstate_callback( 319 | self, userdefinedstate_id, callback 320 | ) -> Callable: 321 | self._userdefinedstate_callbacks[userdefinedstate_id].append(callback) 322 | 323 | def unsubscribe_userdefinedstate_callbacks(self, userdefinedstate_id) -> Callable: 324 | self._userdefinedstate_callbacks.pop(userdefinedstate_id) 325 | 326 | @property 327 | def devices(self) -> typing.Sequence[SHCDevice]: 328 | return list(self._devices_by_id.values()) 329 | 330 | def device(self, device_id) -> SHCDevice: 331 | return self._devices_by_id[device_id] 332 | 333 | @property 334 | def rooms(self) -> typing.Sequence[SHCRoom]: 335 | return list(self._rooms_by_id.values()) 336 | 337 | def room(self, room_id) -> SHCRoom: 338 | if room_id is not None: 339 | return self._rooms_by_id[room_id] 340 | 341 | return SHCRoom(self.api, {"id": "n/a", "name": "n/a", "iconId": "0"}) 342 | 343 | @property 344 | def scenarios(self) -> typing.Sequence[SHCScenario]: 345 | return list(self._scenarios_by_id.values()) 346 | 347 | @property 348 | def scenario_names(self) -> typing.Sequence[str]: 349 | scenario_names = [] 350 | for scenario in self.scenarios: 351 | scenario_names.append(scenario.name) 352 | return scenario_names 353 | 354 | def scenario(self, scenario_id) -> SHCScenario: 355 | return self._scenarios_by_id[scenario_id] 356 | 357 | @property 358 | def messages(self) -> typing.Sequence[SHCMessage]: 359 | return list(self._messages_by_id.values()) 360 | 361 | @property 362 | def emma(self) -> SHCEmma: 363 | return self._emma 364 | 365 | @property 366 | def userdefinedstates(self) -> typing.Sequence[SHCUserDefinedState]: 367 | return list(self._userdefinedstates_by_id.values()) 368 | 369 | def userdefinedstate(self, userdefinedstate_id) -> SHCUserDefinedState: 370 | return self._userdefinedstates_by_id[userdefinedstate_id] 371 | 372 | def authenticate(self): 373 | self._shc_information = SHCInformation(api=self._api, zeroconf=self._zeroconf) 374 | 375 | def mdns_info(self) -> SHCInformation: 376 | return SHCInformation( 377 | api=self._api, authenticate=False, zeroconf=self._zeroconf 378 | ) 379 | 380 | @property 381 | def information(self) -> SHCInformation: 382 | return self._shc_information 383 | 384 | @property 385 | def intrusion_system(self) -> SHCIntrusionSystem: 386 | return self._domains_by_id["IDS"] 387 | 388 | @property 389 | def api(self): 390 | return self._api 391 | 392 | @property 393 | def device_helper(self) -> SHCDeviceHelper: 394 | return self._device_helper 395 | 396 | @property 397 | def rawscan_commands(self): 398 | return [ 399 | "devices", 400 | "device", 401 | "services", 402 | "device_services", 403 | "device_service", 404 | "rooms", 405 | "scenarios", 406 | "messages", 407 | "info", 408 | "information", 409 | "public_information", 410 | "intrusion_detection", 411 | ] 412 | 413 | def rawscan(self, **kwargs): 414 | match (kwargs["command"].lower()): 415 | case "devices": 416 | return self._api.get_devices() 417 | 418 | case "device": 419 | return self._api.get_device(device_id=kwargs["device_id"]) 420 | 421 | case "services": 422 | return self._api.get_services() 423 | 424 | case "device_services": 425 | return self._api.get_device_services(device_id=kwargs["device_id"]) 426 | 427 | case "device_service": 428 | return self._api.get_device_service( 429 | device_id=kwargs["device_id"], service_id=kwargs["service_id"] 430 | ) 431 | 432 | case "rooms": 433 | return self._api.get_rooms() 434 | 435 | case "scenarios": 436 | return self._api.get_scenarios() 437 | 438 | case "messages": 439 | return self._api.get_messages() 440 | 441 | case "info" | "information": 442 | return self._api.get_information() 443 | 444 | case "public_information": 445 | return self._api.get_public_information() 446 | 447 | case "intrusion_detection": 448 | return self._api.get_domain_intrusion_detection() 449 | 450 | case _: 451 | return None 452 | -------------------------------------------------------------------------------- /boschshcpy/services_impl.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from .device_service import SHCDeviceService 4 | 5 | 6 | class TemperatureOffsetService(SHCDeviceService): 7 | @property 8 | def offset(self) -> float: 9 | return float(self.state["offset"] if "offset" in self.state else 0.0) 10 | 11 | @offset.setter 12 | def offset(self, value: float): 13 | self.put_state_element("offset", value) 14 | 15 | @property 16 | def step_size(self) -> float: 17 | return float(self.state["stepSize"] if "stepSize" in self.state else 0.0) 18 | 19 | @property 20 | def min_offset(self) -> float: 21 | return float(self.state["minOffset"] if "minOffset" in self.state else 0.0) 22 | 23 | @property 24 | def max_offset(self) -> float: 25 | return float(self.state["maxOffset"] if "maxOffset" in self.state else 0.0) 26 | 27 | def summary(self): 28 | super().summary() 29 | print(f" TemperatureOffset : {self.offset}") 30 | print(f" stepSize : {self.step_size}") 31 | print(f" minOffset : {self.min_offset}") 32 | print(f" maxOffset : {self.max_offset}") 33 | 34 | 35 | class TemperatureLevelService(SHCDeviceService): 36 | @property 37 | def temperature(self) -> float: 38 | return float(self.state["temperature"] if "temperature" in self.state else 0.0) 39 | 40 | def summary(self): 41 | super().summary() 42 | print(f" Temperature : {self.temperature}") 43 | 44 | 45 | class HumidityLevelService(SHCDeviceService): 46 | @property 47 | def humidity(self) -> float: 48 | return float(self.state["humidity"] if "humidity" in self.state else 0.0) 49 | 50 | def summary(self): 51 | super().summary() 52 | print(f" Humidity : {self.humidity}") 53 | 54 | 55 | class RoomClimateControlService(SHCDeviceService): 56 | class OperationMode(Enum): 57 | AUTOMATIC = "AUTOMATIC" 58 | MANUAL = "MANUAL" 59 | 60 | @property 61 | def operation_mode(self) -> OperationMode: 62 | return self.OperationMode(self.state["operationMode"]) 63 | 64 | @operation_mode.setter 65 | def operation_mode(self, value: OperationMode): 66 | self.put_state_element("operationMode", value.value) 67 | 68 | @property 69 | def setpoint_temperature(self) -> float: 70 | return float(self.state["setpointTemperature"]) 71 | 72 | @setpoint_temperature.setter 73 | def setpoint_temperature(self, value: float): 74 | self.put_state_element("setpointTemperature", value) 75 | 76 | @property 77 | def setpoint_temperature_eco(self) -> float: 78 | return float(self.state["setpointTemperatureForLevelEco"]) 79 | 80 | @property 81 | def setpoint_temperature_comfort(self) -> float: 82 | return float(self.state["setpointTemperatureForLevelComfort"]) 83 | 84 | @property 85 | def ventilation_mode(self) -> bool: 86 | return self.state["ventilationMode"] 87 | 88 | @property 89 | def low(self) -> bool: 90 | return self.state["low"] 91 | 92 | @low.setter 93 | def low(self, value: bool): 94 | self.put_state_element("low", value) 95 | 96 | @property 97 | def boost_mode(self) -> bool: 98 | return self.state["boostMode"] 99 | 100 | @boost_mode.setter 101 | def boost_mode(self, value: bool): 102 | self.put_state_element("boostMode", value) 103 | 104 | @property 105 | def summer_mode(self) -> bool: 106 | return self.state["summerMode"] 107 | 108 | @summer_mode.setter 109 | def summer_mode(self, value: bool) -> bool: 110 | self.put_state_element("summerMode", value) 111 | 112 | @property 113 | def supports_boost_mode(self) -> bool: 114 | return self.state["supportsBoostMode"] 115 | 116 | @property 117 | def show_setpoint_temperature(self) -> bool: 118 | if "showSetpointTemperature" in self.state.keys(): 119 | return self.state["showSetpointTemperature"] 120 | else: 121 | return False 122 | 123 | def summary(self): 124 | super().summary() 125 | print(f" Operation Mode : {self.operation_mode}") 126 | print(f" Setpoint Temperature : {self.setpoint_temperature}") 127 | print(f" Setpoint Temperature ECO : {self.setpoint_temperature_eco}") 128 | print(f" Setpoint Temperature CMF : {self.setpoint_temperature_comfort}") 129 | print(f" Ventilation Mode : {self.ventilation_mode}") 130 | print(f" Low : {self.low}") 131 | print(f" Boost Mode : {self.boost_mode}") 132 | print(f" Summer Mode : {self.summer_mode}") 133 | print(f" Supports Boost Mode : {self.supports_boost_mode}") 134 | print(f" Show Setpoint Temperature: {self.show_setpoint_temperature}") 135 | 136 | 137 | class HeatingCircuitService(SHCDeviceService): 138 | class OperationMode(Enum): 139 | AUTOMATIC = "AUTOMATIC" 140 | MANUAL = "MANUAL" 141 | 142 | @property 143 | def operation_mode(self) -> OperationMode: 144 | return self.OperationMode(self.state["operationMode"]) 145 | 146 | @operation_mode.setter 147 | def operation_mode(self, value: OperationMode): 148 | self.put_state_element("operationMode", value.value) 149 | 150 | @property 151 | def setpoint_temperature(self) -> float: 152 | return float(self.state["setpointTemperature"]) 153 | 154 | @setpoint_temperature.setter 155 | def setpoint_temperature(self, value: float): 156 | self.put_state_element("setpointTemperature", value) 157 | 158 | @property 159 | def setpoint_temperature_eco(self) -> float: 160 | return float(self.state["setpointTemperatureForLevelEco"]) 161 | 162 | @setpoint_temperature_eco.setter 163 | def setpoint_temperature_eco(self, value: float): 164 | self.put_state_element("setpointTemperatureForLevelEco", value) 165 | 166 | @property 167 | def setpoint_temperature_comfort(self) -> float: 168 | return float(self.state["setpointTemperatureForLevelComfort"]) 169 | 170 | @setpoint_temperature_comfort.setter 171 | def setpoint_temperature_comfort(self, value: float): 172 | self.put_state_element("setpointTemperatureForLevelComfort", value) 173 | 174 | @property 175 | def temperature_override_mode_active(self) -> bool: 176 | return self.state["temperatureOverrideModeActive"] 177 | 178 | @property 179 | def temperature_override_feature_enabled(self) -> bool: 180 | return self.state["temperatureOverrideFeatureEnabled"] 181 | 182 | @property 183 | def energy_saving_feature_enabled(self) -> bool: 184 | return self.state["energySavingFeatureEnabled"] 185 | 186 | @property 187 | def on(self) -> bool: 188 | return self.state["on"] 189 | 190 | def summary(self): 191 | super().summary() 192 | print(f" Operation Mode : {self.operation_mode}") 193 | print(f" Setpoint Temperature : {self.setpoint_temperature}") 194 | print(f" Setpoint Temperature ECO : {self.setpoint_temperature_eco}") 195 | print(f" Setpoint Temperature CMF : {self.setpoint_temperature_comfort}") 196 | print( 197 | f" Temp Override Mode Active : {self.temperature_override_mode_active}" 198 | ) 199 | print( 200 | f" Temp Override Feat Enabled : {self.temperature_override_feature_enabled}" 201 | ) 202 | print(f" Energy Saving Feat Enabled : {self.energy_saving_feature_enabled}") 203 | print(f" On : {self.on}") 204 | 205 | 206 | class SilentModeService(SHCDeviceService): 207 | class State(Enum): 208 | MODE_SILENT = "MODE_SILENT" 209 | MODE_NORMAL = "MODE_NORMAL" 210 | 211 | @property 212 | def mode(self) -> State: 213 | return self.State(self.state["mode"]) 214 | 215 | 216 | class ShutterContactService(SHCDeviceService): 217 | class State(Enum): 218 | CLOSED = "CLOSED" 219 | OPEN = "OPEN" 220 | 221 | @property 222 | def value(self) -> State: 223 | return self.State(self.state["value"]) 224 | 225 | def summary(self): 226 | super().summary() 227 | print(f" Value : {self.value}") 228 | 229 | 230 | class BypassService(SHCDeviceService): 231 | class State(Enum): 232 | BYPASS_INACTIVE = "BYPASS_INACTIVE" 233 | BYPASS_ACTIVE = "BYPASS_ACTIVE" 234 | UNKNOWN = "UNKNOWN" 235 | 236 | @property 237 | def value(self) -> State: 238 | return self.State(self.state["state"]) 239 | 240 | def summary(self): 241 | super().summary() 242 | print(f" State : {self.value}") 243 | 244 | 245 | class VibrationSensorService(SHCDeviceService): 246 | class State(Enum): 247 | NO_VIBRATION = "NO_VIBRATION" 248 | VIBRATION_DETECTED = "VIBRATION_DETECTED" 249 | UNKNOWN = "UNKNOWN" 250 | 251 | class SensitivityState(Enum): 252 | VERY_HIGH = "VERY_HIGH" 253 | HIGH = "HIGH" 254 | MEDIUM = "MEDIUM" 255 | LOW = "LOW" 256 | VERY_LOW = "VERY_LOW" 257 | 258 | @property 259 | def value(self) -> State: 260 | return self.State(self.state["value"]) 261 | 262 | @property 263 | def enabled(self) -> bool: 264 | return self.state["enabled"] 265 | 266 | @property 267 | def sensitivity(self) -> SensitivityState: 268 | return self.SensitivityState(self.state["sensitivity"]) 269 | 270 | def summary(self): 271 | super().summary() 272 | print(f" Value : {self.value}") 273 | print(f" Sensitivity : {self.sensitivity}") 274 | print(f" Enabled : {self.enabled}") 275 | 276 | 277 | class ValveTappetService(SHCDeviceService): 278 | class State(Enum): 279 | VALVE_ADAPTION_SUCCESSFUL = "VALVE_ADAPTION_SUCCESSFUL" 280 | VALVE_ADAPTION_IN_PROGRESS = "VALVE_ADAPTION_IN_PROGRESS" 281 | RANGE_TOO_BIG = "RANGE_TOO_BIG" 282 | RUN_TO_START_POSITION = "RUN_TO_START_POSITION" 283 | IN_START_POSITION = "IN_START_POSITION" 284 | NOT_AVAILABLE = "NOT_AVAILABLE" 285 | NO_VALVE_BODY_ERROR = "NO_VALVE_BODY_ERROR" 286 | 287 | @property 288 | def position(self) -> int: 289 | return int(self.state["position"]) 290 | 291 | @property 292 | def value(self) -> State: 293 | return self.State(self.state["value"]) 294 | 295 | def summary(self): 296 | super().summary() 297 | print(f" Position : {self.position}") 298 | 299 | 300 | class PowerSwitchService(SHCDeviceService): 301 | class State(Enum): 302 | ON = "ON" 303 | OFF = "OFF" 304 | 305 | @property 306 | def value(self) -> State: 307 | return self.State(self.state["switchState"]) 308 | 309 | @property 310 | def powerofftime(self) -> int: 311 | return int(self.state["automaticPowerOffTime"]) 312 | 313 | def summary(self): 314 | super().summary() 315 | print(f" switchState : {self.value}") 316 | print(f" automaticPowerOffTime : {self.powerofftime}") 317 | 318 | 319 | class PowerMeterService(SHCDeviceService): 320 | @property 321 | def powerconsumption(self) -> float: 322 | return float(self.state["powerConsumption"]) 323 | 324 | @property 325 | def energyconsumption(self) -> float: 326 | return float(self.state["energyConsumption"]) 327 | 328 | def summary(self): 329 | super().summary() 330 | print(f" powerConsumption : {self.powerconsumption}") 331 | print(f" energyConsumption : {self.energyconsumption}") 332 | 333 | 334 | class RoutingService(SHCDeviceService): 335 | class State(Enum): 336 | ENABLED = "ENABLED" 337 | DISABLED = "DISABLED" 338 | 339 | @property 340 | def value(self) -> State: 341 | return self.State(self.state["value"]) 342 | 343 | def summary(self): 344 | super().summary() 345 | print(f" value : {self.value}") 346 | 347 | 348 | class PowerSwitchProgramService(SHCDeviceService): 349 | class State(Enum): 350 | MANUAL = "MANUAL" 351 | AUTOMATIC = "AUTOMATIC" 352 | 353 | @property 354 | def value(self) -> State: 355 | return self.State(self.state["operationMode"]) 356 | 357 | def summary(self): 358 | super().summary() 359 | print(f" operationMode : {self.value}") 360 | 361 | 362 | class BinarySwitchService(SHCDeviceService): 363 | @property 364 | def value(self) -> bool: 365 | return self.state["on"] 366 | 367 | def summary(self): 368 | super().summary() 369 | print(f" switchState : {self.value}") 370 | 371 | 372 | class MultiLevelSwitchService(SHCDeviceService): 373 | @property 374 | def value(self) -> int: 375 | return self.state["level"] 376 | 377 | def summary(self): 378 | super().summary() 379 | print(f" multiLevelSwitchState : {self.value}") 380 | 381 | 382 | class MultiLevelSensorService(SHCDeviceService): 383 | @property 384 | def illuminance(self) -> int: 385 | return self.state["illuminance"] 386 | 387 | def summary(self): 388 | super().summary() 389 | print(f" multiLevelSensorState : {self.illuminance}") 390 | 391 | 392 | class HueColorTemperatureService(SHCDeviceService): 393 | @property 394 | def value(self) -> int: 395 | return self.state["colorTemperature"] 396 | 397 | @property 398 | def min_value(self) -> int: 399 | return self.state["colorTemperatureRange"]["minCt"] 400 | 401 | @property 402 | def max_value(self) -> int: 403 | return self.state["colorTemperatureRange"]["maxCt"] 404 | 405 | def summary(self): 406 | super().summary() 407 | print(f" colorTemperature : {self.value}") 408 | print(f" minColorTemperature : {self.min_value}") 409 | print(f" maxColorTemperature : {self.max_value}") 410 | 411 | 412 | class HSBColorActuatorService(SHCDeviceService): 413 | @property 414 | def value(self) -> int: 415 | return self.state["rgb"] 416 | 417 | @property 418 | def gamut(self) -> str: 419 | return self.state["gamut"] 420 | 421 | @property 422 | def min_value(self) -> int: 423 | return self.state["colorTemperatureRange"]["minCt"] 424 | 425 | @property 426 | def max_value(self) -> int: 427 | return self.state["colorTemperatureRange"]["maxCt"] 428 | 429 | def summary(self): 430 | super().summary() 431 | print(f" rgb : {self.value}") 432 | print(f" gamut : {self.gamut}") 433 | print(f" minColorTemperature : {self.min_value}") 434 | print(f" maxColorTemperature : {self.max_value}") 435 | 436 | 437 | class SmokeDetectorCheckService(SHCDeviceService): 438 | class State(Enum): 439 | NONE = "NONE" 440 | SMOKE_TEST_OK = "SMOKE_TEST_OK" 441 | SMOKE_TEST_REQUESTED = "SMOKE_TEST_REQUESTED" 442 | SMOKE_TEST_FAILED = "SMOKE_TEST_FAILED" 443 | 444 | @property 445 | def value(self) -> State: 446 | return self.State(self.state["value"]) 447 | 448 | def summary(self): 449 | super().summary() 450 | print(f" smokeDetectorCheckState : {self.value}") 451 | 452 | 453 | class AlarmService(SHCDeviceService): 454 | class State(Enum): 455 | IDLE_OFF = "IDLE_OFF" 456 | INTRUSION_ALARM = "INTRUSION_ALARM" 457 | SECONDARY_ALARM = "SECONDARY_ALARM" 458 | PRIMARY_ALARM = "PRIMARY_ALARM" 459 | 460 | @property 461 | def value(self) -> State: 462 | return self.State(self.state["value"]) 463 | 464 | def summary(self): 465 | super().summary() 466 | print(f" alarmState : {self.value}") 467 | 468 | 469 | class ShutterControlService(SHCDeviceService): 470 | class State(Enum): 471 | STOPPED = "STOPPED" 472 | MOVING = "MOVING" 473 | CALIBRATING = "CALIBRATING" 474 | OPENING = "OPENING" 475 | CLOSING = "CLOSING" 476 | 477 | def __init__(self, api, raw_device_service): 478 | super().__init__(api=api, raw_device_service=raw_device_service) 479 | 480 | @property 481 | def operation_state(self) -> State: 482 | return self.State(self.state["operationState"]) 483 | 484 | @property 485 | def calibrated(self) -> bool: 486 | return self.state["calibrated"] 487 | 488 | @property 489 | def level(self) -> float: 490 | return self.state["level"] 491 | 492 | def summary(self): 493 | super().summary() 494 | print(f" operationState : {self.operation_state}") 495 | print(f" Level : {self.level}") 496 | print(f" Calibrated : {self.calibrated}") 497 | 498 | 499 | class BlindsControlService(SHCDeviceService): 500 | class BlindsType(Enum): 501 | DEGREE_90 = "DEGREE_90" 502 | DEGREE_180 = "DEGREE_180" 503 | 504 | @property 505 | def current_angle(self) -> float: 506 | return self.state["currentAngle"] 507 | 508 | @property 509 | def target_angle(self) -> float: 510 | return self.state["targetAngle"] 511 | 512 | @target_angle.setter 513 | def target_angle(self, value: float): 514 | self.put_state_element("targetAngle", value) 515 | 516 | @property 517 | def blinds_type(self) -> BlindsType: 518 | return self.state["blindsType"] 519 | 520 | def summary(self): 521 | super().summary() 522 | print(f" currentAngle : {self.current_angle}") 523 | print(f" targetAngle : {self.target_angle}") 524 | 525 | 526 | class BlindsSceneControlService(SHCDeviceService): 527 | @property 528 | def level(self) -> float: 529 | return self.state["level"] 530 | 531 | @level.setter 532 | def level(self, value: float): 533 | self.put_state_element("level", value) 534 | 535 | @property 536 | def angle(self) -> float: 537 | return self.state["angle"] 538 | 539 | @angle.setter 540 | def angle(self, value: float): 541 | self.put_state_element("angle", value) 542 | 543 | def summary(self): 544 | super().summary() 545 | print(f" level : {self.level}") 546 | print(f" angle : {self.angle}") 547 | 548 | 549 | class CameraLightService(SHCDeviceService): 550 | class State(Enum): 551 | ON = "ON" 552 | OFF = "OFF" 553 | NONE = "NONE" 554 | 555 | @property 556 | def value(self) -> State: 557 | return self.State(self.state["value"] if "value" in self.state else "NONE") 558 | 559 | def summary(self): 560 | super().summary() 561 | print(f" value : {self.value}") 562 | 563 | 564 | class PrivacyModeService(SHCDeviceService): 565 | class State(Enum): 566 | ENABLED = "ENABLED" 567 | DISABLED = "DISABLED" 568 | 569 | @property 570 | def value(self) -> State: 571 | return self.State(self.state["value"] if "value" in self.state else "DISABLED") 572 | 573 | def summary(self): 574 | super().summary() 575 | print(f" value : {self.value}") 576 | 577 | 578 | class CameraNotificationService(SHCDeviceService): 579 | class State(Enum): 580 | ENABLED = "ENABLED" 581 | DISABLED = "DISABLED" 582 | 583 | @property 584 | def value(self) -> State: 585 | return self.State(self.state["value"] if "value" in self.state else "DISABLED") 586 | 587 | def summary(self): 588 | super().summary() 589 | print(f" value : {self.value}") 590 | 591 | 592 | class ChildProtectionService(SHCDeviceService): 593 | @property 594 | def childLockActive(self) -> bool: 595 | return self.state["childLockActive"] 596 | 597 | def summary(self): 598 | super().summary() 599 | print(f" childLockActive : {self.childLockActive}") 600 | 601 | 602 | class ImpulseSwitchService(SHCDeviceService): 603 | @property 604 | def impulse_state(self) -> bool: 605 | return self.state["impulseState"] 606 | 607 | @property 608 | def impulse_length(self) -> int: 609 | return self.state["impulseLength"] 610 | 611 | @property 612 | def instant_of_last_impulse(self) -> str: 613 | if not "instantOfLastImpulse" in self.state: 614 | return None 615 | return self.state["instantOfLastImpulse"] 616 | 617 | 618 | class KeypadService(SHCDeviceService): 619 | class KeyState(Enum): 620 | LOWER_BUTTON = "LOWER_BUTTON" 621 | LOWER_LEFT_BUTTON = "LOWER_LEFT_BUTTON" 622 | LOWER_RIGHT_BUTTON = "LOWER_RIGHT_BUTTON" 623 | UPPER_BUTTON = "UPPER_BUTTON" 624 | UPPER_LEFT_BUTTON = "UPPER_LEFT_BUTTON" 625 | UPPER_RIGHT_BUTTON = "UPPER_RIGHT_BUTTON" 626 | 627 | class KeyEvent(Enum): 628 | PRESS_SHORT = "PRESS_SHORT" 629 | PRESS_LONG = "PRESS_LONG" 630 | PRESS_LONG_RELEASED = "PRESS_LONG_RELEASED" 631 | 632 | @property 633 | def keyCode(self) -> int: 634 | return self.state["keyCode"] if "keyCode" in self.state else 0 635 | 636 | @property 637 | def keyName(self) -> KeyState: 638 | if not "keyName" in self.state: 639 | return None 640 | return self.KeyState(self.state["keyName"]) 641 | 642 | @property 643 | def eventType(self) -> KeyEvent: 644 | if not "eventType" in self.state: 645 | return None 646 | return self.KeyEvent(self.state["eventType"]) 647 | 648 | @property 649 | def eventTimestamp(self) -> int: 650 | if not "eventTimestamp" in self.state: 651 | return 0 652 | return self.state["eventTimestamp"] 653 | 654 | def summary(self): 655 | super().summary() 656 | print(f" keyCode : {self.keyCode}") 657 | print(f" keyName : {self.keyName}") 658 | print(f" eventType : {self.eventType}") 659 | print(f" eventTimestamp : {self.eventTimestamp}") 660 | 661 | 662 | class LatestMotionService(SHCDeviceService): 663 | @property 664 | def latestMotionDetected(self) -> str: 665 | return ( 666 | self.state["latestMotionDetected"] 667 | if "latestMotionDetected" in self.state 668 | else "n/a" 669 | ) 670 | 671 | def summary(self): 672 | super().summary() 673 | print(f" latestMotionDetected : {self.latestMotionDetected}") 674 | 675 | 676 | class AirQualityLevelService(SHCDeviceService): 677 | class RatingState(Enum): 678 | GOOD = "GOOD" 679 | MEDIUM = "MEDIUM" 680 | BAD = "BAD" 681 | 682 | @property 683 | def combinedRating(self) -> RatingState: 684 | return self.RatingState(self.state["combinedRating"]) 685 | 686 | @property 687 | def description(self) -> str: 688 | return self.state["description"] 689 | 690 | @property 691 | def temperature(self) -> int: 692 | return self.state["temperature"] 693 | 694 | @property 695 | def temperatureRating(self) -> RatingState: 696 | return self.RatingState(self.state["temperatureRating"]) 697 | 698 | @property 699 | def humidity(self) -> int: 700 | return self.state["humidity"] 701 | 702 | @property 703 | def humidityRating(self) -> RatingState: 704 | return self.RatingState(self.state["humidityRating"]) 705 | 706 | @property 707 | def purity(self) -> int: 708 | return self.state["purity"] 709 | 710 | @property 711 | def purityRating(self) -> RatingState: 712 | return self.RatingState(self.state["purityRating"]) 713 | 714 | def summary(self): 715 | super().summary() 716 | print(f" combinedRating : {self.combinedRating}") 717 | print(f" description : {self.description}") 718 | print(f" temperature : {self.temperature}") 719 | print(f" temperatureRating : {self.temperatureRating}") 720 | print(f" humidity : {self.humidity}") 721 | print(f" humidityRating : {self.humidityRating}") 722 | print(f" purity : {self.purity}") 723 | print(f" purityRating : {self.purityRating}") 724 | 725 | 726 | class SurveillanceAlarmService(SHCDeviceService): 727 | class State(Enum): 728 | ALARM_OFF = "ALARM_OFF" 729 | ALARM_ON = "ALARM_ON" 730 | ALARM_MUTED = "ALARM_MUTED" 731 | 732 | @property 733 | def value(self) -> State: 734 | return self.State(self.state["value"]) 735 | 736 | def summary(self): 737 | super().summary() 738 | print(f" value : {self.value}") 739 | 740 | 741 | class SmokeDetectionControlService(SHCDeviceService): 742 | def summary(self): 743 | super().summary() 744 | print(f" not yet implemented!") 745 | 746 | 747 | class BatteryLevelService(SHCDeviceService): 748 | class State(Enum): 749 | LOW_BATTERY = "LOW_BATTERY" 750 | CRITICAL_LOW = "CRITICAL_LOW" 751 | CRITICALLY_LOW_BATTERY = "CRITICALLY_LOW_BATTERY" 752 | OK = "OK" 753 | NOT_AVAILABLE = "NOT_AVAILABLE" 754 | 755 | @property 756 | def warningLevel(self) -> State: 757 | faults = ( 758 | self._raw_device_service["faults"] 759 | if "faults" in self._raw_device_service 760 | else None 761 | ) 762 | if not faults: 763 | return self.State("OK") 764 | assert len(faults["entries"]) == 1 765 | assert "type" in faults["entries"][0] 766 | return self.State(faults["entries"][0]["type"]) 767 | 768 | def summary(self): 769 | super().summary() 770 | print(f" warningLevel : {self.warningLevel}") 771 | 772 | 773 | class ThermostatService(SHCDeviceService): 774 | class State(Enum): 775 | ON = "ON" 776 | OFF = "OFF" 777 | 778 | @property 779 | def childLock(self) -> State: 780 | return self.State(self.state["childLock"]) 781 | 782 | def summary(self): 783 | super().summary() 784 | print(f" childLock : {self.childLock}") 785 | 786 | 787 | class CommunicationQualityService(SHCDeviceService): 788 | class State(Enum): 789 | BAD = "BAD" 790 | GOOD = "GOOD" 791 | MEDIUM = "MEDIUM" 792 | NORMAL = "NORMAL" 793 | UNKNOWN = "UNKNOWN" 794 | FETCHING = "FETCHING" 795 | 796 | @property 797 | def value(self) -> State: 798 | return self.State(self.state["quality"]) 799 | 800 | def summary(self): 801 | super().summary() 802 | print(f" quality : {self.value}") 803 | 804 | 805 | class WaterLeakageSensorService(SHCDeviceService): 806 | class State(Enum): 807 | LEAKAGE_DETECTED = "LEAKAGE_DETECTED" 808 | NO_LEAKAGE = "NO_LEAKAGE" 809 | 810 | @property 811 | def value(self) -> State: 812 | return self.State(self.state["state"]) 813 | 814 | def summary(self): 815 | super().summary() 816 | print(f" waterLeakageSensorState : {self.value}") 817 | 818 | 819 | class WaterLeakageSensorTiltService(SHCDeviceService): 820 | class State(Enum): 821 | ENABLED = "ENABLED" 822 | DISABLED = "DISABLED" 823 | 824 | @property 825 | def pushNotificationState(self) -> State: 826 | return self.State(self.state["pushNotificationState"]) 827 | 828 | @property 829 | def acousticSignalState(self) -> State: 830 | return self.State(self.state["acousticSignalState"]) 831 | 832 | def summary(self): 833 | super().summary() 834 | print(f" pushNotificationState : {self.pushNotificationState}") 835 | print(f" acousticSignalState : {self.acousticSignalState}") 836 | 837 | 838 | class WaterLeakageSensorCheckService(SHCDeviceService): 839 | @property 840 | def value(self) -> str: 841 | return self.state["result"] 842 | 843 | def summary(self): 844 | super().summary() 845 | print(f" waterLeakageSensorCheck : {self.value}") 846 | 847 | 848 | class PresenceSimulationConfigurationService(SHCDeviceService): 849 | @property 850 | def enabled(self) -> bool: 851 | return self.state["enabled"] 852 | 853 | @enabled.setter 854 | def enabled(self, value: bool): 855 | self.put_state_element("enabled", value) 856 | 857 | def summary(self): 858 | super().summary() 859 | print(f" presenceSimulationConfigurationState : {self.enabled}") 860 | 861 | 862 | SERVICE_MAPPING = { 863 | "AirQualityLevel": AirQualityLevelService, 864 | "Alarm": AlarmService, 865 | "BatteryLevel": BatteryLevelService, 866 | "BinarySwitch": BinarySwitchService, 867 | "BlindsControl": BlindsControlService, 868 | "BlindsSceneControl": BlindsSceneControlService, 869 | "Bypass": BypassService, 870 | "CameraLight": CameraLightService, 871 | "CameraNotification": CameraNotificationService, 872 | "ChildProtection": ChildProtectionService, 873 | "CommunicationQuality": CommunicationQualityService, 874 | "HeatingCircuit": HeatingCircuitService, 875 | "HSBColorActuator": HSBColorActuatorService, 876 | "HueColorTemperature": HueColorTemperatureService, 877 | "HumidityLevel": HumidityLevelService, 878 | "ImpulseSwitch": ImpulseSwitchService, 879 | "Keypad": KeypadService, 880 | "LatestMotion": LatestMotionService, 881 | "MultiLevelSwitch": MultiLevelSwitchService, 882 | "MultiLevelSensor": MultiLevelSensorService, 883 | "PowerMeter": PowerMeterService, 884 | "PowerSwitch": PowerSwitchService, 885 | "PowerSwitchProgram": PowerSwitchProgramService, 886 | "PresenceSimulationConfiguration": PresenceSimulationConfigurationService, 887 | "PrivacyMode": PrivacyModeService, 888 | "RoomClimateControl": RoomClimateControlService, 889 | "Routing": RoutingService, 890 | "ShutterContact": ShutterContactService, 891 | "ShutterControl": ShutterControlService, 892 | "SilentMode": SilentModeService, 893 | "SmokeDetectorCheck": SmokeDetectorCheckService, 894 | "SurveillanceAlarm": SurveillanceAlarmService, 895 | "TemperatureLevel": TemperatureLevelService, 896 | "TemperatureOffset": TemperatureOffsetService, 897 | "Thermostat": ThermostatService, 898 | "ValveTappet": ValveTappetService, 899 | "VibrationSensor": VibrationSensorService, 900 | "WaterLeakageSensor": WaterLeakageSensorService, 901 | "WaterLeakageSensorTilt": WaterLeakageSensorTiltService, 902 | "WaterLeakageSensorCheck": WaterLeakageSensorCheckService, 903 | } 904 | 905 | # "SmokeDetectionControl": SmokeDetectionControlService, 906 | # "ElectricalFaults": ElectricalFaultsService, 907 | # "SwitchConfiguration": SwitchConfigurationService, 908 | # "Linking": LinkingService, 909 | 910 | SUPPORTED_DEVICE_SERVICE_IDS = SERVICE_MAPPING.keys() 911 | 912 | 913 | def build(api, raw_device_service): 914 | device_service_id = raw_device_service["id"] 915 | assert ( 916 | device_service_id in SUPPORTED_DEVICE_SERVICE_IDS 917 | ), "Device service is supported" 918 | return SERVICE_MAPPING[device_service_id]( 919 | api=api, raw_device_service=raw_device_service 920 | ) 921 | -------------------------------------------------------------------------------- /boschshcpy/models_impl.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, Flag, auto 2 | 3 | from .device import SHCDevice 4 | 5 | 6 | class SHCBatteryDevice(SHCDevice): 7 | from .services_impl import BatteryLevelService 8 | 9 | def __init__(self, api, raw_device, raw_device_services): 10 | super().__init__(api, raw_device, raw_device_services) 11 | self._batterylevel_service = self.device_service("BatteryLevel") 12 | 13 | @property 14 | def supports_batterylevel(self): 15 | return self._batterylevel_service is not None 16 | 17 | @property 18 | def batterylevel(self) -> BatteryLevelService.State: 19 | if self.supports_batterylevel: 20 | return self._batterylevel_service.warningLevel 21 | return self.BatteryLevelService.State.NOT_AVAILABLE 22 | 23 | 24 | class _CommunicationQuality(SHCDevice): 25 | from .services_impl import ( 26 | CommunicationQualityService, 27 | ) 28 | 29 | def __init__(self, api, raw_device, raw_device_services): 30 | super().__init__(api, raw_device, raw_device_services) 31 | self._communicationquality_service = self.device_service("CommunicationQuality") 32 | 33 | @property 34 | def communicationquality(self) -> CommunicationQualityService.State: 35 | return self._communicationquality_service.value 36 | 37 | 38 | class _PowerMeter(SHCDevice): 39 | from .services_impl import ( 40 | PowerMeterService, 41 | ) 42 | 43 | def __init__(self, api, raw_device, raw_device_services): 44 | super().__init__(api, raw_device, raw_device_services) 45 | self._powermeter_service = self.device_service("PowerMeter") 46 | 47 | @property 48 | def energyconsumption(self) -> float: 49 | return self._powermeter_service.energyconsumption 50 | 51 | @property 52 | def powerconsumption(self) -> float: 53 | return self._powermeter_service.powerconsumption 54 | 55 | 56 | class _ChildProtection(SHCDevice): 57 | from .services_impl import ( 58 | ChildProtectionService, 59 | ) 60 | 61 | def __init__(self, api, raw_device, raw_device_services): 62 | super().__init__(api, raw_device, raw_device_services) 63 | self._childprotection_service = self.device_service("ChildProtection") 64 | 65 | @property 66 | def child_lock(self) -> bool: 67 | return self._childprotection_service.childLockActive 68 | 69 | @child_lock.setter 70 | def child_lock(self, state: bool): 71 | self._childprotection_service.put_state_element("childLockActive", state) 72 | 73 | 74 | class _Thermostat(SHCDevice): 75 | from .services_impl import ( 76 | ThermostatService, 77 | ) 78 | 79 | def __init__(self, api, raw_device, raw_device_services): 80 | super().__init__(api, raw_device, raw_device_services) 81 | self._thermostat_service = self.device_service("Thermostat") 82 | 83 | @property 84 | def child_lock(self) -> ThermostatService.State: 85 | return self._thermostat_service.childLock 86 | 87 | @child_lock.setter 88 | def child_lock(self, state: bool): 89 | self._thermostat_service.put_state_element( 90 | "childLock", "ON" if state else "OFF" 91 | ) 92 | 93 | 94 | class _PowerSwitch(SHCDevice): 95 | from .services_impl import ( 96 | PowerSwitchService, 97 | ) 98 | 99 | def __init__(self, api, raw_device, raw_device_services): 100 | super().__init__(api, raw_device, raw_device_services) 101 | self._powerswitch_service = self.device_service("PowerSwitch") 102 | 103 | @property 104 | def switchstate(self) -> PowerSwitchService.State: 105 | return self._powerswitch_service.value 106 | 107 | @switchstate.setter 108 | def switchstate(self, state: bool): 109 | self._powerswitch_service.put_state_element( 110 | "switchState", "ON" if state else "OFF" 111 | ) 112 | 113 | 114 | class _PowerSwitchProgram(SHCDevice): 115 | from .services_impl import ( 116 | PowerSwitchProgramService, 117 | ) 118 | 119 | def __init__(self, api, raw_device, raw_device_services): 120 | super().__init__(api, raw_device, raw_device_services) 121 | self._powerswitchprogram_service = self.device_service("PowerSwitchProgram") 122 | 123 | # To be implemented 124 | 125 | 126 | class _TemperatureLevel(SHCDevice): 127 | from .services_impl import TemperatureLevelService 128 | 129 | def __init__(self, api, raw_device, raw_device_services): 130 | super().__init__(api, raw_device, raw_device_services) 131 | self._temperaturelevel_service = self.device_service("TemperatureLevel") 132 | 133 | @property 134 | def temperature(self) -> float: 135 | return self._temperaturelevel_service.temperature 136 | 137 | 138 | class _HumidityLevel(SHCDevice): 139 | from .services_impl import HumidityLevelService 140 | 141 | def __init__(self, api, raw_device, raw_device_services): 142 | super().__init__(api, raw_device, raw_device_services) 143 | self._humiditylevel_service = self.device_service("HumidityLevel") 144 | 145 | @property 146 | def humidity(self) -> float: 147 | return self._humiditylevel_service.humidity 148 | 149 | 150 | class _TemperatureOffset(SHCDevice): 151 | from .services_impl import ( 152 | TemperatureOffsetService, 153 | ) 154 | 155 | def __init__(self, api, raw_device, raw_device_services): 156 | super().__init__(api, raw_device, raw_device_services) 157 | self._temperatureoffset_service = self.device_service("TemperatureOffset") 158 | 159 | @property 160 | def offset(self) -> float: 161 | return self._temperatureoffset_service.offset 162 | 163 | @offset.setter 164 | def offset(self, value: float): 165 | self._temperatureoffset_service.offset = value 166 | 167 | @property 168 | def step_size(self) -> float: 169 | return self._temperatureoffset_service.step_size 170 | 171 | @property 172 | def min_offset(self) -> float: 173 | return self._temperatureoffset_service.min_offset 174 | 175 | @property 176 | def max_offset(self) -> float: 177 | return self._temperatureoffset_service.max_offset 178 | 179 | 180 | class _SilentMode(SHCDevice): 181 | from .services_impl import ( 182 | SilentModeService, 183 | ) 184 | 185 | def __init__(self, api, raw_device, raw_device_services): 186 | super().__init__(api, raw_device, raw_device_services) 187 | self._silentmode_service = self.device_service("SilentMode") 188 | 189 | @property 190 | def supports_silentmode(self): 191 | return self._silentmode_service is not None 192 | 193 | @property 194 | def silentmode(self) -> SilentModeService.State: 195 | if self.supports_silentmode: 196 | return self._silentmode_service.mode 197 | 198 | @silentmode.setter 199 | def silentmode(self, state: bool): 200 | if self.supports_silentmode: 201 | self._silentmode_service.put_state_element( 202 | "mode", "MODE_SILENT" if state else "MODE_NORMAL" 203 | ) 204 | 205 | 206 | class SHCSmokeDetector(SHCBatteryDevice): 207 | from .services_impl import AlarmService, SmokeDetectorCheckService 208 | 209 | def __init__(self, api, raw_device, raw_device_services): 210 | super().__init__(api, raw_device, raw_device_services) 211 | 212 | self._alarm_service = self.device_service("Alarm") 213 | self._smokedetectorcheck_service = self.device_service("SmokeDetectorCheck") 214 | 215 | @property 216 | def alarmstate(self) -> AlarmService.State: 217 | return self._alarm_service.value 218 | 219 | @alarmstate.setter 220 | def alarmstate(self, state: str): 221 | self._alarm_service.put_state_element("value", state) 222 | 223 | @property 224 | def smokedetectorcheck_state(self) -> SmokeDetectorCheckService.State: 225 | return self._smokedetectorcheck_service.value 226 | 227 | def smoketest_requested(self): 228 | self._smokedetectorcheck_service.put_state_element( 229 | "value", "SMOKE_TEST_REQUESTED" 230 | ) 231 | 232 | 233 | class SHCSmartPlug(_PowerMeter, _PowerSwitch, _PowerSwitchProgram): 234 | from .services_impl import ( 235 | RoutingService, 236 | ) 237 | 238 | def __init__(self, api, raw_device, raw_device_services): 239 | super().__init__(api, raw_device, raw_device_services) 240 | 241 | self._routing_service = self.device_service("Routing") 242 | 243 | @property 244 | def routing(self) -> RoutingService.State: 245 | return self._routing_service.value 246 | 247 | @routing.setter 248 | def routing(self, state: bool): 249 | self._routing_service.put_state_element( 250 | "value", "ENABLED" if state else "DISABLED" 251 | ) 252 | 253 | 254 | class SHCSmartPlugCompact( 255 | _CommunicationQuality, _PowerMeter, _PowerSwitch, _PowerSwitchProgram 256 | ): 257 | pass 258 | 259 | 260 | class SHCLightSwitch(_ChildProtection, _PowerSwitch, _PowerSwitchProgram): 261 | pass 262 | 263 | 264 | class SHCLightSwitchBSM(SHCLightSwitch, _PowerMeter): 265 | pass 266 | 267 | 268 | class SHCLightControl(_CommunicationQuality, _PowerMeter): 269 | pass 270 | 271 | 272 | class SHCMicromoduleRelay( 273 | _CommunicationQuality, _ChildProtection, _PowerSwitch, _PowerSwitchProgram 274 | ): 275 | from .services_impl import ( 276 | ImpulseSwitchService, 277 | ) 278 | 279 | class RelayType(Enum): 280 | BUTTON = "BUTTON" 281 | SWITCH = "SWITCH" 282 | 283 | def __init__(self, api, raw_device, raw_device_services): 284 | super().__init__(api, raw_device, raw_device_services) 285 | 286 | self._impulseswitch_service = self.device_service("ImpulseSwitch") 287 | 288 | @property 289 | def relay_type(self) -> RelayType: 290 | return ( 291 | self.RelayType.BUTTON 292 | if self._impulseswitch_service is not None 293 | else self.RelayType.SWITCH 294 | ) 295 | 296 | def trigger_impulse_state(self): 297 | if self._impulseswitch_service: 298 | self._impulseswitch_service.put_state_element("impulseState", True) 299 | 300 | @property 301 | def impulse_length(self) -> int: 302 | return self._impulseswitch_service.impulse_length 303 | 304 | @impulse_length.setter 305 | def impulse_length(self, impulse_length: int): 306 | self._impulseswitch_service.put_state_element("impulseLength", impulse_length) 307 | 308 | @property 309 | def instant_of_last_impulse(self) -> str: 310 | if self._impulseswitch_service: 311 | return self._impulseswitch_service.instant_of_last_impulse 312 | 313 | 314 | class SHCShutterControl(SHCDevice): 315 | from .services_impl import ShutterControlService 316 | 317 | def __init__(self, api, raw_device, raw_device_services): 318 | super().__init__(api, raw_device, raw_device_services) 319 | self._service = self.device_service("ShutterControl") 320 | 321 | @property 322 | def level(self) -> float: 323 | return self._service.level 324 | 325 | @level.setter 326 | def level(self, level): 327 | self._service.put_state_element("level", level) 328 | 329 | def stop(self): 330 | self._service.put_state_element("operationState", "STOPPED") 331 | 332 | @property 333 | def operation_state(self) -> ShutterControlService.State: 334 | return self._service.operation_state 335 | 336 | 337 | class SHCMicromoduleShutterControl( 338 | SHCShutterControl, _CommunicationQuality, _ChildProtection, _PowerMeter 339 | ): 340 | pass 341 | 342 | 343 | class SHCMicromoduleBlinds(SHCMicromoduleShutterControl): 344 | from .services_impl import BlindsControlService, BlindsSceneControlService 345 | 346 | def __init__(self, api, raw_device, raw_device_services): 347 | super().__init__(api, raw_device, raw_device_services) 348 | self._blindscontrol_service = self.device_service("BlindsControl") 349 | self._blindsscenecontrol_service = self.device_service("BlindsSceneControl") 350 | 351 | @property 352 | def current_angle(self) -> float: 353 | return self._blindscontrol_service.current_angle 354 | 355 | @property 356 | def target_angle(self) -> float: 357 | return self._blindscontrol_service.target_angle 358 | 359 | @target_angle.setter 360 | def target_angle(self, value: float): 361 | self._blindscontrol_service.target_angle = value 362 | 363 | @property 364 | def blinds_level(self) -> float: 365 | return self._blindsscenecontrol_service.level 366 | 367 | @blinds_level.setter 368 | def blinds_level(self, level: float): 369 | self._blindsscenecontrol_service.level = level 370 | 371 | @property 372 | def blinds_type(self) -> BlindsControlService.BlindsType: 373 | return self._blindscontrol_service.blinds_type 374 | 375 | def stop_blinds(self): 376 | self._api.put_shading_shutters_stop(self.id) 377 | 378 | 379 | class SHCShutterContact(SHCBatteryDevice): 380 | from .services_impl import ShutterContactService 381 | 382 | def __init__(self, api, raw_device, raw_device_services): 383 | super().__init__(api, raw_device, raw_device_services) 384 | self._service = self.device_service("ShutterContact") 385 | 386 | @property 387 | def device_class(self) -> str: 388 | return self.profile 389 | 390 | @property 391 | def state(self) -> ShutterContactService.State: 392 | return self._service.value 393 | 394 | 395 | class SHCShutterContact2(SHCShutterContact, _CommunicationQuality): 396 | from .services_impl import BypassService 397 | 398 | def __init__(self, api, raw_device, raw_device_services): 399 | super().__init__(api, raw_device, raw_device_services) 400 | self._bypass_service = self.device_service("Bypass") 401 | 402 | @property 403 | def bypass(self) -> BypassService.State: 404 | return self._bypass_service.value 405 | 406 | @bypass.setter 407 | def bypass(self, state: bool): 408 | self._bypass_service.put_state_element( 409 | "state", "BYPASS_ACTIVE" if state else "BYPASS_INACTIVE" 410 | ) 411 | 412 | 413 | class SHCShutterContact2Plus(SHCShutterContact2): 414 | from .services_impl import VibrationSensorService 415 | 416 | def __init__(self, api, raw_device, raw_device_services): 417 | super().__init__(api, raw_device, raw_device_services) 418 | self._vibrationsensor_service = self.device_service("VibrationSensor") 419 | 420 | @property 421 | def vibrationsensor(self) -> VibrationSensorService.State: 422 | return self._vibrationsensor_service.value 423 | 424 | @property 425 | def enabled(self) -> bool: 426 | return self._vibrationsensor_service.enabled 427 | 428 | @enabled.setter 429 | def enabled(self, state: bool): 430 | self._vibrationsensor_service.put_state_element("enabled", state) 431 | 432 | @property 433 | def sensitivity(self) -> VibrationSensorService.SensitivityState: 434 | return self._vibrationsensor_service.sensitivity 435 | 436 | @sensitivity.setter 437 | def sensitivity(self, state: VibrationSensorService.SensitivityState): 438 | self._vibrationsensor_service.put_state_element("sensitivity", state.name) 439 | 440 | 441 | class SHCCamera360(SHCDevice): 442 | from .services_impl import CameraNotificationService, PrivacyModeService 443 | 444 | def __init__(self, api, raw_device, raw_device_services): 445 | super().__init__(api, raw_device, raw_device_services) 446 | 447 | self._privacymode_service = self.device_service("PrivacyMode") 448 | self._cameranotification_service = self.device_service("CameraNotification") 449 | 450 | @property 451 | def privacymode(self) -> PrivacyModeService.State: 452 | return self._privacymode_service.value 453 | 454 | @privacymode.setter 455 | def privacymode(self, state: bool): 456 | self._privacymode_service.put_state_element( 457 | "value", "DISABLED" if state else "ENABLED" 458 | ) 459 | 460 | @property 461 | def cameranotification(self) -> CameraNotificationService.State: 462 | if self._cameranotification_service: 463 | return self._cameranotification_service.value 464 | 465 | @cameranotification.setter 466 | def cameranotification(self, state: bool): 467 | if self._cameranotification_service: 468 | self._cameranotification_service.put_state_element( 469 | "value", "ENABLED" if state else "DISABLED" 470 | ) 471 | 472 | 473 | class SHCCameraEyes(SHCCamera360): 474 | from .services_impl import ( 475 | CameraLightService, 476 | ) 477 | 478 | def __init__(self, api, raw_device, raw_device_services): 479 | super().__init__(api, raw_device, raw_device_services) 480 | self._cameralight_service = self.device_service("CameraLight") 481 | 482 | @property 483 | def cameralight(self) -> CameraLightService.State: 484 | if self._cameralight_service: 485 | return self._cameralight_service.value 486 | 487 | @cameralight.setter 488 | def cameralight(self, state: bool): 489 | if self._cameralight_service: 490 | self._cameralight_service.put_state_element( 491 | "value", "ON" if state else "OFF" 492 | ) 493 | 494 | 495 | class SHCThermostat( 496 | SHCBatteryDevice, 497 | _CommunicationQuality, 498 | _SilentMode, 499 | _Thermostat, 500 | _TemperatureLevel, 501 | _TemperatureOffset, 502 | ): 503 | from .services_impl import ( 504 | ValveTappetService, 505 | TemperatureOffsetService, 506 | ) 507 | 508 | def __init__(self, api, raw_device, raw_device_services): 509 | super().__init__(api, raw_device, raw_device_services) 510 | self._valvetappet_service = self.device_service("ValveTappet") 511 | 512 | @property 513 | def position(self) -> int: 514 | return self._valvetappet_service.position 515 | 516 | @property 517 | def valvestate(self) -> ValveTappetService.State: 518 | return self._valvetappet_service.value 519 | 520 | 521 | class SHCClimateControl(_TemperatureLevel): 522 | from .services_impl import RoomClimateControlService 523 | 524 | def __init__(self, api, raw_device, raw_device_services): 525 | super().__init__(api, raw_device, raw_device_services) 526 | self._roomclimatecontrol_service = self.device_service("RoomClimateControl") 527 | 528 | @property 529 | def setpoint_temperature(self) -> float: 530 | return self._roomclimatecontrol_service.setpoint_temperature 531 | 532 | @setpoint_temperature.setter 533 | def setpoint_temperature(self, temperature: float): 534 | self._roomclimatecontrol_service.setpoint_temperature = temperature 535 | 536 | @property 537 | def operation_mode(self) -> RoomClimateControlService.OperationMode: 538 | return self._roomclimatecontrol_service.operation_mode 539 | 540 | @operation_mode.setter 541 | def operation_mode(self, mode: RoomClimateControlService.OperationMode): 542 | self._roomclimatecontrol_service.operation_mode = mode 543 | 544 | @property 545 | def boost_mode(self) -> bool: 546 | return self._roomclimatecontrol_service.boost_mode 547 | 548 | @boost_mode.setter 549 | def boost_mode(self, value: bool): 550 | self._roomclimatecontrol_service.boost_mode = value 551 | 552 | @property 553 | def supports_boost_mode(self) -> bool: 554 | return self._roomclimatecontrol_service.supports_boost_mode 555 | 556 | @property 557 | def low(self) -> bool: 558 | return self._roomclimatecontrol_service.low 559 | 560 | @low.setter 561 | def low(self, value: bool): 562 | self._roomclimatecontrol_service.low = value 563 | 564 | @property 565 | def summer_mode(self) -> bool: 566 | return self._roomclimatecontrol_service.summer_mode 567 | 568 | @summer_mode.setter 569 | def summer_mode(self, value: bool): 570 | self._roomclimatecontrol_service.summer_mode = value 571 | 572 | 573 | class SHCHeatingCircuit(SHCDevice): 574 | from .services_impl import HeatingCircuitService 575 | 576 | def __init__(self, api, raw_device, raw_device_services): 577 | super().__init__(api, raw_device, raw_device_services) 578 | self._heating_circuit_service = self.device_service("HeatingCircuit") 579 | 580 | @property 581 | def setpoint_temperature(self) -> float: 582 | return self._heating_circuit_service.setpoint_temperature 583 | 584 | @setpoint_temperature.setter 585 | def setpoint_temperature(self, temperature: float): 586 | self._heating_circuit_service.setpoint_temperature = temperature 587 | 588 | @property 589 | def operation_mode(self) -> HeatingCircuitService.OperationMode: 590 | return self._heating_circuit_service.operation_mode 591 | 592 | @operation_mode.setter 593 | def operation_mode(self, mode: HeatingCircuitService.OperationMode): 594 | self._heating_circuit_service.operation_mode = mode 595 | 596 | @property 597 | def temperature_override_mode_active(self) -> bool: 598 | return self._heating_circuit_service.temperature_override_mode_active 599 | 600 | @property 601 | def temperature_override_feature_enabled(self) -> bool: 602 | return self._heating_circuit_service.temperature_override_feature_enabled 603 | 604 | @property 605 | def energy_saving_feature_enabled(self) -> bool: 606 | return self._heating_circuit_service.energy_saving_feature_enabled 607 | 608 | @property 609 | def on(self) -> bool: 610 | return self._heating_circuit_service.on 611 | 612 | 613 | class SHCWallThermostat(SHCBatteryDevice, _TemperatureLevel, _HumidityLevel): 614 | pass 615 | 616 | 617 | class SHCRoomThermostat2( 618 | SHCWallThermostat, 619 | _CommunicationQuality, 620 | _Thermostat, 621 | _TemperatureOffset, 622 | ): 623 | pass 624 | 625 | 626 | class SHCUniversalSwitch(SHCBatteryDevice): 627 | from .services_impl import KeypadService 628 | 629 | def __init__(self, api, raw_device, raw_device_services): 630 | super().__init__(api, raw_device, raw_device_services) 631 | self._keypad_service = self.device_service("Keypad") 632 | 633 | @property 634 | def keystates(self) -> dict[str]: 635 | return ["LOWER_BUTTON", "UPPER_BUTTON"] 636 | 637 | @property 638 | def eventtypes(self) -> Enum: 639 | return self._keypad_service.KeyEvent 640 | 641 | @property 642 | def keycode(self) -> int: 643 | return self._keypad_service.keyCode 644 | 645 | @property 646 | def keyname(self) -> KeypadService.KeyState: 647 | return self._keypad_service.keyName 648 | 649 | @property 650 | def eventtype(self) -> KeypadService.KeyEvent: 651 | return self._keypad_service.eventType 652 | 653 | @property 654 | def eventtimestamp(self) -> int: 655 | return self._keypad_service.eventTimestamp 656 | 657 | 658 | class SHCUniversalSwitch2(SHCUniversalSwitch): 659 | from .services_impl import KeypadService 660 | 661 | def __init__(self, api, raw_device, raw_device_services): 662 | super().__init__(api, raw_device, raw_device_services) 663 | 664 | @property 665 | def keystates(self) -> dict[str]: 666 | return [ 667 | "LOWER_LEFT_BUTTON", 668 | "LOWER_RIGHT_BUTTON", 669 | "UPPER_LEFT_BUTTON", 670 | "UPPER_RIGHT_BUTTON", 671 | ] 672 | 673 | 674 | class SHCMotionDetector(SHCBatteryDevice): 675 | from .services_impl import LatestMotionService, MultiLevelSensorService 676 | 677 | def __init__(self, api, raw_device, raw_device_services): 678 | super().__init__(api, raw_device, raw_device_services) 679 | self._service = self.device_service("LatestMotion") 680 | self._multi_level_sensor_service = self.device_service("MultiLevelSensor") 681 | 682 | @property 683 | def latestmotion(self) -> str: 684 | return self._service.latestMotionDetected 685 | 686 | @property 687 | def illuminance(self) -> str: 688 | return self._multi_level_sensor_service.illuminance 689 | 690 | 691 | class SHCTwinguard(SHCBatteryDevice): 692 | from .services_impl import AirQualityLevelService, SmokeDetectorCheckService 693 | 694 | def __init__(self, api, raw_device, raw_device_services): 695 | super().__init__(api, raw_device, raw_device_services) 696 | self._airqualitylevel_service = self.device_service("AirQualityLevel") 697 | self._smokedetectorcheck_service = self.device_service("SmokeDetectorCheck") 698 | 699 | @property 700 | def description(self) -> str: 701 | return self._airqualitylevel_service.description 702 | 703 | @property 704 | def combined_rating(self) -> AirQualityLevelService.RatingState: 705 | return self._airqualitylevel_service.combinedRating 706 | 707 | @property 708 | def temperature(self) -> int: 709 | return self._airqualitylevel_service.temperature 710 | 711 | @property 712 | def temperature_rating(self) -> AirQualityLevelService.RatingState: 713 | return self._airqualitylevel_service.temperatureRating 714 | 715 | @property 716 | def humidity(self) -> int: 717 | return self._airqualitylevel_service.humidity 718 | 719 | @property 720 | def humidity_rating(self) -> AirQualityLevelService.RatingState: 721 | return self._airqualitylevel_service.humidityRating 722 | 723 | @property 724 | def purity(self) -> int: 725 | return self._airqualitylevel_service.purity 726 | 727 | @property 728 | def purity_rating(self) -> AirQualityLevelService.RatingState: 729 | return self._airqualitylevel_service.purityRating 730 | 731 | @property 732 | def smokedetectorcheck_state(self) -> SmokeDetectorCheckService.State: 733 | return self._smokedetectorcheck_service.value 734 | 735 | def smoketest_requested(self): 736 | self._smokedetectorcheck_service.put_state_element( 737 | "value", "SMOKE_TEST_REQUESTED" 738 | ) 739 | 740 | 741 | class SHCSmokeDetectionSystem(SHCDevice): 742 | from .services_impl import SurveillanceAlarmService 743 | 744 | def __init__(self, api, raw_device, raw_device_services): 745 | super().__init__(api, raw_device, raw_device_services) 746 | self._surveillancealarm_service = self.device_service("SurveillanceAlarm") 747 | # self._smokedetectioncontrol_service = self.device_service("SmokeDetectionControl") 748 | 749 | @property 750 | def alarm(self) -> SurveillanceAlarmService.State: 751 | return self._surveillancealarm_service.value 752 | 753 | 754 | class SHCPresenceSimulationSystem(SHCDevice): 755 | from .services_impl import PresenceSimulationConfigurationService 756 | 757 | def __init__(self, api, raw_device, raw_device_services): 758 | super().__init__(api, raw_device, raw_device_services) 759 | self._presencesimulationconfiguration_service = self.device_service( 760 | "PresenceSimulationConfiguration" 761 | ) 762 | 763 | @property 764 | def enabled(self) -> bool: 765 | return self._presencesimulationconfiguration_service.enabled 766 | 767 | @enabled.setter 768 | def enabled(self, value: bool): 769 | self._presencesimulationconfiguration_service.enabled = value 770 | 771 | 772 | class SHCLight(SHCDevice): 773 | from .services_impl import ( 774 | BinarySwitchService, 775 | HSBColorActuatorService, 776 | HueColorTemperatureService, 777 | MultiLevelSwitchService, 778 | ) 779 | 780 | class Capabilities(Flag): 781 | BRIGHTNESS = auto() 782 | COLOR_TEMP = auto() 783 | COLOR_HSB = auto() 784 | 785 | def __init__(self, api, raw_device, raw_device_services): 786 | super().__init__(api, raw_device, raw_device_services) 787 | 788 | self._binaryswitch_service = self.device_service("BinarySwitch") 789 | self._multilevelswitch_service = self.device_service("MultiLevelSwitch") 790 | self._huecolortemperature_service = self.device_service("HueColorTemperature") 791 | self._hsbcoloractuator_service = self.device_service("HSBColorActuator") 792 | 793 | self._capabilities = self.Capabilities(0) 794 | if self._multilevelswitch_service: 795 | self._capabilities |= self.Capabilities.BRIGHTNESS 796 | if self._huecolortemperature_service: 797 | self._capabilities |= self.Capabilities.COLOR_TEMP 798 | if self._hsbcoloractuator_service: 799 | self._capabilities |= self.Capabilities.COLOR_HSB 800 | 801 | @property 802 | def binarystate(self) -> bool: 803 | return self._binaryswitch_service.value 804 | 805 | @binarystate.setter 806 | def binarystate(self, state: bool): 807 | self._binaryswitch_service.put_state_element("on", True if state else False) 808 | 809 | @property 810 | def brightness(self) -> int: 811 | if self.supports_brightness: 812 | return self._multilevelswitch_service.value 813 | return 0 814 | 815 | @brightness.setter 816 | def brightness(self, state: int): 817 | if self.supports_brightness: 818 | self._multilevelswitch_service.put_state_element("level", state) 819 | 820 | @property 821 | def color(self) -> int: 822 | if self.supports_color_temp: 823 | return self._huecolortemperature_service.value 824 | return 0 825 | 826 | @color.setter 827 | def color(self, state: int): 828 | if self.supports_color_temp: 829 | self._huecolortemperature_service.put_state_element( 830 | "colorTemperature", state 831 | ) 832 | 833 | @property 834 | def rgb(self) -> int: 835 | if self.supports_color_hsb: 836 | return self._hsbcoloractuator_service.value 837 | return 0 838 | 839 | @rgb.setter 840 | def rgb(self, state: int): 841 | if self.supports_color_hsb: 842 | self._hsbcoloractuator_service.put_state_element("rgb", state) 843 | 844 | @property 845 | def min_color_temperature(self) -> int: 846 | if self.supports_color_temp: 847 | return self._huecolortemperature_service.min_value 848 | if self.supports_color_hsb: 849 | return self._hsbcoloractuator_service.min_value 850 | return 0 851 | 852 | @property 853 | def max_color_temperature(self) -> int: 854 | if self.supports_color_temp: 855 | return self._huecolortemperature_service.max_value 856 | if self.supports_color_hsb: 857 | return self._hsbcoloractuator_service.max_value 858 | return 0 859 | 860 | @property 861 | def supports_brightness(self) -> bool: 862 | return bool(self._capabilities & self.Capabilities.BRIGHTNESS) 863 | 864 | @property 865 | def supports_color_temp(self) -> bool: 866 | return bool(self._capabilities & self.Capabilities.COLOR_TEMP) 867 | 868 | @property 869 | def supports_color_hsb(self) -> bool: 870 | return bool(self._capabilities & self.Capabilities.COLOR_HSB) 871 | 872 | 873 | class SHCWaterLeakageSensor(SHCBatteryDevice): 874 | from .services_impl import WaterLeakageSensorService, WaterLeakageSensorTiltService 875 | 876 | def __init__(self, api, raw_device, raw_device_services): 877 | super().__init__(api, raw_device, raw_device_services) 878 | 879 | self._leakage_service = self.device_service("WaterLeakageSensor") 880 | self._tilt_service = self.device_service("WaterLeakageSensorTilt") 881 | self._sensor_check_service = self.device_service("WaterLeakageSensorCheck") 882 | 883 | @property 884 | def leakage_state(self) -> WaterLeakageSensorService.State: 885 | return self._leakage_service.value 886 | 887 | @property 888 | def acoustic_signal_state(self) -> WaterLeakageSensorTiltService.State: 889 | return self._tilt_service.acousticSignalState 890 | 891 | @property 892 | def push_notification_state(self) -> WaterLeakageSensorTiltService.State: 893 | return self._tilt_service.pushNotificationState 894 | 895 | @property 896 | def sensor_check_state(self) -> str: 897 | return self._sensor_check_service.value 898 | 899 | 900 | class SHCMicromoduleDimmer( 901 | SHCLight, _CommunicationQuality, _ChildProtection, _PowerSwitch 902 | ): 903 | # from .services_impl import ( 904 | # # Services TBD: 905 | # # ElectricalFaultsService, 906 | # # DimmerConfiguration, 907 | # # SwitchConfiguration, 908 | # ) 909 | 910 | @property 911 | def binarystate(self) -> bool: 912 | if self._powerswitch_service: 913 | return self._powerswitch_service.value == self.PowerSwitchService.State.ON 914 | 915 | @binarystate.setter 916 | def binarystate(self, state: bool): 917 | if self._powerswitch_service: 918 | self._powerswitch_service.put_state_element( 919 | "switchState", "ON" if state else "OFF" 920 | ) 921 | 922 | 923 | MODEL_MAPPING = { 924 | "SWD": SHCShutterContact, 925 | "SWD2": SHCShutterContact2, 926 | "SWD2_DUAL": SHCShutterContact2, 927 | "SWD2_PLUS": SHCShutterContact2Plus, 928 | "BBL": SHCShutterControl, 929 | "MICROMODULE_AWNING": SHCMicromoduleShutterControl, 930 | "MICROMODULE_SHUTTER": SHCMicromoduleShutterControl, 931 | "PSM": SHCSmartPlug, 932 | "BSM": SHCLightSwitchBSM, 933 | "MICROMODULE_BLINDS": SHCMicromoduleBlinds, 934 | "MICROMODULE_LIGHT_ATTACHED": SHCLightSwitch, 935 | "MICROMODULE_LIGHT_CONTROL": SHCLightControl, 936 | "MICROMODULE_RELAY": SHCMicromoduleRelay, 937 | "PLUG_COMPACT": SHCSmartPlugCompact, 938 | "PLUG_COMPACT_DUAL": SHCSmartPlugCompact, 939 | "SD": SHCSmokeDetector, 940 | "SMOKE_DETECTOR2": SHCSmokeDetector, 941 | "CAMERA_EYES": SHCCameraEyes, 942 | "CAMERA_360": SHCCamera360, 943 | "ROOM_CLIMATE_CONTROL": SHCClimateControl, 944 | "TRV": SHCThermostat, 945 | "TRV_GEN2": SHCThermostat, 946 | "TRV_GEN2_DUAL": SHCThermostat, 947 | "THB": SHCWallThermostat, 948 | "BWTH": SHCWallThermostat, 949 | "BWTH24": SHCWallThermostat, 950 | "RTH2_BAT": SHCRoomThermostat2, 951 | "RTH2_230": SHCRoomThermostat2, 952 | "WRC2": SHCUniversalSwitch, 953 | "SWITCH2": SHCUniversalSwitch2, 954 | "MD": SHCMotionDetector, 955 | "PRESENCE_SIMULATION_SERVICE": SHCPresenceSimulationSystem, 956 | "TWINGUARD": SHCTwinguard, 957 | "SMOKE_DETECTION_SYSTEM": SHCSmokeDetectionSystem, 958 | "LEDVANCE_LIGHT": SHCLight, 959 | "HUE_LIGHT": SHCLight, 960 | "WLS": SHCWaterLeakageSensor, 961 | "HEATING_CIRCUIT": SHCHeatingCircuit, 962 | "MICROMODULE_DIMMER": SHCMicromoduleDimmer, 963 | } 964 | 965 | SUPPORTED_MODELS = MODEL_MAPPING.keys() 966 | 967 | 968 | def build(api, raw_device, raw_device_services) -> SHCDevice: 969 | device_model = raw_device["deviceModel"] 970 | assert device_model in SUPPORTED_MODELS, "Device model is supported" 971 | return MODEL_MAPPING[device_model]( 972 | api=api, raw_device=raw_device, raw_device_services=raw_device_services 973 | ) 974 | --------------------------------------------------------------------------------