├── .coveragerc ├── .gitignore ├── .pylintrc ├── .travis.yml ├── LICENSE ├── README.rst ├── examples └── lightbulb.py ├── pyhap ├── __init__.py ├── accessory.py ├── characteristic.py ├── characteristics │ ├── __init__.py │ ├── accessory_identifier.py │ ├── administrator_only_access.py │ ├── air_particulate_density.py │ ├── air_particulate_size.py │ ├── air_quality.py │ ├── audio_feedback.py │ ├── battery_level.py │ ├── brightness.py │ ├── carbon_dioxide_detected.py │ ├── carbon_dioxide_level.py │ ├── carbon_dioxide_peak_level.py │ ├── carbon_monoxide_detected.py │ ├── carbon_monoxide_level.py │ ├── carbon_monoxide_peak_level.py │ ├── category.py │ ├── charging_state.py │ ├── configure_bridged_accessory.py │ ├── configure_bridged_accessory_status.py │ ├── contact_sensor_state.py │ ├── cooling_threshold_temperature.py │ ├── current_ambient_light_level.py │ ├── current_door_state.py │ ├── current_heating_cooling_state.py │ ├── current_horizontal_tilt_angle.py │ ├── current_position.py │ ├── current_relative_humidity.py │ ├── current_temperature.py │ ├── current_time.py │ ├── current_vertical_tilt_angle.py │ ├── day_of_the_week.py │ ├── discover_bridged_accessories.py │ ├── discovered_bridged_accessories.py │ ├── firmware_revision.py │ ├── hardware_revision.py │ ├── heating_threshold_temperature.py │ ├── hold_position.py │ ├── hue.py │ ├── identify.py │ ├── leak_detected.py │ ├── link_quality.py │ ├── lock_control_point.py │ ├── lock_current_state.py │ ├── lock_last_known_action.py │ ├── lock_management_auto_security_timeout.py │ ├── lock_target_state.py │ ├── logs.py │ ├── manufacturer.py │ ├── model.py │ ├── motion_detected.py │ ├── name.py │ ├── obstruction_detected.py │ ├── occupancy_detected.py │ ├── on.py │ ├── outlet_in_use.py │ ├── pair_setup.py │ ├── pair_verify.py │ ├── pairing_features.py │ ├── pairing_pairings.py │ ├── position_state.py │ ├── programmable_switch_event.py │ ├── programmable_switch_output_state.py │ ├── reachable.py │ ├── rotation_direction.py │ ├── rotation_speed.py │ ├── saturation.py │ ├── security_system_alarm_type.py │ ├── security_system_current_state.py │ ├── security_system_target_state.py │ ├── serial_number.py │ ├── smoke_detected.py │ ├── software_revision.py │ ├── status_active.py │ ├── status_fault.py │ ├── status_jammed.py │ ├── status_low_battery.py │ ├── status_tampered.py │ ├── target_door_state.py │ ├── target_heating_cooling_state.py │ ├── target_horizontal_tilt_angle.py │ ├── target_position.py │ ├── target_relative_humidity.py │ ├── target_temperature.py │ ├── target_vertical_tilt_angle.py │ ├── temperature_display_units.py │ ├── time_update.py │ ├── tunnel_connection_timeout_.py │ ├── tunneled_accessory_advertising.py │ ├── tunneled_accessory_connected.py │ ├── tunneled_accessory_state_number.py │ └── version.py ├── config.py ├── exception.py ├── http_server.py ├── pair.py ├── pyhap.py ├── route.py ├── service.py ├── srp.py ├── tlv.py └── util.py ├── pyhap_generator ├── generator.py └── templates │ └── characteristic.py.template ├── setup.py └── test ├── __init__.py ├── test_accessory.py ├── test_characteristic.py ├── test_config.py ├── test_pair.py ├── test_pyhap.py ├── test_route.py ├── test_service.py ├── test_srp.py ├── test_tlv.py └── test_util.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | pyhap/characteristics/* 4 | 5 | [report] 6 | show_missing = true 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [master] 2 | ignore = characteristics 3 | 4 | [message control] 5 | disable = 6 | design, 7 | duplicate-code, 8 | fixme, 9 | invalid-name, 10 | missing-docstring 11 | 12 | [reports] 13 | output-format = parseable 14 | score = no 15 | 16 | [format] 17 | max-line-length = 120 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.6.3" 5 | 6 | install: 7 | - pip install . 8 | - pip install pylint mypy pytest-cov coveralls 9 | 10 | script: 11 | - pylint pyhap test 12 | - mypy --ignore-missing-imports pyhap 13 | - pytest --cov pyhap 14 | 15 | after_success: 16 | - coveralls 17 | 18 | notifications: 19 | email: false 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dmitry Budaev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyhap 2 | ===== 3 | 4 | .. image:: https://travis-ci.org/condemil/pyhap.svg?branch=master 5 | :target: https://travis-ci.org/condemil/pyhap 6 | 7 | .. image:: https://img.shields.io/pypi/v/pyhap.svg 8 | :target: https://pypi.python.org/pypi/pyhap 9 | 10 | .. image:: https://coveralls.io/repos/github/condemil/pyhap/badge.svg 11 | :target: https://coveralls.io/github/condemil/pyhap 12 | 13 | The project is on early stage, the API may change. Use version pinning to prevent unexpected changes. 14 | 15 | Requirements 16 | ------------ 17 | 18 | * Python 3.6+ 19 | * `cryptography `_ 20 | * `ed25519 `_ 21 | * `zeroconf `_ 22 | 23 | 24 | Installation 25 | ------------ 26 | 27 | $ pip install pyhap 28 | 29 | Or add pyhap to your application's `requirements.txt` / `setup.py` / `Pipfile`. 30 | 31 | 32 | Usage 33 | ----- 34 | 35 | Check `examples `_ 36 | -------------------------------------------------------------------------------- /examples/lightbulb.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pyhap.accessory import ( 4 | Accessory, 5 | Accessories, 6 | ) 7 | from pyhap.characteristics import ( 8 | Brightness, 9 | On, 10 | ) 11 | from pyhap.config import JsonConfig 12 | from pyhap.pyhap import start 13 | from pyhap.service import ( 14 | LightbulbService, 15 | ) 16 | 17 | IP_ADDRESS = 'XXX.XXX.XXX.XXX' # change to your IP address 18 | 19 | 20 | def main(): 21 | logging.basicConfig(level=logging.INFO) 22 | 23 | lightbulb1 = Accessory(name='Acme LED Light Bulb', model='LEDBulb1,1', manufacturer='Acme') 24 | 25 | lightbulb1_lightbulb = LightbulbService() 26 | lightbulb1_lightbulb.add_characteristic(On(True, bulb_on)) 27 | lightbulb1_lightbulb.add_characteristic(Brightness(50, bulb_brightness)) 28 | lightbulb1.add_service(lightbulb1_lightbulb) 29 | 30 | accessories = Accessories() 31 | accessories.add(lightbulb1) 32 | 33 | config = JsonConfig(IP_ADDRESS) 34 | start(config, accessories) 35 | 36 | 37 | async def bulb_on(value: bool) -> bool: 38 | if value: 39 | print('Bulb is on') 40 | else: 41 | print('Bulb is off') 42 | return True # non-true result indicates that the accessory is not availbale 43 | 44 | 45 | async def bulb_brightness(value) -> bool: 46 | print(f'Brightness is {value}%') 47 | return True 48 | 49 | 50 | if __name__ == '__main__': 51 | main() 52 | -------------------------------------------------------------------------------- /pyhap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/condemil/pyhap/d1f6e739f670566a18db5adf8b52afbb476862b4/pyhap/__init__.py -------------------------------------------------------------------------------- /pyhap/accessory.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | from typing import ( 3 | Any, 4 | Callable, 5 | Dict, 6 | Iterable, 7 | Iterator, 8 | List, 9 | Optional, 10 | Tuple, 11 | ) 12 | 13 | from pyhap.config import HAPStatusCode 14 | from pyhap.characteristic import ( 15 | Characteristic, 16 | CharacteristicPermission, 17 | ) 18 | from pyhap.characteristics import ( 19 | FirmwareRevision, 20 | HardwareRevision, 21 | Identify, 22 | Manufacturer, 23 | Model, 24 | Name, 25 | SerialNumber, 26 | ) 27 | from pyhap.service import Service 28 | from pyhap.service import AccessoryInformationService 29 | from pyhap.util import ( 30 | serial_number_hash, 31 | uuid_to_aduuid, 32 | ) 33 | 34 | logger = getLogger('pyhap') 35 | IdentifyCallback = Optional[Callable[..., None]] 36 | 37 | 38 | class Accessory: 39 | def __init__(self, name: str, model: str, manufacturer: str, serial_number: Optional[str] = None, 40 | firmware_revision: str = '0.0.1', hardware_revision: Optional[str] = None, 41 | identify_callback: IdentifyCallback = None) -> None: 42 | self._accessory_id: int = None # set by bridge 43 | self.services: List[Service] = [] 44 | self.characteristics: Dict[int, Characteristic] = {} 45 | self.object_count = 1 46 | self._identify_callback = identify_callback 47 | 48 | if not serial_number: 49 | serial_number = serial_number_hash(name) 50 | 51 | accessory_information = AccessoryInformationService() 52 | 53 | characteristics = (Name(name), Model(model), Manufacturer(manufacturer), SerialNumber(serial_number), 54 | FirmwareRevision(firmware_revision), Identify(False)) 55 | 56 | for characteristic in characteristics: 57 | accessory_information.add_characteristic(characteristic) 58 | 59 | if hardware_revision: 60 | accessory_information.add_characteristic(HardwareRevision(hardware_revision)) 61 | 62 | self.add_service(accessory_information) 63 | 64 | @property 65 | def accessory_id(self) -> int: 66 | return self._accessory_id 67 | 68 | @accessory_id.setter 69 | def accessory_id(self, value: int): 70 | for characteristic in self.characteristics.values(): 71 | characteristic.accessory_id = value 72 | self._accessory_id = value 73 | 74 | def add_service(self, service: Service): 75 | service.instance_id = self.object_count 76 | self.object_count += 1 77 | 78 | for characteristic in service.characteristics: 79 | characteristic.instance_id = self.object_count 80 | self.characteristics[characteristic.instance_id] = characteristic 81 | self.object_count += 1 82 | 83 | self.services.append(service) 84 | 85 | def get_characteristic(self, characteristic_id: int) -> Characteristic: 86 | characteristic = self.characteristics[characteristic_id] 87 | return characteristic 88 | 89 | async def identify(self): 90 | if self._identify_callback: 91 | await self._identify_callback(self) 92 | 93 | def __json__(self): 94 | return { 95 | 'aid': self.accessory_id, 96 | 'services': self.services, 97 | } 98 | 99 | 100 | class Accessories(Iterable): 101 | # TODO: checks p.92 102 | def __init__(self, bridge: Optional[Accessory] = None) -> None: # pylint: disable=super-init-not-called 103 | self.accessories: Dict[int, Accessory] = {} 104 | self.accessory_count = 0 105 | 106 | if not bridge: 107 | bridge = Accessory(name='PyHAP', model='PyHAP1,1', manufacturer='PyHAP') 108 | self.add(bridge) 109 | 110 | def __iter__(self) -> Iterator[Accessory]: 111 | for value in self.accessories.values(): 112 | yield value 113 | 114 | def add(self, accessory): 115 | self.accessory_count += 1 116 | accessory.accessory_id = self.accessory_count 117 | self.accessories[accessory.accessory_id] = accessory 118 | 119 | def get_characteristic(self, accessory_id: int, characteristic_id: int) -> Characteristic: 120 | return self.accessories[accessory_id].get_characteristic(characteristic_id) 121 | 122 | def read_characteristic(self, query: dict) -> Tuple[bool, dict]: 123 | data = query['id'].split(',') 124 | include_metadata = query.get('meta', 0) 125 | include_permissions = query.get('perms', 0) 126 | include_type = query.get('type', 0) 127 | include_ev = query.get('ev', 0) 128 | result = [] 129 | has_errors = False 130 | 131 | for item in data: 132 | accessory_id, characteristic_id = item.split('.') 133 | item_result: Dict[str, Any] = { 134 | 'aid': int(accessory_id), 135 | 'iid': int(characteristic_id), 136 | } 137 | characteristic = self.get_characteristic(int(accessory_id), int(characteristic_id)) 138 | 139 | if CharacteristicPermission.pair_read in characteristic.permissions: 140 | item_result['value'] = characteristic.value 141 | else: 142 | item_result['status'] = HAPStatusCode.write_only.value 143 | has_errors = True 144 | 145 | if include_metadata: 146 | # TODO add metadata if include_metadata in params 147 | pass 148 | if include_permissions: 149 | item_result['perms'] = characteristic.serialize_permissions() 150 | if include_type: 151 | item_result['type'] = uuid_to_aduuid(characteristic.characteristic_uuid) 152 | if include_ev: 153 | # TODO include events, implement events p.89 154 | pass 155 | 156 | result.append(item_result) 157 | 158 | if has_errors: 159 | # all characteristics should have status in case at least one have failed 160 | for item in result: 161 | item['status'] = item.get('status', HAPStatusCode.success.value) 162 | 163 | return has_errors, {'characteristics': result} 164 | 165 | async def write_characteristic(self, data: list) -> list: 166 | result = [] 167 | has_errors = False 168 | for item in data: 169 | value = item.get('value') 170 | 171 | if value is None: 172 | continue 173 | 174 | item_result = { 175 | 'aid': item['aid'], 176 | 'iid': item['iid'], 177 | } 178 | characteristic: Characteristic = self.get_characteristic(item['aid'], item['iid']) 179 | 180 | if CharacteristicPermission.pair_write not in characteristic.permissions: 181 | logger.info('Write call to characteristic %s without write permission', 182 | characteristic.__class__.__name__) 183 | item_result['status'] = HAPStatusCode.read_only.value 184 | result.append(item_result) 185 | has_errors = True 186 | continue 187 | 188 | if characteristic.characteristic_type == bool: 189 | characteristic.value = bool(value) 190 | elif characteristic.characteristic_type == float: 191 | characteristic.value = float(value) 192 | else: 193 | characteristic.value = value 194 | 195 | callback_result: bool = False 196 | 197 | try: 198 | logger.debug(f'Write value {value} to characteristic {characteristic.__class__.__name__}') 199 | if characteristic.callback: 200 | callback_result = await characteristic.callback(characteristic.value) 201 | else: 202 | callback_result = True # do not threat missing callback as error 203 | except Exception as e: # pylint: disable=broad-except 204 | logger.error(f'Callback exception: {str(e)}', exc_info=True) # callback can throw exception, ignore it 205 | 206 | if not callback_result: 207 | item_result['status'] = HAPStatusCode.service_unavailable.value 208 | has_errors = True 209 | 210 | result.append(item_result) 211 | 212 | if has_errors: 213 | return result 214 | 215 | return [] 216 | 217 | def __json__(self): 218 | return { 219 | 'accessories': list(self.accessories.values()) 220 | } 221 | -------------------------------------------------------------------------------- /pyhap/characteristic.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from enum import Enum 3 | from typing import ( 4 | Callable, 5 | Generic, 6 | List, 7 | Optional, 8 | Type, 9 | TypeVar, 10 | ) 11 | from uuid import UUID 12 | 13 | from pyhap.util import uuid_to_aduuid 14 | 15 | T = TypeVar('T', int, float, bool, str, None) 16 | 17 | 18 | class CharacteristicPermission(Enum): 19 | pair_read = 'pr' 20 | pair_write = 'pw' 21 | notify = 'ev' 22 | broadcast = 'b' 23 | 24 | 25 | class Characteristic(Generic[T]): 26 | def __init__(self, value: T = None, callback: Optional[Callable] = None) -> None: 27 | self._check_value(value) 28 | self._value: T = value 29 | self._accessory_id: int = None # set by bridge 30 | self._instance_id: int = None # set by bridge 31 | self.callback: Callable = callback 32 | 33 | @property 34 | def value(self) -> T: 35 | return self._value 36 | 37 | @value.setter 38 | def value(self, value: T) -> None: 39 | self._check_value(value) 40 | self._value = value 41 | 42 | @property 43 | @abstractmethod 44 | def characteristic_uuid(self) -> UUID: 45 | raise NotImplementedError() # pragma: no cover 46 | 47 | @property 48 | @abstractmethod 49 | def characteristic_type(self) -> Type: 50 | raise NotImplementedError() # pragma: no cover 51 | 52 | @property 53 | @abstractmethod 54 | def characteristic_format(self) -> str: 55 | raise NotImplementedError() # pragma: no cover 56 | 57 | @property 58 | def permissions(self) -> List[CharacteristicPermission]: 59 | raise NotImplementedError() # pragma: no cover 60 | 61 | @property 62 | def accessory_id(self) -> int: 63 | return self._accessory_id 64 | 65 | @accessory_id.setter 66 | def accessory_id(self, value: int): 67 | self._accessory_id = value 68 | 69 | @property 70 | def instance_id(self) -> int: 71 | return self._instance_id 72 | 73 | @instance_id.setter 74 | def instance_id(self, value: int): 75 | self._instance_id = value 76 | 77 | def serialize_permissions(self) -> List[str]: 78 | result = [] 79 | for permission in self.permissions: 80 | result.append(permission.value) 81 | return result 82 | 83 | def __json__(self) -> dict: 84 | result = { 85 | 'type': uuid_to_aduuid(self.characteristic_uuid), 86 | 'perms': self.serialize_permissions(), 87 | 'format': self.characteristic_format, 88 | 'aid': self.accessory_id, 89 | 'iid': self.instance_id, 90 | } 91 | 92 | if self._value is not None and CharacteristicPermission.pair_read in self.permissions: 93 | result['value'] = self._value 94 | 95 | if self.characteristic_format == 'string': 96 | result['maxLen'] = 64 97 | 98 | return result 99 | 100 | def _check_value(self, value: T): 101 | if not isinstance(value, self.characteristic_type): 102 | raise ValueError(f'Failed to set value {value} for {self.characteristic_format} characteristic ' 103 | f'{self.__class__.__name__}') 104 | -------------------------------------------------------------------------------- /pyhap/characteristics/__init__.py: -------------------------------------------------------------------------------- 1 | from pyhap.characteristics.accessory_identifier import AccessoryIdentifier 2 | from pyhap.characteristics.administrator_only_access import AdministratorOnlyAccess 3 | from pyhap.characteristics.air_particulate_density import AirParticulateDensity 4 | from pyhap.characteristics.air_particulate_size import AirParticulateSize 5 | from pyhap.characteristics.air_quality import AirQuality 6 | from pyhap.characteristics.audio_feedback import AudioFeedback 7 | from pyhap.characteristics.battery_level import BatteryLevel 8 | from pyhap.characteristics.brightness import Brightness 9 | from pyhap.characteristics.carbon_dioxide_detected import CarbonDioxideDetected 10 | from pyhap.characteristics.carbon_dioxide_level import CarbonDioxideLevel 11 | from pyhap.characteristics.carbon_dioxide_peak_level import CarbonDioxidePeakLevel 12 | from pyhap.characteristics.carbon_monoxide_detected import CarbonMonoxideDetected 13 | from pyhap.characteristics.carbon_monoxide_level import CarbonMonoxideLevel 14 | from pyhap.characteristics.carbon_monoxide_peak_level import CarbonMonoxidePeakLevel 15 | from pyhap.characteristics.category import Category 16 | from pyhap.characteristics.charging_state import ChargingState 17 | from pyhap.characteristics.configure_bridged_accessory import ConfigureBridgedAccessory 18 | from pyhap.characteristics.configure_bridged_accessory_status import ConfigureBridgedAccessoryStatus 19 | from pyhap.characteristics.contact_sensor_state import ContactSensorState 20 | from pyhap.characteristics.cooling_threshold_temperature import CoolingThresholdTemperature 21 | from pyhap.characteristics.current_ambient_light_level import CurrentAmbientLightLevel 22 | from pyhap.characteristics.current_door_state import CurrentDoorState 23 | from pyhap.characteristics.current_heating_cooling_state import CurrentHeatingCoolingState 24 | from pyhap.characteristics.current_horizontal_tilt_angle import CurrentHorizontalTiltAngle 25 | from pyhap.characteristics.current_position import CurrentPosition 26 | from pyhap.characteristics.current_relative_humidity import CurrentRelativeHumidity 27 | from pyhap.characteristics.current_temperature import CurrentTemperature 28 | from pyhap.characteristics.current_time import CurrentTime 29 | from pyhap.characteristics.current_vertical_tilt_angle import CurrentVerticalTiltAngle 30 | from pyhap.characteristics.day_of_the_week import DayoftheWeek 31 | from pyhap.characteristics.discover_bridged_accessories import DiscoverBridgedAccessories 32 | from pyhap.characteristics.discovered_bridged_accessories import DiscoveredBridgedAccessories 33 | from pyhap.characteristics.firmware_revision import FirmwareRevision 34 | from pyhap.characteristics.hardware_revision import HardwareRevision 35 | from pyhap.characteristics.heating_threshold_temperature import HeatingThresholdTemperature 36 | from pyhap.characteristics.hold_position import HoldPosition 37 | from pyhap.characteristics.hue import Hue 38 | from pyhap.characteristics.identify import Identify 39 | from pyhap.characteristics.leak_detected import LeakDetected 40 | from pyhap.characteristics.link_quality import LinkQuality 41 | from pyhap.characteristics.lock_control_point import LockControlPoint 42 | from pyhap.characteristics.lock_current_state import LockCurrentState 43 | from pyhap.characteristics.lock_last_known_action import LockLastKnownAction 44 | from pyhap.characteristics.lock_management_auto_security_timeout import LockManagementAutoSecurityTimeout 45 | from pyhap.characteristics.lock_target_state import LockTargetState 46 | from pyhap.characteristics.logs import Logs 47 | from pyhap.characteristics.manufacturer import Manufacturer 48 | from pyhap.characteristics.model import Model 49 | from pyhap.characteristics.motion_detected import MotionDetected 50 | from pyhap.characteristics.name import Name 51 | from pyhap.characteristics.obstruction_detected import ObstructionDetected 52 | from pyhap.characteristics.occupancy_detected import OccupancyDetected 53 | from pyhap.characteristics.on import On 54 | from pyhap.characteristics.outlet_in_use import OutletInUse 55 | from pyhap.characteristics.pair_setup import PairSetup 56 | from pyhap.characteristics.pair_verify import PairVerify 57 | from pyhap.characteristics.pairing_features import PairingFeatures 58 | from pyhap.characteristics.pairing_pairings import PairingPairings 59 | from pyhap.characteristics.position_state import PositionState 60 | from pyhap.characteristics.programmable_switch_event import ProgrammableSwitchEvent 61 | from pyhap.characteristics.programmable_switch_output_state import ProgrammableSwitchOutputState 62 | from pyhap.characteristics.reachable import Reachable 63 | from pyhap.characteristics.rotation_direction import RotationDirection 64 | from pyhap.characteristics.rotation_speed import RotationSpeed 65 | from pyhap.characteristics.saturation import Saturation 66 | from pyhap.characteristics.security_system_alarm_type import SecuritySystemAlarmType 67 | from pyhap.characteristics.security_system_current_state import SecuritySystemCurrentState 68 | from pyhap.characteristics.security_system_target_state import SecuritySystemTargetState 69 | from pyhap.characteristics.serial_number import SerialNumber 70 | from pyhap.characteristics.smoke_detected import SmokeDetected 71 | from pyhap.characteristics.software_revision import SoftwareRevision 72 | from pyhap.characteristics.status_active import StatusActive 73 | from pyhap.characteristics.status_fault import StatusFault 74 | from pyhap.characteristics.status_jammed import StatusJammed 75 | from pyhap.characteristics.status_low_battery import StatusLowBattery 76 | from pyhap.characteristics.status_tampered import StatusTampered 77 | from pyhap.characteristics.target_door_state import TargetDoorState 78 | from pyhap.characteristics.target_heating_cooling_state import TargetHeatingCoolingState 79 | from pyhap.characteristics.target_horizontal_tilt_angle import TargetHorizontalTiltAngle 80 | from pyhap.characteristics.target_position import TargetPosition 81 | from pyhap.characteristics.target_relative_humidity import TargetRelativeHumidity 82 | from pyhap.characteristics.target_temperature import TargetTemperature 83 | from pyhap.characteristics.target_vertical_tilt_angle import TargetVerticalTiltAngle 84 | from pyhap.characteristics.temperature_display_units import TemperatureDisplayUnits 85 | from pyhap.characteristics.time_update import TimeUpdate 86 | from pyhap.characteristics.tunnel_connection_timeout_ import TunnelConnectionTimeout 87 | from pyhap.characteristics.tunneled_accessory_advertising import TunneledAccessoryAdvertising 88 | from pyhap.characteristics.tunneled_accessory_connected import TunneledAccessoryConnected 89 | from pyhap.characteristics.tunneled_accessory_state_number import TunneledAccessoryStateNumber 90 | from pyhap.characteristics.version import Version 91 | -------------------------------------------------------------------------------- /pyhap/characteristics/accessory_identifier.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class AccessoryIdentifier(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000057-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return str 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'string' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/administrator_only_access.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class AdministratorOnlyAccess(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000001-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/air_particulate_density.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class AirParticulateDensity(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000064-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/air_particulate_size.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class AirParticulateSize(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000065-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/air_quality.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class AirQuality(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000095-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/audio_feedback.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class AudioFeedback(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000005-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/battery_level.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class BatteryLevel(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000068-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/brightness.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class Brightness(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000008-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/carbon_dioxide_detected.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CarbonDioxideDetected(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000092-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/carbon_dioxide_level.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CarbonDioxideLevel(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000093-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/carbon_dioxide_peak_level.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CarbonDioxidePeakLevel(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000094-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/carbon_monoxide_detected.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CarbonMonoxideDetected(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000069-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/carbon_monoxide_level.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CarbonMonoxideLevel(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000090-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/carbon_monoxide_peak_level.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CarbonMonoxidePeakLevel(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000091-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/category.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class Category(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('000000A3-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/charging_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class ChargingState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000008F-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/configure_bridged_accessory.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class ConfigureBridgedAccessory(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('000000A0-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'tlv' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_write, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/configure_bridged_accessory_status.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class ConfigureBridgedAccessoryStatus(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000009D-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'tlv' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/contact_sensor_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class ContactSensorState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000006A-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/cooling_threshold_temperature.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CoolingThresholdTemperature(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000000D-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/current_ambient_light_level.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CurrentAmbientLightLevel(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000006B-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/current_door_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CurrentDoorState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000000E-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/current_heating_cooling_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CurrentHeatingCoolingState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000000F-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/current_horizontal_tilt_angle.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CurrentHorizontalTiltAngle(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000006C-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/current_position.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CurrentPosition(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000006D-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/current_relative_humidity.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CurrentRelativeHumidity(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000010-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/current_temperature.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CurrentTemperature(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000011-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/current_time.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CurrentTime(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000009B-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return str 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'string' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/current_vertical_tilt_angle.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class CurrentVerticalTiltAngle(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000006E-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/day_of_the_week.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class DayoftheWeek(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000098-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/discover_bridged_accessories.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class DiscoverBridgedAccessories(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000009E-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/discovered_bridged_accessories.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class DiscoveredBridgedAccessories(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000009F-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/firmware_revision.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class FirmwareRevision(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000052-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return str 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'string' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/hardware_revision.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class HardwareRevision(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000053-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return str 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'string' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/heating_threshold_temperature.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class HeatingThresholdTemperature(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000012-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/hold_position.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class HoldPosition(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000006F-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_write, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/hue.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class Hue(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000013-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/identify.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class Identify(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000014-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_write, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/leak_detected.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class LeakDetected(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000070-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/link_quality.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class LinkQuality(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000009C-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/lock_control_point.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class LockControlPoint(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000019-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'tlv' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_write, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/lock_current_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class LockCurrentState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000001D-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/lock_last_known_action.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class LockLastKnownAction(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000001C-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/lock_management_auto_security_timeout.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class LockManagementAutoSecurityTimeout(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000001A-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/lock_target_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class LockTargetState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000001E-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/logs.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class Logs(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000001F-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'tlv' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/manufacturer.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class Manufacturer(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000020-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return str 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'string' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/model.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class Model(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000021-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return str 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'string' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/motion_detected.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class MotionDetected(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000022-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/name.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class Name(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000023-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return str 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'string' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/obstruction_detected.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class ObstructionDetected(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000024-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/occupancy_detected.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class OccupancyDetected(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000071-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/on.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class On(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000025-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/outlet_in_use.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class OutletInUse(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000026-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/pair_setup.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class PairSetup(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000004C-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'tlv' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/pair_verify.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class PairVerify(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000004E-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'tlv' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/pairing_features.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class PairingFeatures(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000004F-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/pairing_pairings.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class PairingPairings(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000050-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'tlv' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/position_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class PositionState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000072-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/programmable_switch_event.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class ProgrammableSwitchEvent(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000073-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/programmable_switch_output_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class ProgrammableSwitchOutputState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000074-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/reachable.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class Reachable(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000063-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/rotation_direction.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class RotationDirection(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000028-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/rotation_speed.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class RotationSpeed(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000029-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/saturation.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class Saturation(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000002F-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/security_system_alarm_type.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class SecuritySystemAlarmType(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000008E-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/security_system_current_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class SecuritySystemCurrentState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000066-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/security_system_target_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class SecuritySystemTargetState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000067-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/serial_number.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class SerialNumber(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000030-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return str 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'string' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/smoke_detected.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class SmokeDetected(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000076-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/software_revision.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class SoftwareRevision(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000054-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return str 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'string' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | ] 33 | -------------------------------------------------------------------------------- /pyhap/characteristics/status_active.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class StatusActive(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000075-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/status_fault.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class StatusFault(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000077-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/status_jammed.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class StatusJammed(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000078-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/status_low_battery.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class StatusLowBattery(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000079-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/status_tampered.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class StatusTampered(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000007A-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/target_door_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TargetDoorState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000032-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/target_heating_cooling_state.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TargetHeatingCoolingState(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000033-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/target_horizontal_tilt_angle.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TargetHorizontalTiltAngle(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000007B-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/target_position.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TargetPosition(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000007C-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/target_relative_humidity.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TargetRelativeHumidity(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000034-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/target_temperature.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TargetTemperature(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000035-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/target_vertical_tilt_angle.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TargetVerticalTiltAngle(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000007D-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/temperature_display_units.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TemperatureDisplayUnits(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000036-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.pair_write, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/time_update.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TimeUpdate(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('0000009A-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/tunnel_connection_timeout_.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TunnelConnectionTimeout(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000061-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return int 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'int' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_write, 32 | CharacteristicPermission.pair_read, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/tunneled_accessory_advertising.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TunneledAccessoryAdvertising(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000060-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_write, 32 | CharacteristicPermission.pair_read, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/tunneled_accessory_connected.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TunneledAccessoryConnected(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000059-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return bool 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'bool' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_write, 32 | CharacteristicPermission.pair_read, 33 | CharacteristicPermission.notify, 34 | ] 35 | -------------------------------------------------------------------------------- /pyhap/characteristics/tunneled_accessory_state_number.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class TunneledAccessoryStateNumber(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000058-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return float 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'float' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/characteristics/version.py: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class Version(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('00000037-0000-1000-8000-0026BB765291') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return str 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return 'string' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | CharacteristicPermission.pair_read, 32 | CharacteristicPermission.notify, 33 | ] 34 | -------------------------------------------------------------------------------- /pyhap/config.py: -------------------------------------------------------------------------------- 1 | """Configuration of PyHAP accessory""" 2 | 3 | import json 4 | from abc import abstractmethod 5 | from enum import Enum, Flag 6 | from typing import ( 7 | List, 8 | Optional, 9 | Tuple, 10 | ) 11 | 12 | from pyhap.util import ( 13 | CustomJSONEncoder, 14 | generate_device_id, 15 | generate_setup_code, 16 | generate_signing_key, 17 | ) 18 | 19 | 20 | class StatusFlag(Flag): 21 | """Accessory status""" 22 | not_paired = 0x01 # accessory has not been paired with any controllers 23 | no_wifi_configured = 0x02 # accessory has not been configured to join a Wi-Fi network 24 | problem = 0x04 # a problem has been detected on the accessory 25 | 26 | 27 | class AccessoryCategory(Enum): 28 | """Available categories that represents accessory type""" 29 | other = 1 30 | bridge = 2 31 | fan = 3 32 | garage = 4 33 | lightbulb = 5 34 | door_lock = 6 35 | outlet = 7 36 | switch = 8 37 | thermostat = 9 38 | sensor = 10 39 | security_system = 11 40 | door = 12 41 | window = 13 42 | window_covering = 14 43 | programmable_switch = 15 44 | range_extender = 16 45 | ip_camera = 17 46 | video_door_bell = 18 47 | air_purifier = 19 48 | 49 | 50 | class ControllerPermission(Enum): 51 | """Permission of paired controller""" 52 | user = 0x00 53 | admin = 0x01 54 | 55 | 56 | class HAPStatusCode(Enum): 57 | success = 0 58 | insufficient_privileges = -70401 59 | service_unavailable = -70402 60 | resource_busy = -70403 61 | read_only = -70404 62 | write_only = -70405 63 | no_notification = -70406 64 | out_of_resources = -70407 65 | timeout = -70408 66 | resource_not_exists = -70409 67 | invalid_value = -70410 68 | insufficient_authorization = -70411 69 | 70 | 71 | Pairing = Tuple[Optional[str], Optional[bytes], Optional[ControllerPermission]] 72 | 73 | 74 | class Config: 75 | """Contains required configuration values for PyHAP accessory""" 76 | def __init__(self, server_ip: str) -> None: 77 | self._server_ip = server_ip 78 | self._server_port: int 79 | self._device_id: str 80 | self._configuration_number: int 81 | self._setup_code: str 82 | self._pair_setup_mode: bool = False 83 | self._pairings: dict 84 | self._accessory_ltsk: str 85 | 86 | @property 87 | def server_ip(self) -> str: 88 | """Web server IP to listen for HTTP requests""" 89 | return self._server_ip 90 | 91 | @property 92 | def server_port(self) -> int: 93 | """Web server port to listen for HTTP requests""" 94 | return self._server_port 95 | 96 | @property 97 | def device_id(self) -> str: 98 | """Also known as Accessory's Pairing Identifier (AccessoryPairingID) 99 | 100 | Unique random number, must be regenerated at every 'factory reset'. 101 | 102 | Must be formatted as 'XX:XX:XX:XX:XX:XX', where 'XX' is a hex byte. 103 | """ 104 | return self._device_id 105 | 106 | @property 107 | def configuration_number(self) -> int: 108 | """Current configuration number. 109 | 110 | Must be incremented when accessory, service, or characteristic is added or removed. 111 | Maximum value must be uint32 and it should start over from 1 after overflow. 112 | """ 113 | return self._configuration_number 114 | 115 | # TODO: add configuration number setter with overflow handling and saving config after change 116 | 117 | @property 118 | def model_name(self) -> str: 119 | """Model name of the accessory""" 120 | return 'PyHAP' 121 | 122 | @property 123 | def service_type(self) -> str: 124 | """Fully qualified service type name""" 125 | return '_hap._tcp.local.' 126 | 127 | @property 128 | def unsuccessful_authentication_attempts(self) -> int: 129 | """How many times accessory end up with unsuccessful authentication attempt""" 130 | # TODO: add proper increment 131 | return 0 132 | 133 | @property 134 | def accessory_ltsk(self) -> bytes: 135 | """Accessory's Ed25519 long-term secret key""" 136 | return bytes(bytearray.fromhex(self._accessory_ltsk)) 137 | 138 | @property 139 | def setup_code(self) -> str: 140 | """Setup code is used to pair with iOS device""" 141 | return self._setup_code 142 | 143 | @property 144 | def pair_setup_mode(self) -> bool: 145 | """Returns True in case accessory is currently performing a pair setup operation""" 146 | return self._pair_setup_mode 147 | 148 | @pair_setup_mode.setter 149 | def pair_setup_mode(self, value: bool) -> None: 150 | """Set to True in case accessory is currently performing a pair setup operation""" 151 | self._pair_setup_mode = value 152 | 153 | @property 154 | def paired(self) -> bool: 155 | """Returns True in case accessory has paired controllers""" 156 | return len(self._pairings) > 0 157 | 158 | def add_pairing(self, ios_device_pairing_id: str, ios_device_public_key: bytes, permission: ControllerPermission): 159 | self._pairings[ios_device_pairing_id] = (ios_device_pairing_id, ios_device_public_key.hex(), permission.value) 160 | self.save() 161 | 162 | def get_pairing(self, ios_device_pairing_id: str) -> Pairing: 163 | pairing = self._pairings.get(ios_device_pairing_id) 164 | if not pairing: 165 | return None, None, None 166 | return ios_device_pairing_id, bytes(bytearray.fromhex(pairing[1])), ControllerPermission(pairing[2]) 167 | 168 | def remove_pairing(self, ios_device_pairing_id: str) -> None: 169 | if ios_device_pairing_id in self._pairings: 170 | del self._pairings[ios_device_pairing_id] 171 | self.save() 172 | 173 | def get_pairings(self) -> List[Pairing]: 174 | result: List[Pairing] = [] 175 | for _, pairing in self._pairings.items(): 176 | result.append((pairing[0], bytes(bytearray.fromhex(pairing[1])), ControllerPermission(pairing[2]))) 177 | return result 178 | 179 | def from_dict(self, _dict: dict) -> None: 180 | self._server_port = _dict.get('server_port', 8080) 181 | self._device_id = _dict.get('device_id', generate_device_id()) 182 | self._configuration_number = _dict.get('configuration_number', 1) 183 | self._setup_code = _dict.get('setup_code', generate_setup_code()) 184 | self._pairings = _dict.get('pairings', {}) 185 | self._accessory_ltsk = _dict.get('accessory_ltsk', generate_signing_key()) 186 | 187 | def to_dict(self) -> dict: 188 | return { 189 | 'server_port': self._server_port, 190 | 'device_id': self._device_id, 191 | 'configuration_number': self._configuration_number, 192 | 'setup_code': self._setup_code, 193 | 'pairings': self._pairings, 194 | 'accessory_ltsk': self._accessory_ltsk, 195 | } 196 | 197 | @abstractmethod 198 | def load(self) -> None: 199 | """Loads up config from storage""" 200 | raise NotImplementedError() 201 | 202 | @abstractmethod 203 | def save(self) -> None: 204 | """Saves config to storage""" 205 | raise NotImplementedError() 206 | 207 | 208 | class JsonConfig(Config): 209 | def __init__(self, server_ip, config_filepath='pyhap_config.json'): 210 | super().__init__(server_ip) 211 | self.config_filepath = config_filepath 212 | self.load() 213 | self.save() # save back just after load to avoid missing values 214 | 215 | def load(self) -> None: 216 | try: 217 | with open(self.config_filepath) as f: 218 | self.from_dict(json.load(f)) 219 | except FileNotFoundError: 220 | self.from_dict({}) 221 | except json.JSONDecodeError: 222 | self.from_dict({}) 223 | 224 | def save(self) -> None: 225 | with open(self.config_filepath, 'w+') as f: 226 | json.dump(self.to_dict(), f, sort_keys=True, indent=4, cls=CustomJSONEncoder) 227 | -------------------------------------------------------------------------------- /pyhap/exception.py: -------------------------------------------------------------------------------- 1 | class SecurityError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /pyhap/http_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from asyncio import ( 3 | AbstractEventLoop, 4 | StreamReader, 5 | StreamWriter, 6 | ) 7 | from asyncio.base_events import Server 8 | from email.utils import formatdate 9 | from functools import wraps 10 | from http import HTTPStatus 11 | from logging import getLogger 12 | from typing import ( 13 | Dict, 14 | Optional, 15 | ) 16 | from urllib.parse import ( 17 | parse_qsl, 18 | urlsplit, 19 | ) 20 | 21 | from cryptography.exceptions import InvalidTag 22 | from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 23 | 24 | from pyhap.exception import SecurityError 25 | 26 | logger = getLogger('pyhap.http_server') 27 | 28 | AUTH_TAG_LENGTH = 16 29 | ENCRYPTED_DATA_LENGTH = 2 30 | ENCRYPTED_CHUNK_MAX_SIZE = 1024 31 | 32 | 33 | class Request: 34 | def __init__(self, global_context: dict, context: dict, method: str, headers: dict, query: dict, 35 | reader: StreamReader) -> None: 36 | self.global_context = global_context 37 | self.context = context 38 | self.method = method 39 | self.headers = headers 40 | self.query = query 41 | self.reader = reader 42 | 43 | async def read(self): 44 | content_length = int(self.headers['content-length']) 45 | return await self.reader.read(content_length) 46 | 47 | 48 | class Response: 49 | def __init__(self, content_type: str = 'text/html', status: HTTPStatus = HTTPStatus.OK, data: bytes = b'', 50 | keep_alive: bool = True, upgrade=False) -> None: 51 | if not isinstance(data, bytes): 52 | raise ValueError(f'Response data should be bytes, received {type(data)}') 53 | 54 | self.content_type = content_type 55 | self.status = status 56 | self.data = data 57 | self.keep_alive = keep_alive 58 | self.upgrade = upgrade 59 | 60 | 61 | class Handler: 62 | def __init__(self, reader: StreamReader, writer: StreamWriter, routes: dict, global_context: dict) -> None: 63 | self.reader = reader 64 | self.writer = writer 65 | self.routes = routes 66 | self.global_context = global_context 67 | self.context = {'encrypted': False} # context is available only within http keep-alive socket connection 68 | self.close_connection = False 69 | self.encrypted_request_count = 0 70 | self.encrypted_response_count = 0 71 | self.decrypt_key: bytes = None 72 | 73 | self.http_method: str = None 74 | self.request_path: str = None 75 | self.http_version: str = None 76 | self.headers: Dict[str, str] = None 77 | self.query: Dict[str, str] = {} 78 | 79 | async def start_handling(self): 80 | while not self.close_connection: 81 | await self.handle() 82 | 83 | async def handle(self) -> None: 84 | self.close_connection = True 85 | 86 | if self.context['encrypted']: 87 | try: 88 | reader = await self.decrypt_stream(self.reader) 89 | except SecurityError as e: 90 | logger.info(str(e)) 91 | return 92 | else: 93 | reader = self.reader 94 | 95 | if not await self.parse_request(reader): 96 | return 97 | 98 | request = Request(self.global_context, self.context, self.http_method, self.headers, self.query, reader) 99 | route = self.routes[self.request_path] 100 | 101 | if route: 102 | try: 103 | response = await route(request) 104 | except SecurityError as e: 105 | logger.info(str(e)) 106 | return 107 | else: 108 | logger.info(f'Handler for path {self.request_path} is not found') 109 | response = Response(status=HTTPStatus.NOT_FOUND) 110 | 111 | if not isinstance(response, Response): 112 | logger.warning(f'Response for path {self.request_path} was not returned from handler') 113 | response = Response(status=HTTPStatus.NOT_FOUND) 114 | 115 | if response.upgrade and not self.context.get('encrypt_key'): 116 | logger.info('Attempt to upgrade to encrypted stream without encrypt_key in context') 117 | await self.send_error(HTTPStatus.LENGTH_REQUIRED) 118 | return 119 | 120 | await self.send_response(response) 121 | 122 | if response.upgrade: 123 | logger.debug('Upgrade to encrypted stream') 124 | self.context['encrypted'] = True 125 | 126 | async def parse_request(self, reader: StreamReader) -> bool: 127 | request_line = await reader.readline() 128 | 129 | if not request_line: 130 | return False # client disconnected 131 | 132 | self.http_method, raw_url, self.http_version = request_line.decode().split() # GET / HTTP/1.1 133 | 134 | url = urlsplit(raw_url) 135 | self.request_path = url.path 136 | self.query = dict(parse_qsl(url.query)) 137 | 138 | if self.http_version != 'HTTP/1.1': 139 | await self.send_error(HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, f'Invalid HTTP version ({self.http_version})') 140 | return False 141 | 142 | self.headers = await self.parse_headers(reader) 143 | 144 | if self.http_method != 'GET' and not self.headers.get('content-length'): 145 | await self.send_error(HTTPStatus.LENGTH_REQUIRED) 146 | return False 147 | 148 | connection = self.headers.get('connection', '') 149 | 150 | if connection.lower() == 'keep-alive': 151 | self.close_connection = False 152 | 153 | return True 154 | 155 | @staticmethod 156 | async def parse_headers(reader: StreamReader) -> Dict[str, str]: 157 | headers: Dict[str, str] = {} 158 | 159 | while True: 160 | raw_header = await reader.readline() 161 | header = raw_header.decode() 162 | 163 | if header == '\r\n': 164 | break 165 | 166 | key, value = header.split(':', 1) 167 | headers[key.lower()] = value.strip() 168 | 169 | return headers 170 | 171 | async def decrypt_stream(self, reader: StreamReader) -> StreamReader: 172 | data_length = await reader.read(ENCRYPTED_DATA_LENGTH) 173 | 174 | if not data_length: 175 | raise SecurityError('Connection closed') 176 | 177 | data_length_int = int.from_bytes(data_length, byteorder='little') + AUTH_TAG_LENGTH 178 | 179 | encrypted_data = await reader.read(data_length_int) 180 | 181 | chacha = ChaCha20Poly1305(self.context['decrypt_key']) 182 | 183 | nonce = b'\x00\x00\x00\x00' + self.encrypted_request_count.to_bytes(8, byteorder='little') 184 | try: 185 | decrypted_data = chacha.decrypt(nonce, encrypted_data, data_length) 186 | except InvalidTag: 187 | decrypted_data = None 188 | 189 | if not decrypted_data: 190 | raise SecurityError('Unable to decrypt encrypted data') 191 | 192 | self.encrypted_request_count += 1 193 | 194 | decrypted_reader = StreamReader() 195 | decrypted_reader.feed_data(decrypted_data) 196 | return decrypted_reader 197 | 198 | async def send_response(self, response: Response): 199 | if response.keep_alive: 200 | self.close_connection = False 201 | connection = 'keep-alive' 202 | else: 203 | self.close_connection = True 204 | connection = 'close' 205 | 206 | headers = ( 207 | 'HTTP/1.1 {} {}\r\n' 208 | 'Server: PyHAP\r\n' 209 | 'Date: {}\r\n' 210 | 'Content-Length: {}\r\n' 211 | 'Content-Type: {}\r\n' 212 | 'Connection: {}\r\n' 213 | '\r\n' 214 | ).format( 215 | response.status.value, 216 | response.status.phrase, 217 | formatdate(usegmt=True), 218 | str(len(response.data)), 219 | response.content_type, 220 | connection, 221 | ).encode() 222 | 223 | # call write once to prevent http response split to several tcp packets 224 | if self.context['encrypted']: 225 | self.writer.write(self.encrypt_data(headers + response.data)) 226 | else: 227 | self.writer.write(headers + response.data) 228 | await self.writer.drain() 229 | 230 | def encrypt_data(self, encrypted_data: bytes) -> bytes: 231 | data_length = len(encrypted_data).to_bytes(2, byteorder='little') 232 | 233 | chacha = ChaCha20Poly1305(self.context['encrypt_key']) 234 | 235 | nonce = b'\x00\x00\x00\x00' + self.encrypted_response_count.to_bytes(8, byteorder='little') 236 | self.encrypted_response_count += 1 237 | 238 | return data_length + chacha.encrypt(nonce, encrypted_data, data_length) 239 | 240 | async def send_error(self, status: HTTPStatus, description: str = None): 241 | if not description: 242 | description = status.description 243 | logger.error(f'HTTP Error: {status.value}: {status.phrase} ({description})') 244 | await self.send_response(Response(status=status, data=description.encode(), keep_alive=False)) 245 | 246 | 247 | class HTTPServer: 248 | def __init__(self, routes: dict, loop: Optional[AbstractEventLoop] = None) -> None: 249 | self.loop = loop or asyncio.get_event_loop() 250 | self.routes = routes 251 | self.global_context: dict = {} 252 | self.handlers: set = set() 253 | 254 | async def handler(self, reader: StreamReader, writer: StreamWriter): 255 | handler = Handler(reader, writer, self.routes, self.global_context) 256 | self.handlers.add(handler) 257 | await handler.start_handling() 258 | self.handlers.remove(handler) 259 | 260 | def run(self, host: str, port: int): 261 | server: Server = self.loop.run_until_complete(asyncio.start_server(self.handler, host, port)) 262 | 263 | try: 264 | self.loop.run_forever() 265 | except KeyboardInterrupt: 266 | pass 267 | 268 | for handler in self.handlers: 269 | handler.close_connection = True 270 | 271 | server.close() 272 | self.loop.run_until_complete(server.wait_closed()) 273 | self.loop.close() 274 | 275 | 276 | def encrypted(func): 277 | @wraps(func) 278 | def wrapper(request: Request): 279 | if not request.context['encrypted']: 280 | raise SecurityError('Call for route without authentication') 281 | return func(request) 282 | 283 | return wrapper 284 | -------------------------------------------------------------------------------- /pyhap/pair.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | from typing import List 3 | 4 | import ed25519 5 | from cryptography.exceptions import InvalidTag 6 | from cryptography.hazmat.backends import default_backend 7 | from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey 8 | from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 9 | from cryptography.hazmat.primitives.hashes import SHA512 10 | from cryptography.hazmat.primitives.kdf.hkdf import HKDF 11 | 12 | from pyhap.config import ( 13 | Config, 14 | ControllerPermission, 15 | ) 16 | from pyhap.srp import Srp 17 | from pyhap.tlv import ( 18 | TlvCode, 19 | TlvState, 20 | TlvError, 21 | tlv_parser 22 | ) 23 | 24 | logger = getLogger('pyhap') 25 | 26 | 27 | MAX_AUTHENTICATION_ATTEMPTS = 100 28 | SRP_USERNAME = 'Pair-Setup' 29 | NONCE_SETUP_M5 = b'\x00\x00\x00\x00PS-Msg05' 30 | NONCE_SETUP_M6 = b'\x00\x00\x00\x00PS-Msg06' 31 | NONCE_VERIFY_M2 = b'\x00\x00\x00\x00PV-Msg02' 32 | NONCE_VERIFY_M3 = b'\x00\x00\x00\x00PV-Msg03' 33 | SALT_CONTROLLER = b'Pair-Setup-Controller-Sign-Salt' 34 | INFO_CONTROLLER = b'Pair-Setup-Controller-Sign-Info' 35 | SALT_ACCESSORY = b'Pair-Setup-Accessory-Sign-Salt' 36 | INFO_ACCESSORY = b'Pair-Setup-Accessory-Sign-Info' 37 | SALT_ENCRYPT = b'Pair-Setup-Encrypt-Salt' 38 | INFO_ENCRYPT = b'Pair-Setup-Encrypt-Info' 39 | SALT_VERIFY = b'Pair-Verify-Encrypt-Salt' 40 | INFO_VERIFY = b'Pair-Verify-Encrypt-Info' 41 | SALT_CONTROL = b'Control-Salt' 42 | INFO_CONTROL_WRITE = b'Control-Write-Encryption-Key' 43 | INFO_CONTROL_READ = b'Control-Read-Encryption-Key' 44 | 45 | 46 | def srp_start(config: Config, context: dict, expected_tlv_state: TlvState) -> List[dict]: 47 | """pair_setup M1 and M2""" 48 | if config.paired: 49 | return _error(TlvState.m2, TlvError.unavailable, 'Accessory already paired, cannot accept additional pairings') 50 | 51 | if config.unsuccessful_authentication_attempts > MAX_AUTHENTICATION_ATTEMPTS: 52 | return _error(TlvState.m2, TlvError.max_tries, 'Max authentication attempts reached') 53 | 54 | if expected_tlv_state != TlvState.m1: 55 | return _error(TlvState.m2, TlvError.unknown, 'Unexpected pair_setup state') 56 | 57 | if config.pair_setup_mode: 58 | return _error(TlvState.m2, TlvError.busy, 'Currently perform pair setup operation with a different controller') 59 | 60 | config.pair_setup_mode = True 61 | srp = Srp(SRP_USERNAME, config.setup_code) 62 | context['srp'] = srp 63 | 64 | return [{ 65 | TlvCode.state: TlvState.m2, 66 | TlvCode.public_key: srp.public_key, 67 | TlvCode.salt: srp.salt, 68 | }] 69 | 70 | 71 | def srp_verify(context: dict, expected_tlv_state: TlvState, client_public_key: bytes, 72 | client_proof: bytes) -> List[dict]: 73 | """pair_setup M3 and M4""" 74 | srp = context.get('srp') 75 | 76 | if expected_tlv_state != TlvState.m3 or not srp: 77 | return _error(TlvState.m4, TlvError.unknown, 'Unexpected pair_setup state') 78 | 79 | srp.compute_shared_session_key(client_public_key) 80 | 81 | if not srp.verify_proof(client_proof): 82 | return _error(TlvState.m4, TlvError.authentication, 'Incorrect setup code, try again') 83 | 84 | return [{ 85 | TlvCode.state: TlvState.m4, 86 | TlvCode.proof: srp.session_key_proof, 87 | }] 88 | 89 | 90 | def exchange(config: Config, context: dict, expected_tlv_state: TlvState, encrypted_data: bytes) -> List[dict]: 91 | """pair_setup M5 and M6""" 92 | srp = context.get('srp') 93 | 94 | if expected_tlv_state != TlvState.m5 or not srp: 95 | return _error(TlvState.m6, TlvError.unknown, 'Unexpected pair_setup state') 96 | 97 | hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_ENCRYPT, info=INFO_ENCRYPT, backend=default_backend()) 98 | decrypt_key = hkdf.derive(srp.session_key) 99 | 100 | chacha = ChaCha20Poly1305(decrypt_key) 101 | 102 | try: 103 | data = chacha.decrypt(NONCE_SETUP_M5, encrypted_data, None) 104 | except InvalidTag: 105 | return _error(TlvState.m6, TlvError.authentication, 'pair_setup M5: invalid auth tag during chacha decryption') 106 | 107 | try: 108 | tlv = tlv_parser.decode(data)[0] 109 | except ValueError: 110 | return _error(TlvState.m6, TlvError.authentication, 'unable to decode decrypted tlv data') 111 | 112 | hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_CONTROLLER, info=INFO_CONTROLLER, backend=default_backend()) 113 | ios_device_x = hkdf.derive(srp.session_key) 114 | ios_device_info = ios_device_x + tlv[TlvCode.identifier].encode() + tlv[TlvCode.public_key] 115 | 116 | if not _verify_ed25519(key=tlv[TlvCode.public_key], message=ios_device_info, signature=tlv[TlvCode.signature]): 117 | return _error(TlvState.m6, TlvError.authentication, 'ios_device_info ed25519 signature verification is failed') 118 | 119 | config.add_pairing(tlv[TlvCode.identifier], tlv[TlvCode.public_key], ControllerPermission.admin) # save pairing 120 | 121 | # M6 response generation 122 | hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_ACCESSORY, info=INFO_ACCESSORY, backend=default_backend()) 123 | accessory_x = hkdf.derive(srp.session_key) 124 | 125 | signing_key = ed25519.SigningKey(config.accessory_ltsk) 126 | public_key = signing_key.get_verifying_key().to_bytes() 127 | accessory_info = accessory_x + config.device_id.encode() + public_key 128 | accessory_signature = signing_key.sign(accessory_info) 129 | 130 | sub_tlv = tlv_parser.encode([{ 131 | TlvCode.identifier: config.device_id, 132 | TlvCode.public_key: public_key, 133 | TlvCode.signature: accessory_signature, 134 | }]) 135 | 136 | encrypted_data = chacha.encrypt(NONCE_SETUP_M6, sub_tlv, None) 137 | 138 | config.pair_setup_mode = False 139 | return [{ 140 | TlvCode.state: TlvState.m6, 141 | TlvCode.encrypted_data: encrypted_data, 142 | }] 143 | 144 | 145 | def verify_start(config: Config, context: dict, ios_device_public_key: bytes) -> List[dict]: 146 | """pair_verify M1 and M2""" 147 | curve25519 = X25519PrivateKey.generate() 148 | accessory_curve25519_public_key: bytes = curve25519.public_key().public_bytes() 149 | shared_secret: bytes = curve25519.exchange(X25519PublicKey.from_public_bytes(ios_device_public_key)) 150 | 151 | accessory_info: bytes = accessory_curve25519_public_key + config.device_id.encode() + ios_device_public_key 152 | signing_key = ed25519.SigningKey(config.accessory_ltsk) 153 | accessory_signature = signing_key.sign(accessory_info) 154 | 155 | sub_tlv = tlv_parser.encode([{ 156 | TlvCode.identifier: config.device_id, 157 | TlvCode.signature: accessory_signature, 158 | }]) 159 | 160 | hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_VERIFY, info=INFO_VERIFY, backend=default_backend()) 161 | session_key = hkdf.derive(shared_secret) 162 | 163 | chacha = ChaCha20Poly1305(session_key) 164 | encrypted_data = chacha.encrypt(NONCE_VERIFY_M2, sub_tlv, None) 165 | 166 | context['session_key'] = session_key 167 | context['shared_secret'] = shared_secret 168 | context['accessory_curve25519_public_key'] = accessory_curve25519_public_key 169 | context['ios_device_curve25519_public_key'] = ios_device_public_key 170 | 171 | return [{ 172 | TlvCode.state: TlvState.m2, 173 | TlvCode.public_key: accessory_curve25519_public_key, 174 | TlvCode.encrypted_data: encrypted_data, 175 | }] 176 | 177 | 178 | def verify_finish(config: Config, context: dict, encrypted_data: bytes) -> List[dict]: 179 | """pair_verify M3 and M4""" 180 | session_key = context.get('session_key') 181 | accessory_curve25519_public_key = context.get('accessory_curve25519_public_key') 182 | ios_device_curve25519_public_key = context.get('ios_device_curve25519_public_key') 183 | 184 | if not session_key or not accessory_curve25519_public_key or not ios_device_curve25519_public_key: 185 | return _error(TlvState.m4, TlvError.authentication, 186 | 'verify_finished call before successful verify_start') 187 | 188 | chacha = ChaCha20Poly1305(session_key) 189 | 190 | try: 191 | data = chacha.decrypt(NONCE_VERIFY_M3, encrypted_data, None) 192 | except InvalidTag: 193 | return _error(TlvState.m4, TlvError.authentication, 'invalid auth tag during chacha decryption') 194 | 195 | try: 196 | tlv = tlv_parser.decode(data)[0] 197 | except ValueError: 198 | return _error(TlvState.m4, TlvError.authentication, 'unable to decode decrypted tlv data') 199 | 200 | ios_device_ltpk = config.get_pairing(tlv[TlvCode.identifier])[1] 201 | 202 | if not ios_device_ltpk: 203 | return _error(TlvState.m4, TlvError.authentication, 204 | 'unable to find requested ios device in config file') 205 | 206 | ios_device_info = ios_device_curve25519_public_key + tlv[TlvCode.identifier].encode() + \ 207 | accessory_curve25519_public_key 208 | 209 | if not _verify_ed25519(ios_device_ltpk, message=ios_device_info, signature=tlv[TlvCode.signature]): 210 | return _error(TlvState.m4, TlvError.authentication, 211 | 'ios_device_info ed25519 signature verification is failed') 212 | 213 | context['paired'] = True 214 | context['ios_device_pairing_id'] = tlv[TlvCode.identifier] 215 | 216 | hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_CONTROL, info=INFO_CONTROL_WRITE, backend=default_backend()) 217 | context['decrypt_key'] = hkdf.derive(context['shared_secret']) 218 | 219 | hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_CONTROL, info=INFO_CONTROL_READ, backend=default_backend()) 220 | context['encrypt_key'] = hkdf.derive(context['shared_secret']) 221 | 222 | return [{ 223 | TlvCode.state: TlvState.m4, 224 | }] 225 | 226 | 227 | def list_pairings(config: Config) -> List[dict]: 228 | response: List[dict] = [] 229 | 230 | for pairing in config.get_pairings(): 231 | response.append({ 232 | TlvCode.identifier: pairing[0], 233 | TlvCode.public_key: pairing[1], 234 | TlvCode.permissions: pairing[2].value, 235 | }) 236 | 237 | if response: 238 | response[0][TlvCode.state] = TlvState.m2 239 | 240 | return response 241 | 242 | 243 | def add_pairing(config: Config, ios_device_pairing_id: str, ios_device_public_key: bytes, 244 | permission: ControllerPermission) -> List[dict]: 245 | _, saved_ios_device_public_key, _ = config.get_pairing(ios_device_pairing_id) # type: ignore 246 | 247 | if saved_ios_device_public_key and ios_device_public_key != saved_ios_device_public_key: 248 | return _error(TlvState.m2, TlvError.unknown, 249 | 'Received iOS device public key doesn\'t match with previously saved key') 250 | 251 | config.add_pairing(ios_device_pairing_id, ios_device_public_key, permission) 252 | 253 | return [{ 254 | TlvCode.state: TlvState.m2, 255 | }] 256 | 257 | 258 | def remove_pairing(config: Config, ios_device_pairing_id: str) -> List[dict]: 259 | config.remove_pairing(ios_device_pairing_id) 260 | 261 | return [{ 262 | TlvCode.state: TlvState.m2, 263 | }] 264 | 265 | 266 | def _error(tlv_state: TlvState, tlv_error: TlvError, reason: str) -> List[dict]: 267 | logger.error(f'State {tlv_state} error {tlv_error}: {reason}') 268 | return [{ 269 | TlvCode.state: tlv_state, 270 | TlvCode.error: tlv_error, 271 | }] 272 | 273 | 274 | def _verify_ed25519(key: bytes, message: bytes, signature: bytes) -> bool: 275 | verifying_key = ed25519.VerifyingKey(key) 276 | 277 | try: 278 | verifying_key.verify(signature, message) 279 | return True 280 | except ed25519.BadSignatureError: 281 | return False 282 | -------------------------------------------------------------------------------- /pyhap/pyhap.py: -------------------------------------------------------------------------------- 1 | from asyncio import AbstractEventLoop 2 | from logging import getLogger 3 | from socket import inet_aton 4 | from typing import Optional 5 | 6 | from zeroconf import ServiceInfo, Zeroconf 7 | 8 | from pyhap.accessory import Accessories 9 | from pyhap.config import ( 10 | AccessoryCategory, 11 | Config, 12 | StatusFlag, 13 | ) 14 | from pyhap import route 15 | from pyhap.http_server import HTTPServer 16 | from pyhap.tlv import TlvState 17 | 18 | 19 | logger = getLogger('pyhap') 20 | 21 | 22 | def start(config: Config, accessories: Accessories, loop: Optional[AbstractEventLoop] = None): 23 | # TODO: increment configuration_number in case hash of accessory list json is changed before starting up mdns 24 | logger.info('Starting up PyHAP, setup code: %s', config.setup_code) 25 | mdns_server = MDNSServer(config) 26 | mdns_server.start() 27 | http_server = WebServer(config, accessories, loop) 28 | http_server.start() 29 | 30 | 31 | class MDNSServer: 32 | """Announce accessory on the network via mDNS / DNS-SD 33 | 34 | To debug service: avahi-browse -r -k _hap._tcp 35 | """ 36 | # TODO: restart / reload zeroconf on config change 37 | def __init__(self, config: Config) -> None: 38 | self.zeroconf = Zeroconf() 39 | self.hap_service: ServiceInfo = None 40 | self.config = config 41 | 42 | def update_service(self): 43 | self.hap_service = ServiceInfo( 44 | type_=self.config.service_type, 45 | name=f'{self.config.model_name}.{self.config.service_type}', 46 | address=inet_aton(self.config.server_ip), 47 | port=self.config.server_port, 48 | properties={ 49 | 'c#': str(self.config.configuration_number), 50 | 'ff': '0', # feature flag: enable HAP pairing # TODO: disable pairing once paired 51 | 'id': self.config.device_id, 52 | 'md': self.config.model_name, 53 | 'pv': '1.0', # protocol version 54 | 's#': '1', # current state number, this must have a value of '1' 55 | 'sf': str(StatusFlag.not_paired.value), # pylint: disable=no-member 56 | 'ci': str(AccessoryCategory.bridge.value), # accessory category identifier 57 | }, 58 | ) 59 | 60 | def start(self): 61 | """Start announcing accessory on the network""" 62 | self.update_service() 63 | self.zeroconf.register_service(self.hap_service) 64 | 65 | def restart(self): 66 | self.stop() 67 | self.update_service() 68 | self.start() 69 | 70 | def stop(self): 71 | self.zeroconf.unregister_service(self.hap_service) 72 | 73 | def close(self): 74 | self.zeroconf.close() 75 | 76 | 77 | class WebServer: 78 | def __init__(self, config: Config, accessories_obj: Accessories, loop: Optional[AbstractEventLoop] = None) -> None: 79 | self.http_server = HTTPServer({ 80 | '/accessories': route.accessories, 81 | '/characteristics': route.characteristics, 82 | '/identify': route.identify, 83 | '/pair-setup': route.pair_setup, 84 | '/pair-verify': route.pair_verify, 85 | '/pairings': route.pairings, 86 | }, loop) 87 | 88 | self.http_server.global_context['accessories'] = accessories_obj 89 | self.http_server.global_context['config'] = config 90 | self.http_server.global_context['pair_setup_expected_state'] = TlvState.m1 91 | 92 | def start(self): 93 | config = self.http_server.global_context['config'] 94 | logger.debug(f'Serving at http://{config.server_ip}:{config.server_port}') 95 | self.http_server.run(config.server_ip, config.server_port) 96 | -------------------------------------------------------------------------------- /pyhap/route.py: -------------------------------------------------------------------------------- 1 | import json 2 | from http import HTTPStatus 3 | from logging import getLogger 4 | 5 | from pyhap.accessory import Accessories 6 | from pyhap.config import ( 7 | ControllerPermission, 8 | HAPStatusCode, 9 | ) 10 | from pyhap.http_server import ( 11 | Request, 12 | Response, 13 | encrypted, 14 | ) 15 | from pyhap.pair import ( 16 | add_pairing, 17 | exchange, 18 | list_pairings, 19 | remove_pairing, 20 | srp_start, 21 | srp_verify, 22 | verify_start, 23 | verify_finish, 24 | ) 25 | from pyhap.tlv import ( 26 | tlv_parser, 27 | TlvCode, 28 | TlvError, 29 | TlvState, 30 | TlvMethod, 31 | ) 32 | from pyhap.util import CustomJSONEncoder 33 | 34 | logger = getLogger('pyhap') 35 | PAIRING_CONTENT_TYPE = 'application/pairing+tlv8' 36 | JSON_CONTENT_TYPE = 'application/hap+json' 37 | 38 | 39 | @encrypted 40 | async def accessories(request: Request) -> Response: 41 | logger.debug('/accessories called') 42 | accs: Accessories = request.global_context['accessories'] 43 | logger.debug('Accessories: %s', json.dumps(accs, cls=CustomJSONEncoder)) 44 | return Response(JSON_CONTENT_TYPE, data=json.dumps(accs, cls=CustomJSONEncoder).encode()) 45 | 46 | 47 | @encrypted 48 | async def characteristics(request: Request) -> Response: 49 | logger.debug(f'{request.method} /characteristics called') 50 | accs: Accessories = request.global_context['accessories'] 51 | 52 | if request.method == 'GET': 53 | logger.debug(f'{request.method} /characteristics query: {request.query}') 54 | error, result = accs.read_characteristic(request.query) 55 | logger.debug('Characteristics: %s', json.dumps(result, cls=CustomJSONEncoder)) 56 | if error: 57 | status = HTTPStatus.MULTI_STATUS 58 | else: 59 | status = HTTPStatus.OK 60 | return Response(JSON_CONTENT_TYPE, status, data=json.dumps(result, cls=CustomJSONEncoder).encode()) 61 | elif request.method == 'PUT': 62 | data = await request.read() 63 | error_data = await accs.write_characteristic(json.loads(data)['characteristics']) 64 | if error_data: 65 | return Response(status=HTTPStatus.MULTI_STATUS, data=json.dumps(error_data).encode()) 66 | 67 | return Response(status=HTTPStatus.NO_CONTENT) 68 | else: 69 | raise ValueError('Unknown http method received: {}'.format(request.method)) 70 | 71 | 72 | async def identify(request: Request) -> Response: 73 | logger.debug('/identify called') 74 | global_context = request.global_context 75 | config = global_context['config'] 76 | accs: Accessories = global_context['accessories'] 77 | 78 | if request.method != 'POST': 79 | return Response(status=HTTPStatus.NOT_FOUND) 80 | elif config.paired: 81 | return Response(JSON_CONTENT_TYPE, status=HTTPStatus.BAD_REQUEST, 82 | data=json.dumps({'status': HAPStatusCode.insufficient_privileges.value}).encode()) 83 | 84 | for accessory in accs: 85 | await accessory.identify() 86 | 87 | return Response(status=HTTPStatus.NO_CONTENT) 88 | 89 | 90 | async def pair_setup(request: Request) -> Response: 91 | global_context = request.global_context 92 | config = global_context['config'] 93 | 94 | parsed_body = tlv_parser.decode(await request.read())[0] 95 | requested_state = parsed_body.get(TlvCode.state) 96 | expected_state = global_context['pair_setup_expected_state'] 97 | 98 | logger.debug(f'Requested pair_setup state: {requested_state}') 99 | 100 | if requested_state == TlvState.m1 and parsed_body.get(TlvCode.method) == TlvMethod.reserved: 101 | result = srp_start(config, request.context, expected_state) 102 | global_context['pair_setup_expected_state'] = TlvState.m3 103 | elif requested_state == TlvState.m3: 104 | result = srp_verify(request.context, expected_state, parsed_body[TlvCode.public_key], 105 | parsed_body[TlvCode.proof]) 106 | global_context['pair_setup_expected_state'] = TlvState.m5 107 | elif requested_state == TlvState.m5: 108 | result = exchange(config, request.context, expected_state, parsed_body[TlvCode.encrypted_data]) 109 | global_context['pair_setup_expected_state'] = TlvState.m1 110 | else: 111 | raise ValueError('Unknown data received: {}'.format(parsed_body)) 112 | 113 | if TlvCode.error in result[0]: 114 | config.pair_setup_mode = False 115 | global_context['pair_setup_expected_state'] = TlvState.m1 116 | 117 | return Response(PAIRING_CONTENT_TYPE, data=tlv_parser.encode(result)) 118 | 119 | 120 | async def pair_verify(request: Request) -> Response: 121 | config = request.global_context['config'] 122 | upgrade = False 123 | 124 | parsed_body = tlv_parser.decode(await request.read())[0] 125 | requested_state = parsed_body.get(TlvCode.state) 126 | 127 | logger.debug(f'Requested pair_verify state: {requested_state}') 128 | 129 | if requested_state == TlvState.m1: 130 | result = verify_start(config, request.context, parsed_body[TlvCode.public_key]) 131 | elif requested_state == TlvState.m3: 132 | result = verify_finish(config, request.context, parsed_body[TlvCode.encrypted_data]) 133 | if request.context.get('paired'): 134 | upgrade = True # verify_finish end up without errors, upgrade to fully encrypted communication 135 | else: 136 | raise ValueError('Unknown data received: {}'.format(parsed_body)) 137 | 138 | return Response(PAIRING_CONTENT_TYPE, data=tlv_parser.encode(result), upgrade=upgrade) 139 | 140 | 141 | @encrypted 142 | async def pairings(request: Request) -> Response: 143 | logger.debug('/pairings called') 144 | 145 | config = request.global_context['config'] 146 | 147 | if config.get_pairing(request.context['ios_device_pairing_id'])[2] != ControllerPermission.admin: 148 | logger.error('Controller without admin permission is trying to call /pairings') 149 | return Response(PAIRING_CONTENT_TYPE, data=tlv_parser.encode([{ 150 | TlvCode.state: TlvState.m2, 151 | TlvCode.error: TlvError.authentication, 152 | }])) 153 | 154 | parsed_body = tlv_parser.decode(await request.read())[0] 155 | method = parsed_body.get(TlvCode.method) 156 | requested_state = parsed_body.get(TlvCode.state) 157 | keep_alive = True 158 | 159 | if method == TlvMethod.list_pairings and requested_state == TlvState.m1: 160 | logger.debug('/pairings list_pairings called') 161 | result = list_pairings(config) 162 | elif method == TlvMethod.add_pairing and requested_state == TlvState.m1: 163 | logger.debug('/pairings add_pairing called') 164 | ios_device_pairing_id = parsed_body[TlvCode.identifier] 165 | ios_device_public_key = parsed_body[TlvCode.public_key] 166 | permission = parsed_body[TlvCode.permissions] 167 | result = add_pairing(config, ios_device_pairing_id, ios_device_public_key, ControllerPermission(permission)) 168 | elif method == TlvMethod.remove_pairing and requested_state == TlvState.m1: 169 | logger.debug('/pairings remove_pairing called') 170 | ios_device_pairing_id = parsed_body[TlvCode.identifier] 171 | result = remove_pairing(config, ios_device_pairing_id) 172 | if not config.get_pairing(ios_device_pairing_id)[0]: 173 | keep_alive = False 174 | else: 175 | raise ValueError('Unknown data received: {}'.format(parsed_body)) 176 | 177 | return Response(PAIRING_CONTENT_TYPE, data=tlv_parser.encode(result), keep_alive=keep_alive) 178 | -------------------------------------------------------------------------------- /pyhap/service.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import ( 3 | List, 4 | Type, 5 | ) 6 | from uuid import UUID 7 | 8 | from pyhap.characteristic import Characteristic 9 | from pyhap.util import ( 10 | aduuid_to_uuid, 11 | uuid_to_aduuid, 12 | ) 13 | 14 | 15 | class Service: 16 | def __init__(self): 17 | self.instance_id = None # set by bridge 18 | self.characteristics: List[Characteristic] = [] 19 | 20 | @property 21 | @abstractmethod 22 | def service_uuid(self) -> UUID: 23 | raise NotImplementedError() 24 | 25 | def add_characteristic(self, characteristic: Characteristic): 26 | self.characteristics.append(characteristic) 27 | 28 | def __json__(self): 29 | return { 30 | 'type': uuid_to_aduuid(self.service_uuid), 31 | 'iid': self.instance_id, 32 | 'characteristics': self.characteristics 33 | } 34 | 35 | 36 | def generate_service(name, service_uuid: str) -> Type[Service]: 37 | return type(name, (Service,), { 38 | 'service_uuid': property(lambda self: aduuid_to_uuid(service_uuid)) 39 | }) 40 | 41 | 42 | AccessoryInformationService = generate_service('AccessoryInformationService', '3e') 43 | FanService = generate_service('Fan', '40') 44 | GarageDoorOpenerService = generate_service('GarageDoorOpener', '41') 45 | LightbulbService = generate_service('LightbulbService', '43') 46 | LockManagementService = generate_service('LockManagementService', '44') 47 | LockMechanismService = generate_service('LockMechanismService', '45') 48 | OutletService = generate_service('OutletService', '47') 49 | SwitchService = generate_service('SwitchService', '49') 50 | ThermostatService = generate_service('ThermostatService', '4a') 51 | AirQualityService = generate_service('AirQualityService', '8d') 52 | SecuritySystemService = generate_service('SecuritySystemService', '7e') 53 | CarbonMonoxideSensorService = generate_service('CarbonMonoxideSensorService', '7f') 54 | ContactSensorService = generate_service('ContactSensorService', '80') 55 | HumiditySensorService = generate_service('HumiditySensorService', '82') 56 | LeakSensorService = generate_service('LeakSensorService', '83') 57 | LightSensorService = generate_service('LightSensorService', '84') 58 | MotionSensorService = generate_service('MotionSensorService', '85') 59 | OccupancySensorService = generate_service('MotionSensorService', '86') 60 | SmokeSensorService = generate_service('SmokeSensorService', '87') 61 | StatelessProgrammableSwitchService = generate_service('StatelessProgrammableSwitchService', '89') 62 | TemperatureService = generate_service('TemperatureService', '8A') 63 | WindowService = generate_service('WindowService', '8B') 64 | WindowCoveringService = generate_service('WindowCoveringService', '8C') 65 | BatteryService = generate_service('BatteryService', '96') 66 | CarbonDioxideSensorService = generate_service('CarbonDioxideSensorService', '97') 67 | CameraRTPStreamManagementService = generate_service('CameraRTPStreamManagementService', '110') 68 | MicrophoneService = generate_service('MicrophoneService', '112') 69 | SpeakerService = generate_service('SpeakerService', '113') 70 | DoorbellService = generate_service('DoorbellService', '121') 71 | FanV2Service = generate_service('FanV2Service', 'B7') 72 | SlatService = generate_service('SlatService', 'B9') 73 | FilterMaintenanceService = generate_service('FilterMaintenanceService', 'BA') 74 | AirPurifierService = generate_service('AirPurifierService', 'BB') 75 | ServiceLabelService = generate_service('ServiceLabelService', 'CC') 76 | -------------------------------------------------------------------------------- /pyhap/srp.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha512 2 | from os import urandom 3 | from typing import Union 4 | 5 | N_3072 = int( 6 | 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E08' 7 | '8A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B' 8 | '302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9' 9 | 'A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE6' 10 | '49286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8' 11 | 'FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D' 12 | '670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C' 13 | '180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718' 14 | '3995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D' 15 | '04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7D' 16 | 'B3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D226' 17 | '1AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200C' 18 | 'BBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFC' 19 | 'E0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF', 16) 20 | 21 | g = 5 22 | g_padded = g.to_bytes(384, byteorder='big') # left padded with 0x00 bytes to extend length to 3072 bits (384 bytes) 23 | 24 | 25 | def H(*args: Union[bytes, int]) -> bytes: 26 | h = sha512() 27 | for arg in args: 28 | if isinstance(arg, int): 29 | arg = to_bytes(arg) 30 | h.update(arg) 31 | return h.digest() 32 | 33 | 34 | def calculate_M(I: bytes, s: bytes, A: int, B: int, K) -> bytes: 35 | H_xor = bytes(map(lambda i: i[0] ^ i[1], zip(H(g), H(N_3072)))) 36 | return H(H_xor, H(I), s, A, B, K) 37 | 38 | 39 | def to_bytes(data: int) -> bytes: 40 | return data.to_bytes((data.bit_length() + 7) // 8, byteorder='big') 41 | 42 | 43 | def to_int(data: bytes) -> int: 44 | return int.from_bytes(data, byteorder='big') 45 | 46 | 47 | def generate_salt() -> bytes: 48 | return urandom(16) 49 | 50 | 51 | def generate_private_key() -> int: 52 | return to_int(urandom(32)) 53 | 54 | 55 | class Srp: 56 | """ SRP 6a protocol implementation (server part) 57 | 58 | Accessory is a server and iOS device is a client 59 | 60 | A - client public key 61 | B - server public key 62 | b - server private key 63 | H_AMK - server proof of session key 64 | I - username 65 | K - session key 66 | M - client proof of session key 67 | S - premaster secret 68 | s - salt 69 | u - random scrambling parameter 70 | v - server password verifier 71 | """ 72 | def __init__(self, username: str, password: str) -> None: 73 | self.I = username.encode() 74 | self.s = generate_salt() 75 | self.b = generate_private_key() 76 | k = to_int(H(N_3072, g_padded)) 77 | x = to_int(H(self.s, H(username.encode() + b':' + password.encode()))) 78 | self.v = pow(g, x, N_3072) 79 | self.B: int = (k * self.v + pow(g, self.b, N_3072)) % N_3072 80 | self.u: int = None 81 | self.S: int = None 82 | self.K: bytes = None 83 | self.M: bytes = None 84 | self.H_AMK: bytes = None 85 | 86 | @property 87 | def public_key(self) -> bytes: 88 | return to_bytes(self.B) 89 | 90 | @property 91 | def salt(self) -> bytes: 92 | return self.s 93 | 94 | @property 95 | def session_key_proof(self) -> bytes: 96 | return self.H_AMK 97 | 98 | @property 99 | def session_key(self) -> bytes: 100 | return self.K 101 | 102 | def compute_shared_session_key(self, A: bytes) -> None: 103 | A_int = to_int(A) 104 | self.u = to_int(H(A_int, self.B)) 105 | self.S = pow(A_int * pow(self.v, self.u, N_3072), self.b, N_3072) 106 | self.K = H(self.S) 107 | self.M = calculate_M(self.I, self.s, A_int, self.B, self.K) 108 | self.H_AMK = H(A_int, self.M, self.K) 109 | 110 | def verify_proof(self, M: bytes) -> bool: 111 | return M == self.M 112 | -------------------------------------------------------------------------------- /pyhap/tlv.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from enum import Enum, IntEnum 3 | from io import BytesIO 4 | from typing import ( 5 | Dict, 6 | List, 7 | Tuple, 8 | Union, 9 | ) 10 | 11 | 12 | class TlvMethod(IntEnum): 13 | reserved = 0 14 | pair_setup = 1 15 | pair_verify = 2 16 | add_pairing = 3 17 | remove_pairing = 4 18 | list_pairings = 5 19 | 20 | 21 | class TlvError(IntEnum): 22 | unknown = 0x01 # generic error to handle unexpected errors 23 | authentication = 0x02 # setup code or signature verification failed 24 | backoff = 0x03 # client must look at the retry delay tlv item and wait that many seconds before retrying 25 | max_peers = 0x04 # server cannot accept any more pairings 26 | max_tries = 0x05 # server reached its maximum number of authentication attempts 27 | unavailable = 0x06 # server pairing method is unavailable 28 | busy = 0x07 # server busy and cannot accept pairing request at this time 29 | 30 | 31 | class TlvCode(Enum): 32 | method = 0x00 # method to use for pairing 33 | identifier = 0x01 # identifier for authentication 34 | salt = 0x02 # 16+ bytes of random salt 35 | public_key = 0x03 # curve25519, srp public key or signed ed25519 key 36 | proof = 0x04 # ed25519 or srp proof 37 | encrypted_data = 0x05 # encrypted data with auth tag at end 38 | state = 0x06 # state of the pairing process 39 | error = 0x07 # error code, must only be present if error code is not 0 40 | retry_delay = 0x08 # seconds to delay until retrying a setup code 41 | certificate = 0x09 # x.509 certificate 42 | signature = 0x0a # ed25519 43 | permissions = 0x0b # bit value describing permissions of the controller being added, 0 - regular user, 1 - admin 44 | fragment_data = 0x0c # non-last fragment of data, if length is 0, it is ack 45 | fragment_last = 0x0d # last fragment data 46 | separator = 0xff # zero-length tlv that separates different tlvs in a list 47 | 48 | 49 | class TlvState(IntEnum): 50 | m1 = 0x01 51 | m2 = 0x02 52 | m3 = 0x03 53 | m4 = 0x04 54 | m5 = 0x05 55 | m6 = 0x06 56 | 57 | 58 | class TlvParser: 59 | def __init__(self, values: List[Tuple[TlvCode, type]]) -> None: 60 | self.code_type_map: Dict[TlvCode, type] = {} 61 | 62 | for value, data_type in values: 63 | self.code_type_map[value] = data_type 64 | 65 | @staticmethod 66 | def get_by_code(code: int) -> TlvCode: 67 | return TlvCode(code) 68 | 69 | @staticmethod 70 | def get_by_name(name: str) -> TlvCode: 71 | result = None 72 | try: 73 | result = TlvCode[name] 74 | except KeyError: 75 | pass 76 | 77 | if not result: 78 | raise ValueError(f'Unable to identify TlvCode for {name}') 79 | 80 | return result 81 | 82 | @staticmethod 83 | def get_name(code: int) -> str: 84 | return TlvCode(code).name 85 | 86 | def get_type(self, code: TlvCode) -> type: 87 | result = None 88 | try: 89 | result = self.code_type_map[code] 90 | except KeyError: 91 | pass 92 | 93 | if not result: 94 | raise ValueError(f'Unable to identify type for {code}') 95 | 96 | return result 97 | 98 | def decode(self, data: bytes) -> List[dict]: 99 | """Decodes tlv byte stream. 100 | 101 | Supports merging records with same tlv code. 102 | Returns list of entries separated by TlvCode.separator. 103 | """ 104 | result = [] 105 | entry: dict = {} 106 | previous_tlv_code = None 107 | with BytesIO(data) as f: 108 | while True: 109 | tlv_code_raw: bytes = f.read(1) 110 | if not tlv_code_raw: 111 | break 112 | tlv_code = self.get_by_code(int.from_bytes(tlv_code_raw, 'little')) 113 | tlv_size = int.from_bytes(f.read(1), 'little') 114 | 115 | if tlv_code == TlvCode.separator: 116 | result.append(entry) 117 | entry = {} 118 | continue 119 | 120 | raw_tlv_data = f.read(tlv_size) 121 | tlv_type = self.get_type(tlv_code) 122 | tlv_data: Union[str, int, bytes] 123 | if tlv_type == str: 124 | try: 125 | tlv_data = raw_tlv_data.decode() # type: ignore 126 | except UnicodeDecodeError: 127 | tlv_data = None 128 | if not tlv_data: 129 | raise ValueError(f'Unable to decode {tlv_code} string from bytes: {raw_tlv_data.hex()}') 130 | if tlv_type == int: 131 | if tlv_size != 1: 132 | raise ValueError('Only short (1-byte length) integers is supported') 133 | tlv_data = int.from_bytes(raw_tlv_data, 'little') # type: ignore 134 | if tlv_type == bytes: 135 | tlv_data = raw_tlv_data 136 | 137 | if tlv_code == previous_tlv_code: 138 | entry[tlv_code] = entry[tlv_code] + tlv_data # append data to previous tlv 139 | else: 140 | entry[tlv_code] = tlv_data 141 | 142 | previous_tlv_code = tlv_code 143 | 144 | result.append(entry) 145 | 146 | return result 147 | 148 | def encode(self, data: List[dict]) -> bytes: 149 | result = b'' 150 | for i, entry in enumerate(data): 151 | if i != 0: 152 | # add separator 153 | result += struct.pack(' str: 11 | """Generates 8 random bytes joined by semicolon""" 12 | return ':'.join(urandom(1).hex().upper() for _ in range(8)) 13 | 14 | 15 | def generate_signing_key() -> str: 16 | """Generated 32 random bytes and returns hex representation""" 17 | return urandom(32).hex() 18 | 19 | 20 | def generate_setup_code() -> str: 21 | """Generates numeric setup code in the following format: ddd-dd-ddd""" 22 | return '{:03d}-{:02d}-{:03d}'.format(randint(0, 999), randint(0, 99), randint(0, 999)) 23 | 24 | 25 | def uuid_to_aduuid(uuid: UUID) -> str: 26 | """Converts Apple-defined UUID to a short form 27 | 28 | Includes only the first 8 characters with leading zeros removed 29 | 30 | In case of non-Apple-defined UUID returns full UUID 31 | """ 32 | if str(uuid).endswith('-0000-1000-8000-0026bb765291'): 33 | first_part = str(uuid).split('-')[0] 34 | return first_part.lstrip('0').upper() 35 | 36 | return str(uuid).upper() 37 | 38 | 39 | def aduuid_to_uuid(uuid: str) -> UUID: 40 | """Converts a short form of Apple-defined UUID to UUID 41 | 42 | In case of non-Apple-defined UUID returns full UUID back 43 | """ 44 | if len(uuid) > 8: 45 | return UUID(uuid) 46 | 47 | if len(uuid) < 8: 48 | leading_zeros = '0' * (8 - len(uuid)) 49 | uuid = leading_zeros + uuid 50 | 51 | return UUID(uuid + '-0000-1000-8000-0026bb765291') 52 | 53 | 54 | def serial_number_hash(data: str) -> str: 55 | """Hashes any string to 6 bytes serial number 56 | 57 | Example: 'PyHAP' -> '3331779EC7A8' 58 | 59 | Hash function is BLAKE2b with 6 bytes digest size 60 | """ 61 | h = blake2b(digest_size=6) # type: ignore 62 | h.update(data.encode()) 63 | return h.hexdigest().upper() 64 | 65 | 66 | def hs_to_rgb(hue: int, saturation: int) -> Tuple[int, int, int]: 67 | red, green, blue = colorsys.hsv_to_rgb(hue/360, saturation/100, 1) 68 | return round(red * 255), round(green * 255), round(blue * 255) 69 | 70 | 71 | class CustomJSONEncoder(json.JSONEncoder): 72 | def default(self, o): # pylint: disable=method-hidden 73 | if hasattr(o, '__json__'): 74 | return o.__json__() 75 | return json.JSONEncoder.default(self, o) 76 | -------------------------------------------------------------------------------- /pyhap_generator/generator.py: -------------------------------------------------------------------------------- 1 | import plistlib 2 | from os import path, makedirs 3 | from typing import ( 4 | Optional, 5 | Tuple, 6 | ) 7 | 8 | plist_filepath = '/Applications/HomeKit Accessory Simulator.app/' \ 9 | 'Contents/Frameworks/HAPAccessoryKit.framework/Resources/default.metadata.plist' 10 | 11 | current_directory = path.dirname(path.realpath(__file__)) 12 | characteristics_directory = path.realpath(path.join(current_directory, '..', 'pyhap', 'characteristics')) 13 | template_filepath = path.realpath(path.join(current_directory, 'templates/characteristic.py.template')) 14 | 15 | characteristic_formats = { 16 | 'string': ('str', 'string'), 17 | 'bool': ('bool', 'bool'), 18 | 'uint8': ('int', 'int'), 19 | 'uint16': ('int', 'int'), 20 | 'uint32': ('int', 'int'), 21 | 'int32': ('int', 'int'), 22 | 'float': ('float', 'float'), 23 | 'tlv8': ('int', 'tlv'), 24 | } 25 | 26 | 27 | def main(): 28 | makedirs(characteristics_directory, exist_ok=True) 29 | 30 | plist = get_plist() 31 | 32 | characteristic_init = '' 33 | characteristic_init_filepath = path.join(characteristics_directory, '__init__.py') 34 | 35 | for characteristic in plist['Characteristics']: 36 | underscore_name, class_name = generate_characteristic(characteristic) 37 | if underscore_name and class_name: 38 | characteristic_init += f'from pyhap.characteristics.{underscore_name} import {class_name}\n' 39 | 40 | with open(characteristic_init_filepath, 'w+') as f: 41 | f.write(characteristic_init) 42 | 43 | 44 | def generate_characteristic(characteristic: dict) -> Tuple[Optional[str], Optional[str]]: 45 | if characteristic['Format'] not in characteristic_formats: 46 | print('Unknown characteristic format:', characteristic['Format']) 47 | return None, None 48 | 49 | characteristic_type, characteristic_format = characteristic_formats[characteristic['Format']] 50 | 51 | template = open(template_filepath).read() 52 | 53 | class_name = gen_class_name(characteristic['Name']) 54 | permissions = gen_permissions(characteristic['Properties']) 55 | 56 | template = template.format( 57 | class_name=class_name, 58 | type=characteristic_type, 59 | uuid=characteristic['UUID'], 60 | format=characteristic_format, 61 | permissions=permissions 62 | ) 63 | 64 | underscore_name = gen_underscore_name(characteristic['Name']) 65 | characteristic_filepath = path.join(characteristics_directory, underscore_name + '.py') 66 | 67 | with open(characteristic_filepath, 'w+') as f: 68 | f.write(template) 69 | 70 | return underscore_name, class_name 71 | 72 | 73 | def gen_class_name(name: str) -> str: 74 | return name.replace(' ', '') 75 | 76 | 77 | def gen_underscore_name(name: str) -> str: 78 | return name.replace(' ', '_').lower() 79 | 80 | 81 | def gen_permissions(permissions: list) -> str: 82 | permission_types = { 83 | 'read': 'CharacteristicPermission.pair_read', 84 | 'write': 'CharacteristicPermission.pair_write', 85 | 'cnotify': 'CharacteristicPermission.notify', 86 | } 87 | converted_permissions = [] 88 | 89 | for permission in permissions: 90 | if permission not in permission_types: 91 | print('Unknown permission:', permission) 92 | continue 93 | 94 | converted_permissions.append(f'{permission_types[permission]},\n') 95 | 96 | spaces = ' ' * 12 97 | 98 | return spaces.join(converted_permissions) 99 | 100 | 101 | def get_plist() -> dict: 102 | with open(plist_filepath, 'rb') as f: 103 | return plistlib.load(f) 104 | 105 | 106 | if __name__ == '__main__': 107 | main() 108 | -------------------------------------------------------------------------------- /pyhap_generator/templates/characteristic.py.template: -------------------------------------------------------------------------------- 1 | # Autogenerated, do not edit. All changes will be undone. 2 | 3 | from typing import ( 4 | List, 5 | Type, 6 | ) 7 | from uuid import UUID 8 | 9 | from pyhap.characteristic import ( 10 | Characteristic, 11 | CharacteristicPermission, 12 | ) 13 | 14 | 15 | class {class_name}(Characteristic): 16 | @property 17 | def characteristic_uuid(self) -> UUID: 18 | return UUID('{uuid}') 19 | 20 | @property 21 | def characteristic_type(self) -> Type: 22 | return {type} 23 | 24 | @property 25 | def characteristic_format(self) -> str: 26 | return '{format}' 27 | 28 | @property 29 | def permissions(self) -> List[CharacteristicPermission]: 30 | return [ 31 | {permissions} ] 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from setuptools import setup 4 | 5 | with open(path.join(path.dirname(__file__), 'README.rst')) as f: 6 | long_description = f.read().strip() 7 | 8 | setup( 9 | name='pyhap', 10 | version='0.1.1', 11 | packages=['pyhap', 'pyhap.characteristics'], 12 | install_requires=[ 13 | 'cryptography', 14 | 'ed25519', 15 | 'zeroconf', 16 | ], 17 | description='Python implementation of HomeKit Accessory Protocol', 18 | long_description=long_description, 19 | url='https://github.com/condemil/pyhap', 20 | license='MIT', 21 | author='Dmitry Budaev', 22 | python_requires='>=3.6.0', 23 | classifiers=[ 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 3', 28 | 'Programming Language :: Python :: 3.6', 29 | 'Topic :: Home Automation', 30 | 'Topic :: Software Development :: Libraries', 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | 4 | class AsyncMock(Mock): 5 | async def __call__(self, *args, **kwargs): 6 | result = super().__call__(*args, **kwargs) 7 | return result 8 | -------------------------------------------------------------------------------- /test/test_accessory.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from unittest import TestCase 4 | from unittest.mock import Mock 5 | 6 | from test import AsyncMock 7 | from pyhap.accessory import ( 8 | Accessories, 9 | Accessory, 10 | ) 11 | from pyhap.characteristic import Characteristic 12 | from pyhap.characteristics import ( 13 | Brightness, 14 | On, 15 | Hue, 16 | ) 17 | from pyhap.service import LightbulbService 18 | from pyhap.util import CustomJSONEncoder 19 | 20 | 21 | class TestAccessories(TestCase): 22 | def test_add(self): 23 | accessories = Accessories() 24 | accessory = Mock() 25 | 26 | self.assertEqual(accessories.accessory_count, 1) 27 | accessories.add(accessory) 28 | self.assertEqual(accessory.accessory_id, 2) 29 | self.assertEqual(accessories.accessories[2], accessory) 30 | self.assertEqual(accessories.accessory_count, 2) 31 | 32 | def test_iter(self): 33 | accessories = Accessories() 34 | 35 | for a in accessories: 36 | self.assertEqual(a, accessories.accessories[1]) 37 | 38 | @staticmethod 39 | def test_identify(): 40 | callback = AsyncMock() 41 | accessory = Accessory(name='test_name', model='test_model', manufacturer='test_manufacturer', 42 | identify_callback=callback) 43 | asyncio.get_event_loop().run_until_complete(accessory.identify()) 44 | callback.assert_called_once() 45 | 46 | def test_get_characteristic(self): 47 | accessories = Accessories() 48 | characteristic = accessories.get_characteristic(1, 2) 49 | self.assertIsInstance(characteristic, Characteristic) 50 | 51 | def test_read_characteristic(self): 52 | accessories = Accessories() 53 | error, characteristics = accessories.read_characteristic({ 54 | 'id': '1.2,1.3', 55 | 'meta': '1', 56 | 'perms': '1', 57 | 'type': '1', 58 | 'include_ev': '1', 59 | }) 60 | 61 | self.assertFalse(error) 62 | 63 | self.assertEqual(characteristics['characteristics'][0], { 64 | 'aid': 1, 65 | 'iid': 2, 66 | 'value': 'PyHAP', 67 | 'perms': ['pr'], 68 | 'type': '23', 69 | }) 70 | 71 | self.assertEqual(characteristics['characteristics'][1], { 72 | 'aid': 1, 73 | 'iid': 3, 74 | 'value': 'PyHAP1,1', 75 | 'perms': ['pr'], 76 | 'type': '21', 77 | }) 78 | 79 | def test_read_characteristic_write_only(self): 80 | accessories = Accessories() 81 | error, characteristics = accessories.read_characteristic({'id': '1.7'}) 82 | 83 | self.assertTrue(error) 84 | 85 | self.assertEqual(characteristics['characteristics'][0], { 86 | 'aid': 1, 87 | 'iid': 7, 88 | 'status': -70405, 89 | }) 90 | 91 | def test_write_characteristic(self): 92 | accessory = Accessory(name='PyHAP', model='PyHAP1,1', manufacturer='PyHAP', hardware_revision='0') 93 | service = LightbulbService() 94 | 95 | bool_characteristic = On(False) 96 | int_characteristic = Brightness(8) 97 | float_characteristic = Hue(5.0) 98 | 99 | service.add_characteristic(bool_characteristic) 100 | service.add_characteristic(int_characteristic) 101 | service.add_characteristic(float_characteristic) 102 | 103 | accessories = Accessories() 104 | accessory.add_service(service) 105 | accessories.add(accessory) 106 | 107 | # bool characteristic 108 | callback = AsyncMock() 109 | bool_characteristic.callback = callback 110 | 111 | self.assertEqual(bool_characteristic.value, False) 112 | result = asyncio.get_event_loop().run_until_complete( 113 | accessories.write_characteristic([{'aid': 2, 'iid': 10, 'value': True}]) 114 | ) 115 | callback.assert_called_once_with(True) 116 | self.assertEqual(result, []) 117 | self.assertEqual(bool_characteristic.value, True) 118 | 119 | # int characteristic write 120 | callback = AsyncMock() 121 | int_characteristic.callback = callback 122 | 123 | self.assertEqual(int_characteristic.value, 8) 124 | result = asyncio.get_event_loop().run_until_complete( 125 | accessories.write_characteristic([{'aid': 2, 'iid': 11, 'value': 12}]) 126 | ) 127 | callback.assert_called_once_with(12) 128 | self.assertEqual(result, []) 129 | self.assertEqual(int_characteristic.value, 12) 130 | 131 | # float characteristic write 132 | callback = AsyncMock() 133 | float_characteristic.callback = callback 134 | 135 | self.assertEqual(float_characteristic.value, 5.0) 136 | result = asyncio.get_event_loop().run_until_complete( 137 | accessories.write_characteristic([{'aid': 2, 'iid': 12, 'value': 7.0}]) 138 | ) 139 | callback.assert_called_once_with(7.0) 140 | self.assertEqual(result, []) 141 | self.assertEqual(float_characteristic.value, 7.0) 142 | 143 | # None value during write, leave previous value 144 | previous_value = bool_characteristic.value 145 | result = asyncio.get_event_loop().run_until_complete( 146 | accessories.write_characteristic([{'aid': 2, 'iid': 10}]) 147 | ) 148 | self.assertEqual(result, []) 149 | self.assertEqual(bool_characteristic.value, previous_value) 150 | bool_characteristic.value = previous_value 151 | 152 | # None callback 153 | bool_characteristic.callback = None 154 | result = asyncio.get_event_loop().run_until_complete( 155 | accessories.write_characteristic([{'aid': 2, 'iid': 10, 'value': True}]) 156 | ) 157 | self.assertEqual(result, []) 158 | 159 | # Exception in callback 160 | bool_characteristic.callback = exception_callback 161 | result = asyncio.get_event_loop().run_until_complete( 162 | accessories.write_characteristic([{'aid': 2, 'iid': 10, 'value': True}]) 163 | ) 164 | self.assertEqual(result, [{ 165 | 'aid': 2, 166 | 'iid': 10, 167 | 'status': -70402, 168 | }]) 169 | 170 | def test_write_characteristic_read_only(self): 171 | accessories = Accessories() 172 | 173 | result = asyncio.get_event_loop().run_until_complete( 174 | accessories.write_characteristic([{'aid': 1, 'iid': 2, 'value': 'test_value'}]) 175 | ) 176 | 177 | self.assertEqual(result, [{ 178 | 'aid': 1, 179 | 'iid': 2, 180 | 'status': -70404, 181 | }]) 182 | 183 | # pylint: disable=line-too-long 184 | def test_json(self): 185 | accessories = Accessories() 186 | result = json.loads(json.dumps(accessories.__json__(), cls=CustomJSONEncoder)) 187 | self.assertEqual(result, { 188 | 'accessories': [{ 189 | 'aid': 1, 190 | 'services': [{ 191 | 'type': '3E', 192 | 'iid': 1, 193 | 'characteristics': [ 194 | {'type': '23', 'perms': ['pr'], 'format': 'string', 'aid': 1, 'iid': 2, 'value': 'PyHAP', 'maxLen': 64}, 195 | {'type': '21', 'perms': ['pr'], 'format': 'string', 'aid': 1, 'iid': 3, 'value': 'PyHAP1,1', 'maxLen': 64}, 196 | {'type': '20', 'perms': ['pr'], 'format': 'string', 'aid': 1, 'iid': 4, 'value': 'PyHAP', 'maxLen': 64}, 197 | {'type': '30', 'perms': ['pr'], 'format': 'string', 'aid': 1, 'iid': 5, 'value': '3331779EC7A8', 'maxLen': 64}, 198 | {'type': '52', 'perms': ['pr'], 'format': 'string', 'aid': 1, 'iid': 6, 'value': '0.0.1', 'maxLen': 64}, 199 | {'type': '14', 'perms': ['pw'], 'format': 'bool', 'aid': 1, 'iid': 7} 200 | ] 201 | }] 202 | }] 203 | }) 204 | 205 | 206 | async def exception_callback(): 207 | raise Exception() 208 | -------------------------------------------------------------------------------- /test/test_characteristic.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from uuid import UUID 3 | 4 | from test import AsyncMock 5 | from pyhap.characteristics import Name 6 | 7 | 8 | TEST_ACCESSORY_ID = 5 9 | TEST_INSTANCE_ID = 8 10 | TEST_VALUE = 'test_value' 11 | TEST_NEW_VALUE = 'test_new_value' 12 | TEST_WRONG_TYPE_VALUE = 1 13 | TEST_CHARACTERISTIC_SHORT_UUID = '23' 14 | TEST_PERMISSIONS = ['pr'] 15 | 16 | 17 | class TestCharacteristic(TestCase): 18 | def setUp(self): 19 | self.callback = AsyncMock() 20 | self.characteristic = Name(TEST_VALUE, self.callback) 21 | 22 | def test_get_set_value(self): 23 | self.assertEqual(self.characteristic.value, TEST_VALUE) 24 | self.characteristic.value = TEST_NEW_VALUE 25 | self.assertEqual(self.characteristic.value, TEST_NEW_VALUE) 26 | self.characteristic.value = TEST_VALUE 27 | 28 | with self.assertRaises(ValueError): 29 | self.characteristic.value = TEST_WRONG_TYPE_VALUE 30 | 31 | 32 | def test_characteristic_uuid(self): 33 | self.assertIsInstance(self.characteristic.characteristic_uuid, UUID) 34 | 35 | def test_get_set_accessory_id(self): 36 | self.assertEqual(self.characteristic.accessory_id, None) 37 | self.characteristic.accessory_id = TEST_ACCESSORY_ID 38 | self.assertEqual(self.characteristic.accessory_id, TEST_ACCESSORY_ID) 39 | 40 | def test_get_set_instance_id(self): 41 | self.assertEqual(self.characteristic.instance_id, None) 42 | self.characteristic.instance_id = TEST_INSTANCE_ID 43 | self.assertEqual(self.characteristic.instance_id, TEST_INSTANCE_ID) 44 | 45 | def test_json(self): 46 | self.characteristic.accessory_id = TEST_ACCESSORY_ID 47 | self.characteristic.instance_id = TEST_INSTANCE_ID 48 | self.characteristic.value = TEST_VALUE 49 | self.assertEqual(self.characteristic.__json__(), { 50 | 'type': TEST_CHARACTERISTIC_SHORT_UUID, 51 | 'perms': TEST_PERMISSIONS, 52 | 'format': 'string', 53 | 'aid': TEST_ACCESSORY_ID, 54 | 'iid': TEST_INSTANCE_ID, 55 | 'value': TEST_VALUE, 56 | 'maxLen': 64, 57 | }) 58 | -------------------------------------------------------------------------------- /test/test_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tempfile import NamedTemporaryFile 3 | from unittest import TestCase 4 | from unittest.mock import patch 5 | 6 | from pyhap.config import ( 7 | Config, 8 | ControllerPermission, 9 | JsonConfig, 10 | ) 11 | 12 | SERVER_IP = '0.0.0.0' 13 | DEFAULT_SERVER_PORT = 8080 14 | DEVICE_ID_MOCK = '7B:45:9B:C1:33:42:FB:96' 15 | MODEL_NAME = 'PyHAP' 16 | SERVICE_TYPE = '_hap._tcp.local.' 17 | SIGNING_KEY_MOCK = 'f4e786f11fb7a172fe07a9801aede0c80deffe661ae9cf37423ee1943a8321ed' 18 | SETUP_CODE_MOCK = '925-52-789' 19 | 20 | PARING_ID = 'test pairing' 21 | PUBLIC_KEY = b'test_public_key' 22 | 23 | NOT_AVAILABLE_FILE = '/tmp/not-available-ff6adf36-fdbb-402b-9ac7-5783e58381e8.json' 24 | 25 | 26 | class TestConfig(TestCase): 27 | def setUp(self): 28 | self.patch_generate_device_id = patch('pyhap.config.generate_device_id', new=lambda: DEVICE_ID_MOCK) 29 | self.addCleanup(self.patch_generate_device_id.stop) 30 | self.patch_generate_device_id.start() 31 | 32 | self.patch_generate_signing_key = patch('pyhap.config.generate_signing_key', new=lambda: SIGNING_KEY_MOCK) 33 | self.addCleanup(self.patch_generate_signing_key.stop) 34 | self.patch_generate_signing_key.start() 35 | 36 | self.patch_generate_setup_code = patch('pyhap.config.generate_setup_code', new=lambda: SETUP_CODE_MOCK) 37 | self.addCleanup(self.patch_generate_setup_code.stop) 38 | self.patch_generate_setup_code.start() 39 | 40 | if os.path.exists(NOT_AVAILABLE_FILE): 41 | os.remove(NOT_AVAILABLE_FILE) 42 | 43 | self.config_fp = NamedTemporaryFile() 44 | self.config = JsonConfig(SERVER_IP, self.config_fp.name) 45 | 46 | def tearDown(self): 47 | self.config_fp.close() 48 | 49 | def test_server_ip(self): 50 | self.assertEqual(self.config.server_ip, SERVER_IP) 51 | 52 | def test_server_port(self): 53 | self.assertEqual(self.config.server_port, DEFAULT_SERVER_PORT) 54 | 55 | def test_device_id(self): 56 | self.assertEqual(self.config.device_id, DEVICE_ID_MOCK) 57 | 58 | def test_configuration_number(self): 59 | self.assertEqual(self.config.configuration_number, 1) 60 | 61 | def test_model_name(self): 62 | self.assertEqual(self.config.model_name, MODEL_NAME) 63 | 64 | def test_service_type(self): 65 | self.assertEqual(self.config.service_type, SERVICE_TYPE) 66 | 67 | def test_unsuccessful_authentication_attempts(self): 68 | self.assertEqual(self.config.unsuccessful_authentication_attempts, 0) 69 | 70 | def test_accessory_ltsk(self): 71 | self.assertEqual(self.config.accessory_ltsk.hex(), SIGNING_KEY_MOCK) 72 | 73 | def test_setup_code(self): 74 | self.assertEqual(self.config.setup_code, SETUP_CODE_MOCK) 75 | 76 | def test_pair_setup_mode(self): 77 | self.assertFalse(self.config.pair_setup_mode) 78 | self.config.pair_setup_mode = True 79 | self.assertTrue(self.config.pair_setup_mode) 80 | 81 | def test_paired(self): 82 | self.assertFalse(self.config.paired) 83 | 84 | def test_crud_pairings(self): 85 | self.assertEqual(self.config.get_pairings(), []) 86 | self.config.add_pairing(PARING_ID, PUBLIC_KEY, ControllerPermission.admin) 87 | self.assertEqual(self.config.get_pairings(), [(PARING_ID, PUBLIC_KEY, ControllerPermission.admin)]) 88 | self.assertEqual(self.config.get_pairing(PARING_ID), (PARING_ID, PUBLIC_KEY, ControllerPermission.admin)) 89 | self.config.remove_pairing(PARING_ID) 90 | self.assertEqual(self.config.get_pairings(), []) 91 | self.assertEqual(self.config.get_pairing('not exists'), (None, None, None)) 92 | 93 | def test_load_save(self): 94 | config = Config(SERVER_IP) 95 | 96 | with self.assertRaises(NotImplementedError): 97 | config.load() 98 | 99 | with self.assertRaises(NotImplementedError): 100 | config.save() 101 | 102 | def test_json_file_not_found(self): 103 | config = JsonConfig(SERVER_IP, config_filepath=NOT_AVAILABLE_FILE) 104 | self.assertIsInstance(config, JsonConfig) 105 | -------------------------------------------------------------------------------- /test/test_pyhap.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import patch, Mock 3 | 4 | from pyhap import pyhap 5 | from pyhap.pyhap import ( 6 | MDNSServer, 7 | WebServer, 8 | ) 9 | 10 | 11 | class TestPyHAP(TestCase): 12 | def setUp(self): 13 | patch_mdns_server = patch('pyhap.pyhap.MDNSServer') 14 | self.addCleanup(patch_mdns_server.stop) 15 | self.mock_mdns_server = patch_mdns_server.start() 16 | 17 | patch_web_server = patch('pyhap.pyhap.WebServer') 18 | self.addCleanup(patch_web_server.stop) 19 | self.mock_web_server = patch_web_server.start() 20 | 21 | def test_start(self): 22 | config = Mock() 23 | accessories = Mock() 24 | loop = Mock() 25 | 26 | pyhap.start(config, accessories, loop) 27 | 28 | self.mock_mdns_server.assert_called_once_with(config) 29 | self.mock_mdns_server().start.assert_called_once() 30 | 31 | self.mock_web_server.assert_called_once_with(config, accessories, loop) 32 | self.mock_web_server().start.assert_called_once() 33 | 34 | 35 | class TestMDNSServer(TestCase): 36 | def setUp(self): 37 | patch_zeroconf = patch('pyhap.pyhap.Zeroconf') 38 | self.addCleanup(patch_zeroconf.stop) 39 | self.mock_zeroconf = patch_zeroconf.start() 40 | 41 | patch_service_info = patch('pyhap.pyhap.ServiceInfo') 42 | self.addCleanup(patch_service_info.stop) 43 | self.mock_service_info = patch_service_info.start() 44 | 45 | patch_inet_aton = patch('pyhap.pyhap.inet_aton') 46 | self.addCleanup(patch_inet_aton.stop) 47 | self.mock_inet_aton = patch_inet_aton.start() 48 | 49 | self.mock_config = Mock() 50 | self.mdns_server = MDNSServer(self.mock_config) 51 | 52 | def test_update_service(self): 53 | self.mock_config.model_name = 'test_model_name' 54 | self.mock_config.service_type = 'test_service_type' 55 | self.mock_config.configuration_number = 500 56 | 57 | self.mdns_server.update_service() 58 | 59 | self.assertEqual(self.mdns_server.hap_service, self.mock_service_info()) 60 | self.assertEqual(self.mock_service_info.call_args_list[0][1]['type_'], 'test_service_type') 61 | self.assertEqual(self.mock_service_info.call_args_list[0][1]['name'], 'test_model_name.test_service_type') 62 | self.assertEqual(self.mock_service_info.call_args_list[0][1]['address'], self.mock_inet_aton()) 63 | self.assertEqual(self.mock_service_info.call_args_list[0][1]['port'], self.mock_config.server_port) 64 | self.assertEqual(self.mock_service_info.call_args_list[0][1]['properties'], { 65 | 'c#': '500', 66 | 'ff': '0', 67 | 'id': self.mock_config.device_id, 68 | 'md': self.mock_config.model_name, 69 | 'pv': '1.0', 70 | 's#': '1', 71 | 'sf': '1', 72 | 'ci': '2', 73 | }) 74 | 75 | def test_restart(self): 76 | self.mdns_server.start() 77 | 78 | self.mock_zeroconf().unregister_service = Mock() 79 | self.mock_zeroconf().register_service = Mock() 80 | self.mdns_server.restart() 81 | 82 | # stop 83 | self.mock_zeroconf().unregister_service.assert_called_with(self.mock_service_info()) 84 | 85 | # start 86 | self.mock_service_info.assert_called() 87 | self.mock_zeroconf().register_service.assert_called_with(self.mock_service_info()) 88 | 89 | def test_close(self): 90 | self.mdns_server.close() 91 | self.mock_zeroconf().close.assert_called() 92 | 93 | 94 | class TestWebServer(TestCase): 95 | def setUp(self): 96 | patch_route = patch('pyhap.pyhap.route') 97 | self.addCleanup(patch_route.stop) 98 | self.mock_route = patch_route.start() 99 | 100 | patch_http_server = patch('pyhap.pyhap.HTTPServer') 101 | self.addCleanup(patch_http_server.stop) 102 | self.mock_http_server = patch_http_server.start() 103 | 104 | def test_init(self): 105 | config = Mock() 106 | accessories = Mock() 107 | 108 | web_server = WebServer(config, accessories) 109 | 110 | self.assertEqual(web_server.http_server, self.mock_http_server()) 111 | 112 | def test_start(self): 113 | config = Mock() 114 | accessories = Mock() 115 | 116 | web_server = WebServer(config, accessories) 117 | web_server.start() 118 | 119 | self.mock_http_server().run.assert_called_once() 120 | -------------------------------------------------------------------------------- /test/test_service.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pyhap.service import ( 4 | AccessoryInformationService, 5 | Service, 6 | ) 7 | 8 | 9 | class TestService(TestCase): 10 | def test_json(self): 11 | service = AccessoryInformationService() 12 | service.instance_id = 5 13 | self.assertEqual(service.__json__(), { 14 | 'type': '3E', 15 | 'iid': 5, 16 | 'characteristics': [] 17 | }) 18 | 19 | def test_abstract(self): 20 | with self.assertRaises(NotImplementedError): 21 | service = Service() 22 | service.service_uuid # pylint: disable=pointless-statement 23 | -------------------------------------------------------------------------------- /test/test_srp.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import patch 3 | 4 | from pyhap import srp 5 | 6 | USERNAME = 'alice' 7 | PASSWORD = 'password123' 8 | TEST_s = bytes.fromhex('BEB25379 D1A8581E B5A72767 3A2441EE') 9 | TEST_A = bytes.fromhex( 10 | 'FAB6F5D2 615D1E32 3512E799 1CC37443 F487DA60 4CA8C923 0FCB04E5 41DCE628' 11 | '0B27CA46 80B0374F 179DC3BD C7553FE6 2459798C 701AD864 A91390A2 8C93B644' 12 | 'ADBF9C00 745B942B 79F9012A 21B9B787 82319D83 A1F83628 66FBD6F4 6BFC0DDB' 13 | '2E1AB6E4 B45A9906 B82E37F0 5D6F97F6 A3EB6E18 2079759C 4F684783 7B62321A' 14 | 'C1B4FA68 641FCB4B B98DD697 A0C73641 385F4BAB 25B79358 4CC39FC8 D48D4BD8' 15 | '67A9A3C1 0F8EA121 70268E34 FE3BBE6F F89998D6 0DA2F3E4 283CBEC1 393D52AF' 16 | '724A5723 0C604E9F BCE583D7 613E6BFF D67596AD 121A8707 EEC46944 95703368' 17 | '6A155F64 4D5C5863 B48F61BD BF19A53E AB6DAD0A 186B8C15 2E5F5D8C AD4B0EF8' 18 | 'AA4EA500 8834C3CD 342E5E0F 167AD045 92CD8BD2 79639398 EF9E114D FAAAB919' 19 | 'E14E8509 89224DDD 98576D79 385D2210 902E9F9B 1F2D86CF A47EE244 635465F7' 20 | '1058421A 0184BE51 DD10CC9D 079E6F16 04E7AA9B 7CF7883C 7D4CE12B 06EBE160' 21 | '81E23F27 A231D184 32D7D1BB 55C28AE2 1FFCF005 F57528D1 5A88881B B3BBB7FE' 22 | ) 23 | TEST_b = bytes.fromhex('E487CB59 D31AC550 471E81F0 0F6928E0 1DDA08E9 74A004F4 9E61F5D1 05284D20') 24 | TEST_B = bytes.fromhex( 25 | '40F57088 A482D4C7 733384FE 0D301FDD CA9080AD 7D4F6FDF 09A01006 C3CB6D56' 26 | '2E41639A E8FA21DE 3B5DBA75 85B27558 9BDB2798 63C56280 7B2B9908 3CD1429C' 27 | 'DBE89E25 BFBD7E3C AD3173B2 E3C5A0B1 74DA6D53 91E6A06E 465F037A 40062548' 28 | '39A56BF7 6DA84B1C 94E0AE20 8576156F E5C140A4 BA4FFC9E 38C3B07B 88845FC6' 29 | 'F7DDDA93 381FE0CA 6084C4CD 2D336E54 51C464CC B6EC65E7 D16E548A 273E8262' 30 | '84AF2559 B6264274 215960FF F47BDD63 D3AFF064 D6137AF7 69661C9D 4FEE4738' 31 | '2603C88E AA098058 1D077584 61B777E4 356DDA58 35198B51 FEEA308D 70F75450' 32 | 'B71675C0 8C7D8302 FD7539DD 1FF2A11C B4258AA7 0D234436 AA42B6A0 615F3F91' 33 | '5D55CC3B 966B2716 B36E4D1A 06CE5E5D 2EA3BEE5 A1270E87 51DA45B6 0B997B0F' 34 | 'FDB0F996 2FEE4F03 BEE780BA 0A845B1D 92714217 83AE6601 A61EA2E3 42E4F2E8' 35 | 'BC935A40 9EAD19F2 21BD1B74 E2964DD1 9FC845F6 0EFC0933 8B60B6B2 56D8CAC8' 36 | '89CCA306 CC370A0B 18C8B886 E95DA0AF 5235FEF4 393020D2 B7F30569 04759042' 37 | ) 38 | TEST_v = bytes.fromhex( 39 | '9B5E0617 01EA7AEB 39CF6E35 19655A85 3CF94C75 CAF2555E F1FAF759 BB79CB47' 40 | '7014E04A 88D68FFC 05323891 D4C205B8 DE81C2F2 03D8FAD1 B24D2C10 9737F1BE' 41 | 'BBD71F91 2447C4A0 3C26B9FA D8EDB3E7 80778E30 2529ED1E E138CCFC 36D4BA31' 42 | '3CC48B14 EA8C22A0 186B222E 655F2DF5 603FD75D F76B3B08 FF895006 9ADD03A7' 43 | '54EE4AE8 8587CCE1 BFDE3679 4DBAE459 2B7B904F 442B041C B17AEBAD 1E3AEBE3' 44 | 'CBE99DE6 5F4BB1FA 00B0E7AF 06863DB5 3B02254E C66E781E 3B62A821 2C86BEB0' 45 | 'D50B5BA6 D0B478D8 C4E9BBCE C2176532 6FBD1405 8D2BBDE2 C33045F0 3873E539' 46 | '48D78B79 4F0790E4 8C36AED6 E880F557 427B2FC0 6DB5E1E2 E1D7E661 AC482D18' 47 | 'E528D729 5EF74372 95FF1A72 D4027717 13F16876 DD050AE5 B7AD53CC B90855C9' 48 | '39566483 58ADFD96 6422F524 98732D68 D1D7FBEF 10D78034 AB8DCB6F 0FCF885C' 49 | 'C2B2EA2C 3E6AC866 09EA058A 9DA8CC63 531DC915 414DF568 B09482DD AC1954DE' 50 | 'C7EB714F 6FF7D44C D5B86F6B D1158109 30637C01 D0F6013B C9740FA2 C633BA89' 51 | ) 52 | TEST_u = bytes.fromhex( 53 | '03AE5F3C 3FA9EFF1 A50D7DBB 8D2F60A1 EA66EA71 2D50AE97 6EE34641 A1CD0E51' 54 | 'C4683DA3 83E8595D 6CB56A15 D5FBC754 3E07FBDD D316217E 01A391A1 8EF06DFF' 55 | ) 56 | TEST_S = bytes.fromhex( 57 | 'F1036FEC D017C823 9C0D5AF7 E0FCF0D4 08B009E3 6411618A 60B23AAB BFC38339' 58 | '72682312 14BAACDC 94CA1C53 F442FB51 C1B027C3 18AE238E 16414D60 D1881B66' 59 | '486ADE10 ED02BA33 D098F6CE 9BCF1BB0 C46CA2C4 7F2F174C 59A9C61E 2560899B' 60 | '83EF6113 1E6FB30B 714F4E43 B735C9FE 6080477C 1B83E409 3E4D456B 9BCA492C' 61 | 'F9339D45 BC42E67C E6C02C24 3E49F5DA 42A869EC 855780E8 4207B8A1 EA6501C4' 62 | '78AAC0DF D3D22614 F531A00D 826B7954 AE8B14A9 85A42931 5E6DD366 4CF47181' 63 | '496A9432 9CDE8005 CAE63C2F 9CA4969B FE840019 24037C44 6559BDBB 9DB9D4DD' 64 | '142FBCD7 5EEF2E16 2C843065 D99E8F05 762C4DB7 ABD9DB20 3D41AC85 A58C05BD' 65 | '4E2DBF82 2A934523 D54E0653 D376CE8B 56DCB452 7DDDC1B9 94DC7509 463A7468' 66 | 'D7F02B1B EB168571 4CE1DD1E 71808A13 7F788847 B7C6B7BF A1364474 B3B7E894' 67 | '78954F6A 8E68D45B 85A88E4E BFEC1336 8EC0891C 3BC86CF5 00978801 78D86135' 68 | 'E7287234 58538858 D715B7B2 47406222 C1019F53 603F0169 52D49710 0858824C' 69 | ) 70 | TEST_K = bytes.fromhex( 71 | '5CBC219D B052138E E1148C71 CD449896 3D682549 CE91CA24 F098468F 06015BEB' 72 | '6AF245C2 093F98C3 651BCA83 AB8CAB2B 580BBF02 184FEFDF 26142F73 DF95AC50' 73 | ) 74 | TEST_M = bytes.fromhex( 75 | '5F7C14AB 57ED0E94 FD1D78C6 B4DD09ED 7E340B7E 05D419A9 FD760F6B 35E523D1' 76 | '310777A1 AE1D2826 F596F3A8 5116CC45 7C7C964D 4F44DED5 559DA818 C88B617F' 77 | ) 78 | TEST_H_AMK = bytes.fromhex( 79 | '2FA0E81F 5CB73B88 FA096427 0F321DD6 41F2227A 5D805C40 F1BFE96A AF6A19FF' 80 | 'CE8E2328 7965A39E AB9D5A02 215F89E1 28177ED2 C4F103E6 55A04553 1BCBF7AD' 81 | ) 82 | 83 | 84 | class TestSrp(TestCase): 85 | def setUp(self): 86 | generate_salt_patch = patch('pyhap.srp.generate_salt', return_value=TEST_s) 87 | generate_private_key_patch = patch('pyhap.srp.generate_private_key', return_value=srp.to_int(TEST_b)) 88 | 89 | with generate_salt_patch, generate_private_key_patch: 90 | self.srp = srp.Srp(USERNAME, PASSWORD) 91 | self.srp.compute_shared_session_key(TEST_A) 92 | 93 | def test_server_password_verifier(self): 94 | self.assertEqual(srp.to_bytes(self.srp.v), TEST_v) 95 | 96 | def test_server_public_key(self): 97 | self.assertEqual(self.srp.public_key, TEST_B) 98 | 99 | def test_random_scrambling_parameter(self): 100 | self.assertEqual(srp.to_bytes(self.srp.u), TEST_u) 101 | 102 | def test_premastered_secret(self): 103 | self.assertEqual(srp.to_bytes(self.srp.S), TEST_S) 104 | 105 | def test_session_key(self): 106 | self.assertEqual(self.srp.session_key, TEST_K) 107 | 108 | def test_session_key_client_proof(self): 109 | self.assertEqual(self.srp.M, TEST_M) 110 | 111 | def test_session_key_server_proof(self): 112 | self.assertEqual(self.srp.session_key_proof, TEST_H_AMK) 113 | 114 | def test_salt(self): 115 | self.assertEqual(self.srp.salt, TEST_s) 116 | 117 | def test_verify_proof(self): 118 | self.assertTrue(self.srp.verify_proof(TEST_M)) 119 | 120 | def test_generate_salt(self): 121 | salt1 = srp.generate_salt() 122 | salt2 = srp.generate_salt() 123 | 124 | self.assertEqual(type(salt1), bytes) 125 | self.assertEqual(len(salt1), 16) 126 | self.assertNotEqual(salt1, salt2) 127 | 128 | def test_generate_private_key(self): 129 | private_key1 = srp.generate_private_key() 130 | private_key2 = srp.generate_private_key() 131 | 132 | self.assertEqual(type(private_key1), int) 133 | self.assertNotEqual(private_key1, private_key2) 134 | -------------------------------------------------------------------------------- /test/test_tlv.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pyhap.tlv import TlvCode, TlvParser, tlv_parser 4 | 5 | 6 | class TestTlv(TestCase): 7 | def test_tlv_decode(self): 8 | data = bytes([ 9 | 0x06, # state 10 | 0x01, # 1 byte value size 11 | 0x03, # M3 12 | 0x01, # identifier 13 | 0x05, # 5 byte value size 14 | 0x68, # ASCII 'h' 15 | 0x65, # ASCII 'e' 16 | 0x6c, # ASCII 'l' 17 | 0x6c, # ASCII 'l' 18 | 0x6f, # ASCII 'o' 19 | ]) 20 | 21 | result = tlv_parser.decode(data)[0] 22 | expected_result = { 23 | TlvCode.state: 3, 24 | TlvCode.identifier: 'hello' 25 | } 26 | self.assertEqual(result, expected_result) 27 | 28 | with self.assertRaises(ValueError): 29 | tlv_parser.decode(bytes([ 30 | 0xfa, # unknown TlvCode 31 | ])) 32 | 33 | with self.assertRaises(ValueError): 34 | tlv_parser.decode(bytes([ 35 | 0x01, # identifier (string type) 36 | 0x01, # 1 byte value size 37 | 0xf0, # invalid unicode symbol 38 | ])) 39 | 40 | with self.assertRaises(ValueError): 41 | tlv_parser.decode(bytes([ 42 | 0x00, # method (integer type) 43 | 0x02, # 2 byte value size 44 | 0x00, # first integer byte 45 | 0x00, # second integer byte (only 1-byte length integers is supported) 46 | ])) 47 | 48 | def test_tlv_decode_merge(self): 49 | data = [ 50 | 0x06, # state 51 | 0x01, # 1 byte value size 52 | 0x03, # M3 53 | 0x09, # certificate 54 | 0xff, # 255 byte value size 55 | 0x61, # ASCII 'a' 56 | ] 57 | data.extend([0x61] * 254) # 254 more bytes containing 0x61 (ASCII 'a') 58 | data.extend([ 59 | 0x09, # certificate, continuation of previous TLV 60 | 0x2d, # 45 byte value size 61 | 0x61, # ASCII 'a' 62 | ]) 63 | data.extend([0x61] * 44) # 44 more bytes containing 0x61 (ASCII 'a') 64 | data.extend([ 65 | 0x01, # identifier, new TLV item 66 | 0x05, # 5 byte value size 67 | 0x68, # ASCII 'h' 68 | 0x65, # ASCII 'e' 69 | 0x6c, # ASCII 'l' 70 | 0x6c, # ASCII 'l' 71 | 0x6f, # ASCII 'o' 72 | ]) 73 | 74 | result = tlv_parser.decode(bytes(data))[0] 75 | expected_result = { 76 | TlvCode.state: 3, 77 | TlvCode.certificate: b'a'*300, 78 | TlvCode.identifier: 'hello' 79 | } 80 | self.assertEqual(result, expected_result) 81 | 82 | def test_tlv_decode_separated(self): 83 | data = bytes([ 84 | 0x01, # identifier 85 | 0x05, # 5 byte value size 86 | 0x68, # ASCII 'h' 87 | 0x65, # ASCII 'e' 88 | 0x6c, # ASCII 'l' 89 | 0x6c, # ASCII 'l' 90 | 0x6f, # ASCII 'o' 91 | 0x0b, # permissions 92 | 0x01, # 1 byte value size 93 | 0x00, # user permission 94 | 0xff, # separator 95 | 0x00, # 0 byte value size 96 | 0x01, # identifier 97 | 0x05, # 5 byte value size 98 | 0x77, # ASCII 'w' 99 | 0x6f, # ASCII 'o' 100 | 0x72, # ASCII 'r' 101 | 0x6c, # ASCII 'l' 102 | 0x64, # ASCII 'd' 103 | 0x0b, # permissions 104 | 0x01, # 1 byte value size 105 | 0x01, # admin permission 106 | ]) 107 | 108 | result = tlv_parser.decode(data) 109 | expected_result = [{ 110 | TlvCode.identifier: 'hello', 111 | TlvCode.permissions: 0 112 | }, { 113 | TlvCode.identifier: 'world', 114 | TlvCode.permissions: 1 115 | }] 116 | self.assertEqual(result, expected_result) 117 | 118 | def test_tlv_encode(self): 119 | data = [{ 120 | TlvCode.state: 3, 121 | TlvCode.identifier: 'hello', 122 | }] 123 | 124 | result = tlv_parser.encode(data) 125 | expected_result = bytes([ 126 | 0x06, # state 127 | 0x01, # 1 byte value size 128 | 0x03, # M3 129 | 0x01, # identifier 130 | 0x05, # 5 byte value size 131 | 0x68, # ASCII 'h' 132 | 0x65, # ASCII 'e' 133 | 0x6c, # ASCII 'l' 134 | 0x6c, # ASCII 'l' 135 | 0x6f, # ASCII 'o' 136 | ]) 137 | self.assertEqual(result, expected_result) 138 | 139 | def test_tlv_encode_merge(self): 140 | data = [{ 141 | TlvCode.state: 3, 142 | TlvCode.certificate: b'a'*300, 143 | TlvCode.identifier: 'hello', 144 | }] 145 | 146 | result = tlv_parser.encode(data) 147 | expected_result = [ 148 | 0x06, # state 149 | 0x01, # 1 byte value size 150 | 0x03, # M3 151 | 0x09, # certificate 152 | 0xff, # 255 byte value size 153 | 0x61, # ASCII 'a' 154 | ] 155 | expected_result.extend([0x61] * 254) # 254 more bytes containing 0x61 (ASCII 'a') 156 | expected_result.extend([ 157 | 0x09, # certificate, continuation of previous TLV 158 | 0x2d, # 45 byte value size 159 | 0x61, # ASCII 'a' 160 | ]) 161 | expected_result.extend([0x61] * 44) # 44 more bytes containing 0x61 (ASCII 'a') 162 | expected_result.extend([ 163 | 0x01, # identifier, new TLV item 164 | 0x05, # 5 byte value size 165 | 0x68, # ASCII 'h' 166 | 0x65, # ASCII 'e' 167 | 0x6c, # ASCII 'l' 168 | 0x6c, # ASCII 'l' 169 | 0x6f, # ASCII 'o' 170 | ]) 171 | self.assertEqual(result, bytes(expected_result)) 172 | 173 | def test_tlv_encode_separated(self): 174 | data = [{ 175 | TlvCode.identifier: 'hello', 176 | TlvCode.permissions: 0 177 | }, { 178 | TlvCode.identifier: 'world', 179 | TlvCode.permissions: 1 180 | }] 181 | 182 | result = tlv_parser.encode(data) 183 | expected_result = bytes([ 184 | 0x01, # identifier 185 | 0x05, # 5 byte value size 186 | 0x68, # ASCII 'h' 187 | 0x65, # ASCII 'e' 188 | 0x6c, # ASCII 'l' 189 | 0x6c, # ASCII 'l' 190 | 0x6f, # ASCII 'o' 191 | 0x0b, # permissions 192 | 0x01, # 1 byte value size 193 | 0x00, # user permission 194 | 0xff, # separator 195 | 0x00, # 0 bytes value size 196 | 0x01, # identifier 197 | 0x05, # 5 byte value size 198 | 0x77, # ASCII 'w' 199 | 0x6f, # ASCII 'o' 200 | 0x72, # ASCII 'r' 201 | 0x6c, # ASCII 'l' 202 | 0x64, # ASCII 'd' 203 | 0x0b, # permissions 204 | 0x01, # 1 byte value size 205 | 0x01, # admin permission 206 | ]) 207 | self.assertEqual(result, expected_result) 208 | 209 | def test_get_by_code(self): 210 | self.assertEqual(TlvParser.get_by_code(0), TlvCode.method) 211 | self.assertEqual(TlvParser.get_by_code(255), TlvCode.separator) 212 | 213 | with self.assertRaises(ValueError): 214 | TlvParser.get_by_code(250) 215 | 216 | def test_get_by_name(self): 217 | self.assertEqual(TlvParser.get_by_name('method'), TlvCode.method) 218 | self.assertEqual(TlvParser.get_by_name('separator'), TlvCode.separator) 219 | 220 | with self.assertRaises(ValueError): 221 | TlvParser.get_by_name('test') 222 | 223 | def test_get_name(self): 224 | self.assertEqual(TlvParser.get_name(0), 'method') 225 | self.assertEqual(TlvParser.get_name(255), 'separator') 226 | 227 | with self.assertRaises(ValueError): 228 | TlvParser.get_name(250) # code 250 is not presented in TlvCode 229 | 230 | def test_get_type(self): 231 | self.assertEqual(tlv_parser.get_type(TlvCode.method), int) 232 | self.assertEqual(tlv_parser.get_type(TlvCode.identifier), str) 233 | 234 | with self.assertRaises(ValueError): 235 | tlv_parser.get_type(TlvCode.separator) # TlvCode.separator type is None 236 | 237 | with self.assertRaises(ValueError): 238 | tlv_parser.get_type(0) # any wrong data 239 | -------------------------------------------------------------------------------- /test/test_util.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from unittest import TestCase 4 | from uuid import UUID 5 | 6 | from pyhap import util 7 | 8 | 9 | TEST_SERIAL_NUMBER_HASH = 'C879FBCC3F07' 10 | DEVICE_ID_REGEX = re.compile(r'^([0-9A-Z]{2}:){7}[0-9A-Z]{2}$') 11 | SINGING_KEY_REGEX = re.compile(r'^[0-9a-z]{64}$') 12 | SETUP_CODE_REGEX = re.compile(r'^\d\d\d-\d\d-\d\d\d$') 13 | 14 | 15 | class TestUtil(TestCase): 16 | def test_generate_device_id(self): 17 | self.assertRegex(util.generate_device_id(), DEVICE_ID_REGEX) 18 | 19 | def test_generate_signing_key(self): 20 | self.assertRegex(util.generate_signing_key(), SINGING_KEY_REGEX) 21 | 22 | def test_generate_setup_code(self): 23 | self.assertRegex(util.generate_setup_code(), SETUP_CODE_REGEX) 24 | 25 | def test_uuid_to_aduuid(self): 26 | self.assertEqual(util.uuid_to_aduuid(UUID('00000001-0000-1000-8000-0026BB765291')), '1') 27 | self.assertEqual(util.uuid_to_aduuid(UUID('00000F25-0000-1000-8000-0026BB765291')), 'F25') 28 | self.assertEqual(util.uuid_to_aduuid(UUID('0000BBAB-0000-1000-8000-0026BB765291')), 'BBAB') 29 | self.assertEqual(util.uuid_to_aduuid(UUID('010004FF-0000-1000-8000-0026BB765291')), '10004FF') 30 | self.assertEqual(util.uuid_to_aduuid(UUID('FF000000-0000-1000-8000-0026BB765291')), 'FF000000') 31 | 32 | random_uuid4 = '69815C8D-2B70-450B-8024-EECE4CC8CE04' 33 | self.assertEqual(util.uuid_to_aduuid(UUID(random_uuid4)), random_uuid4) 34 | 35 | def test_aduuid_to_uuid(self): 36 | self.assertEqual(util.aduuid_to_uuid('1'), UUID('00000001-0000-1000-8000-0026BB765291')) 37 | self.assertEqual(util.aduuid_to_uuid('F25'), UUID('00000F25-0000-1000-8000-0026BB765291')) 38 | self.assertEqual(util.aduuid_to_uuid('BBAB'), UUID('0000BBAB-0000-1000-8000-0026BB765291')) 39 | self.assertEqual(util.aduuid_to_uuid('10004FF'), UUID('010004FF-0000-1000-8000-0026BB765291')) 40 | self.assertEqual(util.aduuid_to_uuid('FF000000'), UUID('FF000000-0000-1000-8000-0026BB765291')) 41 | 42 | random_uuid4 = '69815C8D-2B70-450B-8024-EECE4CC8CE04' 43 | self.assertEqual(util.aduuid_to_uuid(random_uuid4), UUID(random_uuid4)) 44 | 45 | def test_serial_number_hash(self): 46 | self.assertEqual(util.serial_number_hash('test'), TEST_SERIAL_NUMBER_HASH) 47 | 48 | def test_custom_json_encoder(self): 49 | test_class = CustomJsonEncoderClass() 50 | self.assertEqual(json.dumps(test_class, cls=util.CustomJSONEncoder), '{"test_key": "test_value"}') 51 | 52 | with self.assertRaises(TypeError): 53 | test_normal_class = CustomJsonEncoderNormalClass() 54 | json.dumps(test_normal_class, cls=util.CustomJSONEncoder) 55 | 56 | def test_hs_to_rgb(self): 57 | self.assertEqual(util.hs_to_rgb(0, 0), (255, 255, 255)) 58 | self.assertEqual(util.hs_to_rgb(0, 100), (255, 0, 0)) 59 | self.assertEqual(util.hs_to_rgb(359, 0), (255, 255, 255)) 60 | self.assertEqual(util.hs_to_rgb(359, 100), (255, 0, 4)) 61 | 62 | self.assertEqual(util.hs_to_rgb(342, 60), (255, 102, 148)) 63 | self.assertEqual(util.hs_to_rgb(243, 51), (131, 125, 255)) 64 | self.assertEqual(util.hs_to_rgb(132, 46), (138, 255, 161)) 65 | self.assertEqual(util.hs_to_rgb(48, 56), (255, 226, 112)) 66 | 67 | # degrees over 359 68 | self.assertEqual(util.hs_to_rgb(0, 80), util.hs_to_rgb(360, 80)) 69 | self.assertEqual(util.hs_to_rgb(10, 80), util.hs_to_rgb(370, 80)) 70 | 71 | 72 | class CustomJsonEncoderClass: 73 | @staticmethod 74 | def __json__(): 75 | return {'test_key': 'test_value'} 76 | 77 | 78 | class CustomJsonEncoderNormalClass: 79 | def __init__(self): 80 | self.test_key = 'test_value' 81 | --------------------------------------------------------------------------------