class GPIO(ComponentSwitch): def __init__(self, pin, active_high=True, mqtt_topic=None, instance_name=None, **kwargs): mqtt_topic = mqtt_topic or _mqtt.getDeviceTopic( "{!s}/{!s}".format(COMPONENT_NAME, str(pin)), is_request=True) global _unit_index _unit_index += 1 super().__init__(COMPONENT_NAME, __version__, _unit_index, mqtt_topic=mqtt_topic, instance_name=instance_name or "{!s}_{!s}".format(COMPONENT_NAME, pin), **kwargs) self.pin = Pin(pin, machine.Pin.OUT, value=0 if active_high else 1) self._state = not active_high self._active_high = active_high async def _on(self): self.pin.value(1 if self._active_high else 0) return True async def _off(self): self.pin.value(0 if self._active_high else 1) return True
class GPIO(Component): def __init__(self, pin, mqtt_topic=None, friendly_name=None): super().__init__() mqtt_topic = mqtt_topic or _mqtt.getDeviceTopic( "{!s}/{!s}".format(_component_name, str(pin)), is_request=True) self._topic = mqtt_topic self._subscribe(self._topic, self.on_message) self.pin = Pin(pin, machine.Pin.OUT, value=0) self._frn = friendly_name self._name = "{!s}_{!s}".format(_component_name, pin) async def _discovery(self): await self._publishDiscovery(_component_type, self._topic[:-4], self._name, DISCOVERY_SWITCH, self._frn) async def on_message(self, topic, msg, retained): _log = logging.getLogger("gpio") if msg in _mqtt.payload_on: self.pin.value(1) return True elif msg in _mqtt.payload_off: self.pin.value(0) return True else: _log.error("Unknown payload {!r}, GPIO {!s}".format(msg, self.pin)) return False
class LEDNotification(Component): def __init__(self, pin, on_time=50, off_time=50, iters=20, mqtt_topic=None, friendly_name=None): super().__init__() self.pin = Pin(pin, machine.Pin.OUT, value=0) self.on_time = on_time self.off_time = off_time self.iters = iters self.lock = config.Lock() # This makes it possible to use multiple instances of LED global _count self._count = _count _count += 1 mqtt_topic = mqtt_topic or _mqtt.getDeviceTopic( "{!s}{!s}".format(_component_name, self._count), is_request=True) self._topic = mqtt_topic self._frn = friendly_name gc.collect() async def _init(self): await super()._init() self._subscribe(self._topic, self.on_message) await _mqtt.subscribe(self._topic, check_retained_state_topic=False) # not checking retained state as led only activates single-shot and default state is always off async def on_message(self, topic, msg, retain): if self.lock.locked(): return False async with self.lock: if msg in _mqtt.payload_on: _mqtt.schedulePublish(self._topic[:-4], "ON", qos=1) for i in range(0, self.iters): self.pin.value(1) await asyncio.sleep_ms(self.on_time) self.pin.value(0) await asyncio.sleep_ms(self.off_time) await _mqtt.publish(self._topic[:-4], "OFF", qos=1, retain=True) return False # will not publish the state "ON" to mqtt async def _discovery(self): name = "{!s}{!s}".format(_component_name, self._count) await self._publishDiscovery(_component_type, self._topic[:-4], name, DISCOVERY_SWITCH, self._frn)
class WIFILED(ComponentBase): def __init__(self, pin, active_high=True, **kwargs): super().__init__(COMPONENT_NAME, __version__, discover=False, unit_index=0, **kwargs) self.pin = Pin(pin, machine.Pin.OUT, value=0 if active_high else 1) self._active_high = active_high self._next = [] asyncio.create_task(self._loop()) async def _loop(self): mqtt = config.getMQTT() mqtt.registerWifiCallback(self._wifiChanged) mqtt.registerConnectedCallback(self._reconnected) await self._flash(500, 1) sta = network.WLAN(network.STA_IF) st = time.ticks_ms() while True: while self._next: await self._flash(*self._next.pop(0)) await asyncio.sleep(1) if time.ticks_diff(time.ticks_ms(), st) > 60000: # heartbeat st = time.ticks_ms() if sta.isconnected(): await self._flash(20, 1) await asyncio.sleep_ms(250) await self._flash(20, 1) else: await self._flash(500, 3) await asyncio.sleep_ms(500) async def _flash(self, duration, iters): for _ in range(iters): self.pin.value(1 if self._active_high else 0) await asyncio.sleep_ms(duration) self.pin.value(0 if self._active_high else 1) await asyncio.sleep_ms(duration) async def _wifiChanged(self, state): if state is True: self._next.append((50, 2)) else: self._next.append((500, 3)) async def _reconnected(self, client): self._next.append((100, 5))
class LEDNotification(ComponentButton): def __init__(self, pin, on_time=50, off_time=50, iters=20, **kwargs): self.pin = Pin(pin, machine.Pin.OUT, value=0) self.on_time = on_time self.off_time = off_time self.iters = iters # This makes it possible to use multiple instances of LED global _unit_index _unit_index += 1 super().__init__(COMPONENT_NAME, __version__, _unit_index, **kwargs) gc.collect() async def _on(self): for _ in range(self.iters): self.pin.value(1) await asyncio.sleep_ms(self.on_time) self.pin.value(0) await asyncio.sleep_ms(self.off_time)
class Mux: def __init__(self, shift_pin, store_pin, data_pin, number_multiplexer=1): self.shcp = PyPin(shift_pin, machine.Pin.OUT) self.stcp = PyPin(store_pin, machine.Pin.OUT) self.ds = PyPin(data_pin, machine.Pin.OUT) self.__data = bytearray() for i in range(0, 8 * number_multiplexer): self.__data.append(0) self.__size = number_multiplexer self.write() def write(self): self.stcp.value(0) for i in range((8 * self.__size) - 1, -1, -1): self.shcp.value(0) self.ds.value(self.__data[i]) self.shcp.value(1) self.stcp.value(1) def __setitem__(self, a, b): if b != 1 and b != 0: raise ValueError("Value must be 1 or 0") self.__data[a] = b def __getitem__(self, a): return self.__data[a] def __delitem__(self, a): self.__data[a] = 0 def set(self, i): self.__data[i] = 1 def clear(self, i): self.__data[i] = 0 def getSize(self): """ Get number of pins""" return self.__size * 8 def Pin(self, i, *args, **kwargs): return Pin(self, i)
class EC(Component): DEBUG = False def __init__(self, r1, ra, adc, power_pin, ppm_conversion, temp_coef, k, temp_sensor, precision_ec=3, interval=None, topic_ec=None, topic_ppm=None, friendly_name_ec=None, friendly_name_ppm=None): super().__init__() self._interval = interval or config.INTERVAL_SEND_SENSOR self._prec_ec = int(precision_ec) self._adc = ADC(adc) self._ppin = Pin(power_pin, machine.Pin.OUT) self._r1 = r1 self._ra = ra self._ppm_conversion = ppm_conversion self._temp_coef = temp_coef self._k = k self._temp = temp_sensor if hasattr(temp_sensor, "temperature") is False: raise AttributeError( "Temperature sensor {!s}, type {!s} has no async method temperature()".format(temp_sensor, type(temp_sensor))) gc.collect() self._ec25 = None self._ppm = None self._time = 0 # This makes it possible to use multiple instances of MySensor global _count self._count = _count _count += 1 self._topic_ec = topic_ec or _mqtt.getDeviceTopic("{!s}/{!s}".format("EC", self._count)) self._topic_ppm = topic_ppm or _mqtt.getDeviceTopic("{!s}/{!s}".format("PPM", self._count)) self._frn_ec = friendly_name_ec self._frn_ppm = friendly_name_ppm async def _init(self): await super()._init() gen = self._read interval = self._interval while True: await gen() await asyncio.sleep(interval) async def _discovery(self): sens = '"unit_of_meas":"mS",' \ '"val_tpl":"{{ value|float }}",' name = "{!s}{!s}{!s}".format(_component_name, self._count, "EC25") await self._publishDiscovery(_component_type, self._topic_ec, name, sens, self._frn_ec or "EC25") del sens, name gc.collect() sens = '"unit_of_meas":"ppm",' \ '"val_tpl":"{{ value|int }}",' name = "{!s}{!s}{!s}".format(_component_name, self._count, "PPM") await self._publishDiscovery(_component_type, self._topic_ppm, name, sens, self._frn_ppm or "PPM") del sens, name gc.collect() async def _read(self, publish=True): if time.ticks_ms() - self._time < 5000: self._time = time.ticks_ms() await asyncio.sleep(5) temp = await self._temp.temperature(publish=False) if temp is None: await asyncio.sleep(3) temp = await self._temp.temperature(publish=False) if temp is None: _log.warn("Couldn't get temperature, aborting EC measurement") self._ec25 = None self._ppm = None return None, None self._ppin.value(1) vol = self._adc.readVoltage() # vol = self._adc.readVoltage() # micropython on esp is probably too slow to need this. It was intended for arduino if self.DEBUG is True: print("Temp", temp) print("V", vol) self._ppin.value(0) if vol >= self._adc.maxVoltage(): ec25 = 0 ppm = 0 await _log.asyncLog("warn", "Cable not in fluid") else: if vol <= 0.5: _log.warn("Voltage <=0.5, change resistor") rc = (vol * (self._r1 + self._ra)) / (self._adc.maxVoltage() - vol) # rc = rc - self._ra # sensor connected to ground, not a pin, therefore no Ra ec = 1000 / (rc * self._k) ec25 = ec / (1 + self._temp_coef * (temp - 25.0)) ppm = int(ec25 * self._ppm_conversion * 1000) ec25 = round(ec25, self._prec_ec) if self.DEBUG: print("Rc", rc) print("EC", ec) print("EC25", ec25, "MilliSimens") print("PPM", ppm) self._ec25 = ec25 self._ppm = ppm self._time = time.ticks_ms() if publish: await _mqtt.publish(self._topic_ec, ("{0:." + str(self._prec_ec) + "f}").format(ec25)) await _mqtt.publish(self._topic_ppm, ppm) return ec25, ppm async def ec(self, publish=True): if time.ticks_ms() - self._time > 5000: await self._read(publish) return self._ec25 async def ppm(self, publish=True): if time.ticks_ms() - self._time > 5000: await self._read(publish) return self._ppm
class Bell(ComponentSensor): def __init__(self, pin, debounce_time, on_time=None, direction=None, pull_up=True, friendly_name=None, friendly_name_last=None, confirmations=1, ac_signal=False, **kwargs): global _unit_index _unit_index += 1 super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, interval_reading=0.001, interval_publish=0.001, expose_intervals=False, **kwargs) self._PIN_BELL_IRQ_DIRECTION = direction or machine.Pin.IRQ_FALLING self._debounce_time = debounce_time self._on_time = on_time or 500 self._event_bell = EventISR(delay_ms=10) self._pin_bell = Pin(pin, machine.Pin.IN, machine.Pin.PULL_UP if pull_up else None) self._pin_bell.irq(trigger=self._PIN_BELL_IRQ_DIRECTION, handler=self.__irqBell) self._timer_delay = 0 self._last_activation = 0 self._confirmations = confirmations self._ac = ac_signal gc.collect() self._addSensorType("bell", friendly_name=friendly_name, binary_sensor=True, discovery_type=_DISCOVERY_BELL, retained_publication=True, topic=_mqtt.getDeviceTopic("bell{!s}".format( self._count))) self._addSensorType("last_bell", friendly_name=friendly_name_last, topic=_mqtt.getDeviceTopic("last_bell{!s}".format( self._count)), discovery_type=DISCOVERY_TIMELAPSE, retained_publication=True) _log.info("Bell initialized") gc.collect() async def _read(self): if self._event_bell.is_set(): # if event is set, wait the on_time and reset the state to publish "off" await asyncio.sleep_ms( self._on_time - time.ticks_diff(time.ticks_ms(), self.getTimestamp("bell"))) await self._setValue("bell", False) self._event_bell.clear() # print(time.ticks_us(), "loop cleared event") return # print(time.ticks_us(), "loop awaiting event") await self._event_bell.wait() # print(time.ticks_us(), "loop got event") sl = int(self._debounce_time / self._confirmations) if self._confirmations > 0 else 0 confirmations = 0 for _ in range(0, self._confirmations): diff = time.ticks_diff(time.ticks_us(), self._timer_delay) value = self._pin_bell.value() # print(time.ticks_us(), "Timer took", diff / 1000, "ms, pin value", value) if not self._ac and ( self._PIN_BELL_IRQ_DIRECTION == machine.Pin.IRQ_FALLING and value == 1 \ or self._PIN_BELL_IRQ_DIRECTION == machine.Pin.IRQ_RISING and value == 0): # print(time.ticks_us(), "pin value didn't stay") self._event_bell.clear() return False # return False so no value gets published elif self._ac and ( self._PIN_BELL_IRQ_DIRECTION == machine.Pin.IRQ_FALLING and value == 0 \ or self._PIN_BELL_IRQ_DIRECTION == machine.Pin.IRQ_RISING and value == 1): confirmations += 1 self._timer_delay = time.ticks_us() await asyncio.sleep_ms(sl) # print(self._ac, confirmations, self._confirmations) if self._ac: if confirmations > 0: # print(time.ticks_us(), "ac signal confirmed") pass else: # print(time.ticks_us(), "no ac signal, only irq") self._event_bell.clear() return False # print(time.ticks_us(), "irq confirmed") 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 False # return False so no value gets published await self._setValue("bell", True) t = time.localtime() await self._setValue( "last_bell", "{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format( t[0], t[1], t[2], t[3], t[4], t[5])) if diff > 500: _log.warn( "Bell rang {!s}ms ago, activated ringing anyways".format(diff)) def __irqBell(self, p): # print(time.ticks_us(), "irq bell", self._event_bell.is_set()) if self._event_bell.is_set() is True: return # print("event set") self._timer_delay = time.ticks_us() self._event_bell.set() self._last_activation = time.ticks_ms()
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()
class Amux: def __init__(self, s0, s1, s2, s3=None, mux=None, adc=None, return_voltages=False): """ It is possibile to initialize with: - pin numbers (or string on esp8266) - mux object and pin numbers (of mux pins) - Pin objects (either from machine or mux Pin objects [no mux object needed], or Arduino) :type return_voltages: bool, True returns voltages on .read() else raw adc value :type mux: Mux object if a multiplexer is used :type adc: ADC pin number (esp32) or None (esp8266) or Arduino ADC object or any ADC object Amux uses default return values of ADC in .read() --> On esp8266/esp32 raw, on esp32_LoBo voltage s3 is optional, only needed if 16 pins are used, 8 pins possible with s0-s2. Amux can be read like a list: value=amux[2] """ if mux: # MUX pin numbers, not pin objects self._s0 = s0 self._s1 = s1 self._s2 = s2 self._s3 = s3 self._mux = mux else: # Pin will take care of returning the correct object self._s0 = Pin(s0, machine.Pin.OUT) self._s1 = Pin(s1, machine.Pin.OUT) self._s2 = Pin(s2, machine.Pin.OUT) if s3: self._s3 = Pin(s3, machine.Pin.OUT) if s3: self.__size = 16 else: self.__size = 8 self._return_voltages = return_voltages self._adc = _ADC( adc ) # no matter what adc is, _ADC will return an object with the unified ADC API def setReturnVoltages(self, vol): self._return_voltages = vol def __getitem__(self, a): return self.read(a) def getSize(self): """ Get number of pins""" return self.__size def read(self, a, return_voltage=None): if a >= self.__size: raise ValueError("Maximum Port number is {!s}".format(self.__size - 1)) if type(self._s0) == int: # mux pins if self.__size == 16: self._mux[self._s3] = (1 if a & 8 else 0) self._mux[self._s2] = (1 if a & 4 else 0) self._mux[self._s1] = (1 if a & 2 else 0) self._mux[self._s0] = (1 if a & 1 else 0) self._mux.write() else: if self.__size == 16: self._s3.value(1 if a & 8 else 0) self._s2.value(1 if a & 4 else 0) self._s1.value(1 if a & 2 else 0) self._s0.value(1 if a & 1 else 0) if return_voltage is True or return_voltage is None and self._return_voltages is True: return self._adc.readVoltage() else: return self._adc.readRaw() def readVoltage(self, a): return self.read(a, return_voltage=True) def readRaw(self, a): return self.read(a, return_voltage=False) def ADC(self, i, *args, **kwargs): """ compatible to machine.ADC, returns an ADC object""" return ADC(self, i) def atten(self, *args, **kwargs): self._adc.atten(*args, **kwargs) def width(self, *args, **kwargs): self._adc.width(*args, **kwargs)
class Pmux: def __init__(self, s0, s1, s2, pin, s3=None, mux=None, pin_direction="OUT", pin_pull=None): """ It is possibile to initialize with: - pin numbers (or string on esp8266) - mux object and pin numbers (of mux pins) - Pin objects (either from machine or mux Pin objects [no mux object needed]) :type mux: Mux object if a multiplexer is used :type pin: pin number or string (esp8266) :type pin_direction: str of pin_direction s3 is optional, only needed if 16 pins are used, 8 pins possible with s0-s2. pmux can be read like a list: value=amux[2] pmux can be set like a list: amux[2]=1 """ if mux: self.s0 = s0 self.s1 = s1 self.s2 = s2 self.s3 = s3 self.mux = mux else: if type(s0) in (int, str): self.s0 = PyPin(s0, machine.Pin.OUT) self.s1 = PyPin(s1, machine.Pin.OUT) self.s2 = PyPin(s2, machine.Pin.OUT) if s3: self.s3 = PyPin(s3, machine.Pin.OUT) else: self.s0 = s0 self.s1 = s1 self.s2 = s2 if s3: self.s3 = s3 if s3: self.__size = 16 else: self.__size = 8 self._selected_pin = None if pin_direction not in dir(machine.Pin): raise TypeError( "Pin_direction {!r} does not exist".format(pin_direction)) if pin_pull not in dir(machine.Pin): raise TypeError("Pin_pull {!s} does not exist".format(pin_pull)) self.pin = PyPin(pin, getattr(machine.Pin, pin_direction), pin_pull) def __getitem__(self, a): return self.value(a) def __setitem__(self, a, direction): return self.value(a, direction) def getSize(self): """ Get number of pins""" return self.__size def _selectPin(self, a): if a >= self.__size: raise ValueError("Maximum Port number is {!s}".format(self.__size - 1)) if type(self.s0) == int: # mux pins if self.__size == 16: self.mux[self.s3] = (1 if a & 8 else 0) self.mux[self.s2] = (1 if a & 4 else 0) self.mux[self.s1] = (1 if a & 2 else 0) self.mux[self.s0] = (1 if a & 1 else 0) self.mux.write() else: if self.__size == 16: self.s3.value(1 if a & 8 else 0) self.s2.value(1 if a & 4 else 0) self.s1.value(1 if a & 2 else 0) self.s0.value(1 if a & 1 else 0) def value(self, a, value=None): if a != self._selected_pin: # only select pin if needed as it would slow IO-operations down and screw timings self._selectPin(a) if value is None: return self.pin.value() else: return self.pin.value(value) def mode(self, mode): if type(mode) == str: if mode not in dir(machine.Pin): raise TypeError("Mode {!r} is not available".format(mode)) mode = getattr(machine.Pin, mode) self.pin.mode(mode) def pull(self, p=None): return self.pin.pull(p) def drive(self, d=None): return self.pin.drive(d) def init(self, *args, **kwargs): self.pin.init(*args, **kwargs) def Pin(self, p): return Pin(self, p)
class HCSR04(ComponentSensor): def __init__(self, pin_trigger, pin_echo, timeout=30000, temp_sensor: ComponentSensor = None, precision: int = 2, offset: float = 0, sleeping_time: int = 20, iterations: int = 30, percentage_failed_readings_abort: float = 0.66, value_template=None, friendly_name=None, **kwargs): """ HC-SR04 ultrasonic sensor. Be sure to connect it to 5V but use a voltage divider to connect the Echo pin to an ESP. :param pin_trigger: pin number/object of trigger pin :param pin_echo: pin number/object of echo pin :param timeout: reading timeout, corresponds to sensor limit range of 4m :param temp_sensor: temperature sensor object :param precision: int, precision of distance value published and returned :param offset: float, distance value offset, shouldn't be needed :param sleeping_time: int, sleeping time between reading iterations :param iterations: int, reading iterations per sensor reading :param percentage_failed_readings_abort: float, if a higher percentage of readings was bad, the reading will be aborted :param value_template: optional template can be used to measure the reverse distance (e.g. water level) :param friendly_name: friendly name for homeassistant gui by mqtt discovery, defaults to "Distance" """ global _unit_index _unit_index += 1 super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, **kwargs) self._tr = Pin(pin_trigger, mode=machine.Pin.OUT) self._tr.value(0) self._ec = Pin(pin_echo, mode=machine.Pin.IN) self._to = timeout self._sleep = sleeping_time self._iters = iterations self._pfr = percentage_failed_readings_abort if temp_sensor is not None: self.checkSensorType(temp_sensor, SENSOR_TEMPERATURE) self._temp: ComponentSensor = temp_sensor self._addSensorType("distance", precision, offset, value_template or VALUE_TEMPLATE_FLOAT, "cm", friendly_name, discovery_type=DISCOVERY_DISTANCE) def _pulse(self) -> int: """ Send a pulse and wait for the echo pin using machine.time_pulse_us() to measure us. :return: int """ tr = self._tr tr.value(0) time.sleep_us(5) tr.value(1) time.sleep_us(10) tr.value(0) try: return machine.time_pulse_us(self._ec, 1, self._to) except OSError as e: if e.args[0] == 100: # TIMEOUT raise OSError("Object too far") raise e async def _read(self): if self._temp is not None: temp = await self._temp.getValue(SENSOR_TEMPERATURE, publish=self._intrd > 5, timeout=5 if self._intrd > 20 else 0) if temp is None: # only log errors if reading interval allows it await _log.asyncLog("warn", "Couldn't read temp sensor, using fallback calculation", timeout=10 if self._intrd > 20 else 0) else: temp = None val = [] diffs = [] warnings = 0 # probably not going to happen that both warning types occur at the same time warning = "minimum distance reached or different problem" for _ in range(self._iters): try: a = time.ticks_us() pt = self._pulse() b = time.ticks_us() if pt > 175: # ~3cm, sensor minimum distance, often read although other problems val.append(pt) diffs.append(time.ticks_diff(b, a)) else: warnings += 1 except OSError as e: warning = e warnings += 1 await asyncio.sleep_ms(self._sleep) if warnings > self._iters * self._pfr: # self._pbr sensor readings are bad if config.DEBUG: print("HCSR04 len readings", len(val), "/", self._iters, "sleep", self._sleep) print("HCSR04 readings", val) print("HCSR04 t_diffs:", diffs) await self._setValue("distance", None, log_error=False) await _log.asyncLog("error", "Too many bad sensor readings, error:", warning, timeout=10 if self._intrd > 20 else 0) return # removing extreme values until only 5-6 remain val2 = [v for v in val] while len(val) >= 7: val.remove(max(val)) val.remove(min(val)) pt = sum(val) / len(val) if temp is None: dt = (pt / 2) / 29.14 else: dt = (pt / 2) * ((331.5 + (0.6 * temp)) / 10000) if config.DEBUG: print("HCSR04 distance", dt, "temp ", temp, "len readings", len(val2), "/", self._iters, "sleep", self._sleep) print("HCSR04 readings", val2) print("HCSR04 t_diffs:", diffs) print("HCSR04 used readings", val) if dt < 0: await self._setValue("distance", None, log_error=False) await _log.asyncLog("warn", "Sensor reading <0", timeout=10 if self._intrd > 20 else 0) return await self._setValue("distance", dt, timeout=10 if self._intrd > 20 else 0)
class Moisture(Component): def __init__(self, adc_pin, water_voltage, air_voltage, sensor_types, power_pin=None, power_warmup=None, publish_converted_value=False, mqtt_topic=None, interval=None, friendly_name=None, friendly_name_cv=None): super().__init__() self._adc = ADC(adc_pin) if power_pin is None: self._ppin = None else: if type(power_pin) == list: self._ppin = [] for pin in power_pin: self._ppin.append(Pin(pin, machine.Pin.OUT)) else: self._ppin = Pin(power_pin, machine.Pin.OUT) self._power_warmup = power_warmup or None if power_pin is None else 10 self._sensor_types = sensor_types if isinstance( self._adc, pyADC ): # pyADC provides unified single ADC interface, not AMUX raise TypeError("Single ADC (no Amux) can't have multiple sensors") self._water_v = water_voltage self._air_v = air_voltage if type(sensor_types) == list: if type(water_voltage) != list or type(air_voltage) != list: raise TypeError( "Voltages have to be lists with multiple sensor_types") self._pub_cv = publish_converted_value self._topic = mqtt_topic or _mqtt.getDeviceTopic("moisture") self._interval = interval or config.INTERVAL_SEND_SENSOR self._lock = Lock() self._frn = friendly_name self._frn_cv = friendly_name_cv gc.collect() async def _init(self): await super()._init() while True: await self.humidity() await asyncio.sleep(self._interval) def _getConverted(self, sensor_type, voltage): if voltage is None: return None air_voltage = self._air_v if type( self._air_v) != list else self._air_v[sensor_type] water_voltage = self._water_v if type( self._water_v) != list else self._water_v[sensor_type] if sensor_type == 0: # std sensor if voltage > (water_voltage - air_voltage) / 2 + air_voltage: return "ON" # wet else: return "OFF" # dry elif sensor_type == 1: # capacitive if voltage > air_voltage - (air_voltage - water_voltage) / 2: return "OFF" # dry else: return "ON" # wet else: raise NotImplementedError( "Sensor type {!s} not implemented".format(sensor_type)) def _getPercentage(self, sensor_type, voltage): if voltage is None: return None air_voltage = self._air_v if type( self._air_v) != list else self._air_v[sensor_type] water_voltage = self._water_v if type( self._water_v) != list else self._water_v[sensor_type] if sensor_type == 0: # std sensor: diff = water_voltage - air_voltage if voltage < air_voltage: return 0 elif voltage > water_voltage: return 100 return round((voltage - air_voltage) / diff * 100) elif sensor_type == 1: # capacitive diff = air_voltage - water_voltage if voltage > air_voltage: return 0 elif voltage < water_voltage: return 100 return round((diff - (voltage - water_voltage)) / diff * 100) else: raise NotImplementedError( "Sensor type {!s} not implemented".format(sensor_type)) async def _read(self, publish=True) -> list: res = [] i = 0 amux = not isinstance(self._adc, pyADC) async with self._lock: if type(self._sensor_types) == list: sensors = self._sensor_types elif amux is True: sensors = [self._sensor_types] * self._adc.getSize() else: sensors = [self._sensor_types] for sensor in sensors: if self._ppin is not None: if type(self._ppin) == list: self._ppin[sensor].value(1) else: self._ppin.value(1) await asyncio.sleep_ms(self._power_warmup) voltage = None if sensor is None: res.append(None) else: voltage = 0 for j in range(3): voltage += self._adc.readVoltage( i) if amux else self._adc.readVoltage() voltage /= 3 res.append(self._getPercentage(sensor, voltage)) if publish: await _mqtt.publish(self._topic + "/" + str(i), res[-1]) if self._pub_cv: await _mqtt.publish( self._topic + "/" + str(i) + "/conv", self._getConverted(sensor, voltage)) if self._ppin is not None: if type(self._ppin) == list: self._ppin[sensor].value(0) else: self._ppin.value(0) gc.collect() i += 1 if len(res) == 0: return [None] * len(sensors) return res async def _discovery(self): amux = not isinstance(self._adc, pyADC) if type(self._sensor_types) == list: im = len(self._sensor_types) elif amux is True: im = self._adc.getSize() else: im = 1 for i in range(im): if self._pub_cv: name = "{!s}{!s}CV".format(_component_name, i) sens = DISCOVERY_BINARY_SENSOR.format( "moisture") # device_class t = "{!s}/{!s}/conv".format(self._topic, i) await self._publishDiscovery("binary_sensor", t, name, sens, self._frn_cv or "Moisture") name = "{!s}{!s}".format(_component_name, i) t = "{!s}/{!s}".format(self._topic, i) sens = DISCOVERY_SENSOR.format( "humidity", # device_class "%", # unit_of_measurement "{{ value|float }}") # value_template await self._publishDiscovery(_component_type, t, name, sens, self._frn or "Moisture rel.") del name, sens, t gc.collect() async def humidity(self, publish=True) -> list: """ Returns a list of all sensor values. Does currently not conform to new API definitions. """ return await self._read(publish=publish)
class Bell(ComponentSensor): def __init__(self, pin, debounce_time, on_time=None, direction=None, pull_up=True, friendly_name=None, friendly_name_last=None, confirmations=1, ac_signal=False, **kwargs): global _unit_index _unit_index += 1 super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, interval_reading=0.001, interval_publish=0.001, expose_intervals=False, **kwargs) self._low_active = True if direction == 2 else False self._debounce_time = debounce_time self._on_time = on_time or 500 self._pin_bell = Pin(pin, machine.Pin.IN, machine.Pin.PULL_UP if pull_up else None) self._timer_delay = 0 self._last_activation = 0 self._confirmations = confirmations self._ac = ac_signal self._active = False gc.collect() self._addSensorType("bell", friendly_name=friendly_name, binary_sensor=True, discovery_type=_DISCOVERY_BELL, retained_publication=True, topic=_mqtt.getDeviceTopic("bell{!s}".format(self._count))) self._addSensorType("last_bell", friendly_name=friendly_name_last, topic=_mqtt.getDeviceTopic("last_bell{!s}".format(self._count)), discovery_type=DISCOVERY_TIMELAPSE, retained_publication=True) _log.info("Bell initialized") gc.collect() async def _read(self): if self._active: # if bell was active, wait the on_time and reset the state to publish "off" await asyncio.sleep_ms( self._on_time - time.ticks_diff(time.ticks_ms(), self.getTimestamp("bell"))) await self._setValue("bell", False) self._active = False # print(time.ticks_us(), "loop cleared event") return # print(time.ticks_us(), "loop awaiting pin value change") while self._pin_bell.value() == self._low_active: await asyncio.sleep_ms(20) self._timer_delay = time.ticks_us() self._last_activation = time.ticks_ms() # print(time.ticks_us(), "loop pin value changed") sl = int(self._debounce_time / self._confirmations) if self._confirmations > 0 else 0 diff = [] confirmations = 0 for _ in range(0, self._confirmations): self._timer_delay = time.ticks_us() await asyncio.sleep_ms(sl) diff.append(time.ticks_diff(time.ticks_us(), self._timer_delay)) value = self._pin_bell.value() # print(time.ticks_us(), "Timer took", diff / 1000, "ms, pin value", value) if not self._ac and value == self._low_active: # print(time.ticks_us(), "pin value didn't stay") # print(diff) return False # return False so no value gets published elif self._ac and value != self._low_active: confirmations += 1 if self._ac: if confirmations > 0: # print(time.ticks_us(), "ac signal confirmed") pass else: # print(time.ticks_us(), "no ac signal, only irq") return False # print(time.ticks_us(), "pin change confirmed") # print(diff) 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)) return False # return False so no value gets published self._active = True await self._setValue("bell", True) t = time.localtime() await self._setValue("last_bell", "{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(t[0], t[1], t[2], t[3], t[4], t[5])) if diff > 500: _log.warn("Bell rang {!s}ms ago, activated ringing anyways".format(diff))
class HCSR04(ComponentSensor): def __init__(self, pin_trigger, pin_echo, timeout=30000, temp_sensor: ComponentSensor = None, precision=2, offset=0, interval_publish=None, interval_reading=None, mqtt_topic=None, value_template=None, friendly_name=None, discover=True, expose_intervals=False, intervals_topic=None): """ HC-SR04 ultrasonic sensor. Be sure to connect it to 5V but use a voltage divider to connect the Echo pin to an ESP. :param pin_trigger: pin number/object of trigger pin :param pin_echo: pin number/object of echo pin :param timeout: reading timeout, corresponds to sensor limit range of 4m :param temp_sensor: temperature sensor object :param precision: int, precision of distance value published and returned :param offset: float, distance value offset, shouldn't be needed :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.. :param mqtt_topic: distance mqtt topic :param value_template: optional template can be used to measure the reverse distance (e.g. water level) :param friendly_name: friendly name for homeassistant gui by mqtt discovery, defaults to "Distance" :param discover: boolean, if the device should publish its discovery :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 Send a dictionary with keys "reading" and/or "publish" to change either/both intervals. """ global _unit_index _unit_index += 1 super().__init__(COMPONENT_NAME, __version__, _unit_index, discover, interval_publish, interval_reading, mqtt_topic, _log, expose_intervals, intervals_topic) self._tr = Pin(pin_trigger, mode=machine.Pin.OUT) self._tr.value(0) self._ec = Pin(pin_echo, mode=machine.Pin.IN) self._to = timeout self.checkSensorType(temp_sensor, SENSOR_TEMPERATURE) self._temp: ComponentSensor = temp_sensor self._addSensorType("distance", precision, offset, value_template or VALUE_TEMPLATE_FLOAT, "cm", friendly_name, discovery_type=DISCOVERY_DISTANCE) def _pulse(self) -> int: """ Send a pulse and wait for the echo pin using machine.time_pulse_us() to measure us. :return: int """ tr = self._tr tr.value(0) time.sleep_us(5) tr.value(1) time.sleep_us(10) tr.value(0) try: return machine.time_pulse_us(self._ec, 1, self._to) except OSError as e: if e.args[0] == 100: # TIMEOUT raise OSError("Object too far") raise e async def _read(self): if self._temp is not None: temp = await self._temp.getValue( SENSOR_TEMPERATURE, publish=self._intrd > 5, timeout=5 if self._intrd > 20 else 0) if temp is None: # only log errors if reading interval allows it await _log.asyncLog( "warn", "Couldn't read temp sensor, using fallback calculation", timeout=10 if self._intrd > 20 else 0) else: temp = None val = [] warnings = 0 # probably not going to happen that both warning types occur at the same time warning = "minimum distance reached or different problem" for _ in range(20): try: pt = self._pulse() if pt > 175: # ~3cm, sensor minimum distance, often read although other problems val.append(pt) else: warnings += 1 except OSError as e: warning = e warnings += 1 await asyncio.sleep_ms(10) if warnings > 10: # half sensor readings are bad await _log.asyncLog("error", "Too many bad sensor readings, error:", warning, timeout=10 if self._intrd > 20 else 0) return # removing extreme values val.remove(max(val)) val.remove(max(val)) val.remove(min(val)) val.remove(min(val)) pt = 0 for v in val: pt += v pt /= len(val) if temp is None: dt = (pt / 2) / 29.14 else: dt = (pt / 2) * ((331.5 + (0.6 * temp)) / 10000) if dt < 0: await _log.asyncLog("warn", "Sensor reading <0", timeout=10 if self._intrd > 20 else 0) return await self._setValue("distance", dt, timeout=10 if self._intrd > 20 else 0)
class HCSR04(Component): def __init__(self, pin_trigger, pin_echo, timeout=30000, temp_sensor=None, precision=2, offset=0, interval=None, mqtt_topic=None, mqtt_topic_interval=None, value_template=None, friendly_name=None): """ HC-SR04 ultrasonic sensor. Be sure to connect it to 5V but use a voltage divider to connect the Echo pin to an ESP. :param pin_trigger: pin number/object of trigger pin :param pin_echo: pin number/object of echo pin :param timeout: reading timeout, corresponds to sensor limit range of 4m :param temp_sensor: temperature sensor object :param precision: int, precision of distance value published and returned :param offset: float, distance value offset, shouldn't be needed :param interval: float, interval in which the distance value gets measured and published :param mqtt_topic: distance mqtt topic :param mqtt_topic_interval: interval mqtt topic for changing the reading interval :param value_template: optional template can be used to measure the reverse distance (e.g. water level) :param friendly_name: friendly name for homeassistant gui by mqtt discovery, defaults to "Distance" """ super().__init__() self._frn = friendly_name self._valt = value_template self._tr = Pin(pin_trigger, mode=machine.Pin.OUT) self._tr.value(0) self._ec = Pin(pin_echo, mode=machine.Pin.IN) self._to = timeout self._temp = temp_sensor self._pr = int(precision) self._off = float(offset) self._topic = mqtt_topic or _mqtt.getDeviceTopic("hcsr04") self._topic_int = mqtt_topic_interval or _mqtt.getDeviceTopic("hcsr04/interval", is_request=True) self.interval = interval or config.INTERVAL_SEND_SENSOR # can be changed anytime global _count self._count = _count _count += 1 self._subscribe(self._topic_int, self._changeInterval) async def _init(self): await super()._init() gen = self.distance await asyncio.sleep(1) while True: await gen() t = time.ticks_ms() while time.ticks_diff(time.ticks_ms(), t) < self.interval: # this way interval can be changed and sensor reacts within 1s await asyncio.sleep(1) async def _discovery(self): # interval change can't be discovered as homeassistant doesn't offer a type sens = DISCOVERY_DISTANCE.format("{{ value|float }}" if self._valt is None else self._valt) name = "{!s}{!s}".format(_component_name, self._count) await self._publishDiscovery(_component_type, self._topic, name, sens, self._frn or "Distance") async def _changeInterval(self, topic, msg, retain): self.interval = float(msg) return True # will publish the new interval def _pulse(self) -> int: """ Send a pulse and wait for the echo pin using machine.time_pulse_us() to measure us. :return: int """ tr = self._tr tr.value(0) time.sleep_us(5) tr.value(1) time.sleep_us(10) tr.value(0) try: return machine.time_pulse_us(self._ec, 1, self._to) except OSError as e: if e.args[0] == 100: # TIMEOUT raise OSError("Object too far") raise e async def _read(self, temp=None, ignore_errors=False, publish=True) -> float: """ Returns distance in cm. Optionally compensated by temperature in °C. :return: float """ if temp is None: if self._temp is not None: temp = await self._temp.temperature(publish=False) if temp is None: await _log.asyncLog("warn", "Couldn't read temp sensor, using fallback caluclation") val = [] warnings = 0 # probably not going to happen that both warning types occur at the same time warning = "minimum distance reached or different problem" for _ in range(20): try: pt = self._pulse() if pt > 175: # ~3cm, sensor minimum distance, often read although other problems val.append(pt) else: warnings += 1 except OSError as e: warning = e warnings += 1 await asyncio.sleep_ms(10) if warnings > 10: # half sensor readings are bad if ignore_errors is False: await _log.asyncLog("error", "Too many bad sensor readings, error: {!s}".format(warning)) return None # removing extreme values val.remove(max(val)) val.remove(max(val)) val.remove(min(val)) val.remove(min(val)) pt = 0 for i in range(len(val)): pt += val[i] pt /= len(val) if temp is None: dt = (pt / 2) / 29.14 else: dt = (pt / 2) * ((331.5 + (0.6 * temp)) / 10000) if dt < 0: await _log.asyncLog("warn", "Sensor reading <0") return None try: dt = round(dt, self._pr) dt += self._off except Exception as e: await _log.asyncLog("error", "Error rounding value {!s}".format(dt)) return dt if publish: await _mqtt.publish(self._topic, ("{0:." + str(self._pr) + "f}").format(dt)) return dt async def distance(self, temp=None, ignore_errors=False, publish=True) -> float: """ Returns distance in cm, optionally temperature compensated :param temp: temperature value for compensation, optional :param ignore_errors: prevent bad readings from being published to the log in case the application expects those :param publish: if value should be published :return: """ return await self._read(temp, ignore_errors, publish)