Пример #1
0
 def __init__(self,
              pin,
              pull=None,
              released_component=None,
              double_pressed_component=None,
              double_pressed_method="on",
              long_pressed_component=None,
              long_pressed_method="on",
              suppress=False):
     """
     Basic functionality for push is to toggle a device. Double press and long press are
     just extended functionality.
     :param pin: pin number or name
     :param pull: None for no pullup or pull_down, otherwise value of pull configuration
     :param released_component: component name of component to be turned on when button pressed
     :param double_pressed_component: component name of component to be turned on when button double pressed
     :param double_pressed_method: string of the method of the component that is to be called
     :param long_pressed_component: component name of component to be turned on when button long pressed
     :param long_pressed_method: string of the method of the component that is to be called
     :param suppress: Suppress calling release function after double click and long press. Will delay release function by 300ms if double click is used.
     """
     self._component = released_component
     self._event = Event()
     # Synchronous method _event.set() to prevent queue overflows from pressing button too often
     super().__init__(pin, pull, None, "off", self._event, "set",
                      double_pressed_component, double_pressed_method,
                      long_pressed_component, long_pressed_method, suppress)
     asyncio.get_event_loop().create_task(self._watcher())
Пример #2
0
 def getStateChangeEvent(self):
     """
     Returns an event that gets triggered on every state change
     :return: Event
     """
     if self._event is None:
         from pysmartnode.utils.event import Event
         self._event = Event()
     return self._event
Пример #3
0
 def getReadingsEvent(self):
     """
     Returns an event that gets triggered on every read
     :return: Event
     """
     if self._event is None:
         from pysmartnode.utils.event import Event
         self._event = Event()
     return self._event
Пример #4
0
 async def _loop(self):
     if self._PIN_BELL_IRQ_DIRECTION == machine.Pin.IRQ_FALLING:
         self._pin_bell = Pin(self._pin_bell, machine.Pin.IN, machine.Pin.PULL_UP)
     else:
         self._pin_bell = Pin(self._pin_bell, machine.Pin.IN)
     self._event_bell = Event()
     self._timer_lock = Lock()
     self._pin_bell.irq(trigger=self._PIN_BELL_IRQ_DIRECTION, handler=self.__irqBell)
     self._event_bell.clear()
     asyncio.get_event_loop().create_task(self.__bell())
     self._timer_bell = machine.Timer(1)
     await _log.asyncLog("info", "Bell initialized")
     gc.collect()
Пример #5
0
class Bell(Component):
    def __init__(self, pin, debounce_time, on_time=None, irq_direction=None, mqtt_topic=None, friendly_name=None,
                 friendly_name_last=None):
        super().__init__()
        self._topic = mqtt_topic or _mqtt.getDeviceTopic(_component_name)
        self._PIN_BELL_IRQ_DIRECTION = irq_direction or machine.Pin.IRQ_FALLING
        self._debounce_time = debounce_time
        self._on_time = on_time or 500
        self._pin_bell = pin
        self._last_activation = 0
        self._frn = friendly_name
        self._frn_l = friendly_name_last

    async def _init(self):
        await super()._init()
        if self._PIN_BELL_IRQ_DIRECTION == machine.Pin.IRQ_FALLING:
            self._pin_bell = Pin(self._pin_bell, machine.Pin.IN, machine.Pin.PULL_UP)
        else:
            self._pin_bell = Pin(self._pin_bell, machine.Pin.IN)
        self._event_bell = Event()
        self._timer_lock = Lock()
        irq = self._pin_bell.irq(trigger=self._PIN_BELL_IRQ_DIRECTION, handler=self.__irqBell)
        self._event_bell.clear()
        asyncio.get_event_loop().create_task(self.__bell())
        self._timer_bell = machine.Timer(1)
        await _log.asyncLog("info", "Bell initialized")
        gc.collect()

    async def __bell(self):
        while True:
            await self._event_bell
            diff = time.ticks_diff(time.ticks_ms(), self._last_activation)
            if diff > 10000:
                _log.error("Bell rang {!s}s ago, not activated ringing".format(diff / 1000))
                self._event_bell.clear()
                return
            else:
                await _mqtt.publish(self._topic, "ON", qos=1)
                await asyncio.sleep_ms(self._on_time)
                await _mqtt.publish(self._topic, "OFF", qos=1, retain=True)
                if config.RTC_SYNC_ACTIVE:
                    t = time.localtime()
                    await _mqtt.publish(_mqtt.getDeviceTopic("last_bell"),
                                        "{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(t[0],
                                                                                       t[1], t[2],
                                                                                       t[3], t[4],
                                                                                       t[5]),
                                        qos=1, retain=True)
                self._event_bell.clear()
                if diff > 500:
                    _log.warn("Bell rang {!s}ms ago, activated ringing".format(diff))

    def __irqBell(self, p):
        if self._timer_lock.locked() is True or self._event_bell.is_set() is True:
            return
        else:
            self._timer_lock.acquire()  # not checking return value as we checked locked state above
            self._timer_bell.init(period=self._debounce_time,
                                  mode=machine.Timer.ONE_SHOT, callback=self.__irqTime)

    def __irqTime(self, t):
        if self._PIN_BELL_IRQ_DIRECTION == machine.Pin.IRQ_FALLING and self._pin_bell.value() == 0:
            self._last_activation = time.ticks_ms()
            self._event_bell.set()
        elif self._PIN_BELL_IRQ_DIRECTION == machine.Pin.IRQ_RISING and self._pin_bell.value() == 1:
            self._last_activation = time.ticks_ms()
            self._event_bell.set()
        self._timer_bell.deinit()
        self._timer_lock.release()

    async def _discovery(self):
        await self._publishDiscovery("binary_sensor", self._topic, "bell", '"ic":"mdi:bell",', "Doorbell", self._frn)
        gc.collect()
        if config.RTC_SYNC_ACTIVE is True:
            await self._publishDiscovery("sensor", _mqtt.getDeviceTopic("last_bell"), "last_bell", TIMELAPSE_TYPE,
                                         "Last Bell", self._frn_l)
            gc.collect()
