class Accessory: """A representation of a HAP accessory. Inherit from this class to build your own accessories. At the end of the init of this class, the _set_services method is called. Use this to set your HAP services. """ category = CATEGORY_OTHER def __init__(self, driver, display_name, aid=None): """Initialise with the given properties. :param display_name: Name to be displayed in the Home app. :type display_name: str :param aid: The accessory ID, uniquely identifying this accessory. `Accessories` that advertised on the network must have the standalone AID. Defaults to None, in which case the `AccessoryDriver` will assign the standalone AID to this `Accessory`. :type aid: int """ self.aid = aid self.display_name = display_name self.driver = driver self.services = [] self.iid_manager = IIDManager() self.add_info_service() self._set_services() def __repr__(self): """Return the representation of the accessory.""" services = [s.display_name for s in self.services] return "<accessory display_name='{}' services={}>" \ .format(self.display_name, services) def __getstate__(self): state = self.__dict__.copy() state['driver'] = None state['run_sentinel'] = None return state def _set_services(self): """Set the services for this accessory. .. deprecated:: 2.0 Initialize the service inside the accessory `init` method instead. """ @property def available(self): """Accessory is available. If available is False, get_characteristics will return SERVICE_COMMUNICATION_FAILURE for the accessory which will show as unavailable. Expected to be overridden. """ return True def add_info_service(self): """Helper method to add the required `AccessoryInformation` service. Called in `__init__` to be sure that it is the first service added. May be overridden. """ serv_info = self.driver.loader.get_service('AccessoryInformation') serv_info.configure_char('Name', value=self.display_name) serv_info.configure_char('SerialNumber', value='default') self.add_service(serv_info) def set_info_service(self, firmware_revision=None, manufacturer=None, model=None, serial_number=None): """Quick assign basic accessory information.""" serv_info = self.get_service('AccessoryInformation') if firmware_revision: serv_info.configure_char('FirmwareRevision', value=firmware_revision) if manufacturer: serv_info.configure_char('Manufacturer', value=manufacturer) if model: serv_info.configure_char('Model', value=model) if serial_number: if len(serial_number) >= 1: serv_info.configure_char('SerialNumber', value=serial_number) else: logger.warning( "Couldn't add SerialNumber for %s. The SerialNumber must " "be at least one character long.", self.display_name) def add_preload_service(self, service, chars=None): """Create a service with the given name and add it to this acc.""" service = self.driver.loader.get_service(service) if chars: chars = chars if isinstance(chars, list) else [chars] for char_name in chars: char = self.driver.loader.get_char(char_name) service.add_characteristic(char) self.add_service(service) return service def set_primary_service(self, primary_service): """Set the primary service of the acc.""" for service in self.services: service.is_primary_service = service.type_id == \ primary_service.type_id def config_changed(self): """Notify the accessory about configuration changes. These include new services or updated characteristic values, e.g. the Name of a service changed. This method also notifies the driver about the change, so that it can publish the changes to the world. .. note:: If you are changing the configuration of a bridged accessory (i.e. an Accessory that is contained in a Bridge), you should call the `config_changed` method on the Bridge. Deprecated. Use `driver.config_changed()` instead. """ logger.warning( 'This method is now deprecated. Use \'driver.config_changed\' instead.' ) self.driver.config_changed() def add_service(self, *servs): """Add the given services to this Accessory. This also assigns unique IIDS to the services and their Characteristics. .. note:: Do not add or remove characteristics from services that have been added to an Accessory, as this will lead to inconsistent IIDs. :param servs: Variable number of services to add to this Accessory. :type: Service """ for s in servs: self.services.append(s) self.iid_manager.assign(s) s.broker = self for c in s.characteristics: self.iid_manager.assign(c) c.broker = self def get_service(self, name): """Return a Service with the given name. A single Service is returned even if more than one Service with the same name are present. :param name: The display_name of the Service to search for. :type name: str :return: A Service with the given name or None if no such service exists in this Accessory. :rtype: Service """ return next((s for s in self.services if s.display_name == name), None) def xhm_uri(self): """Generates the X-HM:// uri (Setup Code URI) :rtype: str """ buffer = bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00') value_low = int(self.driver.state.pincode.replace(b'-', b''), 10) value_low |= 1 << 28 struct.pack_into('>L', buffer, 4, value_low) if self.category == CATEGORY_OTHER: buffer[4] = buffer[4] | 1 << 7 value_high = self.category >> 1 struct.pack_into('>L', buffer, 0, value_high) encoded_payload = base36.dumps( struct.unpack_from('>L', buffer, 4)[0] + (struct.unpack_from('>L', buffer, 0)[0] * (1 << 32))).upper() encoded_payload = encoded_payload.rjust(9, '0') return 'X-HM://' + encoded_payload + self.driver.state.setup_id def get_characteristic(self, aid, iid): """Get the characteristic for the given IID. The AID is used to verify if the search is in the correct accessory. """ if aid != self.aid: return None return self.iid_manager.get_obj(iid) def to_HAP(self): """A HAP representation of this Accessory. :return: A HAP representation of this accessory. For example: .. code-block:: python { "aid": 1, "services": [{ "iid" 2, "type": ..., ... }] } :rtype: dict """ return { HAP_REPR_AID: self.aid, HAP_REPR_SERVICES: [s.to_HAP() for s in self.services], } def setup_message(self): """Print setup message to console. For QRCode `base36`, `pyqrcode` are required. Installation through `pip install HAP-python[QRCode]` """ pincode = self.driver.state.pincode.decode() if SUPPORT_QR_CODE: xhm_uri = self.xhm_uri() print('Setup payload: {}'.format(xhm_uri), flush=True) print('Scan this code with your HomeKit app on your iOS device:', flush=True) print(QRCode(xhm_uri).terminal(quiet_zone=2), flush=True) print('Or enter this code in your HomeKit app on your iOS device: ' '{}'.format(pincode), flush=True) else: print( 'To use the QR Code feature, use \'pip install ' 'HAP-python[QRCode]\'', flush=True) print('Enter this code in your HomeKit app on your iOS device: {}'. format(pincode), flush=True) @staticmethod def run_at_interval(seconds): """Decorator that runs decorated method every x seconds, until stopped. Can be used with normal and async methods. .. code-block:: python @Accessory.run_at_interval(3) def run(self): print("Hello again world!") :param seconds: The amount of seconds to wait for the event to be set. Determines the interval on which the decorated method will be called. :type seconds: float """ def _repeat(func): async def _wrapper(self, *args): while True: await self.driver.async_add_job(func, self, *args) if await util.event_wait(self.driver.aio_stop_event, seconds): break return _wrapper return _repeat async def run(self): """Called when the Accessory should start doing its thing. Called when HAP server is running, advertising is set, etc. Can be overridden with a normal or async method. """ async def stop(self): """Called when the Accessory should stop what is doing and clean up any resources. Can be overridden with a normal or async method. """ # Driver def publish(self, value, sender, sender_client_addr=None): """Append AID and IID of the sender and forward it to the driver. Characteristics call this method to send updates. :param data: Data to publish, usually from a Characteristic. :type data: dict :param sender: The Service or Characteristic from which the call originated. :type: Service or Characteristic """ acc_data = { HAP_REPR_AID: self.aid, HAP_REPR_IID: self.iid_manager.get_iid(sender), HAP_REPR_VALUE: value, } self.driver.publish(acc_data, sender_client_addr)
class Accessory: """A representation of a HAP accessory. Inherit from this class to build your own accessories. """ category = CATEGORY_OTHER def __init__(self, driver, display_name, aid=None): """Initialise with the given properties. :param display_name: Name to be displayed in the Home app. :type display_name: str :param aid: The accessory ID, uniquely identifying this accessory. `Accessories` that advertised on the network must have the standalone AID. Defaults to None, in which case the `AccessoryDriver` will assign the standalone AID to this `Accessory`. :type aid: int """ self.aid = aid self.display_name = display_name self.driver = driver self.services = [] self.iid_manager = IIDManager() self.add_info_service() def __repr__(self): """Return the representation of the accessory.""" services = [s.display_name for s in self.services] return "<accessory display_name='{}' services={}>".format( self.display_name, services) @property def available(self): """Accessory is available. If available is False, get_characteristics will return SERVICE_COMMUNICATION_FAILURE for the accessory which will show as unavailable. Expected to be overridden. """ return True def add_info_service(self): """Helper method to add the required `AccessoryInformation` service. Called in `__init__` to be sure that it is the first service added. May be overridden. """ serv_info = self.driver.loader.get_service("AccessoryInformation") serv_info.configure_char("Name", value=self.display_name) serv_info.configure_char("SerialNumber", value="default") self.add_service(serv_info) def set_info_service(self, firmware_revision=None, manufacturer=None, model=None, serial_number=None): """Quick assign basic accessory information.""" serv_info = self.get_service("AccessoryInformation") if firmware_revision: serv_info.configure_char("FirmwareRevision", value=firmware_revision) if manufacturer: serv_info.configure_char("Manufacturer", value=manufacturer) if model: serv_info.configure_char("Model", value=model) if serial_number is not None: if len(serial_number) >= 1: serv_info.configure_char("SerialNumber", value=serial_number) else: logger.warning( "Couldn't add SerialNumber for %s. The SerialNumber must " "be at least one character long.", self.display_name, ) def add_preload_service(self, service, chars=None): """Create a service with the given name and add it to this acc.""" service = self.driver.loader.get_service(service) if chars: chars = chars if isinstance(chars, list) else [chars] for char_name in chars: char = self.driver.loader.get_char(char_name) service.add_characteristic(char) self.add_service(service) return service def set_primary_service(self, primary_service): """Set the primary service of the acc.""" for service in self.services: service.is_primary_service = service.type_id == primary_service.type_id def add_service(self, *servs): """Add the given services to this Accessory. This also assigns unique IIDS to the services and their Characteristics. .. note:: Do not add or remove characteristics from services that have been added to an Accessory, as this will lead to inconsistent IIDs. :param servs: Variable number of services to add to this Accessory. :type: Service """ for s in servs: self.services.append(s) self.iid_manager.assign(s) s.broker = self for c in s.characteristics: self.iid_manager.assign(c) c.broker = self def get_service(self, name): """Return a Service with the given name. A single Service is returned even if more than one Service with the same name are present. :param name: The display_name of the Service to search for. :type name: str :return: A Service with the given name or None if no such service exists in this Accessory. :rtype: Service """ return next((s for s in self.services if s.display_name == name), None) def xhm_uri(self): """Generates the X-HM:// uri (Setup Code URI) :rtype: str """ payload = 0 payload |= 0 & 0x7 # version payload <<= 4 payload |= 0 & 0xF # reserved bits payload <<= 8 payload |= self.category & 0xFF # category payload <<= 4 payload |= 2 & 0xF # flags payload <<= 27 payload |= (int(self.driver.state.pincode.replace(b"-", b""), 10) & 0x7FFFFFFF) # pincode encoded_payload = base36.dumps(payload).upper() encoded_payload = encoded_payload.rjust(9, "0") return "X-HM://" + encoded_payload + self.driver.state.setup_id def get_characteristic(self, aid, iid): """Get the characteristic for the given IID. The AID is used to verify if the search is in the correct accessory. """ if aid != self.aid: return None return self.iid_manager.get_obj(iid) def to_HAP(self): """A HAP representation of this Accessory. :return: A HAP representation of this accessory. For example: .. code-block:: python { "aid": 1, "services": [{ "iid" 2, "type": ..., ... }] } :rtype: dict """ return { HAP_REPR_AID: self.aid, HAP_REPR_SERVICES: [s.to_HAP() for s in self.services], } def setup_message(self): """Print setup message to console. For QRCode `base36`, `pyqrcode` are required. Installation through `pip install HAP-python[QRCode]` """ pincode = self.driver.state.pincode.decode() if SUPPORT_QR_CODE: xhm_uri = self.xhm_uri() print("Setup payload: {}".format(xhm_uri), flush=True) print("Scan this code with your HomeKit app on your iOS device:", flush=True) print(QRCode(xhm_uri).terminal(quiet_zone=2), flush=True) print( "Or enter this code in your HomeKit app on your iOS device: " "{}".format(pincode), flush=True, ) else: print( "To use the QR Code feature, use 'pip install " "HAP-python[QRCode]'", flush=True, ) print( "Enter this code in your HomeKit app on your iOS device: {}". format(pincode), flush=True, ) @staticmethod def run_at_interval(seconds): """Decorator that runs decorated method every x seconds, until stopped. Can be used with normal and async methods. .. code-block:: python @Accessory.run_at_interval(3) def run(self): print("Hello again world!") :param seconds: The amount of seconds to wait for the event to be set. Determines the interval on which the decorated method will be called. :type seconds: float """ def _repeat(func): async def _wrapper(self, *args): while True: await self.driver.async_add_job(func, self, *args) if await util.event_wait(self.driver.aio_stop_event, seconds): break return _wrapper return _repeat async def run(self): """Called when the Accessory should start doing its thing. Called when HAP server is running, advertising is set, etc. Can be overridden with a normal or async method. """ async def stop(self): """Called when the Accessory should stop what is doing and clean up any resources. Can be overridden with a normal or async method. """ # Driver def publish(self, value, sender, sender_client_addr=None): """Append AID and IID of the sender and forward it to the driver. Characteristics call this method to send updates. :param data: Data to publish, usually from a Characteristic. :type data: dict :param sender: The Service or Characteristic from which the call originated. :type: Service or Characteristic """ acc_data = { HAP_REPR_AID: self.aid, HAP_REPR_IID: self.iid_manager.get_iid(sender), HAP_REPR_VALUE: value, } self.driver.publish(acc_data, sender_client_addr)
def get_iid_manager(): """Return an IIDManager and a mock object for testing.""" obj_a = Mock() iid_manager = IIDManager() iid_manager.assign(obj_a) return iid_manager, obj_a