예제 #1
0
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
예제 #2
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)
예제 #3
0
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)
예제 #4
0
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()
예제 #5
0
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()
예제 #6
0
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
예제 #7
0
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
예제 #8
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))
예제 #9
0
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
예제 #10
0
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)