From 2e5bde9bf8a9f8bfd8a49ae2a70bddf8c643d219 Mon Sep 17 00:00:00 2001 From: Christoph Walcher Date: Sat, 28 Dec 2019 02:21:23 +0100 Subject: [PATCH 1/2] first steps of camera support --- democameraserver.py | 99 +++++++++++++++ homekit/accessoryserver.py | 26 +++- homekit/model/__init__.py | 18 ++- homekit/model/characteristics/__init__.py | 5 +- .../abstract_characteristic.py | 25 +++- .../model/characteristics/microphone_mute.py | 43 +++++++ .../characteristics/rtp_stream/__init__.py | 36 ++++++ .../selected_rtp_stream_configuration.py | 95 ++++++++++++++ .../rtp_stream/setup_endpoints.py | 103 +++++++++++++++ .../rtp_stream/streaming_status.py | 55 ++++++++ .../supported_audio_stream_configuration.py | 81 ++++++++++++ .../rtp_stream/supported_rtp_configuration.py | 46 +++++++ .../supported_video_stream_configuration.py | 101 +++++++++++++++ homekit/model/services/__init__.py | 5 +- homekit/model/services/microphone_service.py | 29 +++++ homekit/model/services/rtpstream_service.py | 118 ++++++++++++++++++ homekit/protocol/tlv.py | 71 ++++++++++- 17 files changed, 944 insertions(+), 12 deletions(-) create mode 100644 democameraserver.py create mode 100644 homekit/model/characteristics/microphone_mute.py create mode 100644 homekit/model/characteristics/rtp_stream/__init__.py create mode 100644 homekit/model/characteristics/rtp_stream/selected_rtp_stream_configuration.py create mode 100644 homekit/model/characteristics/rtp_stream/setup_endpoints.py create mode 100644 homekit/model/characteristics/rtp_stream/streaming_status.py create mode 100644 homekit/model/characteristics/rtp_stream/supported_audio_stream_configuration.py create mode 100644 homekit/model/characteristics/rtp_stream/supported_rtp_configuration.py create mode 100644 homekit/model/characteristics/rtp_stream/supported_video_stream_configuration.py create mode 100644 homekit/model/services/microphone_service.py create mode 100644 homekit/model/services/rtpstream_service.py diff --git a/democameraserver.py b/democameraserver.py new file mode 100644 index 00000000..3a3aa607 --- /dev/null +++ b/democameraserver.py @@ -0,0 +1,99 @@ +from homekit import AccessoryServer +from homekit.model import CameraAccessory, ManagedRTPStreamService, MicrophoneService +from homekit.model.characteristics.rtp_stream.setup_endpoints import Address, IPVersion +from homekit.model.characteristics.rtp_stream.supported_audio_stream_configuration import \ + SupportedAudioStreamConfiguration, AudioCodecConfiguration, AudioCodecType, AudioCodecParameters, BitRate, \ + SampleRate +from homekit.model.characteristics.rtp_stream.supported_rtp_configuration import SupportedRTPConfiguration, \ + CameraSRTPCryptoSuite +from homekit.model.characteristics.rtp_stream.supported_video_stream_configuration import \ + SupportedVideoStreamConfiguration, VideoCodecConfiguration, VideoCodecParameters, H264Profile, H264Level, \ + VideoAttributes + +import subprocess +import base64 + +if __name__ == '__main__': + try: + httpd = AccessoryServer('demoserver.json') + + accessory = CameraAccessory('Testkamera', 'wiomoc', 'Demoserver', '0001', '0.1') + + + # accessory.set_get_image_snapshot_callback( + # lambda f: open('cam-preview.jpg', 'rb').read()) + + class StreamHandler: + def __init__(self, controller_address, srtp_params_video, **_): + self.srtp_params_video = srtp_params_video + self.controller_address = controller_address + self.ffmpeg_process = None + + def on_start(self, attrs): + self.ffmpeg_process = subprocess.Popen( + ['ffmpeg', '-re', + '-f', 'avfoundation', + '-r', '30.000030', '-i', '0:0', '-threads', '0', + '-vcodec', 'libx264', '-an', '-pix_fmt', 'yuv420p', + '-r', str(attrs.attributes.frame_rate), + '-f', 'rawvideo', '-tune', 'zerolatency', '-vf', + f'scale={attrs.attributes.width}:{attrs.attributes.height}', + '-b:v', '300k', '-bufsize', '300k', + '-payload_type', '99', '-ssrc', '32', '-f', 'rtp', + '-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80', + '-srtp_out_params', base64.b64encode( + self.srtp_params_video.master_key + self.srtp_params_video.master_salt).decode('ascii'), + f'srtp://{self.controller_address.ip_address}:{self.controller_address.video_rtp_port}' + f'?rtcpport={self.controller_address.video_rtp_port}&localrtcpport={self.controller_address.video_rtp_port}' + '&pkt_size=1378' + ]) + + def on_end(self): + if self.ffmpeg_process is not None: + self.ffmpeg_process.terminate() + + def get_ssrc(self): + return (32, 32) + + def get_address(self): + return Address(IPVersion.IPV4, httpd.data.ip, self.controller_address.video_rtp_port, + self.controller_address.audio_rtp_port) + + + stream_service = ManagedRTPStreamService( + StreamHandler, + SupportedRTPConfiguration( + [ + CameraSRTPCryptoSuite.AES_CM_128_HMAC_SHA1_80, + ]), + SupportedVideoStreamConfiguration( + VideoCodecConfiguration( + VideoCodecParameters( + [H264Profile.CONSTRAINED_BASELINE_PROFILE, H264Profile.MAIN_PROFILE, H264Profile.HIGH_PROFILE], + [H264Level.L_3_1, H264Level.L_3_2, H264Level.L_4] + ), [ + VideoAttributes(1920, 1080, 30), + VideoAttributes(320, 240, 15), + VideoAttributes(1280, 960, 30), + VideoAttributes(1280, 720, 30), + VideoAttributes(1280, 768, 30), + VideoAttributes(640, 480, 30), + VideoAttributes(640, 360, 30) + ])), + SupportedAudioStreamConfiguration([ + AudioCodecConfiguration(AudioCodecType.OPUS, + AudioCodecParameters(1, BitRate.VARIABLE, SampleRate.KHZ_24)), + AudioCodecConfiguration(AudioCodecType.AAC_ELD, + AudioCodecParameters(1, BitRate.VARIABLE, SampleRate.KHZ_16)) + ], 0)) + accessory.services.append(stream_service) + microphone_service = MicrophoneService() + accessory.services.append(microphone_service) + httpd.accessories.add_accessory(accessory) + + httpd.publish_device() + print('published device and start serving') + httpd.serve_forever() + except KeyboardInterrupt: + print('unpublish device') + httpd.unpublish_device() diff --git a/homekit/accessoryserver.py b/homekit/accessoryserver.py index f833132b..4d595f29 100644 --- a/homekit/accessoryserver.py +++ b/homekit/accessoryserver.py @@ -41,7 +41,7 @@ from homekit.exceptions import ConfigurationError, ConfigLoadingError, ConfigSavingError, FormatError, \ CharacteristicPermissionError, DisconnectedControllerError from homekit.http_impl import HttpStatusCodes -from homekit.model import Accessories, Categories +from homekit.model import Accessories, Categories, CameraAccessory from homekit.model.characteristics import CharacteristicsTypes from homekit.protocol import TLV from homekit.protocol.statuscodes import HapStatusCodes @@ -316,6 +316,9 @@ def __init__(self, request, client_address, server): }, '/pairings': { 'POST': self._post_pairings + }, + '/resource': { + 'POST': self._post_resource } } self.protocol_version = 'HTTP/1.1' @@ -861,6 +864,27 @@ def _post_pair_verify(self): self.send_error(HttpStatusCodes.METHOD_NOT_ALLOWED) + def _post_resource(self): + format = json.loads(self.body) + accessories = self.server.accessories.accessories + + if 'aid' in format: + aid = format['aid'] + accessories = [accessory for accessory in accessories if accessory.aid == aid] + + if len(accessories) != 0 and isinstance(accessories[0], CameraAccessory) and \ + accessories[0].get_image_snapshot_callback is not None: + accessory = accessories[0] + image = accessory.get_image_snapshot_callback(format) + + self.send_response(HttpStatusCodes.OK) + self.send_header('Content-Type', 'image/jpeg') + self.send_header('Content-Length', len(image)) + self.end_headers() + self.wfile.write(image) + else: + self.send_error(HttpStatusCodes.NOT_FOUND) + def _post_pairings(self): d_req = TLV.decode_bytes(self.body) diff --git a/homekit/model/__init__.py b/homekit/model/__init__.py index 706d2168..071f2076 100644 --- a/homekit/model/__init__.py +++ b/homekit/model/__init__.py @@ -15,14 +15,15 @@ # __all__ = [ - 'AccessoryInformationService', 'BHSLightBulbService', 'FanService', 'LightBulbService', 'ThermostatService', - 'Categories', 'CharacteristicPermissions', 'CharacteristicFormats', 'FeatureFlags', 'Accessory' + 'AccessoryInformationService', 'BHSLightBulbService', 'RTPStreamService', 'ManagedRTPStreamService', 'FanService', + 'LightBulbService', 'ThermostatService', 'MicrophoneService', 'Categories', 'CharacteristicPermissions', + 'CharacteristicFormats', 'FeatureFlags', 'Accessory', 'CameraAccessory' ] import json from homekit.model.mixin import ToDictMixin, get_id from homekit.model.services import AccessoryInformationService, LightBulbService, FanService, \ - BHSLightBulbService, ThermostatService + BHSLightBulbService, ThermostatService, RTPStreamService, ManagedRTPStreamService, MicrophoneService from homekit.model.categories import Categories from homekit.model.characteristics import CharacteristicPermissions, CharacteristicFormats from homekit.model.feature_flags import FeatureFlags @@ -66,6 +67,17 @@ def to_accessory_and_service_list(self): return d +# def __init__(self, session_id, ): + +class CameraAccessory(Accessory): + def __init__(self, name, manufacturer, model, serial_number, firmware_revision): + super().__init__(name, manufacturer, model, serial_number, firmware_revision) + self.get_image_snapshot_callback = None + + def set_get_image_snapshot_callback(self, callback): + self.get_image_snapshot_callback = callback + + class Accessories(ToDictMixin): def __init__(self): self.accessories = [] diff --git a/homekit/model/characteristics/__init__.py b/homekit/model/characteristics/__init__.py index 203b58d8..9f187f60 100644 --- a/homekit/model/characteristics/__init__.py +++ b/homekit/model/characteristics/__init__.py @@ -26,7 +26,8 @@ 'SaturationCharacteristicMixin', 'SerialNumberCharacteristic', 'TargetHeatingCoolingStateCharacteristic', 'TargetHeatingCoolingStateCharacteristicMixin', 'TargetTemperatureCharacteristic', 'TargetTemperatureCharacteristicMixin', 'TemperatureDisplayUnitCharacteristic', 'TemperatureDisplayUnitsMixin', - 'VolumeCharacteristic', 'VolumeCharacteristicMixin' + 'VolumeCharacteristic', 'VolumeCharacteristicMixin', 'MicrophoneMuteCharacteristicMixin', + 'MicrophoneMuteCharacteristic' ] from homekit.model.characteristics.characteristic_permissions import CharacteristicPermissions @@ -59,3 +60,5 @@ from homekit.model.characteristics.temperature_display_unit import TemperatureDisplayUnitsMixin, \ TemperatureDisplayUnitCharacteristic from homekit.model.characteristics.volume import VolumeCharacteristic, VolumeCharacteristicMixin +from homekit.model.characteristics.microphone_mute import MicrophoneMuteCharacteristicMixin, \ + MicrophoneMuteCharacteristic diff --git a/homekit/model/characteristics/abstract_characteristic.py b/homekit/model/characteristics/abstract_characteristic.py index a58ed54c..88b09fae 100644 --- a/homekit/model/characteristics/abstract_characteristic.py +++ b/homekit/model/characteristics/abstract_characteristic.py @@ -24,10 +24,11 @@ from homekit.model.characteristics import CharacteristicsTypes, CharacteristicFormats, CharacteristicPermissions from homekit.protocol.statuscodes import HapStatusCodes from homekit.exceptions import CharacteristicPermissionError, FormatError +from homekit.protocol.tlv import TLVItem class AbstractCharacteristic(ToDictMixin): - def __init__(self, iid: int, characteristic_type: str, characteristic_format: str): + def __init__(self, iid: int, characteristic_type: str, characteristic_format: str, characteristic_tlv_type=None): if type(self) is AbstractCharacteristic: raise Exception('AbstractCharacteristic is an abstract class and cannot be instantiated directly') self.type = CharacteristicsTypes.get_uuid(characteristic_type) # page 65, see ServicesTypes @@ -47,6 +48,8 @@ def __init__(self, iid: int, characteristic_type: str, characteristic_format: st self.valid_values = None # array, not required, see page 67, all numeric entries are allowed values self.valid_values_range = None # 2 element array, not required, see page 67 + self.tlv_type = characteristic_tlv_type + self._set_value_callback = None self._get_value_callback = None @@ -118,6 +121,9 @@ def set_value(self, new_val): if len(new_val) > self.maxLen: raise FormatError(HapStatusCodes.INVALID_VALUE) + if self.format == CharacteristicFormats.tlv8 and new_val is not None: + new_val = TLVItem.decode(self.tlv_type, base64.decodebytes(new_val.encode())) + self.value = new_val if self._set_value_callback: self._set_value_callback(new_val) @@ -155,9 +161,15 @@ def get_value(self): """ if CharacteristicPermissions.paired_read not in self.perms: raise CharacteristicPermissionError(HapStatusCodes.CANT_READ_WRITE_ONLY) - if self._get_value_callback: - return self._get_value_callback() - return self.value + + value = self.value + if self._get_value_callback is not None: + value = self._get_value_callback() + + if self.value is not None and self.format == CharacteristicFormats.tlv8: + return base64.b64encode(TLVItem.encode(value)).decode("ascii") + else: + return value def get_value_for_ble(self): value = self.get_value() @@ -200,7 +212,10 @@ def to_accessory_and_service_list(self): 'format': self.format, } if CharacteristicPermissions.paired_read in self.perms: - d['value'] = self.value + if self.value is not None and self.format == CharacteristicFormats.tlv8: + d['value'] = base64.b64encode(TLVItem.encode(self.value)).decode("ascii") + else: + d['value'] = self.value if self.ev: d['ev'] = self.ev if self.description: diff --git a/homekit/model/characteristics/microphone_mute.py b/homekit/model/characteristics/microphone_mute.py new file mode 100644 index 00000000..e6a0ec4a --- /dev/null +++ b/homekit/model/characteristics/microphone_mute.py @@ -0,0 +1,43 @@ +# +# Copyright 2018 Joachim Lusiardi +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from homekit.model.characteristics import CharacteristicsTypes, CharacteristicFormats, CharacteristicPermissions, \ + AbstractCharacteristic + + +class MicrophoneMuteCharacteristic(AbstractCharacteristic): + """ + Defined on page 157 + """ + + def __init__(self, iid): + AbstractCharacteristic.__init__(self, iid, CharacteristicsTypes.MUTE, CharacteristicFormats.bool) + self.description = 'Mute microphone (on/off)' + self.perms = [CharacteristicPermissions.paired_write, CharacteristicPermissions.paired_read, + CharacteristicPermissions.events] + self.value = False + + +class MicrophoneMuteCharacteristicMixin(object): + def __init__(self, iid): + self._muteCharacteristic = MicrophoneMuteCharacteristic(iid) + self.characteristics.append(self._muteCharacteristic) + + def set_mute_set_callback(self, callback): + self._muteCharacteristic.set_set_value_callback(callback) + + def set_mute_get_callback(self, callback): + self._muteCharacteristic.set_get_value_callback(callback) diff --git a/homekit/model/characteristics/rtp_stream/__init__.py b/homekit/model/characteristics/rtp_stream/__init__.py new file mode 100644 index 00000000..62d3cd24 --- /dev/null +++ b/homekit/model/characteristics/rtp_stream/__init__.py @@ -0,0 +1,36 @@ +# +# Copyright 2018 Joachim Lusiardi +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +__all__ = [ + 'SelectedRTPStreamConfigurationCharacteristicMixin', + 'SelectedRTPStreamConfigurationCharacteristic', 'SetupEndpointsCharacteristicMixin', + 'SetupEndpointsCharacteristic', 'StreamingStatusCharacteristicMixin', + 'StreamingStatusCharacteristic', 'SupportedRTPConfigurationCharacteristic', + 'SupportedVideoStreamConfigurationCharacteristic', 'SupportedAudioStreamConfigurationCharacteristic' +] + +from homekit.model.characteristics.rtp_stream.supported_video_stream_configuration import \ + SupportedVideoStreamConfigurationCharacteristic +from homekit.model.characteristics.rtp_stream.supported_rtp_configuration import \ + SupportedRTPConfigurationCharacteristic +from homekit.model.characteristics.rtp_stream.streaming_status import StreamingStatusCharacteristicMixin, \ + StreamingStatusCharacteristic +from homekit.model.characteristics.rtp_stream.supported_audio_stream_configuration import \ + SupportedAudioStreamConfigurationCharacteristic +from homekit.model.characteristics.rtp_stream.selected_rtp_stream_configuration import \ + SelectedRTPStreamConfigurationCharacteristic, SelectedRTPStreamConfigurationCharacteristicMixin +from homekit.model.characteristics.rtp_stream.setup_endpoints import \ + SetupEndpointsCharacteristic, SetupEndpointsCharacteristicMixin diff --git a/homekit/model/characteristics/rtp_stream/selected_rtp_stream_configuration.py b/homekit/model/characteristics/rtp_stream/selected_rtp_stream_configuration.py new file mode 100644 index 00000000..071debe8 --- /dev/null +++ b/homekit/model/characteristics/rtp_stream/selected_rtp_stream_configuration.py @@ -0,0 +1,95 @@ +# +# Copyright 2018 Joachim Lusiardi +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from enum import IntEnum +from homekit.model.characteristics import CharacteristicsTypes, CharacteristicFormats, CharacteristicPermissions, \ + AbstractCharacteristic +from homekit.model.characteristics.rtp_stream.supported_audio_stream_configuration import AudioCodecType, \ + AudioCodecParameters +from homekit.model.characteristics.rtp_stream.supported_video_stream_configuration import VideoCodecType, \ + VideoCodecParameters, VideoAttributes +from homekit.protocol.tlv import TLVItem + + +class Command(IntEnum): + END = 0 + START = 1 + SUSPEND = 2 + RESUME = 3 + RECONFIGURE = 4 + + +class SessionControl: + id = TLVItem(1, bytes) + command = TLVItem(2, Command) + + +class AudioRTPParameters: + payload_type = TLVItem(1, int) + ssrc = TLVItem(2, int) + maximum_bitrate = TLVItem(3, int) + comfort_noise_type = TLVItem(5, int) + + +class SelectedAudioParameters: + codec_type = TLVItem(1, AudioCodecType) + codec_parameters = TLVItem(2, AudioCodecParameters) + rtp_parameters = TLVItem(3, AudioRTPParameters) + comfort_noise_type = TLVItem(4, int) + + +class VideoRTPParameters: + payload_type = TLVItem(1, int) + ssrc = TLVItem(2, int) + maximum_bitrate = TLVItem(3, int) + maximum_mtu = TLVItem(5, int) + + +class SelectedVideoParameters: + codec_type = TLVItem(1, VideoCodecType) + codec_parameters = TLVItem(2, VideoCodecParameters) + attributes = TLVItem(3, VideoAttributes) + rtp_parameters = TLVItem(4, VideoRTPParameters) + + +class SelectedRTPStreamConfiguration: + session_control = TLVItem(1, SessionControl) + selected_video_parameters = TLVItem(2, SelectedVideoParameters) + selected_audio_parameters = TLVItem(3, SelectedAudioParameters) + + +class SelectedRTPStreamConfigurationCharacteristic(AbstractCharacteristic): + """ + Defined on page 219 + """ + + def __init__(self, iid): + AbstractCharacteristic.__init__(self, iid, CharacteristicsTypes.SELECTED_RTP_STREAM_CONFIGURATION, + CharacteristicFormats.tlv8, SelectedRTPStreamConfiguration) + self.perms = [CharacteristicPermissions.paired_read, CharacteristicPermissions.paired_write] + self.description = 'selected streaming video over an RTP session' + + +class SelectedRTPStreamConfigurationCharacteristicMixin(object): + def __init__(self, iid): + self._selectedRTPStreamConfigurationCharacteristic = SelectedRTPStreamConfigurationCharacteristic(iid) + self.characteristics.append(self._selectedRTPStreamConfigurationCharacteristic) + + def set_selected_rtp_stream_configuration_set_callback(self, callback): + self._selectedRTPStreamConfigurationCharacteristic.set_set_value_callback(callback) + + def set_selected_rtp_stream_configuration_get_callback(self, callback): + self._selectedRTPStreamConfigurationCharacteristic.set_get_value_callback(callback) diff --git a/homekit/model/characteristics/rtp_stream/setup_endpoints.py b/homekit/model/characteristics/rtp_stream/setup_endpoints.py new file mode 100644 index 00000000..689f0c92 --- /dev/null +++ b/homekit/model/characteristics/rtp_stream/setup_endpoints.py @@ -0,0 +1,103 @@ +# +# Copyright 2018 Joachim Lusiardi +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from enum import IntEnum +from homekit.model.characteristics import CharacteristicsTypes, CharacteristicFormats, CharacteristicPermissions, \ + AbstractCharacteristic +from homekit.model.characteristics.rtp_stream.supported_rtp_configuration import CameraSRTPCryptoSuite +from homekit.protocol.tlv import TLVItem + + +class IPVersion(IntEnum): + IPV4 = 0 + IPV6 = 1 + + +class Address: + ip_version = TLVItem(1, IPVersion) + ip_address = TLVItem(2, str) + video_rtp_port = TLVItem(3, int) + audio_rtp_port = TLVItem(4, int) + + def __init__(self, ip_version, ip_address, video_rtp_port, audio_rtp_port): + self.ip_version = ip_version + self.ip_address = ip_address + self.video_rtp_port = video_rtp_port + self.audio_rtp_port = audio_rtp_port + + +class SRTPParameters: + crypto_suite = TLVItem(1, CameraSRTPCryptoSuite) + master_key = TLVItem(2, bytes) + master_salt = TLVItem(3, bytes) + + +class SetupEndpointsRequest: + id = TLVItem(1, bytes) + controller_address = TLVItem(3, Address) + srtp_params_video = TLVItem(4, SRTPParameters) + srtp_params_audio = TLVItem(5, SRTPParameters) + + +class EndpointStatus: + SUCCESS = 0 + BUSY = 1 + ERROR = 2 + + +class SetupEndpointsResponse: + id = TLVItem(1, bytes) + status = TLVItem(2, EndpointStatus) + accessory_address = TLVItem(3, Address) + srtp_params_video = TLVItem(4, SRTPParameters) + srtp_params_audio = TLVItem(5, SRTPParameters) + ssrc_video = TLVItem(6, int) + ssrc_audio = TLVItem(7, int) + + def __init__(self, id, status, accessory_address=None, srtp_params_video=None, srtp_params_audio=None, + ssrc_video=None, ssrc_audio=None): + self.id = id + self.status = status + self.accessory_address = accessory_address + self.srtp_params_video = srtp_params_video + self.srtp_params_audio = srtp_params_audio + self.ssrc_video = ssrc_video + self.ssrc_audio = ssrc_audio + + +class SetupEndpointsCharacteristic(AbstractCharacteristic): + """ + Defined on page 219 + """ + + def __init__(self, iid): + AbstractCharacteristic.__init__(self, iid, CharacteristicsTypes.SETUP_ENDPOINTS, + CharacteristicFormats.tlv8, SetupEndpointsRequest) + self.perms = [CharacteristicPermissions.paired_read, CharacteristicPermissions.paired_write] + self.description = 'setup camera endpoint' + + +class SetupEndpointsCharacteristicMixin(object): + + def __init__(self, iid): + self._setupEndpointsCharacteristic = SetupEndpointsCharacteristic(iid) + self.characteristics.append(self._setupEndpointsCharacteristic) + + def set_setup_endpoints_set_callback(self, callback): + self._setupEndpointsCharacteristic.set_set_value_callback(callback) + + def set_setup_endpoints_get_callback(self, callback): + self._setupEndpointsCharacteristic.set_get_value_callback(callback) diff --git a/homekit/model/characteristics/rtp_stream/streaming_status.py b/homekit/model/characteristics/rtp_stream/streaming_status.py new file mode 100644 index 00000000..c34f3a96 --- /dev/null +++ b/homekit/model/characteristics/rtp_stream/streaming_status.py @@ -0,0 +1,55 @@ +# +# Copyright 2018 Joachim Lusiardi +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from enum import IntEnum +from homekit.model.characteristics import CharacteristicsTypes, CharacteristicFormats, CharacteristicPermissions, \ + AbstractCharacteristic +from homekit.protocol.tlv import TLVItem + + +class StreamingStatusEnum(IntEnum): + AVAILABLE = 0 + IN_USE = 1 + UNAVAILABLE = 2 + + +class StreamingStatus: + status = TLVItem(1, StreamingStatusEnum) + + def __init__(self, status): + self.status = status + + +class StreamingStatusCharacteristic(AbstractCharacteristic): + """ + Defined on page 214 + """ + + def __init__(self, iid): + AbstractCharacteristic.__init__(self, iid, CharacteristicsTypes.STREAMING_STATUS, CharacteristicFormats.tlv8, + StreamingStatus) + self.perms = [CharacteristicPermissions.paired_read, CharacteristicPermissions.events] + self.description = 'status of the stream RTP management service' + self.value = StreamingStatus(StreamingStatusEnum.AVAILABLE) + + +class StreamingStatusCharacteristicMixin(object): + def __init__(self, iid): + self._streamingStatusCharacteristic = StreamingStatusCharacteristic(iid) + self.characteristics.append(self._streamingStatusCharacteristic) + + def set_streaming_status_get_callback(self, callback): + self._streamingStatusCharacteristic.set_get_value_callback(callback) diff --git a/homekit/model/characteristics/rtp_stream/supported_audio_stream_configuration.py b/homekit/model/characteristics/rtp_stream/supported_audio_stream_configuration.py new file mode 100644 index 00000000..6487469f --- /dev/null +++ b/homekit/model/characteristics/rtp_stream/supported_audio_stream_configuration.py @@ -0,0 +1,81 @@ +# +# Copyright 2018 Joachim Lusiardi +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from enum import IntEnum +from homekit.model.characteristics import CharacteristicsTypes, CharacteristicFormats, CharacteristicPermissions, \ + AbstractCharacteristic +from homekit.protocol.tlv import TLVItem + + +class AudioCodecType(IntEnum): + AAC_ELD = 2 + OPUS = 3 + AMR = 5 + AMR_WB = 6 + + +class BitRate(IntEnum): + VARIABLE = 0 + CONSTANT = 1 + + +class SampleRate(IntEnum): + KHZ_8 = 0 + KHZ_16 = 1 + KHZ_24 = 2 + + +class AudioCodecParameters: + channels = TLVItem(1, int) + bitrate = TLVItem(2, BitRate) + samplerate = TLVItem(3, SampleRate) + time = TLVItem(4, int) + + def __init__(self, channels, bitrate, samplerate): + self.channels = channels + self.bitrate = bitrate + self.samplerate = samplerate + + +class AudioCodecConfiguration: + codec_type = TLVItem(1, AudioCodecType) + parameters = TLVItem(2, AudioCodecParameters) + + def __init__(self, codec_type, parameters): + self.codec_type = codec_type + self.parameters = parameters + + +class SupportedAudioStreamConfiguration: + config = TLVItem(1, AudioCodecConfiguration) + comfort_noise_support = TLVItem(2, int) + + def __init__(self, config, comfort_noise_support): + self.config = config + self.comfort_noise_support = comfort_noise_support + + +class SupportedAudioStreamConfigurationCharacteristic(AbstractCharacteristic): + """ + Defined on page 215 + """ + + def __init__(self, iid, value): + AbstractCharacteristic.__init__(self, iid, CharacteristicsTypes.SUPPORTED_AUDIO_CONFIGURATION, + CharacteristicFormats.tlv8, SupportedAudioStreamConfiguration) + self.perms = [CharacteristicPermissions.paired_read] + self.description = 'parameters supported for streaming audio over an RTP session' + self.value = value diff --git a/homekit/model/characteristics/rtp_stream/supported_rtp_configuration.py b/homekit/model/characteristics/rtp_stream/supported_rtp_configuration.py new file mode 100644 index 00000000..98cacde9 --- /dev/null +++ b/homekit/model/characteristics/rtp_stream/supported_rtp_configuration.py @@ -0,0 +1,46 @@ +# +# Copyright 2018 Joachim Lusiardi +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from enum import IntEnum +from homekit.model.characteristics import CharacteristicsTypes, CharacteristicFormats, CharacteristicPermissions, \ + AbstractCharacteristic +from homekit.protocol.tlv import TLVItem + + +class CameraSRTPCryptoSuite(IntEnum): + AES_CM_128_HMAC_SHA1_80 = 0 + AES_256_CM_HMAC_SHA1_80 = 1 + DISABLED = 2 + + +class SupportedRTPConfiguration: + crypto_suite = TLVItem(2, CameraSRTPCryptoSuite) + + def __init__(self, crypto_suite): + self.crypto_suite = crypto_suite + + +class SupportedRTPConfigurationCharacteristic(AbstractCharacteristic): + """ + Defined on page 218 + """ + + def __init__(self, iid, value): + AbstractCharacteristic.__init__(self, iid, CharacteristicsTypes.SUPPORTED_RTP_CONFIGURATION, + CharacteristicFormats.tlv8, SupportedRTPConfiguration) + self.perms = [CharacteristicPermissions.paired_read] + self.description = 'supported rtp configurations management service' + self.value = value diff --git a/homekit/model/characteristics/rtp_stream/supported_video_stream_configuration.py b/homekit/model/characteristics/rtp_stream/supported_video_stream_configuration.py new file mode 100644 index 00000000..e4267cae --- /dev/null +++ b/homekit/model/characteristics/rtp_stream/supported_video_stream_configuration.py @@ -0,0 +1,101 @@ +# +# Copyright 2018 Joachim Lusiardi +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from enum import IntEnum +from homekit.model.characteristics import CharacteristicsTypes, CharacteristicFormats, CharacteristicPermissions, \ + AbstractCharacteristic +from homekit.protocol.tlv import TLVItem + + +class H264Profile(IntEnum): + CONSTRAINED_BASELINE_PROFILE = 0 + MAIN_PROFILE = 1 + HIGH_PROFILE = 2 + + +class H264Level(IntEnum): + L_3_1 = 0 + L_3_2 = 1 + L_4 = 2 + + +class VideoCodecType(IntEnum): + H264 = 0 + + +class PacketizationMode(IntEnum): + NON_INTERLEAVED = 0 + + +class CVOEnabled(IntEnum): + NOT_SUPPORTED = 0 + SUPPORTED = 1 + + +class VideoCodecParameters: + profile = TLVItem(1, H264Profile) + level = TLVItem(2, H264Level) + packetization_mode = TLVItem(3, PacketizationMode) + cvo_enabled = TLVItem(4, CVOEnabled) + + def __init__(self, profile, level, packetization_mode=PacketizationMode.NON_INTERLEAVED, + cvo_enabled=CVOEnabled.NOT_SUPPORTED): + self.profile = profile + self.level = level + self.packetization_mode = packetization_mode + self.cvo_enabled = cvo_enabled + + +class VideoAttributes: + width = TLVItem(1, int) + height = TLVItem(2, int) + frame_rate = TLVItem(3, int) + + def __init__(self, width, height, frame_rate): + self.width = width + self.height = height + self.frame_rate = frame_rate + + +class VideoCodecConfiguration: + codec_type = TLVItem(1, VideoCodecType) + codec_parameters = TLVItem(2, VideoCodecParameters) + attributes = TLVItem(3, VideoAttributes) + + def __init__(self, codec_parameters, attributes, codec_type=VideoCodecType.H264): + self.codec_type = codec_type + self.codec_parameters = codec_parameters + self.attributes = attributes + + +class SupportedVideoStreamConfiguration: + config = TLVItem(1, VideoCodecConfiguration) + + def __init__(self, config): + self.config = config + + +class SupportedVideoStreamConfigurationCharacteristic(AbstractCharacteristic): + """ + Defined on page 219 + """ + + def __init__(self, iid, value): + AbstractCharacteristic.__init__(self, iid, CharacteristicsTypes.SUPPORTED_VIDEO_STREAM_CONFIGURATION, + CharacteristicFormats.tlv8, SupportedVideoStreamConfiguration) + self.perms = [CharacteristicPermissions.paired_read] + self.description = 'parameters supported for streaming video over an RTP session' + self.value = value diff --git a/homekit/model/services/__init__.py b/homekit/model/services/__init__.py index a0c2b1f1..029d6b80 100644 --- a/homekit/model/services/__init__.py +++ b/homekit/model/services/__init__.py @@ -16,7 +16,8 @@ __all__ = [ 'ThermostatService', 'LightBulbService', 'FanService', 'BHSLightBulbService', 'AccessoryInformationService', - 'OutletService', 'AbstractService', 'ServicesTypes' + 'OutletService', 'AbstractService', 'ServicesTypes', 'RTPStreamService', 'ManagedRTPStreamService', + 'MicrophoneService' ] from homekit.model.services.service_types import ServicesTypes @@ -28,3 +29,5 @@ from homekit.model.services.lightbulb_service import LightBulbService from homekit.model.services.outlet_service import OutletService from homekit.model.services.thermostat_service import ThermostatService +from homekit.model.services.rtpstream_service import RTPStreamService, ManagedRTPStreamService +from homekit.model.services.microphone_service import MicrophoneService diff --git a/homekit/model/services/microphone_service.py b/homekit/model/services/microphone_service.py new file mode 100644 index 00000000..433efba5 --- /dev/null +++ b/homekit/model/services/microphone_service.py @@ -0,0 +1,29 @@ +# +# Copyright 2019 Christoph Walcher +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from homekit.model import get_id +from homekit.model.characteristics import MicrophoneMuteCharacteristicMixin +from homekit.model.services import ServicesTypes, AbstractService + + +class MicrophoneService(AbstractService, MicrophoneMuteCharacteristicMixin): + """ + Defined on page 149; Used to mute a microphone + """ + + def __init__(self): + AbstractService.__init__(self, ServicesTypes.get_uuid('public.hap.service.microphone'), get_id()) + MicrophoneMuteCharacteristicMixin.__init__(self, get_id()) diff --git a/homekit/model/services/rtpstream_service.py b/homekit/model/services/rtpstream_service.py new file mode 100644 index 00000000..cdad5b8b --- /dev/null +++ b/homekit/model/services/rtpstream_service.py @@ -0,0 +1,118 @@ +# +# Copyright 2019 Christoph Walcher +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from uuid import UUID + +from homekit.model import get_id +from homekit.model.characteristics.rtp_stream import SetupEndpointsCharacteristicMixin, \ + StreamingStatusCharacteristicMixin, SelectedRTPStreamConfigurationCharacteristicMixin, \ + SupportedVideoStreamConfigurationCharacteristic, SupportedAudioStreamConfigurationCharacteristic, \ + SupportedRTPConfigurationCharacteristic +from homekit.model.characteristics.rtp_stream.selected_rtp_stream_configuration import SelectedRTPStreamConfiguration, \ + Command +from homekit.model.characteristics.rtp_stream.setup_endpoints import SetupEndpointsRequest, EndpointStatus, \ + SetupEndpointsResponse +from homekit.model.characteristics.rtp_stream.streaming_status import StreamingStatus, StreamingStatusEnum +from homekit.model.services import ServicesTypes, AbstractService + + +class RTPStreamService(AbstractService, StreamingStatusCharacteristicMixin, + SetupEndpointsCharacteristicMixin, + SelectedRTPStreamConfigurationCharacteristicMixin): + """ + Defined on page 137; RTP stream management service used to negotiate (camera) live streaming + """ + + def __init__(self, supported_rtp_configuration, supported_video_stream_config, supported_audio_stream_config): + AbstractService.__init__(self, ServicesTypes.get_uuid('public.hap.service.camera-rtp-stream-management'), + get_id()) + StreamingStatusCharacteristicMixin.__init__(self, get_id()) + SetupEndpointsCharacteristicMixin.__init__(self, get_id()) + SelectedRTPStreamConfigurationCharacteristicMixin.__init__(self, get_id()) + + self.append_characteristic( + SupportedRTPConfigurationCharacteristic(get_id(), supported_rtp_configuration)) + self.append_characteristic( + SupportedVideoStreamConfigurationCharacteristic(get_id(), supported_video_stream_config)) + self.append_characteristic( + SupportedAudioStreamConfigurationCharacteristic(get_id(), supported_audio_stream_config)) + + +class Stream: + def __init__(self, uuid, status, srtp_params_video, srtp_params_audio, handler, address): + self.uuid = uuid + self.status = status + self.srtp_params_video = srtp_params_video + self.srtp_params_audio = srtp_params_audio + self.handler = handler + self.address = address + + +class ManagedRTPStreamService(RTPStreamService): + def __init__(self, stream_handler_factory, supported_rtp_configuration, supported_video_stream_config, + supported_audio_stream_config): + super().__init__(supported_rtp_configuration, supported_video_stream_config, + supported_audio_stream_config) + self.set_selected_rtp_stream_configuration_set_callback(self.select_rtp_stream_configuration) + self.set_selected_rtp_stream_configuration_get_callback(self.get_rtp_stream_configuration) + self.set_setup_endpoints_set_callback(self.setup_endpoints_req) + self.set_setup_endpoints_get_callback(self.setup_endpoints_res) + self.set_streaming_status_get_callback(lambda: StreamingStatus(self.get_status())) + self.stream_handler_factory = stream_handler_factory + self.streams = {} + self.last_added = None + self.last_rtp_stream_config = None + + def setup_endpoints_req(self, val: SetupEndpointsRequest): + uuid = UUID(bytes=bytes(val.id)) + stream_handler = self.stream_handler_factory(uuid=uuid, + controller_address=val.controller_address, + srtp_params_video=val.srtp_params_video, + srtp_params_audio=val.srtp_params_audio) + stream = Stream(uuid, EndpointStatus.SUCCESS, srtp_params_video=val.srtp_params_video, + srtp_params_audio=val.srtp_params_audio, handler=stream_handler, address=val.controller_address) + self.streams[uuid] = stream + self.last_added = stream + + def setup_endpoints_res(self): + if self.last_added is not None: + ssrc = self.last_added.handler.get_ssrc() + address = self.last_added.handler.get_address() + return SetupEndpointsResponse(id=self.last_added.uuid.bytes, + status=EndpointStatus.SUCCESS, + accessory_address=address, + srtp_params_video=self.last_added.srtp_params_video, + srtp_params_audio=self.last_added.srtp_params_audio, + ssrc_video=ssrc[0], + ssrc_audio=ssrc[1]) + else: + return SetupEndpointsResponse(id=self.last_added.uuid.bytes, status=EndpointStatus.ERROR) + + def select_rtp_stream_configuration(self, rtp_stream_config: SelectedRTPStreamConfiguration): + if rtp_stream_config is not None: + uuid = UUID(bytes=bytes(rtp_stream_config.session_control.id)) + stream = self.streams[uuid] + self.last_rtp_stream_config = rtp_stream_config + cmd = rtp_stream_config.session_control.command + if cmd == Command.START: + stream.handler.on_start(rtp_stream_config.selected_video_parameters) + elif cmd == Command.END: + stream.handler.on_end() + + def get_rtp_stream_configuration(self): + return self.last_rtp_stream_config + + def get_status(self): + return StreamingStatusEnum.AVAILABLE diff --git a/homekit/protocol/tlv.py b/homekit/protocol/tlv.py index 153d7b2c..be538ece 100644 --- a/homekit/protocol/tlv.py +++ b/homekit/protocol/tlv.py @@ -14,6 +14,8 @@ # limitations under the License. # import logging +from enum import IntEnum +from struct import Struct logger = logging.getLogger('homekit.protocol.tlv') @@ -57,7 +59,7 @@ class TLV: kTLVType_FragmentLast = 13 kTLVType_Separator = 255 kTLVType_Separator_Pair = [255, bytearray(b'')] - kTLVType_SessionID = 0x0e # Table 6-27 page 116 + kTLVType_SessionID = 0x0e # Table 6-27 page 116 # Errors (see table 4-5 page 60) kTLVError_Unknown = bytearray(b'\x01') @@ -207,3 +209,70 @@ def reorder(tlv_array, preferred_order): class TlvParseException(Exception): """Raised upon parse error with some TLV""" pass + + +class TLVItem: + """Resource for tlv object-mapping""" + short_struct = Struct(" Date: Mon, 3 Feb 2020 22:24:15 +0100 Subject: [PATCH 2/2] Add Seperator on consecutive tlv items with same type --- homekit/protocol/tlv.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homekit/protocol/tlv.py b/homekit/protocol/tlv.py index be538ece..64f19f27 100644 --- a/homekit/protocol/tlv.py +++ b/homekit/protocol/tlv.py @@ -245,7 +245,10 @@ def encode(obj): value = obj.__dict__.get(tlv_item.name, None) if value is not None: if isinstance(value, list): - children.extend((tlv_type, TLVItem.encode(value)) for value in value) + for index, value_item in enumerate(value): + children.append((tlv_type, TLVItem.encode(value_item))) + if index + 1 < len(value): + children.append(TLV.kTLVType_Separator_Pair) else: children.append((tlv_type, TLVItem.encode(value))) return TLV.encode_list(children)