def call(self, action_call, action_data={}): """ Call an action from the registry Format: {namespace}.{action} or {namespace}.{component}.{action} """ command = self.parse_call(action_call) action = self._registry.get(command['namespace'], {}).get(command['action']) if not action: # raise MudPiError("Call to action that doesn't exists!") Logger.log( LOG_LEVEL["error"], f'{FONT_YELLOW}Call to action {action_call} that doesn\'t exists!.{FONT_RESET}' ) validated_data = action.validate(action_data) if not validated_data and action_data: # raise MudPiError("Action data was not valid!") Logger.log( LOG_LEVEL["error"], f'{FONT_YELLOW}Action data was not valid for {action_call}{FONT_RESET}' ) self.mudpi.events.publish( 'core', { 'event': 'ActionCall', 'action': action_call, 'data': action_data, 'namespace': command['namespace'] }) action(data=validated_data)
def check_time(self): """ Checks the time to see if it is currently sunrise or sunset """ # Get state object from manager state = self.mudpi.states.get(self.source) if state is not None: _state = state.state else: _state = None if _state: try: _value = self._parse_data(_state) _now = datetime.datetime.now().replace(microsecond=0) if _value: _value = datetime.datetime.strptime(_value, "%Y-%m-%d %I:%M:%S %p").replace(microsecond=0) + self.offset if _now == _value: self.active = True if self._previous_state != self.active: # Trigger is reset, Fire self.trigger(_value.strftime('%Y-%m-%d %I:%M:%S %p')) else: # Trigger not reset check if its multi fire if self.frequency == 'many': self.trigger(_value.strftime('%Y-%m-%d %I:%M:%S %p')) else: self.active = False except Exception as error: Logger.log(LOG_LEVEL["error"], f'Error evaluating thresholds for trigger {self.id}') Logger.log(LOG_LEVEL["debug"], error) self._previous_state = self.active
def update(self): """ Get data from GPIO through nanpy""" if self.node.connected: self.check_connection() try: data = self.node.api.analogRead( self.pin) if self.analog else self.node.api.digitalRead( self.pin) if self.type != 'potentiometer': self.previous_state = self._state self._state = data else: if (data < self.previous_state - self.buffer) or ( data > self.previous_state + self.buffer): self.previous_state = self._state self._state = data self.handle_state() except (SerialManagerError, SocketManagerError, BrokenPipeError, ConnectionResetError, OSError, socket.timeout) as e: if self.node.connected: Logger.log_formatted( LOG_LEVEL["warning"], f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') self.node.reset_connection() self._pin_setup = False else: self._pin_setup = False return None
def update(self): """ Get data from DHT through nanpy""" if self.node.connected: try: self.check_connection() if self._dht: _temp_format = self.mudpi.unit_system == IMPERIAL_SYSTEM temperature = self._dht.readTemperature(_temp_format) humidity = self._dht.readHumidity() data = { 'temperature': round(temperature, 2), 'humidity': round(humidity, 2) } self._state = data except (SerialManagerError, SocketManagerError, BrokenPipeError, ConnectionResetError, OSError, socket.timeout) as e: if self.node.connected: Logger.log_formatted( LOG_LEVEL["warning"], f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') self.node.reset_connection() self._dht = None else: self._dht = False return None
def update(self): """ Get data from DHT device""" humidity = None temperature_c = None if self.check_dht(): try: humidity, temperature_c = Adafruit_DHT.read_retry( self._sensor, self.pin) except Exception as error: # Errors happen fairly often, DHT's are hard to read Logger.log(LOG_LEVEL["debug"], error) if humidity is not None and temperature_c is not None: _temperature = temperature_c if self.mudpi.unit_system == METRIC_SYSTEM else ( temperature_c * 1.8 + 32) readings = { 'temperature': round(_temperature, 2), 'humidity': round(humidity, 2) } self._state = readings return readings else: Logger.log(LOG_LEVEL["debug"], f'DHT Reading was Invalid (Legacy).') time.sleep(2.1) return None
def next_step(self, event_data=None): """ Advance to the next sequnce step Makes sure any delays and durations are done """ # Step must be flagged complete to advance if self._step_complete: if self.active: # If skipping steps trigger unperformed actions if not self._step_triggered: if self.evaluate_thresholds(): self.trigger() # Sequence is already active, advance to next step if self._current_step < self.total_steps - 1: self.reset_step() self._current_step += 1 self.fire({"event": "SequenceStepStarted"}) Logger.log_formatted( LOG_LEVEL["info"], f'Sequence: {FONT_CYAN}{self.name}{FONT_RESET}', f'Step {self._current_step+1}/{self.total_steps}' ) else: # Last step of sequence completed self.active = False self.fire({"event": "SequenceEnded"}) Logger.log_formatted( LOG_LEVEL["info"], f'Sequence {FONT_CYAN}{self.name}{FONT_RESET}', 'Completed', 'success' ) self.reset_duration()
def update(self): """ Get data from T9602 device""" for trynb in range(5): # 5 tries try: data = self.bus.read_i2c_block_data(self.config['address'], 0, 4) break except OSError: Logger.log( LOG_LEVEL["info"], "Single reading error [t9602]. It happens, let's try again..." ) time.sleep(2) humidity = (((data[0] & 0x3F) << 8) + data[1]) / 16384.0 * 100.0 temperature_c = ((data[2] * 64) + (data[3] >> 2)) / 16384.0 * 165.0 - 40.0 humidity = round(humidity, 2) temperature_c = round(temperature_c, 2) if humidity is not None and temperature_c is not None: _temperature = temperature_c if self.mudpi.unit_system == METRIC_SYSTEM else (temperature_c * 1.8 + 32) readings = { 'temperature': _temperature, 'humidity': humidity } self._state = readings return readings else: Logger.log( LOG_LEVEL["error"], 'Failed to get reading [t9602]. Try again!' ) return None
def create(cls, mudpi, extension_name, extensions_module): """ Static method to load extension """ for path in extensions_module.__path__: config_path = os.path.join(path, extension_name, "extension.json") if not os.path.isfile(config_path): continue try: with open(config_path) as f: config = json.loads(f.read()) except FileNotFoundError: Logger.log( LOG_LEVEL["error"], f'{FONT_RED}No extension.json found at {config_path}.{FONT_RESET}' ) continue except Exception as e: Logger.log( LOG_LEVEL["error"], f'{FONT_RED}Error loading extension.json at {config_path} {error}.{FONT_RESET}' ) continue return cls(mudpi, config, f"{extensions_module.__name__}.{extension_name}", os.path.split(config_path)[0]) return None
def handle_event(self, event): """ Handle events from event system """ _event = None try: _event = decode_event_data(event) except Exception as error: _event = decode_event_data(event['data']) if _event == self._last_event: # Event already handled return self._last_event = _event if _event is not None: try: if _event['event'] == 'Message': if _event.get('data', None): self.add_message(_event['data']) elif _event['event'] == 'Clear': self.clear() elif _event['event'] == 'ClearQueue': self.clear_queue() except Exception as error: Logger.log(LOG_LEVEL["error"], f'Error handling event for {self.id}')
def handle_event(self, event): """ Handle the event data from the event system """ _event_data = decode_event_data(event) if _event_data == self._last_event: # Event already handled return self._last_event = _event_data if _event_data.get('event'): try: if _event_data['event'] == 'StateUpdated': if _event_data['component_id'] == self.source: sensor_value = self._parse_data( _event_data["new_state"]["state"]) if self.evaluate_thresholds(sensor_value): self.active = True if self._previous_state != self.active: # Trigger is reset, Fire self.trigger(_event_data) else: # Trigger not reset check if its multi fire if self.frequency == 'many': self.trigger(_event_data) else: self.active = False except Exception as error: Logger.log( LOG_LEVEL["error"], f'Error evaluating thresholds for trigger {self.id}') Logger.log(LOG_LEVEL["debug"], error) self._previous_state = self.active
def stop(self, data=None): """ Stop the timer """ if self.active: self._active = False self.reset() Logger.log( LOG_LEVEL["debug"], f'Timer Sensor {FONT_MAGENTA}{self.name}{FONT_RESET} Stopped')
def update(self): """ Main run loop for sequence to check time past and if it should fire actions """ if self.mudpi.is_prepared: try: if self.active: if not self._step_complete: if not self._delay_complete: if self.step_delay is not None: if self.duration > self.step_delay: self._delay_complete = True self._delay_actual = self.duration self.reset_duration() else: # Waiting break early return else: self._delay_complete = True self.reset_duration() if self._delay_complete: if not self._step_triggered: if self.evaluate_thresholds(): self.trigger() else: if self.current_step.get('thresholds') is not None: # Thresholds failed skip step without trigger self._step_triggered = True self._step_complete = True if self.step_duration is not None and not self._step_complete: if self.duration > self.step_duration: self._step_complete = True self._duration_actual = self.duration self.reset_duration() else: # Waiting break early return else: # No duration set meaning step only advances # manualy by calling actions and events. RTM pass if self._step_complete: self.fire({"event": "SequenceStepEnded"}) # Logger.log( # LOG_LEVEL["debug"], # f'Sequence {FONT_CYAN}{self.name}{FONT_RESET} Step {self._current_step+1} Debug\n' \ # f'Delay: {self.step_delay} Actual: {self._delay_actual} Duration: {self.step_duration} Actual: {self._duration_actual}' # ) return self.next_step() else: # Sequence is not active. self.reset_duration() except Exception as e: Logger.log_formatted(LOG_LEVEL["error"], f'Sequence {self.id}', 'Unexpected Error', 'error') Logger.log(LOG_LEVEL["critical"], e)
def handle_event(self, data={}): """ Handle event from mqtt broker """ if data is not None: try: # _event_data = self.last_event = decode_event_data(data) self._state = data except: Logger.log(LOG_LEVEL["info"], f"Error Decoding Event for MQTT Sensor {self.id}")
def run(self, func=None): """ Create a thread and return it """ if not self._thread: self._thread = threading.Thread(target=self.work, args=(func, )) Logger.log_formatted(LOG_LEVEL["debug"], f"Worker {self.key} ", "Starting", "notice") self._thread.start() Logger.log_formatted(LOG_LEVEL["info"], f"Worker {self.key} ", "Started", "success") return self._thread
def reset(self, event_data=None): """ Reset the entire sequence """ self._current_step = 0 self.reset_step() self.fire({ "event": "SequenceReset" }) Logger.log_formatted( LOG_LEVEL["info"], f'Sequence {FONT_CYAN}{self.name}{FONT_RESET}', 'Reset', 'warning' )
def restore_states(self): """ Restore previous components states on first boot """ _comp_ids = self.components.ids() for state_id in self.states.ids(): if state_id in _comp_ids: comp = self.components.get(state_id) comp.restore_state(self.states.get(state_id)) Logger.log(LOG_LEVEL["debug"], f"Restored State for {state_id}") else: self.states.remove(state_id)
def import_config_dir(self): """ Add config dir to sys path so we can import extensions """ if self.mudpi.config_path is None: Logger.log( LOG_LEVEL["error"], f'{RED_BACK}Could not import config_path - No path was set.{FONT_RESET}' ) return False if self.mudpi.config_path not in sys.path: sys.path.insert(0, self.mudpi.config_path) return True
def connect(self, connection): """ Connect the sensor to redis """ self.conn = connection if self.type in ("current", "forecast"): self.sensor = "https://api.openweathermap.org/data/2.5/onecall?exclude=minutely&%s" % ( str(self.conn)) elif self.type in ("historical"): self.sensor = "https://api.openweathermap.org/data/2.5/onecall/timemachine?%s&dt=" % ( str(self.conn)) Logger.log(LOG_LEVEL["debug"], 'OwmapiSensor: apicall: ' + str(self.sensor))
def handle_event(self, event={}): """ Handle event from redis pubsub """ data = decode_event_data(event['data']) if data is not None: try: # _event_data = self.last_event = decode_event_data(data) self._state = data except: Logger.log( LOG_LEVEL["info"], f"Error Decoding Event for Redis Sensor {self.id}" )
def connect(self): """ Setup connections for all adaptors """ connection_data = {} for key, adaptor in self.adaptors.items(): Logger.log_formatted(LOG_LEVEL["debug"], f"Preparing Event System for {key} ", 'Pending', 'notice') connection_data[key] = adaptor.connect() Logger.log_formatted(LOG_LEVEL["info"], f"Event System Ready on {key} ", 'Connected', 'success') return connection_data
def _install_extension_requirements(mudpi, extension): """ Installs all the extension requirements """ cache = mudpi.cache.setdefault('extensions_requirements_installed', {}) if cache.get(extension.namespace) is not None: # Already processed and installed return cache[extension.namespace] # Handle all the dependencies requirements if extension.has_dependencies: if extension.import_dependencies(): for dependency in extension.loaded_dependencies: try: dependency_extension = get_extension_importer( mudpi, dependency) except Exception as error: Logger.log( LOG_LEVEL["error"], f'Error getting extension <{extension}> dependency: {FONT_YELLOW}{dependency}{FONT_RESET}' ) if not dependency_extension.install_requirements(): Logger.log( LOG_LEVEL["error"], f'Error with extension <{extension}> dependency: {FONT_YELLOW}{dependency}{FONT_RESET} requirements.' ) if not extension.has_requirements: cache[extension.namespace] = extension return cache[extension.namespace] requirement_cache = mudpi.cache.setdefault('requirement_installed', {}) for requirement in extension.requirements: if requirement not in requirement_cache: if not utils.is_package_installed(requirement): Logger.log_formatted( LOG_LEVEL["info"], f'{FONT_YELLOW}{extension.namespace.title()}{FONT_RESET} requirements', 'Installing', 'notice') Logger.log( LOG_LEVEL["debug"], f'Installing package {FONT_YELLOW}{requirement}{FONT_RESET}', ) if not utils.install_package(requirement): Logger.log( LOG_LEVEL["error"], f'Error installing <{extension.title()}> requirement: {FONT_YELLOW}{requirement}{FONT_RESET}' ) return False requirement_cache[requirement] = True # extension.requirements_installed = True cache[extension.namespace] = extension return cache[extension.namespace]
def start(self, event_data=None): """ Start the sequence """ if not self.active: self._current_step = 0 self.active = True self.reset_step() self.fire({ "event": "SequenceStarted" }) Logger.log_formatted( LOG_LEVEL["info"], f'Sequence {FONT_CYAN}{self.name}{FONT_RESET}', 'Started', 'success' )
def stop(self, event_data=None): """ Stop the sequence """ if self.active: self._current_step = 0 self.active = False self.reset_step() self.fire({ "event": "SequenceStopped" }) Logger.log_formatted( LOG_LEVEL["info"], f'Sequence {FONT_CYAN}{self.name}{FONT_RESET}', 'Stopped', 'error' )
def validate(self, config): """ Validate the trigger config """ if not isinstance(config, list): config = [config] for conf in config: if not conf.get('schedule'): Logger.log( LOG_LEVEL["debug"], 'Trigger: No `schedule`, defaulting to every 5 mins') # raise ConfigError('Missing `schedule` in Trigger config.') return config
def check_dht(self): """ Check if the DHT device is setup """ if self._sensor is None: try: self._sensor = self._dht_device except Exception as error: Logger.log( LOG_LEVEL["error"], 'Sensor Initialize Error: DHT (Legacy) Failed to Init') self._sensor = None Logger.log(LOG_LEVEL["debug"], error) return False return True
def init(self): """ Setup the socket """ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.client_threads = [] self._server_ready = threading.Event() self._server_ready.set() self._server_running = False try: self.sock.bind((self.host, self.port)) except socket.error as msg: Logger.log(LOG_LEVEL['error'], f'Failed to create socket. Error Code: {str(msg[0])} Error Message: {msg[1]}') return False return True
def get_extension_importer(mudpi, extension, install_requirements=False): """ Find or create an extension importer, Loads it if not loaded, Checks cache first. Set install_requirements to True to also have all requirements checked through pip. """ # First we check if the namespace is disabled. Could be due to errors or configs disabled_cache = mudpi.cache.setdefault("disabled_namespaces", {}) if extension in disabled_cache: raise MudPiError(f"Extension is {extension} is disabled.") if install_requirements: extension_importer = _extension_with_requirements_installed( mudpi, extension) if extension_importer is not None: return extension_importer importer_cache = mudpi.cache.setdefault("extension_importers", {}) try: extension_importer = importer_cache.get(extension) if extension_importer is not None: return extension_importer except Exception as error: extension_importer = None if extension_importer is None: extension_importer = _get_custom_extensions(mudpi).get(extension) if extension_importer is not None: Logger.log( LOG_LEVEL["warning"], f'{FONT_YELLOW}You are using {extension} which is not provided by MudPi.{FONT_RESET}\nIf you experience errors, remove it.' ) return extension_importer # Component not found look in internal extensions from mudpi import extensions extension_importer = ExtensionImporter.create(mudpi, extension, extensions) if extension_importer is not None: importer_cache[extension] = extension_importer Logger.log_formatted( LOG_LEVEL["info"], f'{extension_importer.namespace.title()} Ready for Import', 'Ready', 'success') else: Logger.log_formatted(LOG_LEVEL["debug"], f'Import Preperations for {extension.title()}', 'error', 'error') Logger.log( LOG_LEVEL["debug"], f'{FONT_YELLOW}`{extension.title()}` was not found.{FONT_RESET}') disabled_cache[extension] = 'Not Found' raise ExtensionNotFound(extension) return extension_importer
def check(self): """ Check trigger schedule thresholds """ if self.mudpi.is_running: try: if pycron.is_now(self.schedule): self.trigger() if not self.active: self.active = True else: if self.active: self.active = False except Exception as error: Logger.log(LOG_LEVEL["error"], "Error evaluating time trigger schedule.") return
def validate(self, config): """ Validate the dht config """ if not isinstance(config, list): config = [config] for conf in config: if not conf.get('pin'): raise ConfigError('Missing `pin` in DHT config.') if str(conf.get('model')) not in DHTSensor.models: conf['model'] = '11' Logger.log(LOG_LEVEL["warning"], 'Sensor Model Error: Defaulting to DHT11') return config
def start(self, data=None): """ Start the timer """ if not self.active: self.reset_duration() self._active = True if self._pause_offset == 0: Logger.log( LOG_LEVEL["debug"], f'Timer Sensor {FONT_MAGENTA}{self.name}{FONT_RESET} Started' ) else: Logger.log( LOG_LEVEL["debug"], f'Timer Sensor {FONT_MAGENTA}{self.name}{FONT_RESET} Resumed' )