Пример #6
0
 def __init__(self,
              TEMP_SENSOR,
              REACTION_TIME,
              HYSTERESIS_LOW,
              HYSTERESIS_HIGH,
              SHUTDOWN_CYCLES,
              START_CYCLES,
              FROST_TEMP=16,
              SHUTDOWN_TEMP=29,
              TARGET_TEMP=22,
              TARGET_TEMP_TOPIC=None,
              STATUS_TOPIC=None,
              POWER_TOPIC=None,
              MODE_TOPIC=None,
              friendly_name_temperature=None,
              friendly_name_mode=None,
              friendly_name_power=None,
              friendly_name_status=None):
     super().__init__()
     # self.__sensor = config.getComponent(TEMP_SENSOR)
     self.__sensor = TEMP_SENSOR  # registerComponents already gets the component
     if self.__sensor is None:
         log.critical(
             "Can't initialize heater as temperature sensor {!r} does not exist!"
             .format(TEMP_SENSOR))
         raise TypeError(
             "temperature sensor {!r} does not exist".format(TEMP_SENSOR))
     if hasattr(self.__sensor, "tempHumid") is False and hasattr(
             self.__sensor, "temperature") is False:
         log.critical(
             "Can't initialize heater as temperature sensor {!r} has no supported coroutine for getting temperature"
             .format(TEMP_SENSOR))
         raise TypeError(
             "temperature sensor {!r} does not have supported API")
     self.__interval = REACTION_TIME
     self.__hysteresis_low = HYSTERESIS_LOW
     self.__hysteresis_high = HYSTERESIS_HIGH
     self.__shutdown_cycles = SHUTDOWN_CYCLES
     self.__start_cycles = START_CYCLES
     self.__frost_temp = FROST_TEMP
     self.__shutdown_temp = SHUTDOWN_TEMP
     self.__target_temp = TARGET_TEMP
     self.__status_topic = STATUS_TOPIC or _mqtt.getDeviceTopic(
         "heater/status")
     self.__power_topic = POWER_TOPIC or _mqtt.getDeviceTopic(
         "heater/power")
     self.__mode_cmd_topic = MODE_TOPIC or _mqtt.getDeviceTopic(
         "heater/mode", is_request=True)
     self.__target_temp_cmd_topic = TARGET_TEMP_TOPIC or _mqtt.getDeviceTopic(
         "heater/temp", is_request=True)
     self._subscribe(self.__mode_cmd_topic, self.setMode)
     self._subscribe(self.__target_temp_cmd_topic, self._requestTemp)
     ######
     # internal variables
     ######
     self.__active_mode = "INTERNAL"
     self.__modes = {"INTERNAL": self.__modeInternal}
     self.__plugins = {}
     self.__target_power = 0
     self.__event = Event()
     self.__cycles_target_reached = -2  # will immediately react to current temperature
     self.__loop_started = False
     self.__timer_time = 0  # has to be object variable so that _watch can update it too
     self.__last_error = None
     self.__setHeaterPower = None  # coro of registered hardware
     self.__initializeHardware = None  # initialization coro if hardware requires it
     #####
     global _heater
     if _heater is None:
         _heater = self
     self._frn_temp = friendly_name_temperature
     self._frn_mode = friendly_name_mode
     self._frn_power = friendly_name_power
     self._frn_status = friendly_name_status
