def read_config(): """Open the YAML configuration file and check the contents""" try: yaml.FullLoader.add_constructor('!secret', secret_yaml) yaml_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), CONFIG_YAML) config = config_from_yaml(data=yaml_file, read_from_file=True) if config: config = check_config(config) return config except ConfigError as e: raise FailedInitialization(f"ConfigError exception: {e}") except FailedInitialization: raise except Exception as e: error_message = buildYAMLExceptionString(exception=e, file=yaml_file) raise FailedInitialization(f"Unexpected exception: {error_message}")
def check_config(mqtt): """Check that the needed YAML options exist.""" errors = False required = { 'enable': bool, 'client': str, 'ip': str, 'port': int, 'username': str, 'password': str } options = dict(mqtt) for key in required: if key not in options.keys(): _LOGGER.error( f"Missing required 'mqtt' option in YAML file: '{key}'") errors = True else: v = options.get(key, None) if not isinstance(v, required.get(key)): _LOGGER.error( f"Expected type '{required.get(key).__name__}' for option 'mqtt.{key}'" ) errors = True if errors: raise FailedInitialization( Exception("Errors detected in 'mqtt' YAML options")) return options
def retrieve_options(config, key, option_list) -> dict: """Retrieve requested options.""" if key not in config.keys(): return {} errors = False options = dict(config[key]) for option, value in option_list.items(): required = value.get('required', None) type = value.get('type', None) if required: if option not in options.keys(): _LOGGER.error( f"Missing required option in YAML file: '{option}'") errors = True else: v = options.get(option, None) if not isinstance(v, type): _LOGGER.error( f"Expected type '{type}' for option '{option}'") errors = True if errors: raise FailedInitialization( f"One or more errors detected in '{key}' YAML options") return options
def check_unsupported(yaml, required, path=''): try: passed = True if not yaml: raise FailedInitialization("YAML file is corrupt or truncated, nothing left to parse") if isinstance(yaml, list): for index, element in enumerate(yaml): for yk in element.keys(): listpath = f"{path}.{yk}[{index}]" yamlValue = dict(element).get(yk, None) for rk in required: supportedSubkeys = rk.get(yk, None) if supportedSubkeys: break if not supportedSubkeys: _LOGGER.info(f"'{listpath}' option is unsupported") return subkeyList = supportedSubkeys.get('keys', None) if subkeyList: passed = check_unsupported(yamlValue, subkeyList, listpath) and passed elif isinstance(yaml, dict) or isinstance(yaml, Configuration): for yk in yaml.keys(): currentpath = path + yk if path == '' else path + '.' + yk yamlValue = dict(yaml).get(yk, None) for rk in required: supportedSubkeys = rk.get(yk, None) if supportedSubkeys: break if not supportedSubkeys: _LOGGER.info(f"'{currentpath}' option is unsupported") return subkeyList = supportedSubkeys.get('keys', None) if subkeyList: passed = check_unsupported(yamlValue, subkeyList, currentpath) and passed else: raise FailedInitialization('Unexpected YAML checking error') except FailedInitialization: raise except Exception as e: raise FailedInitialization(f"Unexpected exception: {e}") return passed
def check_config(config): """Check that the important options are present and unknown options aren't.""" required_keys = [ { 'cs_esphome': {'required': True, 'keys': [ {'circuitsetup': {'required': True, 'keys': [ {'url': {'required': True, 'keys': [], 'type': str}}, {'port': {'required': False, 'keys': [], 'type': int}}, {'password': {'required': False, 'keys': [], 'type': str}}, ]}}, {'influxdb2': {'required': False, 'keys': [ {'org': {'required': True, 'keys': [], 'type': str}}, {'url': {'required': True, 'keys': [], 'type': str}}, {'bucket': {'required': True, 'keys': [], 'type': str}}, {'token': {'required': True, 'keys': [], 'type': str}}, {'pruning': {'required': True, 'keys': [ {'task': {'required': True, 'keys': [ {'name': {'required': True, 'keys': [], 'type': str}}, {'predicate': {'required': True, 'keys': [], 'type': str}}, {'keep_last': {'required': True, 'keys': [], 'type': int}}, ]}}, ]}}, ]}}, {'debug': {'required': False, 'keys': [ {'create_bucket': {'required': False, 'keys': [], 'type': bool}}, {'delete_bucket': {'required': False, 'keys': [], 'type': bool}}, {'fill_data': {'required': False, 'keys': [], 'type': bool}}, ]}}, {'sensors': {'required': True, 'keys': [ {'sensor': {'required': True, 'keys': [ {'enable': {'required': False, 'keys': [], 'type': bool}}, {'sensor_name': {'required': True, 'keys': [], 'type': str}}, {'display_name': {'required': True, 'keys': [], 'type': str}}, {'measurement': {'required': True, 'keys': [], 'type': str}}, {'device': {'required': True, 'keys': [], 'type': str}}, {'location': {'required': True, 'keys': [], 'type': str}}, {'integrate': {'required': False, 'keys': [], 'type': bool}}, ]}}, ]}}, {'settings': {'required': False, 'keys': [ {'sampling': {'required': False, 'keys': [ {'delta_wh': {'required': False, 'keys': [], 'type': int}}, {'integrations': {'required': False, 'keys': [ {'today': {'required': False, 'keys': [], 'type': int}}, {'month': {'required': False, 'keys': [], 'type': int}}, {'year': {'required': False, 'keys': [], 'type': int}}, ]}}, {'locations': {'required': False, 'keys': [ {'today': {'required': False, 'keys': [], 'type': int}}, {'month': {'required': False, 'keys': [], 'type': int}}, {'year': {'required': False, 'keys': [], 'type': int}}, ]}}, ]}}, {'watchdog': {'required': False, 'keys': [], 'type': int}}, ]}}, ]}, }, ] try: result = check_required_keys(dict(config), required_keys) check_unsupported(dict(config), required_keys) except FailedInitialization: raise except Exception as e: raise FailedInitialization(f"Unexpected exception: {e}") return config if result else None
def check_required_keys(yaml, required, path='') -> bool: passed = True for keywords in required: for rk, rv in keywords.items(): currentpath = path + rk if path == '' else path + '.' + rk requiredKey = rv.get('required') requiredSubkeys = rv.get('keys') keyType = rv.get('type', None) typeStr = '' if not keyType else f" (type is '{keyType.__name__}')" if not yaml: raise FailedInitialization(f"YAML file is corrupt or truncated, expecting to find '{rk}' and found nothing") if isinstance(yaml, list): for index, element in enumerate(yaml): path = f"{currentpath}[{index}]" yamlKeys = element.keys() if requiredKey: if rk not in yamlKeys: _LOGGER.error(f"'{currentpath}' is required for operation {typeStr}") passed = False continue yamlValue = dict(element).get(rk, None) if yamlValue is None: return passed if rk in yamlKeys and keyType and not isinstance(yamlValue, keyType): _LOGGER.error(f"'{currentpath}' should be type '{keyType.__name__}'") passed = False if isinstance(requiredSubkeys, list): if len(requiredSubkeys): passed = check_required_keys(yamlValue, requiredSubkeys, path) and passed else: raise FailedInitialization(Exception('Unexpected YAML checking error')) elif isinstance(yaml, dict) or isinstance(yaml, Configuration): yamlKeys = yaml.keys() if requiredKey: if rk not in yamlKeys: _LOGGER.error(f"'{currentpath}' is required for operation {typeStr}") passed = False continue yamlValue = dict(yaml).get(rk, None) if yamlValue is None: return passed if rk in yamlKeys and keyType and not isinstance(yamlValue, keyType): _LOGGER.error(f"'{currentpath}' should be type '{keyType.__name__}'") passed = False if isinstance(requiredSubkeys, list): if len(requiredSubkeys): passed = check_required_keys(yamlValue, requiredSubkeys, currentpath) and passed else: raise FailedInitialization('Unexpected YAML checking error') else: raise FailedInitialization('Unexpected YAML checking error') return passed
def start(self): """Initialize the InfluxDB client.""" try: influxdb_options = retrieve_options(self._config, 'influxdb2', _INFLUXDB2_OPTIONS) debug_options = retrieve_options(self._config, 'debug', _DEBUG_OPTIONS) except FailedInitialization as e: _LOGGER.error(f"{e}") return False if len(influxdb_options.keys()) == 0: raise FailedInitialization("missing 'influxdb2' options") result = False try: self._bucket = influxdb_options.get('bucket', None) self._url = influxdb_options.get('url', None) self._token = influxdb_options.get('token', None) self._org = influxdb_options.get('org', None) self._client = InfluxDBClient(url=self._url, token=self._token, org=self._org, enable_gzip=True) if not self._client: raise FailedInitialization( f"failed to get InfluxDBClient from '{self._url}' (check url, token, and/or organization)" ) self._write_api = self._client.write_api(write_options=SYNCHRONOUS) self._query_api = self._client.query_api() self._delete_api = self._client.delete_api() self._tasks_api = self._client.tasks_api() self._organizations_api = self._client.organizations_api() cs_esphome_debug = os.getenv(_DEBUG_ENV_VAR, 'False').lower() in ('true', '1', 't') try: if cs_esphome_debug and debug_options.get( 'delete_bucket', False): self.delete_bucket() _LOGGER.info( f"Deleted bucket '{self._bucket}' at '{self._url}'") except InfluxDBBucketError as e: raise FailedInitialization(f"{e}") try: if not self.connect_bucket( cs_esphome_debug and debug_options.get('create_bucket', False)): raise FailedInitialization( f"Unable to access (or create) bucket '{self._bucket}' at '{self._url}'" ) except InfluxDBBucketError as e: raise FailedInitialization(f"{e}") _LOGGER.info( f"Connected to InfluxDB: '{self._url}', bucket '{self._bucket}'" ) result = True except FailedInitialization as e: _LOGGER.error(f" client {e}") self._client = None except NewConnectionError: _LOGGER.error( f"InfluxDB client unable to connect to host at {self._url}") except ApiException as e: _LOGGER.error( f"InfluxDB client unable to access bucket '{self._bucket}' at {self._url}: {e.reason}" ) except Exception as e: _LOGGER.error(f"Unexpected exception: {e}") finally: return result
def check_config(config): """Check that the important options are present and unknown options aren't.""" required_keys = [ { 'multisma2': { 'required': True, 'keys': [ { 'site': { 'required': True, 'keys': [ { 'name': { 'required': True, 'keys': [], 'type': str } }, { 'region': { 'required': True, 'keys': [], 'type': str } }, { 'tz': { 'required': True, 'keys': [], 'type': str } }, { 'latitude': { 'required': True, 'keys': [], 'type': float } }, { 'longitude': { 'required': True, 'keys': [], 'type': float } }, { 'elevation': { 'required': True, 'keys': [], 'type': float } }, { 'co2_avoided': { 'required': True, 'keys': [], 'type': float } }, ] } }, { 'solar_properties': { 'required': True, 'keys': [ { 'azimuth': { 'required': True, 'keys': [], 'type': float } }, { 'tilt': { 'required': True, 'keys': [], 'type': float } }, { 'area': { 'required': True, 'keys': [], 'type': float } }, { 'efficiency': { 'required': True, 'keys': [], 'type': float } }, { 'rho': { 'required': True, 'keys': [], 'type': float } }, ] } }, { 'influxdb2': { 'required': False, 'keys': [ { 'enable': { 'required': True, 'keys': [], 'type': bool } }, { 'org': { 'required': True, 'keys': [], 'type': str } }, { 'url': { 'required': True, 'keys': [], 'type': str } }, { 'bucket': { 'required': True, 'keys': [], 'type': str } }, { 'token': { 'required': True, 'keys': [], 'type': str } }, { 'pruning': { 'required': True, 'keys': [ { 'task': { 'required': True, 'keys': [ { 'name': { 'required': True, 'keys': [], 'type': str } }, { 'predicate': { 'required': True, 'keys': [], 'type': str } }, { 'keep_last': { 'required': True, 'keys': [], 'type': int } }, ] } }, ] } }, ] } }, { 'mqtt': { 'required': False, 'keys': [ { 'enable': { 'required': True, 'keys': [], 'type': bool } }, { 'client': { 'required': True, 'keys': [], 'type': str } }, { 'ip': { 'required': True, 'keys': [], 'type': str } }, { 'port': { 'required': True, 'keys': [], 'type': int } }, { 'username': { 'required': True, 'keys': [], 'type': str } }, { 'password': { 'required': True, 'keys': [], 'type': str } }, ] } }, { 'inverters': { 'required': True, 'keys': [ { 'inverter': { 'required': True, 'keys': [ { 'name': { 'required': True, 'keys': [], 'type': str } }, { 'url': { 'required': True, 'keys': [], 'type': str } }, { 'username': { 'required': True, 'keys': [], 'type': str } }, { 'password': { 'required': True, 'keys': [], 'type': str } }, ] } }, ] } }, { 'settings': { 'required': False, 'keys': [ { 'sampling': { 'required': False, 'keys': [ { 'fast': { 'required': False, 'keys': [], 'type': int } }, { 'medium': { 'required': False, 'keys': [], 'type': int } }, { 'slow': { 'required': False, 'keys': [], 'type': int } }, ] } }, ] } }, ], }, }, ] try: result = check_required_keys(dict(config), required_keys) check_unsupported(dict(config), required_keys) except FailedInitialization: raise except Exception as e: raise FailedInitialization(f"Unexpected exception: {e}") return config if result else None