END_RESPONSE = b'\x0d\x0a' class Application(IntEnum):
    TEMPERATURE = 0x19
    LIGHTING_FIRST = LIGHTING_30 = 0x30
    LIGHTING_31 = 0x31
    LIGHTING_32 = 0x32
    LIGHTING_33 = 0x33
    LIGHTING_34 = 0x34
    LIGHTING_35 = 0x35
    LIGHTING_36 = 0x36
    LIGHTING_37 = 0x37
    LIGHTING = LIGHTING_38 = 0x38
    LIGHTING_39 = 0x39
    LIGHTING_3a = 0x3a
    LIGHTING_3b = 0x3b
    LIGHTING_3c = 0x3c
    LIGHTING_3d = 0x3d
    LIGHTING_3e = 0x3e
    LIGHTING_3f = 0x3f
    LIGHTING_40 = 0x40
    LIGHTING_41 = 0x41
    LIGHTING_42 = 0x42
    LIGHTING_43 = 0x43
    LIGHTING_44 = 0x44
    LIGHTING_45 = 0x45
    LIGHTING_46 = 0x46
    LIGHTING_47 = 0x47
    LIGHTING_48 = 0x48
    LIGHTING_49 = 0x49
    LIGHTING_4a = 0x4a
    LIGHTING_4b = 0x4b
    LIGHTING_4c = 0x4c
    LIGHTING_4d = 0x4d
    LIGHTING_4e = 0x4e
    LIGHTING_4f = 0x4f
    LIGHTING_50 = 0x50
    LIGHTING_51 = 0x51
    LIGHTING_52 = 0x52
    LIGHTING_53 = 0x53
    LIGHTING_54 = 0x54
    LIGHTING_55 = 0x55
    LIGHTING_56 = 0x56
    LIGHTING_57 = 0x57
    LIGHTING_58 = 0x58
    LIGHTING_59 = 0x59
    LIGHTING_5a = 0x5a
    LIGHTING_5b = 0x5b
    LIGHTING_5c = 0x5c
    LIGHTING_5d = 0x5d
    LIGHTING_5e = 0x5e
    LIGHTING_LAST = LIGHTING_5f = 0x5f
    CLOCK = 0xDF
    ENABLE = 0xCB
    MASTER_APPLICATION = STATUS_REQUEST = 0xff class GroupState(IntEnum):
    MISSING = 0x00
    ON = 0x01
    OFF = 0x02
    ERROR = 0x03 class ClockSAL(SAL, abc.ABC):
    """
    Base type for clock and timekeeping application SALs.
    """

    @property
    def application(self) -> Application:
        return Application.CLOCK

    @staticmethod
    def decode_sals(data: bytes) -> Sequence[ClockSAL]:
        """
        Decodes a clock broadcast application packet and returns it's SAL(s).

        :param data: SAL data to be parsed.
        :returns: The SAL messages contained within the given data.
        :rtype: list of cbus.protocol.application.clock.ClockSAL

        """
        output = []

        while data:
            # parse the data
            command_code = data[0]
            data = data[1:]

            if (command_code & 0x80) == 0x80:
                warnings.warn(
                    'Got unknown clock command {:x}; long form is not '
                    'used.'.format(command_code), UserWarning)
                break

            if (command_code & 0xE0) != 0:
                warnings.warn(
                    'Got unknown clock command {:x}; don\'t know how to '
                    'process the other bits.'.format(command_code),
                    UserWarning)
                break

            if (command_code & 0xf8) == ClockCommand.UPDATE_NETWORK_VARIABLE:
                sal, data = ClockUpdateSAL.decode(data, command_code)
            elif command_code == ClockCommand.REQUEST_REFRESH:
                sal, data = ClockRequestSAL.decode(data)
            else:
                # unknown
                warnings.warn(
                    'Got unknown clock command {:x}; last stage '
                    'dropout'.format(command_code), UserWarning)
                break

            if sal:
                output.append(sal)
        return output class ClockUpdateSAL(ClockSAL):
    """
    Clock update event SAL.

    Informs the network of the current time.

    """

    @property
    def is_date(self):
        return isinstance(self.val, date)

    @property
    def is_time(self):
        return isinstance(self.val, time)

    def __init__(self, val: Union[date, time]):
        """
        Creates a new SAL Clock update message.

        Use ``clock_update_sal(val)`` instead of this constructor, as that
        method handles ``datetime.datetime`` objects (in addition to
        ``datetime.date`` and ``datetime.time``, always returns
        ``Sequence[ClockUpdateSAL]``.

        :param val: The value of that variable. Dates are represented in
                    native date format, and times are represented in native
                    time format.
        """
        super(ClockUpdateSAL, self).__init__()
        self.val = val

    @classmethod
    def decode(cls, data: bytes,
               command_code: int) -> Tuple[Optional[ClockSAL], bytes]:
        """
        Do not call this method directly -- use ClockSAL.decode
        """

        variable = data[0]
        data_length = command_code & 0x07  # len(variable name + value)
        val = data[1:data_length]
        data = data[data_length:]

        if variable == ClockAttribute.DATE:
            # date (23.5.1.2)
            # length must be 0x06
            if data_length != 0x06:
                warnings.warn(
                    f'Ignoring date variable with length {data_length} '
                    f'(expected 6)', UserWarning)
                return None, data

            # now decode the date
            year, month, day, dow = unpack('>HBBB', val)
            # note: dow / day of week is ignored

            return cls(date(year, month, day)), data

        elif variable == ClockAttribute.TIME:
            # time (23.5.1.1)
            # length must be 0x05
            if data_length != 0x05:
                warnings.warn(
                    f'Ignoring date variable with length {data_length} '
                    f'(expected 5)', UserWarning)
                return None, data

            # now decode the time
            hour, minute, second, dst = unpack('>BBBB', val)
            # note: dst / daylight savings flag is ignored

            return cls(time(hour, minute, second)), data
        else:
            warnings.warn(
                f'Tried to decode unknown clock update variable {variable:x}',
                UserWarning)

            # attempt to skip the bad data and recover
            return None, data[data_length:]

    def encode(self) -> bytes:
        if isinstance(self.val, time):
            # time
            # TODO: implement DST flag
            val = pack(
                '>BBBB', self.val.hour, self.val.minute, self.val.second, 255)
            attr = ClockAttribute.TIME
        elif isinstance(self.val, date):
            # date
            val = pack(
                '>HBBB', self.val.year, self.val.month, self.val.day,
                self.val.weekday())
            attr = ClockAttribute.DATE
        else:
            # unknown
            raise TypeError(f"Don't know how to pack clock variable {self.val}")

        return super().encode() + bytes([
            0x08 | (len(val) + 1),
            attr]) + val

    def __repr__(self):
        return f'{self.__class__.__name__}(val={self.val!r})' class ClockRequestSAL(ClockSAL):
    """
    Clock request event SAL.

    Requests network time.

    """

    def __init__(self):
        """
        Creates a new SAL Clock request message.
        """
        super(ClockRequestSAL, self).__init__()

    @classmethod
    def decode(cls, data: bytes) -> Tuple[Optional[ClockSAL], bytes]:
        """
        Do not call this method directly -- use ClockSAL.decode
        """

        argument = byte2int(data)
        data = data[1:]

        if argument != 0x03:
            warnings.warn(
                'Request refresh argument != 3 (got %d instead)' % argument,
                UserWarning)
            return None, data

        return cls(), data

    def encode(self) -> bytes:
        return super().encode() + bytes([
            ClockCommand.REQUEST_REFRESH, 0x03]) class LightingSAL(SAL, abc.ABC):
    """
    Base type for lighting application SALs.
    """

    def __init__(self, group_address: int):
        """
        This should not be called directly by your code!

        Use one of the subclasses of cbus.protocol.lighting.LightingSAL
        instead.
        """
        check_ga(group_address)
        self.group_address = group_address

    @property
    def application(self) -> Union[int, Application]:
        # TODO: Support other application IDs
        return Application.LIGHTING

    @staticmethod
    def decode_sals(data: bytes) -> List[LightingSAL]:
        """
        Decodes a lighting application packet and returns it's SAL(s).

        :param data: SAL data to be parsed.

        :returns: The SAL messages contained within the given data.
        :rtype: list of cbus.protocol.application.lighting.LightingSAL

        """
        output = []

        while data:
            # parse the data

            if len(data) < 2:
                # not enough data to go on.
                warnings.warn(
                    'Got 1 byte of stray SAL for lighting application '
                    '(malformed packet): {}'.format(data), UserWarning)
                break

            command_code = data[0]
            group_address = data[1]
            data = data[2:]

            if command_code not in _SAL_HANDLERS:
                warnings.warn(
                    'Got unknown lighting command {}, stopping processing '
                    'prematurely'.format(command_code), UserWarning)
                break

            sal, data = _SAL_HANDLERS[command_code].decode(
                data, command_code, group_address)

            if sal:
                output.append(sal)
        return output

    @abc.abstractmethod
    def encode(self) -> bytes:
        """
        Encodes the SAL into a format for sending over the C-Bus network.
        """
        check_ga(self.group_address)
        return bytes()

    @staticmethod
    @abc.abstractmethod
    def decode(data: bytes, command_code: int,
               group_address: int) -> Tuple[Optional[LightingSAL], bytes]:
        raise NotImplementedError('decode') class LightingRampSAL(LightingSAL):
    """
    Lighting Ramp (fade) event SAL

    Instructs the given group address to fade to a lighting level (brightness)
    over a given duration.

    """

    def __init__(self, group_address: int, duration: int, level: int):
        """
        Creates a new SAL Lighting Ramp message.

        :param group_address: The group address to ramp.
        :type group_address: int

        :param duration: The duration to ramp over, in seconds.
        :type duration: int

        :param level: The level to ramp to, with 0 indicating off, and 255
                      indicating full brightness.
        :type level: int
        """
        super().__init__(group_address=group_address)

        self.duration = duration
        self.level = level

    @staticmethod
    def decode(data: bytes, command_code: int,
               group_address: int) -> Tuple[Optional[LightingSAL], bytes]:
        """
        Do not call this method directly -- use LightingSAL.decode
        """
        duration = ramp_rate_to_duration(command_code)

        if not data:
            warnings.warn(
                'Couldn\'t get level for LightingRampSAL, no more data.',
                UserWarning)
            return None, data

        level = data[0]
        data = data[1:]

        return LightingRampSAL(
            group_address=group_address, duration=duration, level=level), data

    def encode(self) -> bytes:
        if self.level < 0 or self.level > 255:
            raise ValueError(
                f'Ramp level is out of bounds 0..255 (got {self.level})')

        return super().encode() + bytes([
            duration_to_ramp_rate(self.duration), self.group_address,
            self.level
        ])

    def __repr__(self):
        return (
            '<LightingRampSAL object: group_address={}, duration={}, '
            'level={}>'.format(self.group_address, self.duration, self.level)) class LightingOffSAL(LightingSAL):
    """
    Lighting off event SAL

    Instructs a given group address to turn it's load off.
    """

    @staticmethod
    def decode(data: bytes, command_code: int,
               group_address: int) -> Tuple[LightingOffSAL, bytes]:
        """
        Do not call this method directly -- use LightingSAL.decode
        """
        return LightingOffSAL(group_address), data

    def encode(self):
        return super().encode() + bytes([LightCommand.OFF, self.group_address])

    def __repr__(self):
        return '<LightingOffSAL object: group_address=%r>' % self.group_address class LightingOnSAL(LightingSAL):
    """
    Lighting on event SAL

    Instructs a given group address to turn it's load on.
    """

    @staticmethod
    def decode(data: bytes, command_code: int,
               group_address: int) -> Tuple[LightingOnSAL, bytes]:
        """
        Do not call this method directly -- use LightingSAL.decode
        """
        return LightingOnSAL(group_address), data

    def encode(self):
        return super().encode() + bytes([LightCommand.ON, self.group_address])

    def __repr__(self):
        return '<LightingOnSAL object: group_address=%r>' % self.group_address class LightingTerminateRampSAL(LightingSAL):
    """
    Lighting terminate ramp event SAL

    Instructs the given group address to discontinue any ramp operations in
    progress, and use the brightness that they are currently at.
    """

    @staticmethod
    def decode(data: bytes, command_code: int,
               group_address: int) -> Tuple[LightingTerminateRampSAL, bytes]:
        """
        Do not call this method directly -- use LightingSAL.decode
        """
        return LightingTerminateRampSAL(group_address), data

    def encode(self):
        return super().encode() + bytes([
            LightCommand.TERMINATE_RAMP, self.group_address])

    def __repr__(self):
        return ('<LightingTerminateRampSAL object: '
                'group_address={}>'.format(self.group_address)) class StatusRequestSAL(SAL):
    level_request: bool
    group_address: int
    child_application: int

    @classmethod
    def decode_sals(cls, data: bytes) -> Sequence[StatusRequestSAL]:
        output = []

        while data:
            if data[0] in (0x7a, 0xfa):
                # 0x7a version of the status request (binary on/off states)
                # 0xfa version is deprecated and "shouldn't be used". But it
                # totally still is...
                data = data[1:]
                level_request = False
            elif data.startswith(_LEVEL_REQUEST):
                # 7307 version of the status request (levels, in v4)
                data = data[2:]
                level_request = True
            else:
                raise NotImplementedError(
                    'Unknown status request type 0x{:x}'.format(data[0]))

            if len(data) < 2:
                # not enough data to go on
                warnings.warn(
                    'Got incomplete SAL for status request application'
                    '(malformed packet)', UserWarning)

            # Application that the status request is about
            child_application = data[0]
            group_address = data[1]
            data = data[2:]

            if group_address & 0x1f != 0:
                raise ValueError(
                    'group_address report must be a multiple of 0x20')

            output.append(StatusRequestSAL(
                level_request, group_address, child_application))

        return output

    @property
    def application(self) -> Application:
        return Application.STATUS_REQUEST

    def encode(self) -> bytes:
        child_application = int(self.child_application) & 0xff
        group_address = self.group_address & 0xe0
        level_request = bool(self.level_request)

        if level_request:
            return _LEVEL_REQUEST + bytes([child_application, group_address])
        else:
            return bytes([0x7a, child_application, group_address]) class BasePacket(abc.ABC):
    def __init__(
            self,
            checksum: bool = True,
            destination_address_type:
            DestinationAddressType = DestinationAddressType.UNSET,
            rc: int = 0,
            dp: bool = False,
            priority_class: PriorityClass = PriorityClass.CLASS_4):
        # base packet implementation.
        self.checksum = checksum

        self.destination_address_type = destination_address_type
        self.rc = rc
        self.dp = dp
        self.priority_class = priority_class
        self.confirmation = None
        self.source_address = None

    @property
    def flags(self) -> int:
        return ((self.destination_address_type & 0x07) |
                ((self.rc & 0x02) << 3) |
                (0x20 if self.dp else 0) |
                ((self.priority_class & 0x03) << 6))

    @abc.abstractmethod
    def encode(self) -> bytes:
        if self.source_address is None:
            return bytes([self.flags])
        else:
            source_address = self.source_address & 0xff
            return bytes([self.flags, source_address])

    def encode_packet(self) -> bytes:
        return b16encode(self.encode()) class InvalidPacket(_SpecialPacket):
    """Invalid packet data."""
    payload: bytes
    exception: Optional[Exception] = None

    def encode(self):
        return self.payload class SpecialClientPacket(_SpecialPacket, abc.ABC):
    """
    Client -> PCI communications have some special packets, which we make
    subclasses of SpecialClientPacket to make them entirely separate from
    normal packets.

    These have non-standard methods for serialisation.
    """

    def __repr__(self):
        return f'{self.__class__.__name__}()' class RecallCAL:
    """
    Recall CAL request.

    Ref: Serial Interface Guide, s7.1

    """
    param: int
    count: int

    @classmethod
    def decode_cal(cls, data: bytes) -> Tuple[RecallCAL, int]:
        """
        Decodes identify SAL.
        """

        return RecallCAL(data[1], data[2]), 3

    def encode(self) -> bytes:
        return bytes([CAL.RECALL, self.param & 0xff, self.count & 0xff]) class BinaryStatusReport(StatusReport, Sequence[GroupState]):
    _group_states: Sequence[GroupState]

    def __getitem__(self, i: int) -> GroupState:
        return self._group_states[i]

    def __len__(self) -> int:
        return len(self._group_states)

    def __iter__(self) -> Iterator[GroupState]:
        return iter(self._group_states)

    @classmethod
    def decode(cls, data: bytes) -> BinaryStatusReport:
        states = []
        for c in data:
            states += [
                GroupState(c & 0x3),
                GroupState((c >> 2) & 0x03),
                GroupState((c >> 4) & 0x03),
                GroupState((c >> 6) & 0x03)]

        return BinaryStatusReport(states)

    @property
    def block_type(self) -> ExtendedCALType:
        return ExtendedCALType.BINARY

    def encode(self) -> bytes:
        group_states = list(self._group_states)
        r = len(group_states) % 4
        if r != 0:
            # add padding
            group_states += [GroupState.MISSING] * (4 - r)

        # iterate 4 groups at a time
        o = []
        for p in range(0, len(group_states), 4):
            o.append(group_states[p] |
                     (group_states[p + 1] << 2) |
                     (group_states[p + 2] << 4) |
                     (group_states[p + 3] << 6))

        return bytes(o) class StandardCAL(SpecialServerPacket):
    """

    """
    child_application: Union[Application, int]
    block_start: int
    report: BinaryStatusReport

    # Only used if encoding packets
    checksum: bool = True

    def encode(self) -> bytes:
        report = self.report.encode()

        return bytes([
            CAL.STANDARD_STATUS | (len(report) + 3),
            self.child_application & 0xff,
            self.block_start & 0xff]) + report

    def encode_packet(self) -> bytes:
        """
        Standard CALs can be also sent as a packet without headers directly
        onto the network.

        This adds a checksum (if checksum=True) and base16 encodes the CAL
        like it was a normal packet.
        """
        # checksum it, if needed.
        p = self.encode()

        if self.checksum:
            p = add_cbus_checksum(p)

        return b16encode(p)

    @classmethod
    def decode_cal(cls, data: bytes) -> StandardCAL:
        # expects a standard status cal, cropped to exact size, starting
        # from the application byte
        child_application = data[0]
        block_start = data[1]
        payload = data[2:]

        report = BinaryStatusReport.decode(payload)

        return StandardCAL(child_application, block_start, report) class CBusProtocol(BufferedProtocol, abc.ABC):
    """
    A CBus protocol handler.

    """

    def __init__(self, emulate_pci: bool = True):
        """
        :param emulate_pci: If True, this protocol handler will implement
            some of the low-level primitives to emulate a PCI; all
            incoming data will be parsed as if it were sent from software
            expecting to communicate with an actual PCI, and it will emulate
            local echo. Checksums are not required by default.

            If False, this protocol handler will act as if it is
            communicating _with_ a PCI; incoming data will be parsed as if it
            were sent by a PCI. Checksums are required by default.
        """
        super(CBusProtocol, self).__init__(size_limit=MAX_BUFFER_SIZE)
        self.emulate_pci = bool(emulate_pci)  # type: bool
        self.checksum = not self.emulate_pci  # type: bool

    def handle_data(self, buf: bytes) -> int:
        """
        Decodes a single CBus event and calls an event handler appropriate to
        the event.

        Do not override this.

        :param buf: CBus event data
        :type buf: bytes

        :returns: Number of bytes consumed from the buffer
        """
        logger.debug("Incoming data: %r", buf)

        p, remainder = decode_packet(
            buf, checksum=self.checksum, from_pci=not self.emulate_pci)

        if self.emulate_pci and remainder > 0:
            # Local echo
            self.echo(buf[:remainder])

        if p is not None:
            logger.debug("Got packet: %s", p)
            self.handle_cbus_packet(p)

        return remainder

    @abc.abstractmethod
    def handle_cbus_packet(self, p: BasePacket) -> None:
        """
        Called when there is a new packet available to be processed.

        :param p: Packet to process.
        :return: ignored
        """
        pass

    def echo(self, data: bytes) -> None:
        """
        Called when data needs to be echoed to the underlying transport.

        This is only called when running in server mode.

        The default implementation is a stub, and needs to be implemented
        when running in server mode. This should only do something if the
        virtual PCI is in ``basic`` mode.
        """
        pass class ConfirmationPacket(SpecialServerPacket):
    """
    Confirmation special packet.  Serial interface guide s4.3.3.3 p32
    """

    def __init__(self, code: bytes, success: bool):
        super(ConfirmationPacket, self).__init__()

        self._code = code[:1]
        if self._code not in CONFIRMATION_CODES:
            raise ValueError('confirmation code is not valid')

        self._success = bool(success)

    def encode(self) -> bytes:
        return self._code + (b'.' if self._success else b'#')

    @property
    def code(self) -> bytes:
        return self._code

    @property
    def success(self) -> bool:
        return self._success

    def __repr__(self):
        return (f'{self.__class__.__name__}('
                f'code={self.code!r}, success={self.success})') class DeviceManagementPacket(BasePacket):
    def __init__(
            self, checksum: bool = True,
            priority_class: PriorityClass = PriorityClass.CLASS_2,
            parameter: int = 0,
            value: int = 0):
        super().__init__(
            checksum=checksum,
            destination_address_type=DestinationAddressType
            .POINT_TO_POINT_TO_MULTIPOINT,
            dp=True,
            priority_class=priority_class,
        )

        self.parameter = parameter
        self.value = value

    @staticmethod
    def decode_packet(data: bytes, checksum: bool,
                      priority_class: PriorityClass) -> DeviceManagementPacket:
        # serial interface guide s10.2
        # A3 pp 00 vv
        # where:
        #  pp = parameter number
        #  vv = value

        parameter = data[0]
        if data[1] != 0:
            raise ValueError('second byte of DeviceManagementPacket must be 0')

        value = data[2]

        if len(data) != 3:
            raise ValueError(
                'Unexpected DeviceManagementPacket payload length')

        return DeviceManagementPacket(
            checksum=checksum, priority_class=priority_class,
            parameter=parameter, value=value)

    def encode(self) -> bytes:
        # encode the remainder
        parameter = self.parameter & 0xff
        value = self.value & 0xff

        p = super().encode() + bytes([parameter, 0, value])

        # checksum it, if needed.
        if self.checksum:
            p = add_cbus_checksum(p)

        return p

    def __repr__(self):
        return (f'{self.__class__.__name__}(parameter=0x{self.parameter:x}, '
                f'value=0x{self.value:x}, checksum={self.checksum}, '
                f'priority_class={self.priority_class})') class PCIErrorPacket(SpecialServerPacket):

    def __init__(self):
        super(PCIErrorPacket, self).__init__()

    def encode(self) -> bytes:
        return b'!'

    def __repr__(self):
        return f'{self.__class__.__name__}()' class PointToMultipointPacket(BasePacket, Sequence[SAL]):
    """
    Point to Multipoint Packet

    Ref: Serial Interface User Guide, s4.2.9.2
    """

    def __init__(
            self, checksum: bool = True,
            priority_class: PriorityClass = PriorityClass.CLASS_4,
            application: Optional[Application] = None,
            sals: Optional[Union[SAL, Sequence[SAL]]] = None):
        super(PointToMultipointPacket, self).__init__(
            checksum=checksum,
            destination_address_type=DestinationAddressType
            .POINT_TO_MULTIPOINT,
            priority_class=priority_class)
        self.application = application
        self._sals = []

        if isinstance(sals, SAL):
            self.append_sal(sals)
        elif sals:
            for sal in sals:
                self.append_sal(sal)

    def __repr__(self):  # pragma: no cover
        return (
                '<{} object: application={}, source_address={}, '
                'sals={}>'.format(
                    self.__class__.__name__, self.application,
                    self.source_address,
                    self._sals))

    def append_sal(self, sal: SAL) -> None:
        sal_application = int(sal.application)
        if self.application is None:
            self.application = sal_application
        elif self.application != sal_application:
            raise ValueError(
                f'SAL {sal!r} is part of application {sal_application:x}, '
                f'but this Packet has application {self.application:x}')

        self._sals.append(sal)

    def clear_sal(self) -> None:
        """Removes all SALs from this packet."""
        self._sals = []
        self.application = None

    def __len__(self) -> int:
        """Returns the number of SALs associated with this packet."""
        return len(self._sals)

    def __getitem__(self, item: int) -> SAL:
        """Returns the indexed SAL associated with this packet."""
        return self._sals[item]

    def __iter__(self) -> Iterator[SAL]:
        """Returns an iterator over the SALs associated with this packet."""
        return iter(self._sals)

    def index(self, x: SAL, start: int = ..., end: int = ...) -> int:
        """
        Finds a SAL within this packet.

        :raises ValueError: if not present
        """
        return self._sals.index(x, start, end)

    @classmethod
    def decode_packet(
            cls, data: bytes, checksum: bool, priority_class: PriorityClass)\
            -> PointToMultipointPacket:
        application = Application(data[0])
        if data[1] != 0x00:
            raise ValueError('Routing data in PM message?')

        # find an application handler
        handler = get_application(application)
        data = data[2:]
        sals = handler.decode_sals(data)

        return cls(
            checksum=checksum, priority_class=priority_class,
            application=application, sals=sals)

    def encode(self):
        if self.application is None:
            raise ValueError('application must not be None')

        a = int(self.application)
        if a < 0 or a > 0xff:
            raise ValueError('application must be in range 0..255 '
                             '(got {})'.format(a))

        o = bytearray([a, 0])
        for x in self._sals:
            o += x.encode()

        # join the packet
        p = super().encode() + bytes(o)

        # checksum it, if needed.
        if self.checksum:
            p = add_cbus_checksum(p)

        return p class PowerOnPacket(SpecialServerPacket):

    def __init__(self):
        super(PowerOnPacket, self).__init__()

    def encode(self):
        # We send two "Power On" bytes, like C-Bus, because the first one
        # could be lost because of bit errors.
        return b'++' class ResetPacket(SpecialClientPacket):

    def __init__(self):
        super(ResetPacket, self).__init__()

    def encode(self):
        return b'~' class SmartConnectShortcutPacket(SpecialClientPacket):

    def __init__(self):
        super(SmartConnectShortcutPacket, self).__init__()

    def encode(self) -> bytes:
        return b'|'