Пример #7
0
class Heater(Component):
    def __init__(self,
                 TEMP_SENSOR,
                 REACTION_TIME,
                 HYSTERESIS_LOW,
                 HYSTERESIS_HIGH,
                 SHUTDOWN_CYCLES,
                 START_CYCLES,
                 FROST_TEMP=16,
                 SHUTDOWN_TEMP=29,
                 TARGET_TEMP=22,
                 TARGET_TEMP_TOPIC=None,
                 STATUS_TOPIC=None,
                 POWER_TOPIC=None,
                 MODE_TOPIC=None,
                 friendly_name_temperature=None,
                 friendly_name_mode=None,
                 friendly_name_power=None,
                 friendly_name_status=None):
        super().__init__()
        # self.__sensor = config.getComponent(TEMP_SENSOR)
        self.__sensor = TEMP_SENSOR  # registerComponents already gets the component
        if self.__sensor is None:
            log.critical(
                "Can't initialize heater as temperature sensor {!r} does not exist!"
                .format(TEMP_SENSOR))
            raise TypeError(
                "temperature sensor {!r} does not exist".format(TEMP_SENSOR))
        if hasattr(self.__sensor, "tempHumid") is False and hasattr(
                self.__sensor, "temperature") is False:
            log.critical(
                "Can't initialize heater as temperature sensor {!r} has no supported coroutine for getting temperature"
                .format(TEMP_SENSOR))
            raise TypeError(
                "temperature sensor {!r} does not have supported API")
        self.__interval = REACTION_TIME
        self.__hysteresis_low = HYSTERESIS_LOW
        self.__hysteresis_high = HYSTERESIS_HIGH
        self.__shutdown_cycles = SHUTDOWN_CYCLES
        self.__start_cycles = START_CYCLES
        self.__frost_temp = FROST_TEMP
        self.__shutdown_temp = SHUTDOWN_TEMP
        self.__target_temp = TARGET_TEMP
        self.__status_topic = STATUS_TOPIC or _mqtt.getDeviceTopic(
            "heater/status")
        self.__power_topic = POWER_TOPIC or _mqtt.getDeviceTopic(
            "heater/power")
        self.__mode_cmd_topic = MODE_TOPIC or _mqtt.getDeviceTopic(
            "heater/mode", is_request=True)
        self.__target_temp_cmd_topic = TARGET_TEMP_TOPIC or _mqtt.getDeviceTopic(
            "heater/temp", is_request=True)
        self._subscribe(self.__mode_cmd_topic, self.setMode)
        self._subscribe(self.__target_temp_cmd_topic, self._requestTemp)
        ######
        # internal variables
        ######
        self.__active_mode = "INTERNAL"
        self.__modes = {"INTERNAL": self.__modeInternal}
        self.__plugins = {}
        self.__target_power = 0
        self.__event = Event()
        self.__cycles_target_reached = -2  # will immediately react to current temperature
        self.__loop_started = False
        self.__timer_time = 0  # has to be object variable so that _watch can update it too
        self.__last_error = None
        self.__setHeaterPower = None  # coro of registered hardware
        self.__initializeHardware = None  # initialization coro if hardware requires it
        #####
        global _heater
        if _heater is None:
            _heater = self
        self._frn_temp = friendly_name_temperature
        self._frn_mode = friendly_name_mode
        self._frn_power = friendly_name_power
        self._frn_status = friendly_name_status

    async def _discovery(self):
        name = "heater_target_temp"
        await self._publishDiscovery(_component_type,
                                     self.__target_temp_cmd_topic[:-4], name,
                                     DISCOVERY_SWITCH, self._frn_temp
                                     or " Target Temperature")
        gc.collect()
        name = "heater_mode"
        await self._publishDiscovery(_component_type,
                                     self.__mode_cmd_topic[:-4], name,
                                     DISCOVERY_SWITCH, self._frn_mode
                                     or "Mode")
        gc.collect()
        name = "heater_power"
        await self._publishDiscovery(_component_type, self.__power_topic, name,
                                     "", self._frn_power or "Power")
        gc.collect()
        name = "heater_status"
        await self._publishDiscovery(_component_type, self.__status_topic,
                                     name, "", self._frn_status or "Power")
        gc.collect()

    async def _updateMQTTStatus(self):
        await _mqtt.publish(self.__power_topic,
                            self.__target_power,
                            qos=1,
                            retain=True)
        if self.__last_error is None:
            if self.__target_power == 0:
                await _mqtt.publish(self.__status_topic,
                                    "OFF",
                                    qos=1,
                                    retain=True)
            else:
                await _mqtt.publish(self.__status_topic,
                                    "ON",
                                    qos=1,
                                    retain=True)
        else:
            await _mqtt.publish(self.__status_topic,
                                self.__last_error,
                                qos=1,
                                retain=True)
        await _mqtt.publish(self.__target_temp_cmd_topic[:-4],
                            self.__target_temp,
                            qos=1,
                            retain=True)
        await _mqtt.publish(self.__mode_cmd_topic[:-4],
                            self.__active_mode,
                            qos=1,
                            retain=True)

    def registerHardware(self, set_power, hardware_init=None):
        self.__setHeaterPower = set_power
        self.__initializeHardware = hardware_init

    def registerPlugin(self, coro, name):
        if coro is None:
            raise TypeError("Can't register plugin of type None")
        log.debug("Registering plugin {!s}".format(name), local_only=True)
        if coro not in self.__plugins:
            self.__plugins[name] = coro
            return True
        else:
            log.warn("Plugin {!s} already registered")
        return False

    def getInterval(self):
        return self.__interval

    def getHysteresisLow(self):
        return self.__hysteresis_low

    def getHysteresisHigh(self):
        return self.__hysteresis_high

    def getShutdownCycles(self):
        return self.__shutdown_cycles

    def getStartCycles(self):
        return self.__start_cycles

    def getFrostTemperature(self):
        return self.__frost_temp

    def getShutdownTemperature(self):
        return self.__shutdown_temp

    def getTargetTemp(self):
        return self.__target_temp

    def setTargetTemp(self, temp):
        log.debug("Set target temp to {!s}".format(temp), local_only=True)
        self.__target_temp = temp
        if self.__loop_started:
            self.__cycles_target_reached = -2  # makes heater react to temp change immediately

    def getStatusTopic(self):
        return self.__status_topic

    def getPowerTopic(self):
        return self.__power_topic

    def getModeCmdTopic(self):
        return self.__mode_cmd_topic

    def getActiveMode(self):
        return self.__active_mode

    def setEvent(self):
        self.__event.set()

    def getTargetPower(self):
        return self.__target_power

    def setLastError(self, error):
        self.__last_error = error

    def getLastError(self):
        return self.__last_error

    async def _init(self):
        await super()._init()
        await log.asyncLog("info",
                           "Heater Core version {!s}".format(__version__))
        if self.__initializeHardware is not None:
            await self.__initializeHardware()
        asyncio.get_event_loop().create_task(self._timer())
        asyncio.get_event_loop().create_task(self._watch())
        gc.collect()

    def addMode(self, mode, coro):
        self.__modes[mode] = coro

    def hasStarted(self):
        return self.__loop_started

    async def setMode(self, topic, msg, retain):
        if msg not in self.__modes:
            log.error("Mode {!r} not supported".format(msg))
            return None
        if self.__last_error == "FROST":
            log.warn("Can't change mode to {!r} as temperature is below frost")
            return None
        if self.__loop_started:
            # wait for execution of cycle to end
            while self.__event.is_set():
                await asyncio.sleep_ms(50)
        log.debug("setMode {!s}".format(msg), local_only=True)
        self.__active_mode = msg
        self.__cycles_target_reached = -2 if retain else 0
        if self.__loop_started:
            self.__event.set()
        await _mqtt.publish(self.__mode_cmd_topic[:-4],
                            msg,
                            retain=True,
                            qos=1)
        gc.collect()
        return True

    async def _requestTemp(self, topic, msg, retain):
        if self.__loop_started:
            while self.__event.is_set():
                await asyncio.sleep_ms(50)
        try:
            temp = float(msg)
        except ValueError:
            log.error(
                "Error converting requested temp to float: {!r}".format(msg))
            return None
        self.__target_temp = temp
        log.debug("requestTemp {!s}".format(temp), local_only=True)
        # await _mqtt.publish(self.__target_temp_topic[:-4], self.__target_temp, retain=True, qos=1)
        if self.__loop_started:
            self.__cycles_target_reached = -2  # makes heater react to temp change immediately
            self.__event.set()
        return True

    async def _timer(self):
        self.__timer_time = time.ticks_ms()
        while True:
            while (time.ticks_ms() - self.__timer_time) < (self.__interval *
                                                           1000):
                await asyncio.sleep(1)
            self.__timer_time = time.ticks_ms(
            )  # will also be set by _watch when resetting event
            log.debug("Reaction time reached", local_only=True)
            self.__event.set()

    async def _getTemperature(self):
        if hasattr(self.__sensor, "tempHumid"):
            return (await self.__sensor.tempHumid())["temperature"]
        elif hasattr(self.__sensor, "temperature"):
            return await self.__sensor.temperature()
        else:
            raise TypeError("No supported temperature sensor API")

    async def _watch(self):
        await asyncio.sleep(
            10
        )  # gives time to get retained values for targetTemp,Mode,Power,etc; and register plugins
        await self.__setHeaterPower(self.__target_power)
        # otherwise heater power would be floating or depending on how the hardware initializes
        self.__loop_started = True
        temp_reading_errors = 0
        frost_cycles = 0
        while True:
            current_temp = await self._getTemperature()
            log.debug("CurrentTemp: {!s}".format(current_temp),
                      local_only=True)
            do_update = True
            if current_temp is False or current_temp is None:
                if self.__last_error != "NO_TEMP":
                    log.error(
                        "heater could not get current temperature, try {!s}".
                        format(temp_reading_errors))
                    temp_reading_errors += 1
                    if temp_reading_errors >= 3:
                        log.critical(
                            "heater could not get temperature 3 times, shutting down heater"
                        )
                        self.__last_error = "NO_TEMP"
                        await self.setHeaterPower(0)
                self.__timer_time = time.ticks_ms(
                ) - self.__interval / 2 * 1000  # wait only 1/2 reaction time
                do_update = False
            elif current_temp < self.__frost_temp and self.__target_power < 100:
                if self.__last_error != "FROST":
                    if self.__active_mode != "INTERNAL":
                        self.__active_mode = "INTERNAL"  # if different mode was active, it has failed
                    await self.setHeaterPower(100)
                    self.__last_error = "FROST"
                else:
                    frost_cycles += 1
                    if frost_cycles >= 3:
                        log.critical(
                            "Heater can't raise temperature above frost")
                temp_reading_errors = 0
                do_update = False
            elif current_temp > self.__shutdown_temp:
                if self.__last_error != "TOO_HOT":
                    if self.__active_mode != "INTERNAL":
                        self.__active_mode = "INTERNAL"  # if different mode was active, it has failed (or summer)
                    log.error(
                        "heater shutdown due to too high temperature of {!r}°C"
                        .format(current_temp))
                    await self.setHeaterPower(0)
                    self.__last_error = "TOO_HOT"
                # could be summer
                temp_reading_errors = 0
                frost_cycles = 0
                do_update = False
            else:
                frost_cycles = 0
                temp_reading_errors = 0
            if do_update:
                last_error = self.__last_error
                data = {"current_temp": current_temp}
                for plugin in self.__plugins:
                    try:
                        await self.__plugins[plugin](self, data)
                    except Exception as e:
                        log.error("Plugin {!s} failed: {}".format(plugin, e))
                await self.__modes[self.__active_mode](self, data)
                if self.__last_error == last_error:
                    # reset error if not set by the mode coroutine
                    # as error is no longer valid if execution of mode was successful
                    self.__last_error = None
            await self._updateMQTTStatus()
            await self.__event
            self.__event.clear()
            self.__timer_time = time.ticks_ms()
        # in case of shutdown, code currently not reached anyways
        # await self._setHeaterPower(0)

    async def __modeInternal(self, heater, data):
        log.debug(data, local_only=True)
        current_temp = data["current_temp"]
        power = None
        target_power = self.__target_power
        if target_power > 0 and current_temp > self.__target_temp + self.__hysteresis_high:
            self.__cycles_target_reached += 1
            if self.__cycles_target_reached >= self.__shutdown_cycles or self.__cycles_target_reached == -1:
                power = 0
                self.__cycles_target_reached = 0
        elif target_power == 0 and current_temp < self.__target_temp - self.__hysteresis_low:
            self.__cycles_target_reached += 1
            if self.__cycles_target_reached >= self.__start_cycles or self.__cycles_target_reached == -1:
                power = 100
                self.__cycles_target_reached = 0
        else:
            self.__cycles_target_reached = 0
            return
            # temperature in between; will keep 100% if rising and 0% if falling.
            # nothing to do. Could improve this by adding a timer checking if temp is really rising and warn otherwise
        log.debug("cycles_target_reached: {!s}".format(
            self.__cycles_target_reached),
                  local_only=True)
        if power is None:
            return
        await self.setHeaterPower(power)

    async def setHeaterPower(self, power):
        if self.__setHeaterPower is None:
            log.error("No hardware registered to control heater")
            await _mqtt.publish(self.__status_topic,
                                "ERR: NO HARDWARE",
                                qos=1,
                                retain=True)
        else:
            if not await self.__setHeaterPower(power):
                log.error(
                    "Could not set heater power to {!s}%, shutting heater down"
                    .format(power))
                await self.__setHeaterPower(0)
                self.__last_error = "ERR: HARDWARE"
            else:
                self.__target_power = power
