Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions democameraserver.py
Original file line number Diff line number Diff line change
@@ -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()
26 changes: 25 additions & 1 deletion homekit/accessoryserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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)

Expand Down
18 changes: 15 additions & 3 deletions homekit/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = []
Expand Down
5 changes: 4 additions & 1 deletion homekit/model/characteristics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
25 changes: 20 additions & 5 deletions homekit/model/characteristics/abstract_characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
43 changes: 43 additions & 0 deletions homekit/model/characteristics/microphone_mute.py
Original file line number Diff line number Diff line change
@@ -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)
36 changes: 36 additions & 0 deletions homekit/model/characteristics/rtp_stream/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Loading