Skip to content

Commit

Permalink
Merge pull request #206 from ikalchev/v2.6.0
Browse files Browse the repository at this point in the history
V2.6.0
#205
  • Loading branch information
ikalchev committed Sep 21, 2019
2 parents 291f175 + c20c87f commit a7424c1
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 9 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ Sections
### Developers
-->

## [2.6.0] - 2019-09-21

### Added
- The `AccessoryDriver` can now advertise on a different address than the one the server is running on. This is useful when pyhap is running behind a NAT. [#203](https://github.com/ikalchev/HAP-python/pull/203)

## [2.5.0] - 2019-04-10

### Added
Expand Down
110 changes: 110 additions & 0 deletions accessories/TV.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from pyhap.accessory import Accessory
from pyhap.const import CATEGORY_TELEVISION


class TV(Accessory):

category = CATEGORY_TELEVISION

NAME = 'Sample TV'
SOURCES = {
'HDMI 1': 3,
'HDMI 2': 3,
'HDMI 3': 3,
}

def __init__(self, *args, **kwargs):
super(TV, self).__init__(*args, **kwargs)

self.set_info_service(
manufacturer='HaPK',
model='Raspberry Pi',
firmware_revision='1.0',
serial_number='1'
)

tv_service = self.add_preload_service(
'Television', ['Name',
'ConfiguredName',
'Active',
'ActiveIdentifier',
'RemoteKey',
'SleepDiscoveryMode'],
)
self._active = tv_service.configure_char(
'Active', value=0,
setter_callback=self._on_active_changed,
)
tv_service.configure_char(
'ActiveIdentifier', value=1,
setter_callback=self._on_active_identifier_changed,
)
tv_service.configure_char(
'RemoteKey', setter_callback=self._on_remote_key,
)
tv_service.configure_char('Name', value=self.NAME)
# TODO: implement persistence for ConfiguredName
tv_service.configure_char('ConfiguredName', value=self.NAME)
tv_service.configure_char('SleepDiscoveryMode', value=1)

for idx, (source_name, source_type) in enumerate(self.SOURCES.items()):
input_source = self.add_preload_service('InputSource', ['Name', 'Identifier'])
input_source.configure_char('Name', value=source_name)
input_source.configure_char('Identifier', value=idx + 1)
# TODO: implement persistence for ConfiguredName
input_source.configure_char('ConfiguredName', value=source_name)
input_source.configure_char('InputSourceType', value=source_type)
input_source.configure_char('IsConfigured', value=1)
input_source.configure_char('CurrentVisibilityState', value=0)

tv_service.add_linked_service(input_source)

tv_speaker_service = self.add_preload_service(
'TelevisionSpeaker', ['Active',
'VolumeControlType',
'VolumeSelector']
)
tv_speaker_service.configure_char('Active', value=1)
# Set relative volume control
tv_speaker_service.configure_char('VolumeControlType', value=1)
tv_speaker_service.configure_char(
'Mute', setter_callback=self._on_mute,
)
tv_speaker_service.configure_char(
'VolumeSelector', setter_callback=self._on_volume_selector,
)

def _on_active_changed(self, value):
print('Turn %s' % ('on' if value else 'off'))

def _on_active_identifier_changed(self, value):
print('Change input to %s' % list(self.SOURCES.keys())[value-1])

def _on_remote_key(self, value):
print('Remote key %d pressed' % value)

def _on_mute(self, value):
print('Mute' if value else 'Unmute')

def _on_volume_selector(self, value):
print('%screase volume' % ('In' if value == 0 else 'De'))


def main():
import logging
import signal

from pyhap.accessory_driver import AccessoryDriver

logging.basicConfig(level=logging.INFO)

driver = AccessoryDriver(port=51826)
accessory = TV(driver, 'TV')
driver.add_accessory(accessory=accessory)

signal.signal(signal.SIGTERM, driver.signal_handler)
driver.start()


if __name__ == '__main__':
main()
29 changes: 23 additions & 6 deletions pyhap/accessory_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from pyhap.loader import Loader
from pyhap.params import get_srp_context
from pyhap.state import State
from pyhap import util

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -81,15 +82,13 @@ class AccessoryMDNSServiceInfo(ServiceInfo):
def __init__(self, accessory, state):
self.accessory = accessory
self.state = state
hname = socket.gethostname()
pubname = hname + '.' if hname.endswith('.local') else hname + '.local.'

adv_data = self._get_advert_data()
super().__init__(
'_hap._tcp.local.',
self.accessory.display_name + '._hap._tcp.local.',
socket.inet_aton(self.state.address), self.state.port,
0, 0, adv_data, pubname)
0, 0, adv_data)

