diff --git a/CHANGELOG.md b/CHANGELOG.md index 49a29e53..f1aac01e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/accessories/TV.py b/accessories/TV.py new file mode 100644 index 00000000..1da75520 --- /dev/null +++ b/accessories/TV.py @@ -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() diff --git a/pyhap/accessory_driver.py b/pyhap/accessory_driver.py index 13b763a7..8d3c3c0e 100644 --- a/pyhap/accessory_driver.py +++ b/pyhap/accessory_driver.py @@ -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__) @@ -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 @@ -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. @@ -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() @@ -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): diff --git a/pyhap/characteristic.py b/pyhap/characteristic.py index 45574bfd..9cdc2d6e 100644 --- a/pyhap/characteristic.py +++ b/pyhap/characteristic.py @@ -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__) @@ -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) diff --git a/pyhap/const.py b/pyhap/const.py index b9f67166..db700165 100644 --- a/pyhap/const.py +++ b/pyhap/const.py @@ -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) @@ -72,3 +72,4 @@ HAP_REPR_STATUS = 'status' HAP_REPR_TYPE = 'type' HAP_REPR_VALUE = 'value' +HAP_REPR_VALID_VALUES = 'valid-values' diff --git a/pyhap/resources/characteristics.json b/pyhap/resources/characteristics.json index 9128162c..72e38f01 100644 --- a/pyhap/resources/characteristics.json +++ b/pyhap/resources/characteristics.json @@ -451,7 +451,9 @@ "UUID": "00000135-0000-1000-8000-0026BB765291", "ValidValues": { "Shown": 0, - "Hidden": 1 + "Hidden": 1, + "State2": 2, + "State3": 3 } }, "DigitalZoom": { diff --git a/tests/test_characteristic.py b/tests/test_characteristic.py index 387be2f5..60bf481b 100644 --- a/tests/test_characteristic.py +++ b/tests/test_characteristic.py @@ -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())