import asyncio
import logging
import random
from typing import Text
from cbus.common import END_RESPONSE, Application, GroupState
from cbus.protocol.application.clock import (
    ClockSAL, ClockUpdateSAL, ClockRequestSAL)
from cbus.protocol.application.lighting import (
    LightingSAL, LightingRampSAL, LightingOffSAL, LightingOnSAL,
    LightingTerminateRampSAL)
from cbus.protocol.application.status_request import StatusRequestSAL
from cbus.protocol.base_packet import (
    BasePacket, InvalidPacket, SpecialClientPacket)
from cbus.protocol.cal.recall import RecallCAL
from cbus.protocol.cal.report import BinaryStatusReport
from cbus.protocol.cal.standard import StandardCAL
from cbus.protocol.cbus_protocol import CBusProtocol
from cbus.protocol.confirm_packet import ConfirmationPacket
from cbus.protocol.dm_packet import DeviceManagementPacket
from cbus.protocol.error_packet import PCIErrorPacket
from cbus.protocol.pm_packet import PointToMultipointPacket
from cbus.protocol.po_packet import PowerOnPacket
from cbus.protocol.reset_packet import ResetPacket
from cbus.protocol.scs_packet import SmartConnectShortcutPacket
    from argparse import ArgumentParser

    def on_clock_request(self):
        """
        Event called when a clock application "request time" is recieved.
        """
        logger.debug("recv: clock request")

    def on_clock_update(self, val):
        """
        Event called when a clock application "update time" is recieved.

        :param variable: Clock variable to update.
        :type variable: int

        :param val: Clock value
        :type variable: datetime.date or datetime.time
        """
        logger.debug("recv: clock update: %r" % val)

        # DEBUG: randomly trigger lights
        p = PointToMultipointPacket(
            self.checksum, sals=LightingOnSAL(random.randint(1, 100)))
        p.source_address = random.randint(1, 100)

        self._send_later(p)

        p = PointToMultipointPacket(
            self.checksum, sals=LightingOffSAL(random.randint(1, 100)))
        p.source_address = random.randint(1, 100)
        self._send_later(p)

    def on_master_application_status(self, group_address: int) -> None:
        """
        Event for Status Request for the master application.

        This expects a binary status report of the presence of every unit on
        the network.
        :param group_address: Group number to start from
        """
        logger.debug(
            'recv: master application status request from %d', group_address)

        # TODO: implement this dynamically
        states = [GroupState.MISSING] + (
            [GroupState.ON] * 10
        ) + ([GroupState.MISSING] * (0xfe - 12)) + [GroupState.ON]
        assert len(states) >= 0xfe

        if self.basic_mode:
            for x in range(0, 0xff, 0x58):
                self._send(StandardCAL(
                    child_application=Application.MASTER_APPLICATION,
                    block_start=x,
                    report=BinaryStatusReport(states[x:][:0x58]),
                ))
        else:
            raise NotImplementedError('not implemented except basic mode')

    # other things.
    @staticmethod
    def _serialize_packet(cmd: BasePacket) -> bytes:
        nl = True
        if isinstance(cmd, ConfirmationPacket):
            nl = False

        cmd = cmd.encode_packet()
        if nl:
            cmd += END_RESPONSE

        return cmd

    def _send_later(self, cmd: BasePacket) -> None:
        self._send_queue.append(self._serialize_packet(cmd))

    def _send(self, cmd: BasePacket):
        """
        Sends a packet of CBus data.

        """
        cmd = self._serialize_packet(cmd)
        logger.debug("send: %r" % cmd)

        self._transport.write(cmd)

        # pull up the send queue
        send_queue = self._send_queue

        # remove the old one
        self._send_queue = []

        # send it all!
        for m in send_queue:
            self._transport.write(m)

    def send_error(self):
        self._send(PCIErrorPacket())

    def send_confirmation(self, code: bytes, ok: bool = True):
        # if not ok:
        #   raise NotImplementedError, 'ok != true not implemented'

        self._send(ConfirmationPacket(code, ok))

    def lighting_group_on(self, source_addr, group_addr):
        """
        Turns on the lights for the given group_addr.

        :param source_addr: Source address of the event.
        :type source_addr: int

        :param group_addr: Group address to turn the lights on for.
        :type group_addr: int

        :returns: Single-byte string with code for the confirmation event.
        :rtype: string

        """

        p = PointToMultipointPacket(
            checksum=self.checksum,

            sals=LightingOnSAL(group_addr))