Пример #8
0
    def __init__(self,
                 temperature_sensor: ComponentSensor,
                 heating_unit: ComponentSwitch,
                 modes: list,
                 interval: float = 300,
                 temp_step=0.1,
                 min_temp: float = 16,
                 max_temp: float = 26,
                 temp_low: float = 20,
                 temp_high: float = 21,
                 away_temp_low: float = 16,
                 away_temp_high: float = 17,
                 friendly_name=None,
                 discover=True):
        self.checkSensorType(temperature_sensor, SENSOR_TEMPERATURE)
        self.checkSwitchType(heating_unit)
        # This makes it possible to use multiple instances of MyComponent
        global _unit_index
        _unit_index += 1
        super().__init__(COMPONENT_NAME, __version__, _unit_index, discover)

        self._temp_step = temp_step
        self._min_temp = min_temp
        self._max_temp = max_temp
        self.temp_sensor: ComponentSensor = temperature_sensor
        self.heating_unit: ComponentSwitch = heating_unit
        self._modes = {}
        if "off" not in modes:
            modes.append("off")
        for mode in modes:
            if mode not in MODES_SUPPORTED:
                _log.error("Mode {!s} not supported".format(mode))
                modes.remove(mode)
            else:
                try:
                    mod = __import__(
                        "pysmartnode.components.devices.climate.{}".format(
                            mode), globals(), locals(), [], 0)
                except ImportError as e:
                    _log.error("Mode {!s} not available: {!s}".format(mode, e))
                    continue
                if hasattr(mod, mode):
                    modeobj = getattr(mod, mode)
                else:
                    _log.error("Mode {!s} has no class {!r}".format(
                        mode, mode))
                    continue
                try:
                    modeobj = modeobj(self)
                except Exception as e:
                    _log.error("Error creating mode {!s} object: {!s}".format(
                        mode, e))
                    continue
                self._modes[mode] = modeobj
        self._frn = friendly_name
        self.state = {
            CURRENT_TEMPERATURE_HIGH: temp_high,  # current temperature high
            CURRENT_TEMPERATURE_LOW: temp_low,  # current temperature low
            AWAY_MODE_STATE: AWAY_OFF,  # away mode "ON"/"OFF"
            STORAGE_AWAY_TEMPERATURE_HIGH:
            away_temp_high,  # away temperature low
            STORAGE_AWAY_TEMPERATURE_LOW:
            away_temp_low,  # away temperature high
            STORAGE_TEMPERATURE_HIGH:
            temp_high,  # temperature high, storage value
            STORAGE_TEMPERATURE_LOW:
            temp_low,  # temperature low, storage value
            CURRENT_MODE: str(self._modes["off"]),
            CURRENT_ACTION: ACTION_OFF
        }
        self.event = Event()
        self.lock = config.Lock()
        # every extneral change (like mode) that could break an ongoing trigger needs
        # to be protected by self.lock.
        self.log = _log
        gc.collect()

        self._mode_topic = _mqtt.getDeviceTopic("{!s}{!s}/statem/set".format(
            COMPONENT_NAME, self._count))
        self._temp_low_topic = _mqtt.getDeviceTopic(
            "{!s}{!s}/statetl/set".format(COMPONENT_NAME, self._count))
        self._temp_high_topic = _mqtt.getDeviceTopic(
            "{!s}{!s}/stateth/set".format(COMPONENT_NAME, self._count))
        self._away_topic = _mqtt.getDeviceTopic("{!s}{!s}/stateaw/set".format(
            COMPONENT_NAME, self._count))
        _mqtt.subscribeSync(self._mode_topic, self.changeMode, self)
        _mqtt.subscribeSync(self._temp_low_topic, self.changeTempLow, self)
        _mqtt.subscribeSync(self._temp_high_topic, self.changeTempHigh, self)
        _mqtt.subscribeSync(self._away_topic, self.changeAwayMode, self)

        self._restore_done = False
        asyncio.get_event_loop().create_task(self._loop(interval))
