def hscp_service_handler( mdns_service: mdns.Service, response: mdns.Response ) -> Optional[ScanHandlerReturn]: """Parse and return a new HSCP service.""" name = mdns_service.properties.get("Machine Name", "Unknown") service = MutableService( get_unique_id(mdns_service.type, mdns_service.name, mdns_service.properties), Protocol.DMAP, port=mdns_service.port, properties=mdns_service.properties, ) service.credentials = mdns_service.properties.get("hG") return name, service
async def service_info( service: MutableService, devinfo: DeviceInfo, services: Mapping[Protocol, BaseService], ) -> None: """Update service with additional information.""" flags = int(service.properties.get("rpfl", "0x0"), 16) if flags & PAIRING_DISABLED_MASK: service.pairing = PairingRequirement.Disabled elif flags & PAIRING_WITH_PIN_SUPPORTED_MASK: service.pairing = PairingRequirement.Mandatory else: service.pairing = PairingRequirement.Unsupported
def homesharing_service_handler( mdns_service: mdns.Service, response: mdns.Response ) -> Optional[ScanHandlerReturn]: """Parse and return a new DMAP (Home Sharing) service.""" name = mdns_service.properties.get("Name", "Unknown") service = MutableService( get_unique_id(mdns_service.type, mdns_service.name, mdns_service.properties), Protocol.DMAP, mdns_service.port, mdns_service.properties, ) service.credentials = mdns_service.properties.get("hG") return name, service
async def service_info( service: MutableService, devinfo: DeviceInfo, services: Mapping[Protocol, BaseService], ) -> None: """Update service with additional information.""" service.requires_password = is_password_required(service) if service.properties.get("acl", "0") == "1": # Access control might say that pairing is not possible, e.g. only devices # belonging to the same home (not supported by pyatv) service.pairing = PairingRequirement.Disabled else: service.pairing = get_pairing_requirement(service)
async def test_service_info_pairing_acl(): raop_service = MutableService("id", Protocol.RAOP, 0, {}) airplay_props = MutableService("id", Protocol.AirPlay, 0, {"acl": "1"}) await service_info( raop_service, DeviceInfo({}), { Protocol.RAOP: raop_service, Protocol.AirPlay: airplay_props }, ) assert raop_service.pairing == PairingRequirement.Disabled
async def test_service_info_pairing(dmap_props, mrp_props, pairing_req): dmap_service = MutableService("id", Protocol.DMAP, 0, dmap_props) mrp_service = MutableService("mrp", Protocol.MRP, 0, mrp_props) assert dmap_service.pairing == PairingRequirement.Unsupported assert mrp_service.pairing == PairingRequirement.Unsupported await service_info( dmap_service, DeviceInfo({}), {Protocol.MRP: mrp_service, Protocol.DMAP: dmap_service}, ) assert dmap_service.pairing == pairing_req assert mrp_service.pairing == PairingRequirement.Unsupported
def mrp_service_handler( mdns_service: mdns.Service, response: mdns.Response) -> Optional[ScanHandlerReturn]: """Parse and return a new MRP service.""" enabled = True # Disable this service if tvOS version is >= 15 as it doesn't work anymore build = mdns_service.properties.get("SystemBuildVersion", "") match = re.match(r"^(\d+)[A-Z]", build) if match: base = int(match.groups()[0]) if base >= 19: _LOGGER.debug("Disabling MRP service since tvOS >= 15") enabled = False name = mdns_service.properties.get("Name", "Unknown") service = MutableService( get_unique_id(mdns_service.type, mdns_service.name, mdns_service.properties), Protocol.MRP, mdns_service.port, properties=mdns_service.properties, enabled=enabled, ) return name, service
async def test_service_info_password(raop_props, mrp_props, requires_password): raop_service = MutableService("id", Protocol.RAOP, 0, raop_props) mrp_service = MutableService("mrp", Protocol.MRP, 0, mrp_props) assert not raop_service.requires_password assert not mrp_service.requires_password await service_info( raop_service, DeviceInfo({}), { Protocol.MRP: mrp_service, Protocol.RAOP: raop_service }, ) assert raop_service.requires_password == requires_password assert not mrp_service.requires_password
async def test_service_info_pairing(airplay_props, mrp_props, pairing_req): mrp_service = MutableService("mrp", Protocol.MRP, 0, mrp_props) airplay_service = MutableService("id", Protocol.AirPlay, 0, airplay_props) assert mrp_service.pairing == PairingRequirement.Unsupported assert airplay_service.pairing == PairingRequirement.Unsupported await service_info( mrp_service, DeviceInfo({}), { Protocol.MRP: mrp_service, Protocol.AirPlay: airplay_service }, ) assert mrp_service.pairing == pairing_req assert airplay_service.pairing == PairingRequirement.Unsupported
async def test_service_info_password(airplay_props, mrp_props, requires_password): airplay_service = MutableService("id", Protocol.AirPlay, 0, airplay_props) mrp_service = MutableService("mrp", Protocol.MRP, 0, mrp_props) assert not airplay_service.requires_password assert not mrp_service.requires_password await service_info( airplay_service, DeviceInfo({}), { Protocol.MRP: mrp_service, Protocol.AirPlay: airplay_service }, ) assert airplay_service.requires_password == requires_password assert not mrp_service.requires_password
def companion_service_handler( mdns_service: mdns.Service, response: mdns.Response) -> Optional[ScanHandlerReturn]: """Parse and return a new Companion service.""" service = MutableService( None, Protocol.Companion, mdns_service.port, mdns_service.properties, ) return mdns_service.name, service
async def test_service_info_pairing(airplay_props, devinfo, pairing_req): airplay_service = MutableService("id", Protocol.AirPlay, 0, airplay_props) assert airplay_service.pairing == PairingRequirement.Unsupported await service_info( airplay_service, DeviceInfo(devinfo), {Protocol.AirPlay: airplay_service}, ) assert airplay_service.pairing == pairing_req
def dmap_service_handler( mdns_service: mdns.Service, response: mdns.Response ) -> Optional[ScanHandlerReturn]: """Parse and return a new DMAP service.""" name = mdns_service.properties.get("CtlN", "Unknown") service = MutableService( get_unique_id(mdns_service.type, mdns_service.name, mdns_service.properties), Protocol.DMAP, mdns_service.port, mdns_service.properties, ) return name, service
def airplay_service_handler( mdns_service: mdns.Service, response: mdns.Response) -> Optional[ScanHandlerReturn]: """Parse and return a new AirPlay service.""" service = MutableService( get_unique_id(mdns_service.type, mdns_service.name, mdns_service.properties), Protocol.AirPlay, mdns_service.port, properties=mdns_service.properties, ) return mdns_service.name, service
async def test_service_info_pairing(raop_props, devinfo, pairing_req): raop_service = MutableService("id", Protocol.RAOP, 0, raop_props) assert raop_service.pairing == PairingRequirement.Unsupported await service_info( raop_service, DeviceInfo(devinfo), {Protocol.RAOP: raop_service}, ) assert raop_service.pairing == pairing_req
def raop_service_handler( mdns_service: mdns.Service, response: mdns.Response) -> Optional[ScanHandlerReturn]: """Parse and return a new RAOP service.""" _, name = mdns_service.name.split("@", maxsplit=1) service = MutableService( get_unique_id(mdns_service.type, mdns_service.name, mdns_service.properties), Protocol.RAOP, mdns_service.port, mdns_service.properties, ) return name, service
async def service_info( service: MutableService, devinfo: DeviceInfo, services: Mapping[Protocol, BaseService], ) -> None: """Update service with additional information. Pairing has never been enforced by MRP (maybe by design), but it is possible to pair if AllowPairing is YES. """ service.pairing = (PairingRequirement.Optional if service.properties.get( "allowpairing", "no").lower() == "yes" else PairingRequirement.Disabled)
async def service_info( service: MutableService, devinfo: DeviceInfo, services: Mapping[Protocol, BaseService], ) -> None: """Update service with additional information.""" airplay_service = services.get(Protocol.AirPlay) if airplay_service and airplay_service.properties.get("acl", "0") == "1": # Access control might say that pairing is not possible, e.g. only devices # belonging to the same home (not supported by pyatv) service.pairing = PairingRequirement.Disabled else: # Same behavior as for AirPlay expected, so re-using that here await airplay_service_info(service, devinfo, services)
async def service_info( service: MutableService, devinfo: DeviceInfo, services: Mapping[Protocol, BaseService], ) -> None: """Update service with additional information. If Home Sharing is enabled, then the "hG" property is present and can be used as credentials. If not enabled, then pairing must be performed. """ service.pairing = ( PairingRequirement.Optional if "hg" in service.properties else PairingRequirement.Mandatory )
def __init__(self, loop, address, port, credentials): """Initialize a new instance of ProxyMrpAppleTV.""" super().__init__(DEVICE_NAME) self.loop = loop self.buffer = b"" self.transport = None self.chacha = None self.connection = MrpConnection(address, port, self.loop) self.protocol = MrpProtocol( self.connection, SRPAuthHandler(), MutableService(None, Protocol.MRP, port, {}, credentials=credentials), )
def __init__(self, loop: asyncio.AbstractEventLoop, address: str, port: int, credentials: str) -> None: """Initialize a new instance of CompanionAppleTVProxy.""" super().__init__(DEVICE_NAME) self.loop = loop self.buffer: bytes = b"" self.transport = None self.chacha: Optional[chacha20.Chacha20Cipher] = None self.connection: Optional[CompanionConnection] = CompanionConnection( self.loop, address, port) self.protocol: CompanionProtocol = CompanionProtocol( self.connection, SRPAuthHandler(), MutableService(None, Protocol.Companion, port, {}, credentials=credentials), ) self._receive_event: asyncio.Event = asyncio.Event() self._receive_task: Optional[asyncio.Future] = None
async def test_get_pairing_requirement(props, expected_req): service = MutableService("id", Protocol.AirPlay, 0, props) assert get_pairing_requirement(service) == expected_req
def test_is_remote_control_supported(props, credentials, expected_supported): service = MutableService("id", Protocol.AirPlay, 0, props) assert is_remote_control_supported(service, credentials) == expected_supported
def test_is_password_required(properties, requires_password): service = MutableService("id", Protocol.RAOP, 0, properties) assert is_password_required(service) == requires_password
def setup( # pylint: disable=too-many-locals core: Core, ) -> Generator[SetupData, None, None]: """Set up a new AirPlay service.""" # TODO: Split up in connect/protocol and Stream implementation stream = AirPlayStream(core.config, core.service) interfaces = { Features: AirPlayFeatures(core.service), Stream: stream, } async def _connect() -> bool: return True def _close() -> Set[asyncio.Task]: stream.close() return set() def _device_info() -> Dict[str, Any]: return device_info(list(scan().keys())[0], core.service.properties) yield SetupData( Protocol.AirPlay, _connect, _close, _device_info, interfaces, set([FeatureName.PlayUrl]), ) credentials = extract_credentials(core.service) # Set up remote control channel if it is supported if not is_remote_control_supported(core.service, credentials): _LOGGER.debug("Remote control not supported by device") elif credentials.type not in [ AuthenticationType.HAP, AuthenticationType.Transient ]: _LOGGER.debug("%s not supported by remote control channel", credentials.type) else: _LOGGER.debug("Remote control channel is supported") control = RemoteControl(core.device_listener) control_port = core.service.port # A protocol requires its correaponding service to function, so add a # dummy one if we don't have one yet mrp_service = core.config.get_service(Protocol.MRP) if mrp_service is None: mrp_service = MutableService(None, Protocol.MRP, control_port, {}) core.config.add_service(mrp_service) ( _, mrp_connect, mrp_close, mrp_device_info, mrp_interfaces, mrp_features, ) = mrp.create_with_connection( Core( core.loop, core.config, mrp_service, core.device_listener, core.session_manager, core.takeover, core.state_dispatcher.create_copy(Protocol.MRP), ), AirPlayMrpConnection(control, core.device_listener), requires_heatbeat=False, # Already have heartbeat on control channel ) async def _connect_rc() -> bool: try: await control.start(str(core.config.address), control_port, credentials) except exceptions.HttpError as ex: if ex.status_code == 470: _LOGGER.debug( "Remote control authorization failed, missing credentials" ) else: _LOGGER.exception( "Failed to set up remote control channel") except Exception: _LOGGER.exception("Failed to set up remote control channel") else: await mrp_connect() return True return False def _close_rc() -> Set[asyncio.Task]: tasks = set() tasks.update(mrp_close()) tasks.update(control.stop()) return tasks yield SetupData( Protocol.MRP, _connect_rc, _close_rc, mrp_device_info, mrp_interfaces, mrp_features, )
async def test_service_info_pairing(properties, expected): service = MutableService(None, Protocol.Companion, 0, properties) assert service.pairing == PairingRequirement.Unsupported await service_info(service, DeviceInfo({}), {Protocol.Companion: service}) assert service.pairing == expected