Пример #1
0
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)
Пример #2
0
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)
Пример #3
0
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