"""Manages configuration file.""" import collections import configparser import logging from typing import Dict, List, Optional Device = collections.namedtuple( 'Device', ['name', 'serial', 'credentials', 'product_type']) DysonLinkCredentials = collections.namedtuple( 'DysonLinkCredentials', ['username', 'password', 'country']) logger = logging.getLogger(__name__) class Config: """Reads the configuration file and provides handy accessors. Args: filename: path (absolute or relative) to the config file (ini format). """ def __init__(self, filename: str): self._filename = filename self._config = self.load(filename) @classmethod def load(cls, filename: str): """Reads configuration file. Returns DysonLinkCredentials or None on error, and a dict of configured device serial numbers mapping to IP addresses """ config = configparser.ConfigParser() logger.info('Reading "%s"', filename) try: config.read(filename) except configparser.Error as ex: logger.critical('Could not read "%s": %s', filename, ex) raise ex return config @property def dyson_credentials(self) -> Optional[DysonLinkCredentials]: """Cloud Dyson API credentials. In the config, this looks like: [Dyson Link] username = user password = pass country = XX Returns: DysonLinkCredentials. """ try: username = self._config['Dyson Link']['username'] password = self._config['Dyson Link']['password'] country = self._config['Dyson Link']['country'] return DysonLinkCredentials(username, password, country) except KeyError as ex: logger.warning( 'Required key missing in "%s": %s', self._filename, ex) return None @property def hosts(self) -> Dict[str, str]: """Loads the Hosts section, which is a serial -> IP address override. This is useful if you don't want to discover devices using zeroconf. The Hosts section looks like this: [Hosts] AB1-UK-AAA0111A = 192.168.1.2 """ try: hosts = self._config.items('Hosts') except configparser.NoSectionError: hosts = [] logger.debug( 'No "Hosts" section found in config file, no manual IP overrides are available') # Convert the hosts tuple (('serial0', 'ip0'), ('serial1', 'ip1')) # into a dict {'SERIAL0': 'ip0', 'SERIAL1': 'ip1'}, making sure that # the serial keys are upper case (configparser downcases everything) return {h[0].upper(): h[1] for h in hosts} @property def devices(self) -> List[Device]: """Consumes all sections looking for device entries. A device looks a bit like this: [AB1-UK-AAA0111A] name = Living room active = true localcredentials = 12345== serial = AB1-UK-AAA0111A ... (and a few other fields) Returns: A list of Device objects. """ sections = self._config.sections() ret = [] for sect in sections: if not self._config.has_option(sect, 'LocalCredentials'): # This is probably not a device entry, so ignore it. continue ret.append(Device( self._config[sect]['Name'], self._config[sect]['Serial'], self._config[sect]['LocalCredentials'], self._config[sect]['ProductType'])) return ret