Пример #9
0
class Climate(Component):
    def __init__(self,
                 temperature_sensor: ComponentSensor,
                 heating_unit: ComponentSwitch,
                 modes: list,
                 interval: float = 300,
                 temp_step=0.1,
                 min_temp: float = 16,
                 max_temp: float = 26,
                 temp_low: float = 20,
                 temp_high: float = 21,
                 away_temp_low: float = 16,
                 away_temp_high: float = 17,
                 friendly_name=None,
                 discover=True):
        self.checkSensorType(temperature_sensor, SENSOR_TEMPERATURE)
        self.checkSwitchType(heating_unit)
        # This makes it possible to use multiple instances of MyComponent
        global _unit_index
        _unit_index += 1
        super().__init__(COMPONENT_NAME, __version__, _unit_index, discover)

        self._temp_step = temp_step
        self._min_temp = min_temp
        self._max_temp = max_temp
        self.temp_sensor: ComponentSensor = temperature_sensor
        self.heating_unit: ComponentSwitch = heating_unit
        self._modes = {}
        if "off" not in modes:
            modes.append("off")
        for mode in modes:
            if mode not in MODES_SUPPORTED:
                _log.error("Mode {!s} not supported".format(mode))
                modes.remove(mode)
            else:
                try:
                    mod = __import__(
                        "pysmartnode.components.devices.climate.{}".format(
                            mode), globals(), locals(), [], 0)
                except ImportError as e:
                    _log.error("Mode {!s} not available: {!s}".format(mode, e))
                    continue
                if hasattr(mod, mode):
                    modeobj = getattr(mod, mode)
                else:
                    _log.error("Mode {!s} has no class {!r}".format(
                        mode, mode))
                    continue
                try:
                    modeobj = modeobj(self)
                except Exception as e:
                    _log.error("Error creating mode {!s} object: {!s}".format(
                        mode, e))
                    continue
                self._modes[mode] = modeobj
        self._frn = friendly_name
        self.state = {
            CURRENT_TEMPERATURE_HIGH: temp_high,  # current temperature high
            CURRENT_TEMPERATURE_LOW: temp_low,  # current temperature low
            AWAY_MODE_STATE: AWAY_OFF,  # away mode "ON"/"OFF"
            STORAGE_AWAY_TEMPERATURE_HIGH:
            away_temp_high,  # away temperature low
            STORAGE_AWAY_TEMPERATURE_LOW:
            away_temp_low,  # away temperature high
            STORAGE_TEMPERATURE_HIGH:
            temp_high,  # temperature high, storage value
            STORAGE_TEMPERATURE_LOW:
            temp_low,  # temperature low, storage value
            CURRENT_MODE: str(self._modes["off"]),
            CURRENT_ACTION: ACTION_OFF
        }
        self.event = Event()
        self.lock = config.Lock()
        # every extneral change (like mode) that could break an ongoing trigger needs
        # to be protected by self.lock.
        self.log = _log
        gc.collect()

        self._mode_topic = _mqtt.getDeviceTopic("{!s}{!s}/statem/set".format(
            COMPONENT_NAME, self._count))
        self._temp_low_topic = _mqtt.getDeviceTopic(
            "{!s}{!s}/statetl/set".format(COMPONENT_NAME, self._count))
        self._temp_high_topic = _mqtt.getDeviceTopic(
            "{!s}{!s}/stateth/set".format(COMPONENT_NAME, self._count))
        self._away_topic = _mqtt.getDeviceTopic("{!s}{!s}/stateaw/set".format(
            COMPONENT_NAME, self._count))
        _mqtt.subscribeSync(self._mode_topic, self.changeMode, self)
        _mqtt.subscribeSync(self._temp_low_topic, self.changeTempLow, self)
        _mqtt.subscribeSync(self._temp_high_topic, self.changeTempHigh, self)
        _mqtt.subscribeSync(self._away_topic, self.changeAwayMode, self)

        self._restore_done = False
        asyncio.get_event_loop().create_task(self._loop(interval))

    async def _init_network(self):
        await _mqtt.awaitSubscriptionsDone(
        )  # wait until subscriptions are done
        # because received messages will take up RAM and the discovery message
        # of climate is very big and could easily fail if RAM is fragmented.
        gc.collect()
        await asyncio.sleep(1)
        gc.collect()
        await super()._init_network()
        # let discovery succeed first because it is a big message
        await _mqtt.subscribe(
            _mqtt.getDeviceTopic("{!s}{!s}/state".format(
                COMPONENT_NAME, self._count)), self._restore, self)
        gc.collect()

    async def _loop(self, interval):
        interval = interval * 1000
        t = time.ticks_ms()
        while not self._restore_done and time.ticks_diff(time.ticks_ms(),
                                                         t) < 30000:
            await asyncio.sleep(1)
            # wait for network to finish so the old state can be restored, or time out (30s)
        if not self._restore_done:
            await _mqtt.unsubscribe(
                _mqtt.getDeviceTopic("{!s}{!s}/state".format(
                    COMPONENT_NAME, self._count)), self)
            self._restore_done = True
            self.event.set()
        await asyncio.sleep(1)
        t = 0
        while True:
            while time.ticks_diff(time.ticks_ms(),
                                  t) < interval and not self.event.is_set():
                await asyncio.sleep_ms(100)
            if self.event.is_set():
                self.event.clear()
            async with self.lock:
                cur_temp = await self.temp_sensor.getValue(SENSOR_TEMPERATURE)
                try:
                    await self._modes[self.state[CURRENT_MODE]
                                      ].trigger(self, cur_temp)
                except Exception as e:
                    _log.error("Error executing mode {!s}: {!s}".format(
                        self.state[CURRENT_MODE], e))
                await _mqtt.publish(_mqtt.getDeviceTopic(
                    "{!s}{!s}/state".format(COMPONENT_NAME, self._count)),
                                    self.state,
                                    qos=1,
                                    retain=True,
                                    timeout=4)
            t = time.ticks_ms()

    async def _restore(self, topic, msg, retain):
        # used to restore the state after a restart
        await _mqtt.unsubscribe(
            _mqtt.getDeviceTopic("{!s}{!s}/state".format(
                COMPONENT_NAME, self._count)), self)
        mode = msg[CURRENT_MODE]
        del msg[CURRENT_MODE]
        del msg[CURRENT_ACTION]  # is going to be set after trigger()
        self.state.update(msg)
        try:
            await self.changeMode(topic, mode, retain)  # uses lock
        except AttributeError as e:
            await _log.asyncLog("error", e, timeout=10)
        self._restore_done = True
        await asyncio.sleep(1)
        self.event.set()

    async def changeAwayMode(self, topic, msg, retain):
        if msg in _mqtt.payload_on:
            if self.state[AWAY_MODE_STATE] == AWAY_ON:
                return False  # no publish needed as done in _loop
            async with self.lock:
                self.state[AWAY_MODE_STATE] = AWAY_ON
                self.state[CURRENT_TEMPERATURE_HIGH] = self.state[
                    STORAGE_AWAY_TEMPERATURE_HIGH]
                self.state[CURRENT_TEMPERATURE_LOW] = self.state[
                    STORAGE_AWAY_TEMPERATURE_LOW]
                self.event.set()
                return False  # no publish needed as done in _loop
        elif msg in _mqtt.payload_off:
            if self.state[AWAY_MODE_STATE] == AWAY_OFF:
                return False  # no publish needed as done in _loop
            async with self.lock:
                self.state[AWAY_MODE_STATE] = AWAY_OFF
                self.state[CURRENT_TEMPERATURE_HIGH] = self.state[
                    STORAGE_TEMPERATURE_HIGH]
                self.state[CURRENT_TEMPERATURE_LOW] = self.state[
                    STORAGE_TEMPERATURE_LOW]
                self.event.set()
                return False  # no publish needed as done in _loop
        else:
            raise TypeError("Unsupported payload {!s}".format(msg))

    async def changeMode(self, topic, msg, retain):
        if msg not in self._modes:
            raise AttributeError("Mode {!s} not supported".format(msg))
        if msg == self.state[CURRENT_MODE]:
            return False  # no publish needed as done in _loop  # mode already active
        async with self.lock:
            mode = self._modes[msg]
            if await self._modes[self.state[CURRENT_MODE]].deactivate(self):
                if await mode.activate(self):
                    self.state[CURRENT_MODE] = msg
                    self.event.set()
                    return False  # no publish needed as done in _loop
                else:
                    self.state[CURRENT_MODE] = MODE_OFF
                    await self._modes[MODE_OFF].activate()
                    self.event.set()
                    return False
            else:
                return False

    async def changeTempHigh(self, topic, msg, retain):
        msg = float(msg)
        if msg > self._max_temp:
            raise ValueError("Can't set temp to {!s}, max temp is {!s}".format(
                msg, self._max_temp))
        if self.state[CURRENT_TEMPERATURE_HIGH] == msg:
            return False  # already set to requested temperature, prevents unneeded event & publish
        self.state[CURRENT_TEMPERATURE_HIGH] = msg
        if self.state[AWAY_MODE_STATE] == AWAY_ON:
            self.state[STORAGE_AWAY_TEMPERATURE_HIGH] = msg
        else:
            self.state[STORAGE_TEMPERATURE_HIGH] = msg
        self.event.set()
        return False

    async def changeTempLow(self, topic, msg, retain):
        msg = float(msg)
        if msg < self._min_temp:
            raise ValueError("Can't set temp to {!s}, min temp is {!s}".format(
                msg, self._min_temp))
        if self.state[CURRENT_TEMPERATURE_LOW] == msg:
            return False  # already set to requested temperature, prevents unneeded event & publish
        self.state[CURRENT_TEMPERATURE_LOW] = msg
        if self.state[AWAY_MODE_STATE] == AWAY_ON:
            self.state[STORAGE_AWAY_TEMPERATURE_LOW] = msg
        else:
            self.state[STORAGE_TEMPERATURE_LOW] = msg
        self.event.set()
        return False

    async def _discovery(self, register=True):
        name = "{!s}{!s}".format(COMPONENT_NAME, self._count)
        base_topic = _mqtt.getRealTopic(_mqtt.getDeviceTopic(name))
        modes = ujson.dumps([str(mode) for mode in self._modes])
        gc.collect()
        if register:
            sens = CLIMATE_DISCOVERY.format(
                base_topic,
                self._frn or name,
                self._composeAvailability(),
                sys_vars.getDeviceID(),
                name,  # unique_id
                _mqtt.getRealTopic(
                    self.temp_sensor.getTopic(SENSOR_TEMPERATURE)),
                # current_temp_topic
                self.temp_sensor.getTemplate(SENSOR_TEMPERATURE),
                # cur_temp_template
                self._temp_step,
                self._min_temp,
                self._max_temp,
                modes,
                sys_vars.getDeviceDiscovery())
        else:
            sens = ""
        gc.collect()
        topic = Component._getDiscoveryTopic(_COMPONENT_TYPE, name)
        await _mqtt.publish(topic, sens, qos=1, retain=True)
