Module pyatv.conf
Configuration used when connecting to a device.
A configuration describes a device, e.g. it's name, IP address and credentials. It is possible to manually create a configuration, but generally scanning for devices will provide configurations for you.
For a configuration to be usable ("ready") it must have either a DMAP
or MRP
configuration (or both), as connecting to plain AirPlay
devices it not supported.
Expand source code
"""Configuration used when connecting to a device.
A configuration describes a device, e.g. it's name, IP address and credentials. It is
possible to manually create a configuration, but generally scanning for devices will
provide configurations for you.
For a configuration to be usable ("ready") it must have either a `DMAP` or `MRP`
configuration (or both), as connecting to plain `AirPlay` devices it not supported.
"""
from ipaddress import IPv4Address
from typing import Dict, List, Mapping, Optional, Tuple, cast
from pyatv import exceptions
from pyatv.const import DeviceModel, OperatingSystem, Protocol
from pyatv.interface import BaseService, DeviceInfo
from pyatv.support.device_info import lookup_model, lookup_version
class AppleTV:
"""Representation of an Apple TV configuration.
An instance of this class represents a single device. A device can have
several services depending on the protocols it supports, e.g. DMAP or
AirPlay.
"""
def __init__(
self,
address: IPv4Address,
name: str,
deep_sleep: bool = False,
model: DeviceModel = DeviceModel.Unknown,
properties: Optional[Mapping[str, str]] = None,
) -> None:
"""Initialize a new AppleTV."""
self._address = address
self._name = name
self._deep_sleep = deep_sleep
self._model = model
self._services: Dict[Protocol, BaseService] = {}
self._properties: Mapping[str, str] = properties or {}
@property
def address(self) -> IPv4Address:
"""IP address of device."""
return self._address
@property
def name(self) -> str:
"""Name of device."""
return self._name
@property
def deep_sleep(self) -> bool:
"""If device is in deep sleep."""
return self._deep_sleep
@property
def ready(self) -> bool:
"""Return if configuration is ready, i.e. has a main service."""
ready_protocols = set(list(Protocol))
# Companion has no unique identifier so it's the only protocol that can't be
# used independently for now
ready_protocols.remove(Protocol.Companion)
intersection = ready_protocols.intersection(self._services.keys())
return len(intersection) > 0
@property
def identifier(self) -> Optional[str]:
"""Return the main identifier associated with this device."""
for prot in [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay, Protocol.RAOP]:
service = self._services.get(prot)
if service:
return service.identifier
return None
@property
def all_identifiers(self) -> List[str]:
"""Return all unique identifiers for this device."""
return [x.identifier for x in self.services if x.identifier is not None]
def add_service(self, service: BaseService) -> None:
"""Add a new service.
If the service already exists, it will be merged.
"""
existing = self._services.get(service.protocol)
if existing is not None:
existing.merge(service)
else:
self._services[service.protocol] = service
def get_service(self, protocol: Protocol) -> Optional[BaseService]:
"""Look up a service based on protocol.
If a service with the specified protocol is not available, None is
returned.
"""
return self._services.get(protocol)
@property
def services(self) -> List[BaseService]:
"""Return all supported services."""
return list(self._services.values())
def main_service(self, protocol: Optional[Protocol] = None) -> BaseService:
"""Return suggested service used to establish connection."""
protocols = (
[protocol]
if protocol is not None
else [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay, Protocol.RAOP]
)
for prot in protocols:
service = self._services.get(prot)
if service is not None:
return service
raise exceptions.NoServiceError("no service to connect to")
def set_credentials(self, protocol: Protocol, credentials: str) -> bool:
"""Set credentials for a protocol if it exists."""
service = self.get_service(protocol)
if service:
service.credentials = credentials
return True
return False
# TODO: The extraction should be generialized and moved somewhere else. It is
# very hard to test rght now.
@property
def device_info(self) -> DeviceInfo:
"""Return general device information."""
properties = self._all_properties()
build: Optional[str] = properties.get("systembuildversion")
version = properties.get("ov")
if not version:
version = properties.get("osvers", lookup_version(build))
model_name: Optional[str] = properties.get("model", properties.get("am"))
if model_name:
model = lookup_model(model_name)
else:
model = self._model
# MRP devices run tvOS (as far as we know now) as well as HomePods for
# some reason
if Protocol.MRP in self._services or model in [
DeviceModel.HomePod,
DeviceModel.HomePodMini,
]:
os_type = OperatingSystem.TvOS
elif Protocol.DMAP in self._services:
os_type = OperatingSystem.Legacy
elif model in [DeviceModel.AirPortExpress, DeviceModel.AirPortExpressGen2]:
os_type = OperatingSystem.AirPortOS
else:
os_type = OperatingSystem.Unknown
mac = properties.get("macaddress", properties.get("deviceid"))
if mac:
mac = mac.upper()
# The waMA property comes from the _airport._tcp.local service, announced by
# AirPort Expresses (used by the admin tool). It contains various information,
# for instance MAC address and software version.
wama = properties.get("wama")
if wama:
props: Mapping[str, str] = dict(
cast(Tuple[str, str], prop.split("=", maxsplit=1))
for prop in ("macaddress=" + wama).split(",")
)
if not mac:
mac = props["macaddress"].replace("-", ":").upper()
version = props.get("syVs")
return DeviceInfo(os_type, version, build, model, mac)
def _all_properties(self) -> Mapping[str, str]:
properties: Dict[str, str] = {}
properties.update(self._properties)
for service in self.services:
properties.update(service.properties)
return properties
def __eq__(self, other) -> bool:
"""Compare instance with another instance."""
if isinstance(other, self.__class__):
return self.identifier == other.identifier
return False
def __str__(self) -> str:
"""Return a string representation of this object."""
device_info = self.device_info
services = "\n".join([" - {0}".format(s) for s in self._services.values()])
identifiers = "\n".join([" - {0}".format(x) for x in self.all_identifiers])
return (
f" Name: {self.name}\n"
f" Model/SW: {device_info}\n"
f" Address: {self.address}\n"
f" MAC: {self.device_info.mac}\n"
f" Deep Sleep: {self.deep_sleep}\n"
f"Identifiers:\n"
f"{identifiers}\n"
f"Services:\n"
f"{services}"
)
# pylint: disable=too-few-public-methods
class DmapService(BaseService):
"""Representation of a DMAP service."""
def __init__(
self,
identifier: Optional[str],
credentials: Optional[str],
port: int = 3689,
properties: Optional[Mapping[str, str]] = None,
) -> None:
"""Initialize a new DmapService."""
super().__init__(
identifier,
Protocol.DMAP,
port,
properties,
)
self.credentials = credentials
# pylint: disable=too-few-public-methods
class MrpService(BaseService):
"""Representation of a MediaRemote Protocol (MRP) service."""
def __init__(
self,
identifier: Optional[str],
port: int,
credentials: Optional[str] = None,
properties: Optional[Mapping[str, str]] = None,
) -> None:
"""Initialize a new MrpService."""
super().__init__(identifier, Protocol.MRP, port, properties)
self.credentials = credentials
# pylint: disable=too-few-public-methods
class AirPlayService(BaseService):
"""Representation of an AirPlay service."""
def __init__(
self,
identifier: Optional[str],
port: int = 7000,
credentials: Optional[str] = None,
properties: Optional[Mapping[str, str]] = None,
) -> None:
"""Initialize a new AirPlayService."""
super().__init__(identifier, Protocol.AirPlay, port, properties)
self.credentials = credentials
# pylint: disable=too-few-public-methods
class CompanionService(BaseService):
"""Representation of a Companion link service."""
def __init__(
self,
port: int,
credentials: Optional[str] = None,
properties: Optional[Mapping[str, str]] = None,
) -> None:
"""Initialize a new CompaniomService."""
super().__init__(None, Protocol.Companion, port, properties)
self.credentials = credentials
# pylint: disable=too-few-public-methods
class RaopService(BaseService):
"""Representation of an RAOP service."""
def __init__(
self,
identifier: Optional[str],
port: int = 7000,
credentials: Optional[str] = None,
properties: Optional[Mapping[str, str]] = None,
) -> None:
"""Initialize a new RaopService."""
super().__init__(identifier, Protocol.RAOP, port, properties)
self.credentials = credentials
Classes
class AirPlayService (identifier: Optional[str], port: int = 7000, credentials: Optional[str] = None, properties: Optional[Mapping[str, str]] = None)
-
Representation of an AirPlay service.
Initialize a new AirPlayService.
Expand source code
class AirPlayService(BaseService): """Representation of an AirPlay service.""" def __init__( self, identifier: Optional[str], port: int = 7000, credentials: Optional[str] = None, properties: Optional[Mapping[str, str]] = None, ) -> None: """Initialize a new AirPlayService.""" super().__init__(identifier, Protocol.AirPlay, port, properties) self.credentials = credentials
Ancestors
Inherited members
class AppleTV (address: ipaddress.IPv4Address, name: str, deep_sleep: bool = False, model: DeviceModel = DeviceModel.Unknown, properties: Optional[Mapping[str, str]] = None)
-
Representation of an Apple TV configuration.
An instance of this class represents a single device. A device can have several services depending on the protocols it supports, e.g. DMAP or AirPlay.
Initialize a new AppleTV.
Expand source code
class AppleTV: """Representation of an Apple TV configuration. An instance of this class represents a single device. A device can have several services depending on the protocols it supports, e.g. DMAP or AirPlay. """ def __init__( self, address: IPv4Address, name: str, deep_sleep: bool = False, model: DeviceModel = DeviceModel.Unknown, properties: Optional[Mapping[str, str]] = None, ) -> None: """Initialize a new AppleTV.""" self._address = address self._name = name self._deep_sleep = deep_sleep self._model = model self._services: Dict[Protocol, BaseService] = {} self._properties: Mapping[str, str] = properties or {} @property def address(self) -> IPv4Address: """IP address of device.""" return self._address @property def name(self) -> str: """Name of device.""" return self._name @property def deep_sleep(self) -> bool: """If device is in deep sleep.""" return self._deep_sleep @property def ready(self) -> bool: """Return if configuration is ready, i.e. has a main service.""" ready_protocols = set(list(Protocol)) # Companion has no unique identifier so it's the only protocol that can't be # used independently for now ready_protocols.remove(Protocol.Companion) intersection = ready_protocols.intersection(self._services.keys()) return len(intersection) > 0 @property def identifier(self) -> Optional[str]: """Return the main identifier associated with this device.""" for prot in [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay, Protocol.RAOP]: service = self._services.get(prot) if service: return service.identifier return None @property def all_identifiers(self) -> List[str]: """Return all unique identifiers for this device.""" return [x.identifier for x in self.services if x.identifier is not None] def add_service(self, service: BaseService) -> None: """Add a new service. If the service already exists, it will be merged. """ existing = self._services.get(service.protocol) if existing is not None: existing.merge(service) else: self._services[service.protocol] = service def get_service(self, protocol: Protocol) -> Optional[BaseService]: """Look up a service based on protocol. If a service with the specified protocol is not available, None is returned. """ return self._services.get(protocol) @property def services(self) -> List[BaseService]: """Return all supported services.""" return list(self._services.values()) def main_service(self, protocol: Optional[Protocol] = None) -> BaseService: """Return suggested service used to establish connection.""" protocols = ( [protocol] if protocol is not None else [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay, Protocol.RAOP] ) for prot in protocols: service = self._services.get(prot) if service is not None: return service raise exceptions.NoServiceError("no service to connect to") def set_credentials(self, protocol: Protocol, credentials: str) -> bool: """Set credentials for a protocol if it exists.""" service = self.get_service(protocol) if service: service.credentials = credentials return True return False # TODO: The extraction should be generialized and moved somewhere else. It is # very hard to test rght now. @property def device_info(self) -> DeviceInfo: """Return general device information.""" properties = self._all_properties() build: Optional[str] = properties.get("systembuildversion") version = properties.get("ov") if not version: version = properties.get("osvers", lookup_version(build)) model_name: Optional[str] = properties.get("model", properties.get("am")) if model_name: model = lookup_model(model_name) else: model = self._model # MRP devices run tvOS (as far as we know now) as well as HomePods for # some reason if Protocol.MRP in self._services or model in [ DeviceModel.HomePod, DeviceModel.HomePodMini, ]: os_type = OperatingSystem.TvOS elif Protocol.DMAP in self._services: os_type = OperatingSystem.Legacy elif model in [DeviceModel.AirPortExpress, DeviceModel.AirPortExpressGen2]: os_type = OperatingSystem.AirPortOS else: os_type = OperatingSystem.Unknown mac = properties.get("macaddress", properties.get("deviceid")) if mac: mac = mac.upper() # The waMA property comes from the _airport._tcp.local service, announced by # AirPort Expresses (used by the admin tool). It contains various information, # for instance MAC address and software version. wama = properties.get("wama") if wama: props: Mapping[str, str] = dict( cast(Tuple[str, str], prop.split("=", maxsplit=1)) for prop in ("macaddress=" + wama).split(",") ) if not mac: mac = props["macaddress"].replace("-", ":").upper() version = props.get("syVs") return DeviceInfo(os_type, version, build, model, mac) def _all_properties(self) -> Mapping[str, str]: properties: Dict[str, str] = {} properties.update(self._properties) for service in self.services: properties.update(service.properties) return properties def __eq__(self, other) -> bool: """Compare instance with another instance.""" if isinstance(other, self.__class__): return self.identifier == other.identifier return False def __str__(self) -> str: """Return a string representation of this object.""" device_info = self.device_info services = "\n".join([" - {0}".format(s) for s in self._services.values()]) identifiers = "\n".join([" - {0}".format(x) for x in self.all_identifiers]) return ( f" Name: {self.name}\n" f" Model/SW: {device_info}\n" f" Address: {self.address}\n" f" MAC: {self.device_info.mac}\n" f" Deep Sleep: {self.deep_sleep}\n" f"Identifiers:\n" f"{identifiers}\n" f"Services:\n" f"{services}" )
Instance variables
var address -> ipaddress.IPv4Address
-
IP address of device.
Expand source code
@property def address(self) -> IPv4Address: """IP address of device.""" return self._address
var all_identifiers -> List[str]
-
Return all unique identifiers for this device.
Expand source code
@property def all_identifiers(self) -> List[str]: """Return all unique identifiers for this device.""" return [x.identifier for x in self.services if x.identifier is not None]
var deep_sleep -> bool
-
If device is in deep sleep.
Expand source code
@property def deep_sleep(self) -> bool: """If device is in deep sleep.""" return self._deep_sleep
var device_info -> DeviceInfo
-
Return general device information.
Expand source code
@property def device_info(self) -> DeviceInfo: """Return general device information.""" properties = self._all_properties() build: Optional[str] = properties.get("systembuildversion") version = properties.get("ov") if not version: version = properties.get("osvers", lookup_version(build)) model_name: Optional[str] = properties.get("model", properties.get("am")) if model_name: model = lookup_model(model_name) else: model = self._model # MRP devices run tvOS (as far as we know now) as well as HomePods for # some reason if Protocol.MRP in self._services or model in [ DeviceModel.HomePod, DeviceModel.HomePodMini, ]: os_type = OperatingSystem.TvOS elif Protocol.DMAP in self._services: os_type = OperatingSystem.Legacy elif model in [DeviceModel.AirPortExpress, DeviceModel.AirPortExpressGen2]: os_type = OperatingSystem.AirPortOS else: os_type = OperatingSystem.Unknown mac = properties.get("macaddress", properties.get("deviceid")) if mac: mac = mac.upper() # The waMA property comes from the _airport._tcp.local service, announced by # AirPort Expresses (used by the admin tool). It contains various information, # for instance MAC address and software version. wama = properties.get("wama") if wama: props: Mapping[str, str] = dict( cast(Tuple[str, str], prop.split("=", maxsplit=1)) for prop in ("macaddress=" + wama).split(",") ) if not mac: mac = props["macaddress"].replace("-", ":").upper() version = props.get("syVs") return DeviceInfo(os_type, version, build, model, mac)
var identifier -> Optional[str]
-
Return the main identifier associated with this device.
Expand source code
@property def identifier(self) -> Optional[str]: """Return the main identifier associated with this device.""" for prot in [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay, Protocol.RAOP]: service = self._services.get(prot) if service: return service.identifier return None
var name -> str
-
Name of device.
Expand source code
@property def name(self) -> str: """Name of device.""" return self._name
var ready -> bool
-
Return if configuration is ready, i.e. has a main service.
Expand source code
@property def ready(self) -> bool: """Return if configuration is ready, i.e. has a main service.""" ready_protocols = set(list(Protocol)) # Companion has no unique identifier so it's the only protocol that can't be # used independently for now ready_protocols.remove(Protocol.Companion) intersection = ready_protocols.intersection(self._services.keys()) return len(intersection) > 0
var services -> List[BaseService]
-
Return all supported services.
Expand source code
@property def services(self) -> List[BaseService]: """Return all supported services.""" return list(self._services.values())
Methods
def add_service(self, service: BaseService) -> NoneType
-
Add a new service.
If the service already exists, it will be merged.
Expand source code
def add_service(self, service: BaseService) -> None: """Add a new service. If the service already exists, it will be merged. """ existing = self._services.get(service.protocol) if existing is not None: existing.merge(service) else: self._services[service.protocol] = service
def get_service(self, protocol: Protocol) -> Optional[BaseService]
-
Look up a service based on protocol.
If a service with the specified protocol is not available, None is returned.
Expand source code
def get_service(self, protocol: Protocol) -> Optional[BaseService]: """Look up a service based on protocol. If a service with the specified protocol is not available, None is returned. """ return self._services.get(protocol)
def main_service(self, protocol: Optional[Protocol] = None) -> BaseService
-
Return suggested service used to establish connection.
Expand source code
def main_service(self, protocol: Optional[Protocol] = None) -> BaseService: """Return suggested service used to establish connection.""" protocols = ( [protocol] if protocol is not None else [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay, Protocol.RAOP] ) for prot in protocols: service = self._services.get(prot) if service is not None: return service raise exceptions.NoServiceError("no service to connect to")
def set_credentials(self, protocol: Protocol, credentials: str) -> bool
-
Set credentials for a protocol if it exists.
Expand source code
def set_credentials(self, protocol: Protocol, credentials: str) -> bool: """Set credentials for a protocol if it exists.""" service = self.get_service(protocol) if service: service.credentials = credentials return True return False
class CompanionService (port: int, credentials: Optional[str] = None, properties: Optional[Mapping[str, str]] = None)
-
Representation of a Companion link service.
Initialize a new CompaniomService.
Expand source code
class CompanionService(BaseService): """Representation of a Companion link service.""" def __init__( self, port: int, credentials: Optional[str] = None, properties: Optional[Mapping[str, str]] = None, ) -> None: """Initialize a new CompaniomService.""" super().__init__(None, Protocol.Companion, port, properties) self.credentials = credentials
Ancestors
Inherited members
class DmapService (identifier: Optional[str], credentials: Optional[str], port: int = 3689, properties: Optional[Mapping[str, str]] = None)
-
Representation of a DMAP service.
Initialize a new DmapService.
Expand source code
class DmapService(BaseService): """Representation of a DMAP service.""" def __init__( self, identifier: Optional[str], credentials: Optional[str], port: int = 3689, properties: Optional[Mapping[str, str]] = None, ) -> None: """Initialize a new DmapService.""" super().__init__( identifier, Protocol.DMAP, port, properties, ) self.credentials = credentials
Ancestors
Inherited members
class MrpService (identifier: Optional[str], port: int, credentials: Optional[str] = None, properties: Optional[Mapping[str, str]] = None)
-
Representation of a MediaRemote Protocol (MRP) service.
Initialize a new MrpService.
Expand source code
class MrpService(BaseService): """Representation of a MediaRemote Protocol (MRP) service.""" def __init__( self, identifier: Optional[str], port: int, credentials: Optional[str] = None, properties: Optional[Mapping[str, str]] = None, ) -> None: """Initialize a new MrpService.""" super().__init__(identifier, Protocol.MRP, port, properties) self.credentials = credentials
Ancestors
Inherited members
class RaopService (identifier: Optional[str], port: int = 7000, credentials: Optional[str] = None, properties: Optional[Mapping[str, str]] = None)
-
Representation of an RAOP service.
Initialize a new RaopService.
Expand source code
class RaopService(BaseService): """Representation of an RAOP service.""" def __init__( self, identifier: Optional[str], port: int = 7000, credentials: Optional[str] = None, properties: Optional[Mapping[str, str]] = None, ) -> None: """Initialize a new RaopService.""" super().__init__(identifier, Protocol.RAOP, port, properties) self.credentials = credentials
Ancestors
Inherited members