class TestDigitalSource(SignalSource): """ Random test digital input signal source. """ def __init__(self, identifier: str, interval: float, text_0: str = 'off', text_1: str = 'on', **kwargs): super().__init__(identifier, **kwargs) self.text_0 = text_0 self.text_1 = text_1 self._timer = RepeatTimer(interval, self._send_value) def _send_value(self) -> None: self._send(random.choice([0, 1]), self.STATUS_OK) def start(self) -> None: super().start() self._timer.start() def stop(self) -> None: super().stop() self._timer.cancel() def format(self, value: float) -> str: return self.text_1 if value != 0 else self.text_0
class PowerMeterApatorEC3Repeating: min_averaging_secs: float _power_meter: PowerMeterApatorEC3 _timer: RepeatTimer reading: Optional[PowerMeterReading] reading_ts: Optional[float] success: bool high: SingleCounter low: SingleCounter callbacks: List[Callable[[Optional[PowerMeterReading]], None]] def __init__(self, power_meter: PowerMeterApatorEC3, interval: float, min_averaging_secs: float): self.min_averaging_secs = min_averaging_secs self._power_meter = power_meter self._timer = RepeatTimer(interval, self._acquire) self.reading = None self.reading_ts = None self.success = False self.high = SingleCounter() self.low = SingleCounter() self.callbacks = [] def add_callback(self, callback: Callable[[Optional[PowerMeterReading]], None]): self.callbacks.append(callback) def start(self): if not self._timer.is_alive(): self._timer.start() def stop(self): self._timer.cancel() self._power_meter.close() def _acquire(self): try: ts = time.time() self.reading = self._power_meter.read() self.reading_ts = ts self._update_high_power() self._update_low_power() self.success = True except SerialException: self.success = False self._fire() def _update_low_power(self): self.low.update(self.reading.consumption_low_sum_kwh, self.reading_ts, self.min_averaging_secs, self.high) def _update_high_power(self): self.high.update(self.reading.consumption_high_sum_kwh, self.reading_ts, self.min_averaging_secs, self.low) def _fire(self): for callback in self.callbacks: callback(self.reading)
class DigitalInSource(SignalSource): """ Digital GPIO input signal source. """ pi: pigpio def __init__(self, identifier: str, pigpio_pi: pigpio, gpio_bcm: int, interval: float, text_0: str = 'off', text_1: str = 'on', **kwargs): super().__init__(identifier, **kwargs) self.pi = pigpio_pi self.gpio_bcm = gpio_bcm self.interval = interval self.text_0 = text_0 self.text_1 = text_1 if self.pi.connected: self.pi.set_mode(self.gpio_bcm, pigpio.INPUT) self.pi.set_pull_up_down(self.gpio_bcm, pigpio.PUD_OFF) else: raise PigpioNotConnectedError( 'pigpio.pi is not connected, input for gpio ' + str(gpio_bcm) + ' will not work') self._timer = RepeatTimer(interval, self._read_and_send_value) def start(self) -> None: super().start() self._timer.start() def stop(self) -> None: super().stop() self._timer.cancel() def _read_and_send_value(self) -> None: reading = self.read_once() if reading is not None: self._send(reading, self.STATUS_OK) else: self._send(0, self.STATUS_MISSING) def read_once(self) -> Optional[int]: return self.pi.read(self.gpio_bcm) if self.pi.connected else None def format(self, value: float) -> str: return self.text_1 if value != 0 else self.text_0 def __repr__(self) -> str: return super().__repr__() + ' gpio_bcm=' + str(self.gpio_bcm)
class TestSource(SignalSource): """ Random test measurement signal source. """ def __init__(self, identifier: str, value: float, interval: float, **kwargs): super().__init__(identifier, **kwargs) self.value = value self.interval = interval self._timer = RepeatTimer(interval, self._send_random_value) def _send_random_value(self) -> None: self._send(round(random.gauss(self.value, 2), 3), self.STATUS_OK) def start(self) -> None: super().start() self._timer.start() def stop(self) -> None: super().stop() self._timer.cancel()
class TestSource(SignalSource): """ Random test measurement signal source. """ def __init__(self, value, interval, **kwargs): super().__init__(**kwargs) self.value = value self.interval = interval self._timer = RepeatTimer(interval, self._send_value) def _send_value(self): self._send(random.gauss(self.value, 2), self.STATUS_OK) def start(self, *args): super().start(*args) self._timer.start() def stop(self, *args): super().stop(*args) self._timer.cancel()
class Ds1820Source(SignalSource): """ Temperature measurement signal source from DS18x20 connected to W1 bus GPIO. """ def __init__(self, identifier: str, sensor_id: str, interval: float, **kwargs): super().__init__(identifier, **kwargs) self.sensor_id = sensor_id self._timer = RepeatTimer(interval, self._read_and_send_value) def start(self) -> None: super().start() self._timer.start() def stop(self) -> None: super().stop() self._timer.cancel() def _read_and_send_value(self) -> None: temp = self.read_once() if temp is not None: self._send(round(temp, 3), self.STATUS_OK) def read_once(self) -> Optional[float]: try: with open('/sys/bus/w1/devices/' + self.sensor_id + '/w1_slave', 'r') as file: file.readline() temp_line = file.readline() match = re.search('.*t=(-?[0-9]+)', temp_line) if match is not None: return float(match.group(1)) / 1000. except OSError: logger.warning("Failed to read DS1820 file for " + self.sensor_id) return None def __repr__(self) -> str: return super().__repr__() + ' id=' + self.sensor_id
class TestDigitalSource(SignalSource): """ Random test digital input signal source. """ def __init__(self, interval, text_0='off', text_1='on', **kwargs): super().__init__(**kwargs) self.text_0 = text_0 self.text_1 = text_1 self._timer = RepeatTimer(interval, self._send_value) def _send_value(self): self._send(random.choice([0, 1]), self.STATUS_OK) def start(self, *args): super().start(*args) self._timer.start() def stop(self, *args): super().stop(*args) self._timer.cancel() def format(self, value): return self.text_1 if value != 0 else self.text_0
class SignalHistory: MAX_SECONDS_DEFAULT = 24 * 3600 # 1 day DELTA_SECONDS_DEFAULT = 60 # every minute MAX_SECONDS_CSV_FILES = 24 * 3600 * 32 # 32 days def __init__(self): self.max_seconds = SignalHistory.MAX_SECONDS_DEFAULT self.delta_seconds = SignalHistory.DELTA_SECONDS_DEFAULT self.max_seconds_csv_files = SignalHistory.MAX_SECONDS_CSV_FILES self.max_csv_lines = self.max_seconds // self.delta_seconds + 1 self.sources = [] self._values_by_source_id = {} self._timer = None self._data_lock = RLock() self._csv_file_basename = None self._csv_file = None self._csv_writer = None self._csv_lines = 0 def __enter__(self): self._data_lock.acquire() def __exit__(self, exc_type, exc_value, traceback): self._data_lock.release() def add_source(self, signal_source): with self._data_lock: if signal_source in self.sources: self.sources.remove(signal_source) self.sources.append(signal_source) self._values_by_source_id[id(signal_source)] = [] def remove_source(self, signal_source): with self._data_lock: self.sources.remove(signal_source) self._values_by_source_id.pop(id(signal_source)) def start(self): if self._timer is None: logger.info('Starting to record history every ' + str(self.delta_seconds) + 's for ' + str(self.max_seconds) + 's') self._begin_new_csv_file() self._timer = RepeatTimer(self.delta_seconds, self.record) self._timer.start() def stop(self): if not self._timer is None: self._timer.cancel() self._timer = None self._close_csv_file() logger.info('Stopped recording history') def get_values(self, signal_source): with self._data_lock: return self._values_by_source_id[id(signal_source)] def record(self): row = [] with self._data_lock: now = time.time() row.append(round(now, 3)) self.__clean_old_history(now) for source in self.sources: value = source.last_value if (value is not None and value.status == SignalSource.STATUS_OK and value.timestamp > now - self.delta_seconds and source.running): self._values_by_source_id[id(source)].append( (now, value.value)) row.append(float(source.value_format.format(value.value))) else: row.append(None) if self._csv_writer is not None: self._csv_writer.writerow(row) self._csv_lines += 1 self._csv_file.flush() if self._csv_lines >= self.max_csv_lines: self._begin_new_csv_file() def __clean_old_history(self, now): with self._data_lock: for source_id in self._values_by_source_id: values = self._values_by_source_id[source_id] while len(values) > 1 and (now - values[1][0] > self.max_seconds): values.pop(0) def write_to_csv(self, file_basename): self._close_csv_file() self._csv_file_basename = file_basename def _begin_new_csv_file(self): self._close_csv_file() if self._csv_file_basename is not None: self._delete_old_csv_files() file_name = self._new_csv_file_name() dir_name = os.path.split(file_name)[0] if dir_name != '': os.makedirs(dir_name, 0o775, True) logger.info("Writing new CSV file '" + file_name + "'") self._csv_file = open(file_name, 'w', newline='', encoding='utf-8') self._csv_writer = csv.writer(self._csv_file) self._csv_writer.writerow( ['Time'] + [source.label for source in self.sources]) self._csv_lines = 1 def _close_csv_file(self): if self._csv_file is not None: logger.info("'Closing CSV file '" + self._csv_file.name + "'") self._csv_file.close() self._csv_file = None self._csv_writer = None self._csv_lines = 0 def _new_csv_file_name(self): return '{:}-{:%Y-%m-%d-%H%M%S}.csv'.format(self._csv_file_basename, datetime.now()) def _delete_old_csv_files(self): try: for file_info in self._list_csv_files(): if file_info[1] + self.max_seconds < time.time( ) - self.max_seconds_csv_files: logger.info("Deleting old CSV file '" + file_info[0] + "'") os.remove(file_info[0]) except: logger.exception('Failed to delete old CSV files') def _list_csv_files(self): """ Read list of existing CSV files and their begin and modification times as list of tuples [(full_file_name, begin_timestamp, last_modified_timestamp), ...]. """ dir_name, file_prefix = os.path.split(self._csv_file_basename) file_pattern = re.compile( '^' + file_prefix + '-(([0-9]{4})-([0-9]{2})-([0-9]{2})-([0-9]{2})([0-9]{2})([0-9]{2})).csv$' ) csv_files = [] for file in os.listdir(dir_name): match = file_pattern.match(file) if match is not None: full_path = os.path.join(dir_name, file) if os.path.isfile(full_path): begin = datetime.strptime(match.group(1), '%Y-%m-%d-%H%M%S').timestamp() modified = os.stat(full_path).st_mtime csv_files.append((full_path, begin, modified)) return sorted(csv_files, key=lambda x: x[1]) def load_from_csv_files(self): logger.info('Trying to restore history from CSV files...') try: begin_time = time.time() - self.max_seconds for file_info in self._list_csv_files(): if file_info[2] > begin_time: self._load_rows_from_csv_file(begin_time, file_info[0]) except: logger.exception('Failed to restore history from CSV files') def _load_rows_from_csv_file(self, begin_time, csv_file): logger.info("Restoring history from CSV file '" + csv_file + "'") len_sources = len(self.sources) with open(csv_file, 'r', encoding='utf-8') as file: csv_reader = csv.reader(file) first_line = True for row in csv_reader: if first_line: first_line = False elif len(row) == 1 + len_sources: row_time = float(row[0]) if row_time >= begin_time: for source, value_string in zip(self.sources, row[1:]): if len(value_string) > 0: value = float(value_string) self._values_by_source_id[id(source)].append( (row_time, value))
class MqttClient: """ Sends all signal changes to a MQTT broker. """ DELTA_SECONDS_DEFAULT = 10 # seconds def __init__(self): self.broker_host = 'localhost' self.broker_user = '' self.broker_port = 1883 self.broker_password = '' self.use_ssl = False self.broker_ca_certs = None self.broker_base_topic = 'datalogger' self.client = mqtt.Client() # self.client.enable_logger(logger) self.client.on_connect = self._on_connect self.client.on_disconnect = self._on_disconnect self.client.on_message = self._on_message self.__started = False self.__timer = None self.delta_seconds = MqttClient.DELTA_SECONDS_DEFAULT self.sources = [] def use_signals_config(self, signal_sources_config): self.broker_host = signal_sources_config['mqtt_broker_host'] self.broker_port = signal_sources_config['mqtt_broker_port'] self.broker_user = signal_sources_config['mqtt_broker_user'] self.broker_password = signal_sources_config['mqtt_broker_password'] self.use_ssl = signal_sources_config['mqtt_use_ssl'] self.broker_ca_certs = signal_sources_config['mqtt_broker_ca_certs'] self.broker_base_topic = signal_sources_config[ 'mqtt_broker_base_topic'] for group in signal_sources_config['groups']: for source in group['sources']: self.sources.append(source) def start(self): if not self.__started: if self.broker_host == '': logger.info( "NOT starting MQTT client because of config with empty broker" ) else: logger.info("Starting MQTT client for broker " + self.broker_host) if self.broker_user != '': self.client.username_pw_set(self.broker_user, self.broker_password) if self.use_ssl: self.client.tls_set(ca_certs=self.broker_ca_certs) self.client.connect_async(self.broker_host, self.broker_port) self.client.loop_start() self.__timer = RepeatTimer(self.delta_seconds, self.publish) self.__timer.start() self.__started = True def stop(self): if self.__started: logger.info("Stopping MQTT client for broker " + self.broker_host) self.__started = False self.__timer.cancel() self.__timer = None self.client.disconnect() self.client.loop_stop(True) def publish(self): for source in self.sources: signal_value = source.last_value if signal_value is not None: topic = self.broker_base_topic + '/' + source.identifier json_value = json.dumps({ 'value': signal_value.value, 'status': signal_value.status, 'formatted': '---' if signal_value.status != SignalSource.STATUS_OK else source.format(signal_value.value), 'timestamp': datetime.fromtimestamp(signal_value.timestamp).astimezone( timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%fZ'), 'unit': source.unit }) self.client.publish(topic, json_value, 0, True) def _on_connect(self, client, userdata, flags, rc): if rc == 0: logger.info("Connected to MQTT broker " + self.broker_host) else: logger.error("Failed to connect to MQTT broker " + self.broker_host + " rc=" + str(rc)) # this would be the place to client.subscribe("#") def _on_disconnect(self, client, userdata, rc): if rc == 0: logger.info("Disconnected from MQTT broker " + self.broker_host) else: logger.error("Connection lost to MQTT broker " + self.broker_host + " rc=" + str(rc)) def _on_message(self, client, userdata, message): # this would be the place to receive subscription messages pass
class UpdateService: """更新状态服务""" __instance_lock = threading.Lock() def __init__(self): self.__service_queue = AirConditionerServiceQueue.instance() self.__wait_queue = WaitQueue.instance() self.__master_machine = MasterMachine.instance() self.timer = RepeatTimer(UPDATE_FREQUENCY, self._task) logger.info('初始化UpdateService') @classmethod def instance(cls): """Singleton""" if not hasattr(cls, '_instance'): with cls.__instance_lock: if not hasattr(cls, '_instance'): cls._instance = cls() return cls._instance def _task(self): """周期定时任务""" reach_temp_services = self.__service_queue.update( self.__master_machine.mode) for service in reach_temp_services: self.__service_queue.remove(service.room.room_id) logger.info('房间' + service.room.room_id + '到达设定温度') service.room.status = room_status.STANDBY timeout_services = self.__wait_queue.update(self.__master_machine.mode) for service in timeout_services: if self.push_service(service) is True: DBFacade.exec(Log.objects.create, room_id=service.room.room_id, operation=operations.DISPATCH, op_time=datetime.datetime.now()) while self.__service_queue.has_space(): service = self.__wait_queue.pop() if service is not None: self.push_service(service) else: break def push_service(self, service: AirConditionerService) -> Optional[bool]: """ 将指定服务放入服务队列或等待队列中 Returns: True表示放入服务队列, False表示放入等待队列, None表示无需服务 """ serving_room = self.__master_machine.get_room( service.room.room_id) # type: Room if (self.__master_machine.mode == master_machine_mode.COOL and serving_room.current_temp <= serving_room.target_temp) \ or (self.__master_machine.mode == master_machine_mode.HOT and serving_room.current_temp >= serving_room.target_temp): return None else: status, service = self.__service_queue.push(service) if status is True: serving_room.status = room_status.SERVING in_service_queue = True else: serving_room.status = room_status.WAITING in_service_queue = False if service is not None: waiting_room = self.__master_machine.get_room( service.room.room_id) waiting_room.status = room_status.WAITING self.__wait_queue.push(service) return in_service_queue def reset(self): self.timer.cancel() self.timer = RepeatTimer(UPDATE_FREQUENCY, self._task)