def _setup_hash(self):
setup_hash_material = self.state.setup_id + self.state.mac
Expand Down Expand Up @@ -131,7 +130,8 @@ class AccessoryDriver:

def __init__(self, *, address=None, port=51234,
persist_file='accessory.state', pincode=None,
encoder=None, loader=None, loop=None):
encoder=None, loader=None, loop=None, mac=None,
listen_address=None, advertised_address=None):
"""
Initialize a new AccessoryDriver object.
Expand All @@ -157,6 +157,19 @@ def __init__(self, *, address=None, port=51234,
:param encoder: The encoder to use when persisting/loading the Accessory state.
:type encoder: AccessoryEncoder
:param mac: The MAC address which will be used to identify the accessory.
If not given, the driver will try to select a MAC address.
:type mac: str
:param listen_address: The local address on the HAPServer will listen.
If not given, the value of the address parameter will be used.
:type listen_address: str
:param advertised_address: The address of the HAPServer announced via mDNS.
This can be used to announce an external address from behind a NAT.
If not given, the value of the address parameter will be used.
:type advertised_address: str
"""
if sys.platform == 'win32':
self.loop = loop or asyncio.ProactorEventLoop()
Expand Down Expand Up @@ -190,8 +203,12 @@ def __init__(self, *, address=None, port=51234,
self.mdns_service_info = None
self.srp_verifier = None

self.state = State(address=address, pincode=pincode, port=port)
network_tuple = (self.state.address, self.state.port)
address = address or util.get_local_address()
advertised_address = advertised_address or address
self.state = State(address=advertised_address, mac=mac, pincode=pincode, port=port)

listen_address = listen_address or address
network_tuple = (listen_address, self.state.port)
self.http_server = HAPServer(network_tuple, self)

def start(self):
Expand Down
7 changes: 6 additions & 1 deletion pyhap/characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

from pyhap.const import (
HAP_PERMISSION_READ, HAP_REPR_DESC, HAP_REPR_FORMAT, HAP_REPR_IID,
HAP_REPR_MAX_LEN, HAP_REPR_PERM, HAP_REPR_TYPE, HAP_REPR_VALUE)
HAP_REPR_MAX_LEN, HAP_REPR_PERM, HAP_REPR_TYPE, HAP_REPR_VALUE,
HAP_REPR_VALID_VALUES)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -241,6 +242,10 @@ def to_HAP(self):
if self.properties[PROP_FORMAT] in HAP_FORMAT_NUMERICS:
hap_rep.update({k: self.properties[k] for k in
self.properties.keys() & PROP_NUMERIC})

if PROP_VALID_VALUES in self.properties:
hap_rep[HAP_REPR_VALID_VALUES] = \
sorted(self.properties[PROP_VALID_VALUES].values())
elif self.properties[PROP_FORMAT] == HAP_FORMAT_STRING:
if len(value) > 64:
hap_rep[HAP_REPR_MAX_LEN] = min(len(value), 256)
Expand Down
3 changes: 2 additions & 1 deletion pyhap/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""This module contains constants used by other modules."""
MAJOR_VERSION = 2
MINOR_VERSION = 5
MINOR_VERSION = 6
PATCH_VERSION = 0
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
Expand Down Expand Up @@ -72,3 +72,4 @@
HAP_REPR_STATUS = 'status'
HAP_REPR_TYPE = 'type'
HAP_REPR_VALUE = 'value'
HAP_REPR_VALID_VALUES = 'valid-values'
4 changes: 3 additions & 1 deletion pyhap/resources/characteristics.json
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,9 @@
"UUID": "00000135-0000-1000-8000-0026BB765291",
"ValidValues": {
"Shown": 0,
"Hidden": 1
"Hidden": 1,
"State2": 2,
"State3": 3
}
},
"DigitalZoom": {
Expand Down
12 changes: 12 additions & 0 deletions tests/test_characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,18 @@ def test_to_HAP_numberic():
}


def test_to_HAP_valid_values():
"""Test created HAP representation for valid values constraint."""
char = get_char(PROPERTIES.copy(), valid={'foo': 0, 'bar': 2, 'baz': 1})
with patch.object(char, 'broker') as mock_broker:
mock_broker.iid_manager.get_iid.return_value = 2

hap_repr = char.to_HAP()

assert 'valid-values' in hap_repr
assert hap_repr['valid-values'] == [0, 1, 2]


def test_to_HAP_string():
"""Test created HAP representation for strings."""
char = get_char(PROPERTIES.copy())
Expand Down

0 comments on commit a7424c1

Please sign in to comment.