Пример #10
0
class ComponentSwitch(Component):
    """
    Generic Switch class.
    Use it according to the template.
    """
    def __init__(self,
                 component_name,
                 version,
                 unit_index: int,
                 command_topic=None,
                 instance_name=None,
                 wait_for_lock=True,
                 discover=True,
                 restore_state=True,
                 friendly_name=None,
                 initial_state=None):
        """
        :param component_name: name of the component that is subclassing this switch (used for discovery and topics)
        :param version: version of the component module. will be logged over mqtt
        :param unit_index: counter of the registerd unit of this sensor_type (used for default topics)
        :param command_topic: command_topic of subclass which controls the switch state. optional.
        :param instance_name: name of the instance. If not provided will get composed of component_name<count>
        :param wait_for_lock: if True then every request waits for the lock to become available,
        :param restore_state: restore the retained state topic state
        meaning the previous device request has to finish before the new one is started.
        Otherwise the new one will get ignored.
        :param friendly_name: friendly name for homeassistant gui
        :param initial_state: intitial state of the switch. By default unknown so first state change request will set initial state.
        """
        super().__init__(component_name,
                         version,
                         unit_index,
                         discover=discover)
        # discover: boolean, if this component should publish its mqtt discovery.
        # This can be used to prevent combined Components from exposing underlying
        # hardware components like a power switch

        self._state = initial_state  # initial state is unknown if None
        self._topic = command_topic or _mqtt.getDeviceTopic(
            "{!s}{!s}/set".format(component_name, self._count))
        _mqtt.subscribeSync(self._topic,
                            self.on_message,
                            self,
                            check_retained_state=restore_state)
        self.lock = config.Lock()
        # in case switch activates a device that will need a while to finish
        self._wfl = wait_for_lock
        self._name = instance_name
        self._event = None
        self._frn = friendly_name
        self._pub_task = None
        gc.collect()

    def getStateChangeEvent(self):
        """
        Returns an event that gets triggered on every state change
        :return: Event
        """
        if self._event is None:
            from pysmartnode.utils.event import Event
            self._event = Event()
        return self._event

    def _setState(self, state):
        if state != self._state and self._event is not None:
            self._event.set(state)
        self._state = state

    async def on_message(self, topic, msg, retain):
        """
        Standard callback to change the device state from mqtt.
        Can be subclassed if extended functionality is needed.
        """
        if msg in _mqtt.payload_on:
            if not self._state:  # False or None (unknown)
                await self.on()
        elif msg in _mqtt.payload_off:
            if self._state is not False:  # True or None (unknown)
                await self.off()
        else:
            raise TypeError("Payload {!s} not supported".format(msg))
        return False  # will not publish the requested state to mqtt as already done by on()/off()

    async def __publish(self, msg):
        await _mqtt.publish(self._topic[:-4], msg, qos=1, retain=True)
        self._pub_task = None

    async def on(self):
        """Turn switch on. Can be used by other components to control this component"""
        if self.lock.locked() is True and self._wfl is False:
            return False
        async with self.lock:
            res = await self._on(
            )  # if _on() returns True the value should be published
            if res is True:
                self._setState(True)
                if self._pub_task:
                    asyncio.cancel(self._pub_task)
                    self._pub_task = None
                self._pub_task = asyncio.get_event_loop().create_task(
                    self.__publish("ON"))
            return res

    async def off(self):
        """Turn switch off. Can be used by other components to control this component"""
        if self.lock.locked() is True and self._wfl is False:
            return False
        async with self.lock:
            res = await self._off(
            )  # if _off() returns True the value should be published
            if res is True:
                self._setState(False)
                if self._pub_task:
                    asyncio.cancel(self._pub_task)
                    self._pub_task = None
                self._pub_task = asyncio.get_event_loop().create_task(
                    self.__publish("OFF"))
            return res

    async def toggle(self):
        """Toggle device state. Can be used by other component to control this component"""
        if self._state is True:
            return await self.off()
        else:
            return await self.on()

    def state(self):
        return self._state

    async def _discovery(self, register=True):
        name = self._name or "{!s}{!s}".format(self.COMPONENT_NAME,
                                               self._count)
        if register:
            await self._publishDiscovery("switch", self._topic[:-4], name,
                                         DISCOVERY_SWITCH, self._frn)
        else:
            await self._deleteDiscovery("switch", name)
        # note that _publishDiscovery does expect the state topic
        # but we have the command topic stored.

    def topic(self):
        return self._topic
