Skip to content

Commit

Permalink
Merge pull request #278 from ikalchev/v3.0.0
Browse files Browse the repository at this point in the history
V3.0.0
  • Loading branch information
ikalchev authored Jul 25, 2020
2 parents 5baa15f + 1e64d02 commit d3c576c
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 44 deletions.
17 changes: 14 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,25 @@ Sections
### Developers
-->

## [3.0.0] - 2020-07-25

### Added
- Support for multiple camera streams. [#273](https://github.com/ikalchev/HAP-python/pull/273)

### Changed
- Use SimpleQueue instead of Queue when available (performance improvements). [#274](https://github.com/ikalchev/HAP-python/pull/274)

### Fixed
- Make sure accessory setup code appears when running under systemd. [#276](https://github.com/ikalchev/HAP-python/pull/276)

## [2.9.2] - 2020-07-05

### Added
- Improve event loop handling. [#270]((https://github.com/ikalchev/HAP-python/pull/270)
- Auto-detect the IP address in the camera demo so it can work out of the box. [#268]((https://github.com/ikalchev/HAP-python/pull/268)
- Improve event loop handling. [#270](https://github.com/ikalchev/HAP-python/pull/270)
- Auto-detect the IP address in the camera demo so it can work out of the box. [#268](https://github.com/ikalchev/HAP-python/pull/268)

### Fixed
- Correctly handling of a single byte read request. [#267]((https://github.com/ikalchev/HAP-python/pull/267)
- Correctly handling of a single byte read request. [#267](https://github.com/ikalchev/HAP-python/pull/267)

## [2.9.1] - 2020-05-31

Expand Down
11 changes: 3 additions & 8 deletions pyhap/accessory.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,6 @@ def __init__(self, driver, display_name, aid=None):
standalone AID. Defaults to None, in which case the `AccessoryDriver`
will assign the standalone AID to this `Accessory`.
:type aid: int
:param setup_id: Setup ID can be provided, although, per spec, should be random
every time the instance is started. If not provided on init, will be random.
4 digit string 0-9 A-Z
:type setup_id: str
"""
self.aid = aid
self.display_name = display_name
Expand Down Expand Up @@ -254,12 +249,12 @@ def setup_message(self):
flush=True)
print(QRCode(xhm_uri).terminal(quiet_zone=2), flush=True)
print('Or enter this code in your HomeKit app on your iOS device: '
'{}'.format(pincode))
'{}'.format(pincode), flush=True)
else:
print('To use the QR Code feature, use \'pip install '
'HAP-python[QRCode]\'')
'HAP-python[QRCode]\'', flush=True)
print('Enter this code in your HomeKit app on your iOS device: {}'
.format(pincode))
.format(pincode), flush=True)

@staticmethod
def run_at_interval(seconds):
Expand Down
18 changes: 14 additions & 4 deletions pyhap/accessory_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
SERVICE_COMMUNICATION_FAILURE = -70402
SERVICE_CALLBACK = 0
SERVICE_CALLBACK_DATA = 1
HAP_SERVICE_TYPE = '_hap._tcp.local.'


def callback(func):
Expand Down Expand Up @@ -86,9 +87,15 @@ def __init__(self, accessory, state):
self.state = state

adv_data = self._get_advert_data()
# Append part of MAC address to prevent name conflicts
name = '{} {}.{}'.format(
self.accessory.display_name,
self.state.mac[-8:].replace(':', ''),
HAP_SERVICE_TYPE
)
super().__init__(
'_hap._tcp.local.',
name=self.accessory.display_name + '._hap._tcp.local.',
HAP_SERVICE_TYPE,
name=name,
port=self.state.port,
weight=0,
priority=0,
Expand Down Expand Up @@ -217,7 +224,9 @@ def __init__(self, *, address=None, port=51234,
self.loader = loader or Loader()
self.aio_stop_event = asyncio.Event(loop=loop)
self.stop_event = threading.Event()
self.event_queue = queue.Queue() # (topic, bytes)
self.event_queue = (
queue.SimpleQueue() if hasattr(queue, "SimpleQueue") else queue.Queue() # pylint: disable=no-member
)
self.send_event_thread = None # the event dispatch thread
self.sent_events = 0
self.accumulated_qsize = 0
Expand Down Expand Up @@ -494,7 +503,8 @@ def send_events(self):
client_addr)
# Maybe consider removing the client_addr from every topic?
self.subscribe_client_topic(client_addr, topic, False)
self.event_queue.task_done()
if hasattr(self.event_queue, "task_done"):
self.event_queue.task_done() # pylint: disable=no-member
self.sent_events += 1
self.accumulated_qsize += self.event_queue.qsize()

Expand Down
90 changes: 64 additions & 26 deletions pyhap/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,6 @@ def __init__(self, options, *args, **kwargs):
:type options: ``dict``
"""
self.streaming_status = STREAMING_STATUS['AVAILABLE']
self.has_srtp = options.get('srtp', False)
self.start_stream_cmd = options.get('start_stream_cmd', FFMPEG_CMD)

Expand All @@ -425,22 +424,52 @@ def __init__(self, options, *args, **kwargs):
super().__init__(*args, **kwargs)

self.add_preload_service('Microphone')
management = self.add_preload_service('CameraRTPStreamManagement')
management.configure_char('StreamingStatus',
getter_callback=self._get_streaming_status)
management.configure_char('SupportedRTPConfiguration',
value=self.get_supported_rtp_config(
options.get('srtp', False)))
management.configure_char('SupportedVideoStreamConfiguration',
value=self.get_supported_video_stream_config(
options['video']))
management.configure_char('SupportedAudioStreamConfiguration',
value=self.get_supported_audio_stream_config(
options['audio']))
management.configure_char('SelectedRTPStreamConfiguration',
setter_callback=self.set_selected_stream_configuration)
management.configure_char('SetupEndpoints',
setter_callback=self.set_endpoints)
self._streaming_status = []
self._management = []
self._setup_stream_management(options)

@property
def streaming_status(self):
"""For backwards compatibility."""
return self._streaming_status[0]

def _setup_stream_management(self, options):
"""Create stream management."""
stream_count = options.get("stream_count", 1)
for stream_idx in range(stream_count):
self._management.append(self._create_stream_management(stream_idx, options))
self._streaming_status.append(STREAMING_STATUS["AVAILABLE"])

def _create_stream_management(self, stream_idx, options):
"""Create a stream management service."""
management = self.add_preload_service("CameraRTPStreamManagement")
management.configure_char(
"StreamingStatus",
getter_callback=lambda: self._get_streaming_status(stream_idx),
)
management.configure_char(
"SupportedRTPConfiguration",
value=self.get_supported_rtp_config(options.get("srtp", False)),
)
management.configure_char(
"SupportedVideoStreamConfiguration",
value=self.get_supported_video_stream_config(options["video"]),
)
management.configure_char(
"SupportedAudioStreamConfiguration",
value=self.get_supported_audio_stream_config(options["audio"]),
)
management.configure_char(
"SelectedRTPStreamConfiguration",
setter_callback=self.set_selected_stream_configuration,
)
management.configure_char(
"SetupEndpoints",
setter_callback=lambda value: self.set_endpoints(
value, stream_idx=stream_idx
),
)
return management

async def _start_stream(self, objs, reconfigure): # pylint: disable=unused-argument
"""Start or reconfigure video streaming for the given session.
Expand Down Expand Up @@ -533,27 +562,28 @@ async def _start_stream(self, objs, reconfigure): # pylint: disable=unused-argu
session_objs = tlv.decode(objs[SELECTED_STREAM_CONFIGURATION_TYPES['SESSION']])
session_id = UUID(bytes=session_objs[SETUP_TYPES['SESSION_ID']])
session_info = self.sessions[session_id]
stream_idx = session_info['stream_idx']

opts.update(session_info)
success = await self.reconfigure_stream(session_info, opts) if reconfigure \
else await self.start_stream(session_info, opts)

if success:
self.streaming_status = STREAMING_STATUS['STREAMING']
self._streaming_status[stream_idx] = STREAMING_STATUS['STREAMING']
else:
logger.error(
'[%s] Failed to start/reconfigure stream, deleting session.',
session_id
)
del self.sessions[session_id]
self.streaming_status = STREAMING_STATUS['AVAILABLE']
self._streaming_status[stream_idx] = STREAMING_STATUS['AVAILABLE']

def _get_streaming_status(self):
def _get_streaming_status(self, stream_idx):
"""Get the streaming status in TLV format.
Called when iOS reads the StreaminStatus ``Characteristic``.
"""
return tlv.encode(b'\x01', self.streaming_status, to_base64=True)
return tlv.encode(b'\x01', self._streaming_status[stream_idx], to_base64=True)

async def _stop_stream(self, objs):
"""Stop the stream for the specified session.
Expand All @@ -567,6 +597,7 @@ async def _stop_stream(self, objs):
session_id = UUID(bytes=session_objs[SETUP_TYPES['SESSION_ID']])

session_info = self.sessions.get(session_id)
stream_idx = session_info['stream_idx']

if not session_info:
logger.error(
Expand All @@ -579,7 +610,7 @@ async def _stop_stream(self, objs):
await self.stop_stream(session_info)
del self.sessions[session_id]

self.streaming_status = STREAMING_STATUS['AVAILABLE']
self._streaming_status[stream_idx] = STREAMING_STATUS['AVAILABLE']

def set_selected_stream_configuration(self, value):
"""Set the selected stream configuration.
Expand Down Expand Up @@ -615,7 +646,12 @@ def set_selected_stream_configuration(self, value):

self.driver.add_job(job, objs)

def set_endpoints(self, value):
def set_streaming_available(self, stream_idx):
"""Send an update to the controller that streaming is available."""
self._streaming_status[stream_idx] = STREAMING_STATUS["AVAILABLE"]
self._management[stream_idx].get_characteristic("StreamingStatus").notify()

def set_endpoints(self, value, stream_idx=None):
"""Configure streaming endpoints.
Called when iOS sets the SetupEndpoints ``Characteristic``. The endpoint
Expand All @@ -624,6 +660,9 @@ def set_endpoints(self, value):
:param value: The base64-encoded stream session details in TLV format.
:param value: ``str``
"""
if stream_idx is None:
stream_idx = 0

objs = tlv.decode(value, from_base64=True)
session_id = UUID(bytes=objs[SETUP_TYPES['SESSION_ID']])

Expand Down Expand Up @@ -702,6 +741,7 @@ def set_endpoints(self, value):

self.sessions[session_id] = {
'id': session_id,
'stream_idx': stream_idx,
'address': address,
'v_port': target_video_port,
'v_srtp_key': to_base64_str(video_master_key + video_master_salt),
Expand All @@ -711,9 +751,7 @@ def set_endpoints(self, value):
'a_ssrc': audio_ssrc
}

self.get_service('CameraRTPStreamManagement')\
.get_characteristic('SetupEndpoints')\
.set_value(response_tlv)
self._management[stream_idx].get_characteristic('SetupEndpoints').set_value(response_tlv)

async def stop(self):
"""Stop all streaming sessions."""
Expand Down
6 changes: 3 additions & 3 deletions pyhap/const.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""This module contains constants used by other modules."""
MAJOR_VERSION = 2
MINOR_VERSION = 9
PATCH_VERSION = 2
MAJOR_VERSION = 3
MINOR_VERSION = 0
PATCH_VERSION = 0
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@


_OPTIONS = {
"stream_count": 4,
"video": {
"codec": {
"profiles": [
Expand Down Expand Up @@ -105,6 +106,7 @@ async def subprocess_exec(*args, **kwargs): # pylint: disable=unused-argument

session_info = {
'id': session_id,
'stream_idx': 0,
'address': '192.168.1.114',
'v_port': 50483,
'v_srtp_key': '2JZgpMkwWUH8ahUtzp8VThtBmbk26hCPJqeWpYDR',
Expand Down

0 comments on commit d3c576c

Please sign in to comment.