def __init__(self, command_on, command_off, expected_return_on=None, expected_execution_time_on=0, expected_return_off=None, expected_execution_time_off=0, iterations=1, iter_delay=10, mqtt_topic=None, friendly_name=None): super().__init__() # This makes it possible to use multiple instances of Switch global _unit_index self._count = _count _unit_index += 1 self._topic = mqtt_topic or _mqtt.getDeviceTopic( "{!s}/{!s}".format(COMPONENT_NAME, self._count), is_request=True) self._subscribe(self._topic, self.on_message) self._frn = friendly_name gc.collect() self.lock = config.Lock( ) # in case switch activates a device that will need a while to finish self._c_on = Popen(command_on, expected_return_on, expected_execution_time_on, iterations, iter_delay) self._c_off = Popen(command_off, expected_return_off, expected_execution_time_off, iterations, iter_delay)
def __init__(self, pin, pwm_values, on_time=500, iters=1, freq=1000, mqtt_topic=None, friendly_name=None): super().__init__() self.pin = PyPin(pin, Pin.OUT) self.on_time = on_time self.values = pwm_values self.iters = iters self.lock = config.Lock() self.pin = PWM(self.pin, freq=freq) self.pin.duty(0) # This makes it possible to use multiple instances of Buzzer 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()
def __init__(self, unit_code, unit, expected_execution_time_on=500, expected_execution_time_off=500, iterations=1, iter_delay=10, mqtt_topic=None, friendly_name=None): super().__init__() self._log = _log # This makes it possible to use multiple instances of Switch global _unit_index self._count = _count _unit_index += 1 self._topic = mqtt_topic or _mqtt.getDeviceTopic( "{!s}{!s}".format(COMPONENT_NAME, self._count), is_request=True) self._subscribe(self._topic, self.on_message) self._frn = friendly_name gc.collect() self.unit_lock = config.Lock() self._c_on = Popen(COMMAND_ON.format(unit_code, unit), EXPECTED_RETURN_ON.format(unit_code, unit), expected_execution_time_on, iterations, iter_delay) self._c_off = Popen(COMMAND_OFF.format(unit_code, unit), EXPECTED_RETURN_OFF.format(unit_code, unit), expected_execution_time_off, iterations, iter_delay)
def __init__(self, mqtt_topic=None, friendly_name=None): super().__init__() # This makes it possible to use multiple instances of Switch global _count self._count = _count _count += 1 self._topic = mqtt_topic or _mqtt.getDeviceTopic( "{!s}/{!s}".format(_component_name, self._count), is_request=True) self._subscribe(self._topic, self.on_message) self._frn = friendly_name gc.collect() self.lock = config.Lock( ) # in case switch activates a device that will need a while to finish
def __init__(self, pin, interval=None, auto_discovery=False): """ The DS18 onewire controller. Reads all connected (and configured) units. :param pin: Pin object or integer or name :param interval: how often the sensors are read and published :param auto_discovery: if True then one object for each found DS18 unit will be created. This is only useful if the Units are not going to be used in other components and only the read temperature is interesting. """ self._interval = interval or config.INTERVAL_SEND_SENSOR ds18x20.DS18X20.__init__(self, onewire.OneWire(Pin(pin))) gc.collect() self._lock = config.Lock() global _ds18_controller _ds18_controller = self asyncio.get_event_loop().create_task(self._loop(auto_discovery))
def __init__(self, command_topic, state_topic, timeout=_TIMEOUT): global _unit_index _unit_index += 1 super().__init__(COMPONENT_NAME, __version__, _unit_index, discover=False) self._state = False self._topic = command_topic self._state_topic = state_topic self.lock = config.Lock() # in case switch activates a device that will need a while to finish self._state_time = 0 self._timeout = timeout _mqtt.subscribeSync(self._state_topic, self.on_message, self)
class RF433(Switch): lock = config.Lock() # only one method can have control over the RF433 device def __init__(self, unit_code, unit, expected_execution_time_on=500, expected_execution_time_off=500, iterations=1, iter_delay=10, mqtt_topic=None, friendly_name=None): super().__init__() self._log = _log # This makes it possible to use multiple instances of Switch global _unit_index self._count = _count _unit_index += 1 self._topic = mqtt_topic or _mqtt.getDeviceTopic("{!s}{!s}".format(COMPONENT_NAME, self._count), is_request=True) self._subscribe(self._topic, self.on_message) self._frn = friendly_name gc.collect() self.unit_lock = config.Lock() self._c_on = Popen(COMMAND_ON.format(unit_code, unit), EXPECTED_RETURN_ON.format(unit_code, unit), expected_execution_time_on, iterations, iter_delay) self._c_off = Popen(COMMAND_OFF.format(unit_code, unit), EXPECTED_RETURN_OFF.format(unit_code, unit), expected_execution_time_off, iterations, iter_delay) async def on_message(self, topic, msg, retain): if self.unit_lock.locked(): return False async with self.lock: async with self.unit_lock: if msg in _mqtt.payload_on: r = await self._c_on.execute() if r is True: await _mqtt.publish(self._topic[:-4], "ON", qos=1, retain=True) # makes it easier to subclass return True else: await self._log.asyncLog("warn", "Got unexpected return: {!s}".format(r)) return False elif msg in _mqtt.payload_off: r = await self._c_off.execute() if r is True: await _mqtt.publish(self._topic[:-4], "OFF", qos=1, retain=True) return True else: await self._log.asyncLog("warn", "Got unexpected return: {!s}".format(r)) return False 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)
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 __init__(self, uart_number, uart_tx, uart_rx, set_pin=None, reset_pin=None, interval_passive_mode=None, active_mode=True, eco_mode=True, interval=None, mqtt_topic=None, friendly_name: list = None): Component.__init__(self) self._interval = interval or config.INTERVAL_SEND_SENSOR self._int_pm = interval_passive_mode or self._interval self._topic = mqtt_topic or _mqtt.getDeviceTopic(_component_name) if type(friendly_name) is not None: if type(friendly_name) == list: if len(friendly_name) != 12: _log.warn( "Length of friendly name is wrong, expected 12 got {!s}" .format(len(friendly_name))) self._frn = None else: self._frn = friendly_name else: _log.warn( "Friendly name got unsupported type {!s}, expect list". format(type(friendly_name))) self._frn = None else: self._frn = None uart = machine.UART(uart_number, tx=uart_tx, rx=uart_rx, baudrate=9600) ############################## # create sensor object sensorModule.PMS5003.__init__(self, uart, config.Lock(), set_pin, reset_pin, interval_passive_mode, active_mode=active_mode, eco_mode=eco_mode) gc.collect()
def __init__(self, pin, on_time=50, off_time=50, iters=20, mqtt_topic=None): if type(pin) == str: pin = config.pins[pin] mqtt_topic = mqtt_topic or mqtt.getDeviceTopic("LEDNotification", is_request=True) self.pin = pin self.on_time = on_time self.off_time = off_time self.iters = iters self.lock = config.Lock() Pin(self.pin, Pin.OUT, value=0) mqtt.scheduleSubscribe(mqtt_topic, self.notification, check_retained=False) # not checking retained as led only activates single-shot self.mqtt_topic = mqtt_topic
def __init__(self, uart_number, uart_tx, uart_rx, set_pin=None, reset_pin=None, interval_passive_mode=None, active_mode=True, eco_mode=True, interval=None, mqtt_topic=None): interval = interval or config.INTERVAL_SEND_SENSOR interval_passive_mode = interval_passive_mode or interval self.component_name = component_name self.topic = mqtt_topic or mqtt.getDeviceTopic(self.component_name) self.log = logging.getLogger(self.component_name) uart = machine.UART(uart_number, tx=uart_tx, rx=uart_rx, baudrate=9600) ############################## # create sensor object super().__init__(uart, config.Lock(), set_pin, reset_pin, interval_passive_mode, active_mode=active_mode, eco_mode=eco_mode) gc.collect() if (interval == interval_passive_mode and active_mode is False) or interval == 0: self.registerCallback(self.airQuality) else: # possible to have different timings in passive_read and publish interval # useful if other components use readings of sensor too asyncio.get_event_loop().create_task(self._loop(self.airQuality(), interval))
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()
def __init__(self, pin, pwm_values, on_time=500, iters=1, freq=1000, mqtt_topic=None): if type(pin) == str: pin = config.pins[pin] mqtt_topic = mqtt_topic or mqtt.getDeviceTopic("Buzzer", is_request=True) self.pin = pin self.on_time = on_time self.values = pwm_values self.iters = iters self.lock = config.Lock() self.pin = PWM(Pin(self.pin, Pin.OUT), freq=freq) self.pin.duty(0) mqtt.scheduleSubscribe(mqtt_topic, self.notification, check_retained=False) # not checking retained as buzzer only activates single-shot self.mqtt_topic = mqtt_topic
def __init__(self, uart_number, uart_tx, uart_rx, set_pin=None, reset_pin=None, interval_reading=0.1, active_mode=True, eco_mode=True, interval_publish=None, mqtt_topic=None, friendly_name: list = None, discover=True, expose_intervals=False, intervals_topic=None): """ :param uart_number: esp32 has multiple uarts :param uart_tx: tx pin number :param uart_rx: rx pin number :param set_pin: optional pin number for set pin :param reset_pin: optional pin number for reset pin :param interval_reading: In passive mode controls the reading interval, defaults to 0.1 in active_mode. :param active_mode: :param eco_mode: :param interval_publish: publish interval, independent of interval_reading and active_mode :param mqtt_topic: :param friendly_name: optional, list of friendly_names for all types. Has to provide a name for every type. :param discover: :param expose_intervals: intervals can be changed through mqtt :param intervals_topic: """ super().__init__(COMPONENT_NAME, __version__, 0, discover, interval_publish, interval_reading, mqtt_topic, _log, expose_intervals, intervals_topic) if type(friendly_name) is not None: if type(friendly_name) == list: if len(friendly_name) != 12: _log.warn( "Length of friendly name is wrong, expected 12 got {!s}" .format(len(friendly_name))) friendly_name = None else: _log.warn( "Friendly name got unsupported type {!s}, expect list". format(type(friendly_name))) friendly_name = None for tp in TYPES: ind = TYPES.index(tp) self._addSensorType( tp, 0, 0, VALUE_TEMPLATE_JSON.format(tp), UNITS[ind], friendly_name[ind] if friendly_name is not None else tp, None, DISCOVERY_PM.format(UNITS[ind], tp)) uart = machine.UART(uart_number, tx=uart_tx, rx=uart_rx, baudrate=9600) self._count = 0 ############################## # create sensor object self.pms = sensorModule.PMS5003(self, uart, config.Lock(), set_pin, reset_pin, interval_reading, active_mode=active_mode, eco_mode=eco_mode) self._active_mode = active_mode gc.collect() if self._active_mode is False: # in passive mode using callback because reading intervals could drift apart # between sensor and ComponentSensor self.pms.registerCallback(self._saveVals)
class DS18(ComponentSensor): """ Helping class to use a singluar DS18 unit. This is not a full component object in terms of mqtt and discovery. This is handled by the controller. It can be used as a temperature component object. """ _pins = {} # pin number/name:onewire() _last_conv = {} # onewire:time _lock = config.Lock() def __init__(self, pin, rom: str = None, auto_detect=False, interval_publish: float = None, interval_reading: float = None, precision_temp: int = 2, offset_temp: float = 0, mqtt_topic=None, friendly_name=None, discover=True, expose_intervals=False, intervals_topic=None): """ Class for a single ds18 unit to provide an interface to a single unit. :param pin: pin number/name/object :param rom: optional, ROM of the specific DS18 unit, can be string or bytearray (in json bytearray not possible). If not given then the first found ds18 unit will be used, no matter the ROM. Makes it possible to have a generic ds18 unit. :param auto_detect: optional, if true and ROM is None then all connected ds18 units will automatically generate a sensor object with the given options. :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 precision_temp: the precision to for returning/publishing values :param offset_temp: temperature offset to adjust bad sensor readings :param mqtt_topic: optional mqtt topic of sensor :param friendly_name: friendly name in homeassistant :param discover: if DS18 object should send discovery message for homeassistnat :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. """ if rom is None and auto_detect: # only a dummy sensor for detecting connected sensors self._interval_reading = interval_reading self._interval_publishing = interval_publish interval_reading = 60 # scan every 60 seconds for new units interval_publish = -1 self._instances = {} # rom:object self._auto_detect = True self._prec = precision_temp self._offs = offset_temp self._discover = discover self._expose = expose_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) if rom or not auto_detect: # sensor with rom or generic sensor self._addSensorType(SENSOR_TEMPERATURE, precision_temp, offset_temp, VALUE_TEMPLATE_FLOAT, "°C", friendly_name) self._auto_detect = False self._generic = True if rom is None and not auto_detect else False if type(pin) == ds18x20.DS18X20: self.sensor: ds18x20.DS18X20 = pin else: self._pins[pin] = ds18x20.DS18X20(onewire.OneWire(Pin(pin))) self.sensor: ds18x20.DS18X20 = self._pins[pin] self._last_conv[self.sensor] = None self.rom: str = rom gc.collect() def _default_name(self): """Change default name to include sensor ROM. Will change name and default topic.""" if self.rom is None or self._generic: return "{!s}".format(COMPONENT_NAME) else: return "{!s}_{!s}".format(COMPONENT_NAME, self.rom) async def _read(self): if self._auto_detect or self._generic: # auto_detect unit or generic sensor roms = [] for _ in range(4): roms_n = self.sensor.scan() for rom in roms_n: if rom not in roms: roms.append(rom) await asyncio.sleep_ms(100) if len(roms) == 0: await _log.asyncLog("error", "Found no ds18 unit", timeout=10) return if self._auto_detect: # auto_detect instance for rom in roms: rom = self.rom2str(rom) if rom not in self._instances: self._instances[rom] = DS18(self.sensor, rom, False, self._interval_publishing, self._interval_reading, self._prec, self._offs, None, None, self._discover, self._expose) for rom in self._instances: if rom not in roms: # sensor not connected anymore await self.removeComponent(roms[rom]) # will stop its loop and remove component and unsubcribe every topic del self._instances[rom] await _log.asyncLog("info", "DS18 removed:", rom, timeout=5) else: # generic ds18 sensor rom = self.rom2str(roms[0]) if rom != self.rom: # sensor replaced self.rom: str = rom await _log.asyncLog("info", "Found new ds18:", rom, timeout=5) if self.rom is not None: # DS18 sensor unit async with self._lock: if self._last_conv[self.sensor] is None or \ time.ticks_diff(time.ticks_ms(), self._last_conv[self.sensor]) > 5000: # if sensors did convert time more than 5 seconds ago, convert temp again self.sensor.convert_temp() await asyncio.sleep_ms(750) value = None err = None for _ in range(3): try: value = self.sensor.read_temp(self.str2rom(self.rom)) except Exception as e: await asyncio.sleep_ms(100) err = e continue if value is None: await _log.asyncLog("error", "Sensor rom", self.rom, "got no value,", err, timeout=10) return if value == 85.0: await _log.asyncLog( "error", "Sensor rom", self.rom, "got value 85.00 [not working correctly]", timeout=10) return await self._setValue(SENSOR_TEMPERATURE, value) @staticmethod def rom2str(rom: bytearray) -> str: return ''.join('%02X' % i for i in iter(rom)) @staticmethod def str2rom(rom: str) -> bytearray: a = bytearray(8) for i in range(8): a[i] = int(rom[i * 2:i * 2 + 2], 16) return a
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))
class Component: """ Use this class as a base for components. Subclass to extend. See the template for examples. """ _discovery_lock = config.Lock() # prevent multiple discoveries from running concurrently and creating Out-Of-Memory errors def __init__(self): self._topics = {} # No RAM allocation for topic strings as they are passed by reference if saved in a variable in subclass. # self._topics is used by mqtt to know which component a message is for. self._next_component = None # needed to keep a list of registered components config.addComponent(self) asyncio.get_event_loop().create_task(self._init()) async def _init(self): for t in self._topics: await _mqtt.subscribe(t, qos=1) if config.MQTT_DISCOVERY_ENABLED is True: async with self._discovery_lock: await self._discovery() def _subscribe(self, topic, cb): self._topics[topic] = cb async def on_reconnect(self): """ Subclass to process a reconnect. Useful if you need to send a message on reconnect. Resubscribing to topics is done by mqtt_handler and doesn't need to be done manually. """ pass async def _discovery(self): """Implement in subclass. Is only called by self._init unless config.MQTT_DISCOVERY_ON_RECONNECT is True.""" pass @staticmethod async def _publishDiscovery(component_type, component_topic, unique_name, discovery_type, friendly_name=None): topic = Component._getDiscoveryTopic(component_type, unique_name) msg = Component._composeDiscoveryMsg(component_topic, unique_name, discovery_type, friendly_name) await _mqtt.publish(topic, msg, qos=1, retain=True) del msg, topic gc.collect() @staticmethod def _composeDiscoveryMsg(component_topic, name, component_type_discovery, friendly_name=None, no_avail=False): """ Helper function to separate dynamic system values from user defineable values. :param component_topic: state topic of the component. device topics (see mqtt) are supported :param name: name of the component, must be unique on the device, typically composed of component name and count :param component_type_discovery: discovery values for the component type, e.g. switch, sensor :param friendly_name: optional a readable name that is used in the gui and entity_id :param no_avail: don't add availability configs (typically only used for the availability component itself) :return: str """ friendly_name = friendly_name or name component_topic = component_topic if _mqtt.isDeviceTopic( component_topic) is False else _mqtt.getRealTopic(component_topic) if no_avail is True: return DISCOVERY_BASE_NO_AVAIL.format( component_topic, # "~" component state topic friendly_name, # name sys_vars.getDeviceID(), name, # unique_id component_type_discovery, # component type specific values sys_vars.getDeviceDiscovery()) # device return DISCOVERY_BASE.format( component_topic, # "~" component state topic friendly_name, # name config.MQTT_HOME, sys_vars.getDeviceID(), # availability_topic sys_vars.getDeviceID(), name, # unique_id component_type_discovery, # component type specific values sys_vars.getDeviceDiscovery()) # device @staticmethod def _getDiscoveryTopic(component_type, name): return "{!s}/{!s}/{!s}/{!s}/config".format( config.MQTT_DISCOVERY_PREFIX, component_type, sys_vars.getDeviceID(), name)