Пример #11
0
class ComponentSensor(Component):
    def __init__(self,
                 component_name,
                 version,
                 unit_index: int,
                 discover,
                 interval_publish=None,
                 interval_reading=None,
                 mqtt_topic=None,
                 log=None,
                 expose_intervals=False,
                 intervals_topic=None):
        """
        :param component_name: Name of the component, used for default topics and logging
        :param version: version of the component module, used for logging purposes
        :param unit_index: counter of the registerd unit of this sensor_type (used for default topics)
        :param discover: if the sensor component should send its homeassistnat discovery message
        :param interval_publish: seconds, set to interval_reading to publish every reading. -1 for not publishing.
        :param interval_reading: seconds, set to -1 for not reading/publishing periodically. >0 possible for reading, 0 not allowed for reading..
        Be careful with relying on reading sensors quickly because every publish
        will take at most 5 seconds per sensor_type until it times out.
        If you rely on quick and reliable sensor reading times choose interval_publish=-1 and start
        your own coroutine for publishing values.
        :param mqtt_topic: optional custom mqtt topic
        :param expose_intervals: Expose intervals to mqtt so they can be changed remotely
        :param intervals_topic: if expose_intervals then use this topic to change intervals.
        Defaults to <home>/<device-id>/<COMPONENT_NAME><_unit_index>/interval/set
        """
        super().__init__(component_name, version, unit_index, discover)
        self._values = {}
        self._log = log or logging.getLogger(component_name)
        # _intpb can be >0, -1 for not publishing or 0/None for INTERVAL_SENSOR_PUBLISH
        self._intpb: float = interval_publish or config.INTERVAL_SENSOR_PUBLISH
        self._intrd: float = config.INTERVAL_SENSOR_READ if interval_reading is None else interval_reading
        if self._intpb < self._intrd:
            raise ValueError(
                "interval_publish can't be lower than interval_reading")
        self._topic = mqtt_topic  # can be None
        self._event = None
        self._reading: bool = False  # cheaper than Lock
        if expose_intervals:
            _mqtt.subscribeSync(intervals_topic or _mqtt.getDeviceTopic(
                "{!s}/interval/set".format(self._default_name())),
                                self.setInterval,
                                self,
                                qos=1,
                                check_retained_state=True)
        self._loop_coro = None
        if self._intrd > 0:  # if interval_reading==-1 no loop will be started
            self._loop_coro = self._loop()
            asyncio.get_event_loop().create_task(self._loop_coro)
            # self._loop_coro will get canceled when component is removed.
        gc.collect()

    async def _remove(self):
        """Called by component base class when a sensor component should be removed"""
        if self._loop_coro is not None:
            asyncio.cancel(self._loop_coro)
        await super()._remove()

    def _addSensorType(self,
                       sensor_type: str,
                       precision: int,
                       offset: float,
                       value_template: str,
                       unit_of_meas: str,
                       friendly_name: str = None,
                       topic: str = None,
                       discovery_type: str = None,
                       binary_sensor: bool = False):
        """
        :param sensor_type: Name of the sensor type, preferrably used by references to .definitons module
        :param precision: digits after separator "."
        :param offset: float offset to account for bad sensor readings
        :param value_template: value template in homeassistant.
        :param unit_of_meas: unit of measurement in homeassistant
        :param friendly_name: friendly name in homeassistant, falls back to sensor_type if not given
        :param topic: Each sensor_type can have its own topic or publish on one topic
        :param discovery_type: custom discovery configuration if sensor is not supported by
        standard types with device_class, unit_of_meas, value_template
        :param binary_sensor: if sensor is a binary_sensor, otherwise default sensor.
        :return:
        """
        tp = [
            int(precision),
            float(offset), value_template, unit_of_meas, friendly_name, topic,
            discovery_type, binary_sensor, None, None
        ]
        # tp[-1] is last sensor reading, tp[-2] is timestamp of last reading (not None)
        self._values[sensor_type] = tp

    def setReadingInterval(self, *args):
        """
        Change the reading interval.
        Call function like setReadingInterval(5).
        Note that changing read interval to be bigger than publish interval will make
        publish interval to behave like read interval.
        :param args: args expected so function can be exposed to mqtt directly.
        :return:
        """
        self._intrd = float(args[0] if len(args) == 1 else args[1])
        return True

    def setPublishInterval(self, *args):
        """
        Change the publish interval.
        Call function like setReadingInterval(5).
        Note that changing publish interval to be smaller than read interval will make
        publish interval to behave like read interval.
        :param args: args expected so function can be exposed to mqtt directly.
        :return:
        """
        self._intpb = float(args[0] if len(args) == 1 else args[1])
        return True

    def setInterval(self, *args):
        """
        Change both intervals using a dictionary. Can be exposed to mqtt
        :param args:
        :return:
        """
        ints = args[0] if len(args) == 1 else args[1]
        if type(ints) != dict:
            raise TypeError("Interval change needs to be dict")
        if "reading" in ints:
            self.setReadingInterval(ints["reading"])
        if "publish" in ints:
            self.setPublishInterval(ints["publish"])
        return True

    def getReadingsEvent(self):
        """
        Returns an event that gets triggered on every read
        :return: Event
        """
        if self._event is None:
            from pysmartnode.utils.event import Event
            self._event = Event()
        return self._event

    async def _publishValues(self, timeout: float = 5):
        d = {}
        t = self._topic or _mqtt.getDeviceTopic(self._default_name())
        for sensor_type in self._values:
            val = self._values[sensor_type]
            if val[-1] is not None:
                if val[5] is None:  # no topic for sensor_type
                    d[sensor_type] = val[-1]
                else:
                    msg = val[-1]
                    if type(msg) == bool and val[7]:  # binary sensor
                        msg = _mqtt.payload_on[
                            0] if msg else _mqtt.payload_off[0]
                    # elif type(msg)==float:
                    #     msg =("{0:." + str(val[0]) + "f}").format(msg)
                    # on some platforms this might make sense as a workaround for 25.3000000001
                    await _mqtt.publish(val[5], msg, qos=1, timeout=timeout)
        if len(d) == 1 and "value_json" not in self._values[list(
                d.keys())[0]][2]:
            # topic has no json template so send it without dict
            d = d[list(d.keys())[0]]
        if type(d) != dict or len(
                d) > 0:  # single value or dict with at least one entry
            await _mqtt.publish(t, d, qos=1, timeout=timeout)

    def _default_name(self):
        """
        Can be used to override the default naming pattern, e.g. if the sensors have a unique id
        """
        return "{!s}{!s}".format(self.COMPONENT_NAME, self._count)

    async def _discovery(self, register=True):
        for sensor_type in self._values:
            val = self._values[sensor_type]
            if len(self._values) > 0:
                name = "{!s}{!s}".format(self._default_name(),
                                         sensor_type[0].upper())
            else:
                name = self._default_name()
            tp = val[6] or self._composeSensorType(sensor_type, val[3], val[2])
            if register:
                await self._publishDiscovery(
                    "binary_sensor" if val[7] else "sensor",
                    self.getTopic(sensor_type), name, tp, val[4]
                    or "{}{}".format(sensor_type[0].upper(), sensor_type[1:]))
            else:
                await self._deleteDiscovery("binary_sensor", name)
            del name, tp
            gc.collect()

    @property
    def sensor_types(self):
        """
        Returns the registered sensor types so other components can check if the
        sensor object supports the type they want to read.
        """
        return self._values.keys()

    def _checkType(self, sensor_type):
        if sensor_type not in self._values:
            raise ValueError("sensor_type {!s} unknown".format(sensor_type))

    async def getValues(self):
        """Returns all current values as a dictionary. No read or publish possible"""
        return dict((x, self._values[x][-1]) for x in self._values)

    def getTimestamps(self):
        return dict((x, self._values[x][-2]) for x in self._values)

    def getTimestamp(self, sensor_type=None):
        """Return timestamp of last successful sensor read (last value that was not None)"""
        self._checkType(sensor_type)
        return self._values[sensor_type][-2]

    async def getValue(self,
                       sensor_type,
                       publish=True,
                       timeout: float = 5,
                       max_age: float = None):
        """
        Return last sensor reading of type "sensor_type".
        Only reads sensor if no loop is reading it periodically unless no_stale is True.
        If a loop is active, it will return the last read value which will be
        at most <self._intrd> old.
        Params publish and timeout will only have an effect if no loop is active or no_stale True.
        :param sensor_type: str representation of the sensor_type.
        :param publish: if value should be published if sensor has to be read for returned value
        :param timeout: timeout for publishing the value
        :param max_age: Specify how old the value can be. If it is older, the sensor will be read again.
        Makes long intervals possible with other components that rely on having a "live" sensor reading.
        :return: float or whatever the sensor_type has as a standard, None if no value available
        """
        self._checkType(sensor_type)
        if max_age:
            if time.ticks_diff(time.ticks_ms(),
                               self._values[sensor_type][-2]) / 1000 > max_age:
                max_age = True
            else:
                max_age = False
        if max_age or self._intrd == -1:
            if self._reading:  # if currently reading, don't read again as value will be "live"
                while self._reading:
                    await asyncio.sleep_ms(100)
            else:
                self._reading = True
                await self._read()
                self._reading = False
            if publish:
                await self._publishValues(timeout=timeout)
        return self._values[sensor_type][-1]

    def getTemplate(self, sensor_type):
        self._checkType(sensor_type)
        return self._values[sensor_type][2]

    def getTopic(self, sensor_type):
        self._checkType(sensor_type)
        return self._values[sensor_type][
            5] or self._topic or _mqtt.getDeviceTopic(self._default_name())

    async def _setValue(self, sensor_type, value, timeout=10):
        """
        Set the newly read value for the given sensor_type
        :param sensor_type:
        :param value:
        :param timeout: timeout for logging if value is None or exception in processing value.
        If sensor is designed to be read quickly and logging if no value could be read is not
        relevant, then the sensor _read function should not call _setValue if it has no value.
        :return:
        """
        self._checkType(sensor_type)
        s = self._values[sensor_type]
        if value is not None:
            if type(value) in (int, float):
                try:
                    value = round(value, s[0])
                    value += s[1]
                except Exception as e:
                    log = self._log or logging.getLogger(self.COMPONENT_NAME)
                    await log.asyncLog("error",
                                       "Error processing value:",
                                       e,
                                       timeout=timeout)
                    value = None
            elif type(value) not in (bool, str):
                log = self._log or logging.getLogger(self.COMPONENT_NAME)
                await log.asyncLog("error",
                                   "Error processing value:",
                                   value,
                                   "no known type",
                                   timeout=timeout)
                value = None

        else:
            log = self._log or logging.getLogger(self.COMPONENT_NAME)
            await log.asyncLog("warn",
                               "Got no value for",
                               sensor_type,
                               timeout=timeout)
        s[-1] = value
        if value:
            s[-2] = time.ticks_ms()

    async def _loop(self):
        await asyncio.sleep(1)
        d = float("inf") if self._intpb == -1 else (self._intpb / self._intrd)
        i = d + 1  # so first reading gets published
        pbc = None
        try:
            while True:
                # d recalculated in loop so _intpb and _intrd can be changed during runtime
                d = float("inf") if self._intpb == -1 else (self._intpb /
                                                            self._intrd)
                pb = i >= d
                i = 1 if pb else i + 1
                t = time.ticks_ms()
                while self._reading:
                    # wait when sensor is read because of a getValue(no_stale=True) request
                    await asyncio.sleep_ms(50)
                self._reading = True
                await self._read()
                self._reading = False
                if self._event:
                    self._event.set()
                if pb:
                    if pbc is not None:
                        asyncio.cancel(pbc)
                    vals = 0
                    # counting sensor_types which have a topic as those get published separately
                    for tp in self._values:
                        if tp[5] is not None:
                            vals += 1
                    vals = vals or 1  # if no type has a topic, one is still used to publish
                    sl = self._intrd * 1000 - time.ticks_diff(
                        time.ticks_ms(), t)
                    if sl / 1000 > 5 * vals:
                        # if enough time is left until next reading then await publishing values
                        # (5 seconds per unique sensor_type)
                        await self._publishValues()
                    else:
                        # otherwise start task to publish values which might get canceled if
                        # it can't finish until next publish is requested.
                        pbc = self._publishValues()
                        asyncio.get_event_loop().create_task(pbc)
                # sleep until the sensor should be read again. Using loop with 100ms makes
                # changing the read interval during runtime possible with a reaction time of 100ms.
                while True:
                    sl = self._intrd * 1000 - time.ticks_diff(
                        time.ticks_ms(), t)
                    sl = 100 if sl > 100 else sl if sl > 0 else 0
                    # sleeping with 0 lets other coros run in between
                    await asyncio.sleep_ms(sl)
                    if sl == 0:  # sleeping done
                        break
        except asyncio.CancelledError:
            if pbc is not None:
                asyncio.cancel(pbc)
            raise
        except NotImplementedError:
            raise
        except Exception as e:
            if config.DEBUG:
                import sys
                sys.print_exception(e)
            await self._log.asyncLog("critical",
                                     "Exception in component loop:", e)

    async def _read(self):
        """
        Subclass to read and store all sensor values.
        See sensor_template for examples of how to implement the _read method.
        :return:
        """
        raise NotImplementedError("No sensor _read method implemented")