diff --git a/COMPONENTS.md b/COMPONENTS.md new file mode 100644 index 0000000..4c0ccd4 --- /dev/null +++ b/COMPONENTS.md @@ -0,0 +1,114 @@ +# Component Classes +Overview over the basic component classes and their methods. + +## ComponentBase +This is the base class for all component implementations. + +It provides the basic API and integration into mqtt (e.g. discovery) and helper functions. +There is a [template](./_templates/component_template.py) demonstrating how to implement a custom Component inheriting from the ComponentBase class. + +### def `__init__`(self, component_name, version, unit_index: int, discover=True, logger=None, **kwargs): +|args|type|required|description| +|--|--|--|--| +|component_name|str|true|Name of the component. Will be logged.| +|version|str|true|Version of the component. Will be logged.| +|unit_index|int|true|Index of current component instance. First instance of e.g. PushButton has index 0, the next instance index 1, etc. Used for automatic topic and name generation.| +|discover|bool|false|Configures if the component should send mqtt discovery messages to get added to Home-Assistant| +|logger|object|false|The logger object for the current component instance. It is typically created in the module or on object creation. If not given, one will automatically be created using the component_name and unit_index. + +### async def removeComponent(component): +Completely remove a component. It will be unregistered and also removed from Home-Assistant (if it was created using discover=True). All subscribed mqtt topics of this component will be unsubscribed. +This function can be called directly and either with a component object or with a component_name as argument, e.g. like `ComponentBase.removeComponent("PushButton0")` +The removal of a component will be logged. + +### async def _remove(self): +Internal coroutine that handles the removal of the component as described for the coroutine *removeComponent()*. +Can be subclassed to extend the functionality, e.g. to stop all running tasks of the component. + +### async def _init_network(self): +Logs the component module, version and name. Also sends the mqtt discovery message if enabled. +The coroutine can be subclassed to extend the functionality, e.g. to restore a certain state after the discovery message has been sent. +**Note:** All components execute this coroutine in one asyncio Tasks sequencially. This means that waiting times will delay the execution of the *_init_network* coroutine of the next component. + +### async def _discovery(self, register=True): +This coroutine has to be implemented in the subclass. Its function is to send the mqtt discovery message for Home-Assistant. If register==False it has to send an empty message, which removes the component from Home-Assistant. +The subclass can use two helper functions for publishing and deleting the discovery messages: ComponentBase._publishDiscovery and ComponentBase._deleteDiscovery. + +### [TODO: some internal functions ommited for now, will add later] + +### checkSensorType(obj, sensor_type): +Checks if the given object is of instance *ComponentSensor* and if it provides the *sensor_type* (e.g. temperature, humidity, ...). If a check fails, it'll raise a TypeError. + +### checkSwitchType(obj): +Checks if the given object is of instance *ComponentSwitch*. If the check fails, it'll raise a TypeError. + +## ComponentSensor +It provides the basic API for all sensors. It inherits from the *ComponentBase* class, which means that all methods of *ComponentBase* are available. + +There is a [template](./_templates/sensor_template.py) demonstrating how to implement a custom Sensor Component inheriting from the ComponentSensor class. + +### def `__init__`(self, component_name, version, unit_index, interval_publish=None, interval_reading=None, mqtt_topic=None, expose_intervals=False, intervals_topic=None, publish_old_values=False, **kwargs): +***Note:*** This class inherits from the *ComponentBase* class. The constructor arguments of the *ComponentBase* class can be used too because they are being forwarded to the base class by ***kwargs*. +|args|type|required|description| +|--|--|--|--| +|component_name|str|true|Name of the component. Will be logged.| +|version|str|true|Version of the component. Will be logged.| +|unit_index|int|true|Index of current component instance. First instance of e.g. PushButton has index 0, the next instance index 1, etc. Used for automatic topic and name generation.| +|interval_publish|float|false|How often a sensor reading should be published. If not given, defaults to *config.INTERVAL_SENSOR_PUBLISH*. Can be set to *-1* to disable publishing of readings. +|interval_reading|float|false|How often the sensor should be read. If not given, defaults to *config.INTERVAL_SENSOR_READ*. Can be set to -1 to disable the automatic reading of the sensor. +|mqtt_topic|str|false|Custom mqtt_topic for sensor reading publications. If not given, one will automatically be created using the *component_name* and *unit_index*. However, every added sensor_type can have its own mqtt topic (e.g. temperature and humidity can be published to different mqtt topics). +|expose_intervals|bool|false|The reading and publication intervals can be exposed to mqtt so they can be changed by a single message to the topic configured in *intervals_topic*. +|intervals_topic|str|false|If *expose_intervals* is enabled, this topic will be subscribed for change requests about the reading and publication intervals. Note: A topic ending with */set* is required. If no topic is given, one will be generated according to this pattern: `//<_unit_index>/interval/set` unless the method *_default_name()* has been overwritten by the subclass. Check the repl output when running for the first time, it will print the topic which is being used. +|publish_old_values|bool|false|Typically the value being published is up-to-date and a publication is being canceled, if it can't finish until the next reading is done. This way there will always be an up-to-date value published. But if the reading interval is so low, that the publication takes longer than the reading, this would result in all publications being canceled. Setting *publish_old_values* to *true* allows the publication to finish, even if new values are available. +|**kwargs|any|false|Allows setting kwargs of the *ComponentBase* class, e.g. *discover=False*. This allows the ComponentBase class to be extended in the future without requiring all subclasses to implement the new constructor arguments. It also keeps the constructors of subclasses cleaner and easier to read. + +### [TODO: describe remaining sensor methods] + +## ComponentSwitch +It provides the basic API for all switches. All components that provide an interface for enabling/disabling (turning on/off) can be described as *Switches*. +It inherits from the *ComponentBase* class, which means that all methods of *ComponentBase* are available. + +There is a [template](./_templates/switch_template.py) demonstrating how to implement a custom Switch Component inheriting from the ComponentSwitch class. + +### def `__init__`(self, component_name, version, unit_index, mqtt_topic=None, instance_name=None, wait_for_lock=True, restore_state=True, friendly_name=None, initial_state=None, **kwargs): +***Note:*** This class inherits from the *ComponentBase* class. The constructor arguments of the *ComponentBase* class can be used too because they are being forwarded to the base class by ***kwargs*. +|args|type|required|description| +|--|--|--|--| +|component_name|str|true|Name of the component. Will be logged.| +|version|str|true|Version of the component. Will be logged.| +|unit_index|int|true|Index of current component instance. First instance of e.g. PushButton has index 0, the next instance index 1, etc. Used for automatic topic and name generation.| +|mqtt_topic|str|false|Custom mqtt_topic for state change requests and state publications. If not given, one will automatically be created according to this pattern: `//<_unit_index>/set`as the command topic and without */set* at the end as the state_topic. Note that any of those topics can be used, the other one will be converted automatically. +|instance_name|str|false|A unique name for the component instance. If not given, one will automatically be created using the *component_name* and *unit_index*. However, because the *unit_index* is a dynmic value depending on the components registered, the instance_name can change when the configuration for the registered components changes. This can be undesired as it results in a different registration in Home-Asssitant. +|wait_for_lock|bool|false|If enabled, every request will wait until it acquires the lock. This way no request will get lost, even if a previous request is still being executed. If disabled, a request will be ignored if the lock is unavailable. +|restore_state|bool|false|Restore the device state which is stored by the mqtt broker as a retained message on the state topic of the component. This is usually preferred because it restores the device to its former state after a reset. +|friendly_name|str|false|A friendly name for the Home-Assistant GUI. Has no other function. +|initial_state|bool|false|Provides the initial state of a device after a reset. If not given, the first state change request will assume that the device is not in the requested state (e.g. "ON" request will assume device is currently "OFF"). In the subclass for a device the initial state could be obtained correctly (e.g. by reading a pin state) and then correctly passed on to the base class constructor. +|**kwargs|any|false|Allows setting kwargs of the *ComponentBase* class, e.g. *discover=False*. This allows the ComponentBase class to be extended in the future without requiring all subclasses to implement the new constructor arguments. It also keeps the constructors of subclasses cleaner and easier to read. + +### [TODO: describe remaining switch methods] + +## ComponentButton +It provides the basic API for all buttons. A button can be described a *PushButton* that has only a single-shot action on activation. It is basically a *Switch* that turns itself off directly after being switched on. It therefore inherits from the *ComponentSwitch* class, which means that all methods of *ComponentSwitch* and *ComponentBase* are available. + +There is a [template](./_templates/switch_button.py) demonstrating how to implement a custom Button Component inheriting from the ComponentSwitch class. + +### def `__init__`(self, component_name, version, unit_index, wait_for_lock=False, initial_state=False, **kwargs): +***Note:*** This class inherits from the *ComponentSwitch* and the *ComponentBase* class. The constructor arguments of the both classes can be used too because they are being forwarded to the base classes by ***kwargs*. +The *ComponentButton* class does not provide any new constructor arguments but has different default parameters. +|args|type|required|description| +|--|--|--|--| +|component_name|str|true|Name of the component. Will be logged.| +|version|str|true|Version of the component. Will be logged.| +|unit_index|int|true|Index of current component instance. First instance of e.g. PushButton has index 0, the next instance index 1, etc. Used for automatic topic and name generation.| +|wait_for_lock|bool|false|Same as *ComponentSwitch*. Defaults to *false* so a single-shot action is not activated again after it has finished if two activation requests were received while the action was being done. +|initial_state|bool|false|Same as *ComponentSwitch*. Defaults to *false* because a *PushButton* is "off" by default and only shortly "on" on activation. +|**kwargs|any|false|Allows setting kwargs of the base classes, e.g. *discover=False*. This allows all base classes to be extended in the future without requiring all subclasses to implement the new constructor arguments. It also keeps the constructors of subclasses cleaner and easier to read. + +### async def on(self): +Turns the button on/starts the single-shot action. The state "ON" will be published on activation and once the action is finished, the state "OFF" will be published. Publications are done in a separate task and don't impact the functionality of the button, even if the network is unavailable. + +### async def off(self): +Purely for compatibility, only returns *True*. + +### async def toggle(self): +Always calls *self.on()*. diff --git a/_templates/button_template.py b/_templates/button_template.py index 82811ee..db11f01 100644 --- a/_templates/button_template.py +++ b/_templates/button_template.py @@ -7,18 +7,15 @@ { package: component: Button - constructor_args: { - # mqtt_topic: null # optional, defaults to //Button<_unit_index>/set - # friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery - # discover: true # optional, if false no discovery message for homeassistant will be sent. - } + constructor_args: {} } +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ # A button is basically a switch with a single-shot action that deactivates itself afterwards. -__updated__ = "2020-03-29" -__version__ = "0.8" +__updated__ = "2020-04-03" +__version__ = "0.81" from pysmartnode import config from pysmartnode.utils.component.button import ComponentButton @@ -35,35 +32,28 @@ class Button(ComponentButton): - def __init__(self, mqtt_topic=None, friendly_name=None, discover=True, **kwargs): - # 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 - + def __init__(self, **kwargs): # This makes it possible to use multiple instances of Button. # It is needed for every default value for mqtt. # Initialize before super()__init__(...) to not pass the wrong value. global _unit_index _unit_index += 1 - ### # set the initial state otherwise it will be "None" (unknown) and the first request # will set it accordingly which in case of a button will always be an activation. initial_state = False # A button will always be False as it is single-shot, # unless you have a device with a long single-shot action active during reboot. # You might be able to poll the current state of a device to set the inital state correctly - # mqtt_topic can be adapted otherwise a default mqtt_topic will be generated if None - super().__init__(COMPONENT_NAME, __version__, _unit_index, mqtt_topic, instance_name=None, - wait_for_lock=False, discover=discover, friendly_name=friendly_name, - initial_state=initial_state, **kwargs) + super().__init__(COMPONENT_NAME, __version__, _unit_index, + wait_for_lock=False, initial_state=initial_state, **kwargs) # If the device needs extra code, launch a new coroutine. ##################### # Change this method according to your device. ##################### - async def _on(self): + async def _on(self) -> bool: """Turn device on.""" pass # no return needed because of single-shot action. diff --git a/_templates/component_template.py b/_templates/component_template.py index b585604..1701264 100644 --- a/_templates/component_template.py +++ b/_templates/component_template.py @@ -23,7 +23,7 @@ import uasyncio as asyncio from pysmartnode import config from pysmartnode import logging -from pysmartnode.utils.component import Component, DISCOVERY_SWITCH +from pysmartnode.utils.component import ComponentBase, DISCOVERY_SWITCH import gc #################### @@ -47,7 +47,7 @@ # component like a sensor or a switch. -class MyComponent(Component): +class MyComponent(ComponentBase): def __init__(self, my_value, # extend or shrink according to your sensor mqtt_topic=None, mqtt_topic2=None, friendly_name=None, discover=True, **kwargs): diff --git a/_templates/sensor_template.py b/_templates/sensor_template.py index 8510610..0581563 100644 --- a/_templates/sensor_template.py +++ b/_templates/sensor_template.py @@ -55,9 +55,7 @@ class MySensor(ComponentSensor): def __init__(self, i2c, precision_temp=2, precision_humid=1, temp_offset=0, humid_offset=0, # extend or shrink according to your sensor - interval_publish=None, interval_reading=None, mqtt_topic=None, - friendly_name_temp=None, friendly_name_humid=None, - discover=True, expose_intervals=False, intervals_topic=None, **kwargs): + friendly_name_temp=None, friendly_name_humid=None, **kwargs): """ :param i2c: i2c object for temperature sensor :param precision_temp: precision of the temperature value, digits after separator "." @@ -82,9 +80,7 @@ def __init__(self, i2c, precision_temp=2, precision_humid=1, # Initialize before super()__init__(...) to not pass the wrong value. 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, - **kwargs) + super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, **kwargs) # 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 diff --git a/_templates/switch_template.py b/_templates/switch_template.py index 7e9ce63..da5d548 100644 --- a/_templates/switch_template.py +++ b/_templates/switch_template.py @@ -10,13 +10,13 @@ constructor_args: { # mqtt_topic: null # optional, defaults to //Switch<_unit_index>/set # friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery - # discover: true # optional, if false no discovery message for homeassistant will be sent. } } +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ -__updated__ = "2020-03-29" -__version__ = "1.9" +__updated__ = "2020-04-03" +__version__ = "1.91" from pysmartnode import config from pysmartnode.utils.component.switch import ComponentSwitch @@ -33,17 +33,19 @@ class Switch(ComponentSwitch): - def __init__(self, mqtt_topic=None, friendly_name=None, discover=True, **kwargs): - # 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 - + def __init__(self, mqtt_topic=None, friendly_name=None, **kwargs): # This makes it possible to use multiple instances of Button. # It is needed for every default value for mqtt. # Initialize before super()__init__(...) to not pass the wrong value. global _unit_index _unit_index += 1 + # mqtt_topic and friendly_name can be removed from the constructor if not initialized + # differently by this module if not given by the user. If removed from the constructor, + # also remove it from super().__init__(...)! + # example: + friendly_name = friendly_name or "mySwitch_name_friendly_{!s}".format(_unit_index) + ### # set the initial state otherwise it will be "None" (unknown) and the first request # will set it accordingly. @@ -52,21 +54,24 @@ def __init__(self, mqtt_topic=None, friendly_name=None, discover=True, **kwargs) # you initialize a pin in a certain state. ### - # mqtt_topic can be adapted otherwise a default mqtt_topic will be generated if None - super().__init__(COMPONENT_NAME, __version__, _unit_index, mqtt_topic, instance_name=None, - wait_for_lock=True, discover=discover, friendly_name=friendly_name, - initial_state=initial_state, **kwargs) + # mqtt_topic can be adapted otherwise a default mqtt_topic will be generated if not + # provided by user configuration. + # friendly_name can be adapted, otherwise it will be unconfigured (which is ok too). + super().__init__(COMPONENT_NAME, __version__, _unit_index, + friendly_name=friendly_name, mqtt_topic=mqtt_topic, + # remove friendly_name and mqtt_topic if removed from constructor + wait_for_lock=True, initial_state=initial_state, **kwargs) # If the device needs extra code, launch a new coroutine. ##################### # Change these methods according to your device. ##################### - async def _on(self): + async def _on(self) -> bool: """Turn device on.""" return True # return True when turning device on was successful. - async def _off(self): + async def _off(self) -> bool: """Turn device off. """ return True # return True when turning device off was successful. ##################### diff --git a/_testing/sensor.py b/_testing/sensor.py new file mode 100644 index 0000000..4088fa7 --- /dev/null +++ b/_testing/sensor.py @@ -0,0 +1,55 @@ +# Author: Kevin Köck +# Copyright Kevin Köck 2020 Released under the MIT license +# Created on 2020-04-02 + +__updated__ = "2020-04-02" +__version__ = "0.2" + +from pysmartnode import config +from pysmartnode import logging +from pysmartnode.utils.component.sensor import ComponentSensor, SENSOR_TEMPERATURE, SENSOR_HUMIDITY +import gc +import time + +#################### +# choose a component name that will be used for logging (not in leightweight_log) and +# a default mqtt topic that can be changed by received or local component configuration +COMPONENT_NAME = "TestSensor" +# define (homeassistant) value templates for all sensor readings +_VAL_T_TEMPERATURE = "{{ value_json.temperature }}" +_VAL_T_HUMIDITY = "{{ value_json.humidity }}" +#################### + +_log = logging.getLogger(COMPONENT_NAME) +_mqtt = config.getMQTT() +gc.collect() + +_unit_index = -1 + + +class Sensor(ComponentSensor): + def __init__(self, interval_reading=0.05, interval_publish=5, publish_old_values=True, + discover=False, **kwargs): + global _unit_index + _unit_index += 1 + super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, discover=discover, + interval_reading=interval_reading, interval_publish=interval_publish, + publish_old_values=publish_old_values, **kwargs) + # 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._addSensorType(SENSOR_TEMPERATURE, 0, 0, _VAL_T_TEMPERATURE, "°C") + self._addSensorType(SENSOR_HUMIDITY, 0, 0, _VAL_T_HUMIDITY, "%") + + ############################## + gc.collect() + self._i = 0 + + async def _read(self): + t = self._i + h = self._i + _log.debug(time.ticks_ms(), self._i, local_only=True) + self._i += 1 + await self._setValue(SENSOR_TEMPERATURE, t) + await self._setValue(SENSOR_HUMIDITY, h) diff --git a/_testing/switch.py b/_testing/switch.py index e3d91c5..c11485f 100644 --- a/_testing/switch.py +++ b/_testing/switch.py @@ -16,8 +16,8 @@ class Switch(_Switch): def __init__(self): super().__init__("testswitch", __version__, 0, - mqtt.getDeviceTopic("switch", is_request=True), - "switch", friendly_name="Testswitch", initial_state=None) + mqtt_topic=mqtt.getDeviceTopic("switch", is_request=True), + friendly_name="Testswitch", initial_state=None, discover=False) log.info("State: {!s}".format(self._state), local_only=True) async def _on(self): diff --git a/dev/moisture.py b/dev/moisture.py index 2f0f224..1fa8656 100644 --- a/dev/moisture.py +++ b/dev/moisture.py @@ -35,7 +35,7 @@ import uasyncio as asyncio from uasyncio import Lock import gc -from pysmartnode.utils.component import Component, DISCOVERY_BINARY_SENSOR +from pysmartnode.utils.component import ComponentBase, DISCOVERY_BINARY_SENSOR COMPONENT_NAME = "Moisture" _COMPONENT_TYPE = "sensor" @@ -48,7 +48,7 @@ # TODO: Divide sensor into multiple components as this is currently just a controller returning # all values and therefore doesn't conform to the new API. Only affects other programs calling humidity() -class Moisture(Component): +class Moisture(ComponentBase): def __init__(self, adc_pin, water_voltage, air_voltage, sensor_types, power_pin=None, power_warmup=None, publish_converted_value=False, diff --git a/dev/phSensor.py b/dev/phSensor.py index 939759f..b312c6a 100644 --- a/dev/phSensor.py +++ b/dev/phSensor.py @@ -39,7 +39,7 @@ from pysmartnode.components.machine.adc import ADC from pysmartnode import logging import uasyncio as asyncio -from pysmartnode.utils.component import Component +from pysmartnode.utils.component import ComponentBase import gc COMPONENT_NAME = "PHsensor" @@ -57,7 +57,7 @@ _VAL_T_ACIDITY = "{{ value|float }}" -class PHsensor(Component): +class PHsensor(ComponentBase): def __init__(self, adc, adc_multi, voltage_calibration_0, pH_calibration_value_0, voltage_calibration_1, pH_calibration_value_1, precision=2, interval=None, mqtt_topic=None, diff --git a/dev/solar.py b/dev/solar.py deleted file mode 100644 index 0ddc6ae..0000000 --- a/dev/solar.py +++ /dev/null @@ -1,30 +0,0 @@ -''' -Created on 2018-07-18 - -@author: Kevin Köck -''' - -""" -example config: -{ - package: .machine.solar - component: Solar - constructor_args: { - pin: 2 # pin number where the relais for the solar charger is connected (HIGH for connected) - adc: 4 # optional, adc pin of the connected light resistor - light_charging: 50 # light percentage above which charging will be started - light_disconnecting: 30 # light percentage below which charging will be stopped - voltage_max_light: 0.5 - voltage_min_light: 3.0 - battery_voltage_stop: 12 - # precision_light: 2 # optional, the precision of the light value published by mqtt - # interval_light: 600 # optional, defaults to 600s, interval in which light value gets published - # mqtt_topic: null # optional, defaults to //solar/charging [ON/OFF] - # mqtt_topic_light: null # optional, defaults to //solar/light [percentage] - # interval_watching: 1 # optional, the interval in which the voltage and light will be checked, defaults to 1s - } -} -""" - -__version__ = "0.1" -__updated__ = "2018-07-16" diff --git a/pysmartnode/components/devices/climate/__init__.py b/pysmartnode/components/devices/climate/__init__.py index d0cea1d..53fa5d3 100644 --- a/pysmartnode/components/devices/climate/__init__.py +++ b/pysmartnode/components/devices/climate/__init__.py @@ -19,7 +19,6 @@ # temp_high: 21 # optional, initial temperature high if no value saved by mqtt # away_temp_low: 16 # optional, initial away temperature low if no value saved by mqtt # away_temp_high: 17 # optional, initial away temperature high if no value saved by mqtt - # disover: true # optional, send mqtt discovery # interval: 300 #optional, defaults to 300s, interval sensor checks situation. Should be >60s # friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery } @@ -35,15 +34,15 @@ # TODO: make it possible to only use one target_temp instead of high/low and away_high/low -__updated__ = "2020-03-29" -__version__ = "0.91" +__updated__ = "2020-04-03" +__version__ = "0.92" from pysmartnode import config from pysmartnode import logging import uasyncio as asyncio import gc import time -from pysmartnode.utils.component import Component +from pysmartnode.utils.component import ComponentBase # imports of ComponentSensor and ComponentSwitch to keep heap fragmentation low # as those will be needed in any case @@ -69,18 +68,18 @@ _unit_index = -1 -class Climate(Component): +class Climate(ComponentBase): 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, **kwargs): + friendly_name=None, **kwargs): 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, **kwargs) + super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, **kwargs) self._temp_step = temp_step self._min_temp = min_temp @@ -298,7 +297,7 @@ async def _discovery(self, register=True): else: sens = "" gc.collect() - topic = Component._getDiscoveryTopic(_COMPONENT_TYPE, name) + topic = ComponentBase._getDiscoveryTopic(_COMPONENT_TYPE, name) await _mqtt.publish(topic, sens, qos=1, retain=True) diff --git a/pysmartnode/components/machine/button.py b/pysmartnode/components/machine/button.py index 7a9d4fc..749a6a5 100644 --- a/pysmartnode/components/machine/button.py +++ b/pysmartnode/components/machine/button.py @@ -40,12 +40,12 @@ } """ -__updated__ = "2020-03-29" -__version__ = "0.6" +__updated__ = "2020-04-03" +__version__ = "0.61" from pysmartnode import logging from pysmartnode.utils.abutton import Pushbutton -from pysmartnode.utils.component import Component +from pysmartnode.utils.component import ComponentBase from pysmartnode.components.machine.pin import Pin import machine import uasyncio as asyncio @@ -55,11 +55,11 @@ COMPONENT_NAME = "Button" -_log = logging.getLogger("button") +_log = logging.getLogger(COMPONENT_NAME) _unit_index = -1 -class Button(Pushbutton, Component): +class Button(Pushbutton, ComponentBase): def __init__(self, pin, pull=None, pressed_component=None, pressed_method="on", released_component=None, released_method="off", double_pressed_component=None, double_pressed_method="on", @@ -93,7 +93,8 @@ def __init__(self, pin, pull=None, pressed_component=None, pressed_method="on", Pushbutton.__init__(self, pin, suppress=suppress) global _unit_index _unit_index += 1 - Component.__init__(self, COMPONENT_NAME, __version__, _unit_index, **kwargs) + ComponentBase.__init__(self, COMPONENT_NAME, __version__, _unit_index, discover=False, + logger=_log, **kwargs) if pressed_component is not None: self.press_func(getattr(pressed_component, pressed_method)) if released_component is not None: diff --git a/pysmartnode/components/machine/deepsleep.py b/pysmartnode/components/machine/deepsleep.py index 0aed405..1a796a4 100644 --- a/pysmartnode/components/machine/deepsleep.py +++ b/pysmartnode/components/machine/deepsleep.py @@ -15,8 +15,8 @@ } """ -__version__ = "0.1" -__updated__ = "2018-07-16" +__version__ = "0.2" +__updated__ = "2020-04-03" import machine import uasyncio as asyncio @@ -27,7 +27,7 @@ async def deepsleep(sleeping_time, wait_before_sleep=None, event=None): if wait_before_sleep is not None: await asyncio.sleep(wait_before_sleep) if event is not None: - await event + await event.wait() if platform == "esp32_LoBo": machine.deepsleep(int(sleeping_time * 1000)) else: diff --git a/pysmartnode/components/machine/easyGPIO.py b/pysmartnode/components/machine/easyGPIO.py index 552a802..e42171c 100644 --- a/pysmartnode/components/machine/easyGPIO.py +++ b/pysmartnode/components/machine/easyGPIO.py @@ -14,29 +14,32 @@ } Makes esp8266 listen to requested gpio changes or return pin.value() if message is published without payload. This component is just a generic interface to device pins, it does not offer ComponentSwitch features. +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ -__updated__ = "2020-03-29" -__version__ = "1.9" +__updated__ = "2020-04-03" +__version__ = "1.91" import gc import machine from pysmartnode.components.machine.pin import Pin from pysmartnode import config from pysmartnode import logging -from pysmartnode.utils.component import Component, DISCOVERY_SWITCH +from pysmartnode.utils.component import ComponentBase, DISCOVERY_SWITCH _mqtt = config.getMQTT() COMPONENT_NAME = "easyGPIO" _COMPONENT_TYPE = "switch" +_log = logging.getLogger("easyGPIO") + gc.collect() -class GPIO(Component): +class GPIO(ComponentBase): def __init__(self, mqtt_topic=None, discover_pins=None, **kwargs): - super().__init__(COMPONENT_NAME, __version__, unit_index=0, **kwargs) + super().__init__(COMPONENT_NAME, __version__, unit_index=0, logger=_log, **kwargs) self._topic = mqtt_topic or _mqtt.getDeviceTopic("easyGPIO/+/set") _mqtt.subscribeSync(self._topic, self.on_message, self, check_retained_state=True) self._d = discover_pins or [] @@ -52,7 +55,6 @@ async def _discovery(self, register=True): @staticmethod async def on_message(topic, msg, retain): - _log = logging.getLogger("easyGPIO") if topic.endswith("/set") is False: if retain: pin = topic[topic.rfind("easyGPIO/") + 9:] diff --git a/pysmartnode/components/machine/remoteConfig.py b/pysmartnode/components/machine/remoteConfig.py index 87a11ff..a74e34b 100644 --- a/pysmartnode/components/machine/remoteConfig.py +++ b/pysmartnode/components/machine/remoteConfig.py @@ -2,10 +2,10 @@ # Copyright Kevin Köck 2019-2020 Released under the MIT license # Created on 2019-09-15 -__updated__ = "2020-03-29" +__updated__ = "2020-04-03" __version__ = "0.91" -from pysmartnode.utils.component import Component +from pysmartnode.utils.component import ComponentBase from pysmartnode import config from pysmartnode import logging import uasyncio as asyncio @@ -20,9 +20,9 @@ WAIT = 1.5 if platform == "esp8266" else 0.5 -class RemoteConfig(Component): +class RemoteConfig(ComponentBase): def __init__(self, **kwargs): - super().__init__(COMPONENT_NAME, __version__, unit_index=0, **kwargs) + super().__init__(COMPONENT_NAME, __version__, unit_index=0, logger=_log, **kwargs) self._topic = "{!s}/login/{!s}/#".format(_mqtt.mqtt_home, _mqtt.client_id) self._icomp = None self._rcomp = [] diff --git a/pysmartnode/components/machine/stats.py b/pysmartnode/components/machine/stats.py index a8ad202..d70f2e3 100644 --- a/pysmartnode/components/machine/stats.py +++ b/pysmartnode/components/machine/stats.py @@ -5,17 +5,16 @@ # This component will be started automatically to provide basic device statistics. # You don't need to configure it to be active. -__updated__ = "2020-03-29" -__version__ = "1.7" +__updated__ = "2020-04-03" +__version__ = "1.71" import gc from pysmartnode import config import uasyncio as asyncio -from pysmartnode.utils.component import Component +from pysmartnode.utils.component import ComponentBase import time from sys import platform -from pysmartnode import logging from pysmartnode.utils import sys_vars try: @@ -42,7 +41,7 @@ '"ic":"mdi:information-outline",' -class STATS(Component): +class STATS(ComponentBase): def __init__(self, **kwargs): super().__init__(COMPONENT_NAME, __version__, unit_index=0, **kwargs) self._interval = config.INTERVAL_SENSOR_PUBLISH @@ -81,7 +80,7 @@ async def _publish(self): h, m = divmod(m, 60) d, h = divmod(h, 24) val["Uptime"] = '{:d}T{:02d}:{:02d}:{:02d}'.format(d, h, m, s) - logging.getLogger("RAM").info(gc.mem_free(), local_only=True) + self._log.info(gc.mem_free(), local_only=True) val["RAM free (bytes)"] = gc.mem_free() if sta is not None: try: diff --git a/pysmartnode/components/machine/wifi_led.py b/pysmartnode/components/machine/wifi_led.py index 4176583..e0a45dd 100644 --- a/pysmartnode/components/machine/wifi_led.py +++ b/pysmartnode/components/machine/wifi_led.py @@ -14,7 +14,7 @@ import gc import machine from pysmartnode.components.machine.pin import Pin -from pysmartnode.utils.component import Component +from pysmartnode.utils.component import ComponentBase import network import uasyncio as asyncio import time @@ -25,7 +25,10 @@ COMPONENT_NAME = "WifiLED" -class WIFILED(Component): +# TODO: add option for heartbeat or always-on mode + + +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) diff --git a/pysmartnode/components/sensors/battery.py b/pysmartnode/components/sensors/battery.py index 55bc065..4f9a1c4 100644 --- a/pysmartnode/components/sensors/battery.py +++ b/pysmartnode/components/sensors/battery.py @@ -14,16 +14,13 @@ multiplier_adc: 2.5 # calculate the needed multiplier to get from the voltage read by adc to the real voltage cutoff_pin: null # optional, pin number or object of a pin that will cut off the power if pin.value(1) precision_voltage: 2 # optional, the precision of the voltage published by mqtt - # interval_publish: 600 #optional, defaults to 600. Set to interval_reading to publish with every reading - # mqtt_topic: null # optional, defaults to //battery # interval_reading: 1 # optional, the interval in which the voltage will be checked, defaults to 1s # friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery # friendly_name_abs: null # optional, friendly name for absolute voltage - # expose_intervals: Expose intervals to mqtt so they can be changed remotely - # intervals_topic: if expose_intervals then use this topic to change intervals. Defaults to //<_unit_index>/interval/set. Send a dictionary with keys "reading" and/or "publish" to change either/both intervals. } } WARNING: This component has not been tested with a battery and only works in theory! +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ __updated__ = "2020-03-29" @@ -55,14 +52,13 @@ class Battery(ComponentSensor): def __init__(self, adc, voltage_max: float, voltage_min: float, multiplier_adc: float, cutoff_pin=None, precision_voltage: int = 2, interval_reading: float = 1, - interval_publish: float = None, mqtt_topic: str = None, friendly_name: str = None, - friendly_name_abs: str = None, discover: bool = True, - expose_intervals: bool = False, intervals_topic: str = None, **kwargs): + interval_publish: float = None, friendly_name: str = None, + friendly_name_abs: str = None, **kwargs): 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, - **kwargs) + super().__init__(COMPONENT_NAME, __version__, _unit_index, + interval_publish=interval_publish, interval_reading=interval_reading, + logger=_log, **kwargs) self._adc = ADC(adc) # unified ADC interface self._voltage_max = voltage_max self._voltage_min = voltage_min diff --git a/pysmartnode/components/sensors/bell.py b/pysmartnode/components/sensors/bell.py index 9a73919..6ca49e5 100644 --- a/pysmartnode/components/sensors/bell.py +++ b/pysmartnode/components/sensors/bell.py @@ -11,12 +11,13 @@ pin: D5 debounce_time: 20 #ms on_time: 500 #ms #optional, time the mqtt message stays at on - direction: 2 #optional, falling 2 (pull-up used in code), rising 1, + direction: 2 #optional, falling 2 (pull-up used in code), rising 1, #mqtt_topic: sometopic #optional, defaults to home/bell # friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery # friendly_name_last: null # optional, friendly name for last_bell } } +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ __updated__ = "2020-03-29" @@ -27,7 +28,7 @@ from pysmartnode import logging from pysmartnode.utils.locksync import Lock from pysmartnode.components.machine.pin import Pin -from pysmartnode.utils.component import Component, DISCOVERY_TIMELAPSE, VALUE_TEMPLATE +from pysmartnode.utils.component import ComponentBase, DISCOVERY_TIMELAPSE, VALUE_TEMPLATE import machine import time import uasyncio as asyncio @@ -40,11 +41,10 @@ gc.collect() -class Bell(Component): +class Bell(ComponentBase): def __init__(self, pin, debounce_time, on_time=None, irq_direction=None, mqtt_topic=None, - friendly_name=None, - friendly_name_last=None, discover=True, **kwargs): - super().__init__(COMPONENT_NAME, __version__, unit_index=0, discover=discover, **kwargs) + friendly_name=None, friendly_name_last=None, **kwargs): + super().__init__(COMPONENT_NAME, __version__, unit_index=0, logger=_log, **kwargs) self._topic = mqtt_topic self._PIN_BELL_IRQ_DIRECTION = irq_direction or machine.Pin.IRQ_FALLING self._debounce_time = debounce_time diff --git a/pysmartnode/components/sensors/dht22.py b/pysmartnode/components/sensors/dht22.py index 462cb49..4c1de4a 100644 --- a/pysmartnode/components/sensors/dht22.py +++ b/pysmartnode/components/sensors/dht22.py @@ -12,15 +12,11 @@ precision_temp: 2 #precision of the temperature value published precision_humid: 1 #precision of the humid value published offset_temp: 0 #offset for temperature to compensate bad sensor reading offsets - offset_humid: 0 #... - #interval: 600 #optional, defaults to 600. -1 means do not automatically read sensor and publish values - #mqtt_topic: sometopic #optional, defaults to home//DHT22 + offset_humid: 0 #... # friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery - # discover: true # optional, if false no discovery message for homeassistant will be sent. - # expose_intervals: Expose intervals to mqtt so they can be changed remotely - # intervals_topic: if expose_intervals then use this topic to change intervals. Defaults to //<_unit_index>/interval/set. Send a dictionary with keys "reading" and/or "publish" to change either/both intervals. } } +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ __updated__ = "2020-03-29" @@ -54,16 +50,12 @@ class DHT22(ComponentSensor): def __init__(self, pin, precision_temp=2, precision_humid=1, - offset_temp=0, offset_humid=0, - interval_publish=None, interval_reading=None, mqtt_topic=None, - friendly_name_temp=None, friendly_name_humid=None, - discover=True, expose_intervals=False, intervals_topic=None, **kwargs): + offset_temp=0, offset_humid=0, friendly_name_temp=None, friendly_name_humid=None, + **kwargs): # This makes it possible to use multiple instances of MySensor and have unique identifier 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, - **kwargs) + super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, **kwargs) self._addSensorType(SENSOR_TEMPERATURE, precision_temp, offset_temp, _VAL_T_TEMPERATURE, "°C", friendly_name_temp) self._addSensorType(SENSOR_HUMIDITY, precision_humid, offset_humid, _VAL_T_HUMIDITY, "%", diff --git a/pysmartnode/components/sensors/ds18.py b/pysmartnode/components/sensors/ds18.py index 3116c44..0c5046b 100644 --- a/pysmartnode/components/sensors/ds18.py +++ b/pysmartnode/components/sensors/ds18.py @@ -11,24 +11,20 @@ pin: 5 # pin number or label (on NodeMCU) # rom: 28FF016664160383" # 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. # auto_detect: false # optional, if true and ROM is None then all connected ds18 units will automatically generate a sensor object with the given options. If a sensor is removed, so will its object. Removed sensors will be removed from Homeassistant too! - # interval_publish: 600 # optional, defaults to 600. Set to interval_reading to publish with every reading - # interval_reading: 120 # optional, defaults to 120. -1 means do not automatically read sensor and publish # precision_temp: 2 # precision of the temperature value published # offset_temp: 0 # offset for temperature to compensate bad sensor reading offsets - # mqtt_topic: sometopic # optional, defaults to home//DS18 # friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery - # discover: true # optional, if false no discovery message for homeassistant will be sent - # expose_intervals: # optional, expose intervals to mqtt so they can be changed remotely - # intervals_topic: # optional, if expose_intervals then use this topic to change intervals. Defaults to //<_unit_index>/interval/set. Send a dictionary with keys "reading" and/or "publish" to change either/both intervals. } } -# This module is to be used if only 1 ds18 sensor is connected and the ROM doesn't matter. -# It therefore provides a generic ds18 component for an exchangeable ds18 unit. -# The sensor can be replaced while the device is running. +Every connected DS18 unit can be configured individually with this module. +However, this module can also be used if only 1 ds18 sensor is connected and the ROM doesn't matter. +Then it provides a generic ds18 component for an exchangeable ds18 unit. The sensor can be replaced while the device is running. +The module can also be used to automatically detect all connected DS18 units. +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ -__updated__ = "2020-03-29" -__version__ = "3.3" +__updated__ = "2020-04-03" +__version__ = "3.4" from pysmartnode import config from pysmartnode import logging @@ -56,59 +52,38 @@ 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 = asyncio.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, **kwargs): + def __init__(self, pin, rom: str = None, auto_detect=False, precision_temp: int = 2, + offset_temp: float = 0, friendly_name=None, **kwargs): """ Class for a single ds18 unit to provide an interface to a single unit. + Alternatively it can be used to automatically detect all connected units + and create objects for those units. :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 //<_unit_index>/interval/set - Send a dictionary with keys "reading" and/or "publish" to change either/both intervals. + :param friendly_name: friendly name in homeassistant. Has no effect if rom is None and auto_detect True """ 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 + interval_reading = kwargs["interval_reading"] if "interval_reading" in kwargs else None + interval_publish = kwargs["interval_publish"] if "interval_publish" in kwargs else None self._instances = {} # rom:object self._auto_detect = True - self._prec = precision_temp - self._offs = offset_temp - self._discover = discover - self._expose = expose_intervals + self._kwargs = kwargs # store kwargs for initialization of detected sensors + kwargs["interval_reading"] = 60 # scan every 60 seconds for new units + kwargs["interval_publish"] = -1 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, - **kwargs) - 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 + super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, **kwargs) + self.rom: str = rom self._generic = True if rom is None and not auto_detect else False if type(pin) == ds18x20.DS18X20: self.sensor: ds18x20.DS18X20 = pin @@ -116,7 +91,15 @@ def __init__(self, pin, rom: str = None, auto_detect=False, interval_publish: fl 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 + 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 + elif self._auto_detect: + self._kwargs["interval_reading"] = interval_reading + self._kwargs["interval_publish"] = interval_publish + self._kwargs["precision_temp"] = precision_temp + self._kwargs["offset_temp"] = offset_temp gc.collect() def _default_name(self): @@ -142,11 +125,7 @@ async def _read(self): 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) + self._instances[rom] = DS18(self.sensor, rom, False, **self._kwargs) for rom in self._instances: if rom not in roms: # sensor not connected anymore await self.removeComponent(roms[rom]) diff --git a/pysmartnode/components/sensors/ecMeter.py b/pysmartnode/components/sensors/ecMeter.py index 9dd1456..cef2d37 100644 --- a/pysmartnode/components/sensors/ecMeter.py +++ b/pysmartnode/components/sensors/ecMeter.py @@ -21,13 +21,11 @@ # read_timeout: 400 # optional, time (in us) that an ADC read can take before the value will be ignored. # iterations: 1 # optional, how often the sensor should be read. average will be used as value # precision_ec: 3 # precision of the ec value published - # interval_reading: 600 # optional, defaults to 600 - # interval_public: 600 # optional, defaults to 600 - # mqtt_topic: sometopic # optional, defaults to home//ecmeter # friendly_name_ec: null # optional, friendly name shown in homeassistant gui with mqtt discovery # friendly_name_ppm: null # optional, friendly name shown in homeassistant gui with mqtt discovery } } +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ """ @@ -49,14 +47,13 @@ https://hackaday.io/project/7008-fly-wars-a-hackers-solution-to-world-hunger/log/24646-three-dollar-ec-ppm-meter-arduino https://www.hackster.io/mircemk/arduino-electrical-conductivity-ec-ppm-tds-meter-c48201 -Readings can be a bit weird. You need to measure your ADC and find calibration values for offset and -adc v_max used in .machine.ADC class. -Also Rg can be changed to further calibrate readings. My GND was theoretically 30R but 50R gave me -way better results. Changing Rg has a big effect on higher EC values. +Readings can be a bit weird. You need to measure your ADC and find calibration values for offset +and ADC v_max used in .machine.ADC class. +Also the cell constant "k" should be calibrated for the plug you use. """ -__updated__ = "2020-03-29" -__version__ = "1.8" +__updated__ = "2020-04-03" +__version__ = "1.9" from pysmartnode import config from pysmartnode import logging @@ -93,21 +90,17 @@ class EC(ComponentSensor): def __init__(self, r1, ra, rg, adc, power_pin, ground_pin, ppm_conversion, temp_coef, k, temp_sensor: ComponentSensor, read_timeout=400, iterations=1, precision_ec=3, - interval_publish=None, interval_reading=None, mqtt_topic=None, - friendly_name_ec=None, friendly_name_ppm=None, discover=True, - expose_intervals=False, intervals_topic=None, **kwargs): + friendly_name_ec=None, friendly_name_ppm=None, **kwargs): # This makes it possible to use multiple instances of MySensor global _unit_index _unit_index += 1 self.checkSensorType(temp_sensor, SENSOR_TEMPERATURE) - super().__init__(COMPONENT_NAME, __version__, _unit_index, discover, interval_publish, - interval_reading, mqtt_topic, _log, expose_intervals, intervals_topic, - **kwargs) + super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, **kwargs) self._temp = temp_sensor self._addSensorType("ec", precision_ec, 0, VALUE_TEMPLATE_JSON.format("ec|float"), "mS", - friendly_name_ec or "EC", mqtt_topic, DISCOVERY_EC) + friendly_name_ec or "EC", self._topic, DISCOVERY_EC) self._addSensorType("ppm", 0, 0, VALUE_TEMPLATE_JSON.format("ppm|int"), "ppm", - friendly_name_ppm or "PPM", mqtt_topic, DISCOVERY_PPM) + friendly_name_ppm or "PPM", self._topic, DISCOVERY_PPM) self._adc = ADC(adc) self._ppin = Pin(power_pin, machine.Pin.IN) # changing to OUTPUT GND when needed diff --git a/pysmartnode/components/sensors/hcsr04.py b/pysmartnode/components/sensors/hcsr04.py index 304931d..6e9c81a 100644 --- a/pysmartnode/components/sensors/hcsr04.py +++ b/pysmartnode/components/sensors/hcsr04.py @@ -7,6 +7,7 @@ Warning: My sensor only read distances reliable with flat surfaces within 80cm. Above that the results were fluctuating heavily. +A stable power source seems to be helpful. example config: { @@ -22,20 +23,13 @@ # sleeping_time: 200 # optional, sleeping time between reading iterations # iterations: 20 # optional, reading iterations per sensor reading # percentage_failed_readings_abort: 0.66 # optional, if a higher percentage of readings was bad, the current reading will be aborted - # interval_publish: 600 #optional, defaults to 600. Set to interval_reading to publish with every reading - # interval_reading: 120 # optional, defaults to 120. -1 means do not automatically read sensor and publish values - # mqtt_topic: null # optional, distance gets published to this topic - # mqtt_topic_interval: null # optional, topic need to have /set at the end. Interval can be changed here # value_template: "{{ 60.0 - float(value) }}" # optional, can be used to measure the reverse distance (e.g. water level) # friendly_name: "Distance" # optional, friendly name for homeassistant gui by mqtt discovery - # discover: true # optional, if false no discovery message for homeassistant will be sent. - # expose_intervals: true # Expose intervals to mqtt so they can be changed remotely - # intervals_topic: sometopic # if expose_intervals then use this topic to change intervals. Defaults to //<_unit_index>/interval/set. Send a dictionary with keys "reading" and/or "publish" to change either/both intervals. } } -# interval change can't be discovered as homeassistant doesn't offer a type HC-SR04 ultrasonic sensor. Be sure to connect it to 5V but use a voltage divider to connect the Echo pin to an ESP. +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ __updated__ = "2020-03-31" @@ -68,9 +62,7 @@ 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, - interval_publish=None, interval_reading=None, mqtt_topic=None, - value_template=None, friendly_name=None, - discover=True, expose_intervals=False, intervals_topic=None, **kwargs): + 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. @@ -83,22 +75,12 @@ def __init__(self, pin_trigger, pin_echo, timeout=30000, temp_sensor: ComponentS :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 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 //<_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, - **kwargs) + 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) diff --git a/pysmartnode/components/sensors/htu21d.py b/pysmartnode/components/sensors/htu21d.py index 868bb71..e5bf40c 100644 --- a/pysmartnode/components/sensors/htu21d.py +++ b/pysmartnode/components/sensors/htu21d.py @@ -12,17 +12,12 @@ precision_temp: 2 # precision of the temperature value published precision_humid: 1 # precision of the humid value published temp_offset: 0 # offset for temperature to compensate bad sensor reading offsets - humid_offset: 0 # ... - # interval_publish: 600 # optional, defaults to 600. Set to interval_reading to publish with every reading - # interval_reading: 120 # optional, defaults to 120. -1 means do not automatically read sensor and publish values - # mqtt_topic: sometopic # optional, defaults to home//HTU0 + humid_offset: 0 # ... # friendly_name_temp: null # optional, friendly name shown in homeassistant gui with mqtt discovery # friendly_name_humid: null # optional, friendly name shown in homeassistant gui with mqtt discovery - # discover: true # optional, if false no discovery message for homeassistant will be sent. - # expose_intervals: false # optional, expose intervals to mqtt so they can be changed remotely - # intervals_topic: null # optional, if expose_intervals then use this topic to change intervals. Defaults to //<_unit_index>/interval/set. Send a dictionary with keys "reading" and/or "publish" to change either/both intervals. } } +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ __updated__ = "2020-03-29" @@ -31,6 +26,7 @@ import gc import uasyncio as asyncio from pysmartnode import config +from pysmartnode import logging from pysmartnode.utils.component.sensor import ComponentSensor, SENSOR_TEMPERATURE, SENSOR_HUMIDITY #################### @@ -43,6 +39,7 @@ #################### _mqtt = config.getMQTT() +_log = logging.getLogger(COMPONENT_NAME) gc.collect() _unit_index = -1 @@ -54,14 +51,11 @@ class HTU21D(ComponentSensor): def __init__(self, i2c, precision_temp: int = 2, precision_humid: int = 2, temp_offset: float = 0, humid_offset: float = 0, - mqtt_topic: str = None, interval_publish: float = None, - interval_reading: float = None, - friendly_name_temp=None, friendly_name_humid=None, discover=True, **kwargs): + friendly_name_temp=None, friendly_name_humid=None, **kwargs): # This makes it possible to use multiple instances of MySensor and have unique identifier global _unit_index _unit_index += 1 - super().__init__(COMPONENT_NAME, __version__, _unit_index, discover, interval_publish, - interval_reading, mqtt_topic, **kwargs) + super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, **kwargs) # 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 diff --git a/pysmartnode/components/sensors/pms5003.py b/pysmartnode/components/sensors/pms5003.py index 70966d0..0dfc105 100644 --- a/pysmartnode/components/sensors/pms5003.py +++ b/pysmartnode/components/sensors/pms5003.py @@ -13,19 +13,13 @@ uart_rx: 26 # uart rx pin # set_pin: null # optional, sets device to sleep/wakes it up, can be done with uart # reset_pin: null # optional, without this pin the device can not be reset - # interval_reading: 0.1 # optional, In passive mode controls the reading interval, defaults to 0.1 in active_mode. - # interval_publish: 600 # publish interval, independent of interval_reading and active_mode, defaults to 600s # active_mode: true # optional, defaults to true, in passive mode device is in sleep between measurements # eco_mode: true # optional, defaults to true, puts device to sleep between passive reads - # interval: 600 #optional, defaults to 600, 0 means publish every value received - # mqtt_topic: sometopic #optional, defaults to home//PMS5003 # friendly_name: [...] # optional, list of friendly names for each published category - # discover: true # optional, if false no discovery message for homeassistant will be sent. - # expose_intervals: Expose intervals to mqtt so they can be changed remotely - # intervals_topic: if expose_intervals then use this topic to change intervals. Defaults to //<_unit_index>/interval/set. Send a dictionary with keys "reading" and/or "publish" to change either/both intervals. } } -Sensor can only be used with esp32 as esp8266 has only 1 uart at 115200 (9600 needed) +Sensor is tested with esp32. ESP8266 has only 1 uart but *could* be used if the repl is disconnected (not tested). +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ __updated__ = "2020-03-29" @@ -65,9 +59,8 @@ class PMS5003(ComponentSensor): 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, **kwargs): + interval_reading=0.1, active_mode=True, eco_mode=True, friendly_name: list = None, + **kwargs): """ :param uart_number: esp32 has multiple uarts :param uart_tx: tx pin number @@ -77,16 +70,10 @@ def __init__(self, uart_number, uart_tx, uart_rx, set_pin=None, reset_pin=None, :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, - **kwargs) + super().__init__(COMPONENT_NAME, __version__, 0, logger=_log, + interval_reading=interval_reading, **kwargs) if type(friendly_name) is not None: if type(friendly_name) == list: if len(friendly_name) != 12: diff --git a/pysmartnode/components/sensors/remoteSensor.py b/pysmartnode/components/sensors/remoteSensor.py index d91ca7b..bcd93e5 100644 --- a/pysmartnode/components/sensors/remoteSensor.py +++ b/pysmartnode/components/sensors/remoteSensor.py @@ -21,12 +21,13 @@ cause problems since the topic won't be stored on startup. You need to wait for network to be finished so the retained state can be restored. Typically if a component is initialized after this sensor and the _init_network is called, the topic value should have been received. +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ # TODO: implement support for multiple sensor_types that share one topic in one component -__updated__ = "2020-03-29" -__version__ = "0.3" +__updated__ = "2020-04-01" +__version__ = "0.4" import gc import time @@ -64,8 +65,9 @@ def __init__(self, sensor_type, mqtt_topic=None, command_topic=None, else: raise TypeError("value_template type {!s} not supported".format(v)) self._log = logging.getLogger("{}_{}{}".format(COMPONENT_NAME, sensor_type, _unit_index)) - super().__init__(COMPONENT_NAME, __version__, _unit_index, False, -1, -1, None, self._log, - False, None, **kwargs) + super().__init__(COMPONENT_NAME, __version__, _unit_index, discover=False, + interval_publish=-1, interval_reading=-1, logger=self._log, + expose_intervals=False, **kwargs) self._addSensorType(sensor_type, 2, 0, value_template, "") # no unit_of_measurement as only used for mqtt discovery self._stale_time = stale_time @@ -123,7 +125,7 @@ async def on_message(self, topic, msg, retain): await self._setValue(list(self.sensor_types)[0], msg) # not changing timeout because there should be no error - async def _publishValues(self, timeout=5): + async def publishValues(self, timeout=5): pass # there is no publish for a remote sensor, even if "accidentally" requested def _default_name(self): diff --git a/pysmartnode/components/sensors/waterSensor.py b/pysmartnode/components/sensors/waterSensor.py index be31931..0f7cddd 100644 --- a/pysmartnode/components/sensors/waterSensor.py +++ b/pysmartnode/components/sensors/waterSensor.py @@ -11,7 +11,6 @@ constructor_args: { adc: 33 power_pin: 5 # optional if connected to permanent power - # interval_publish: -1 # optional, defaults to -1 because sensor will automatically publish on any state change. Can be changed for sending "keepalives" in between changes. # interval_reading: 1 # optional, interval in seconds that the sensor gets polled # cutoff_voltage: 3.3 # optional, defaults to ADC maxVoltage (on ESP 3.3V). Above this voltage means dry # mqtt_topic: "sometopic" # optional, defaults to home//waterSensor/ @@ -20,19 +19,20 @@ # expose_intervals: Expose intervals to mqtt so they can be changed remotely # intervals_topic: if expose_intervals then use this topic to change intervals. Defaults to //<_unit_index>/interval/set. Send a dictionary with keys "reading" and/or "publish" to change either/both intervals. } -} -Will publish on any state change and in the given interval. State changes are detected in the interval_reading. +} +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! +Will publish on any state change. State changes are detected in the interval_reading. Only the polling interval of the first initialized sensor is used. The publish interval is unique to each sensor. -This is to use only one uasyncio task for all sensors to prevent a uasyncio queue overflow. +This is to use only one uasyncio task for all sensors (because old uasyncio would overflow). ** How to connect: Put a Resistor (~10kR) between the power pin (or permanent power) and the adc pin. Connect the wires to the adc pin and gnd. """ -__updated__ = "2020-03-29" -__version__ = "1.7" +__updated__ = "2020-04-03" +__version__ = "1.8" from pysmartnode import config from pysmartnode import logging @@ -56,21 +56,19 @@ class WaterSensor(ComponentSensor): DEBUG = False - def __init__(self, adc, power_pin=None, cutoff_voltage=None, interval_publish=None, - interval_reading=1, mqtt_topic=None, friendly_name=None, discover=True, - expose_intervals=False, intervals_topic=None, **kwargs): - interval_publish = interval_publish or -1 + def __init__(self, adc, power_pin=None, cutoff_voltage=None, + interval_reading=1, friendly_name=None, **kwargs): 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, + super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, + interval_reading=interval_reading, interval_publish=-1, **kwargs) self._adc = ADC(adc) self._ppin = Pin(power_pin, machine.Pin.OUT) if power_pin is not None else None self._cv = cutoff_voltage or self._adc.maxVoltage() self._lv = None self._addSensorType(SENSOR_BINARY_MOISTURE, 0, 0, VALUE_TEMPLATE, "", friendly_name, - mqtt_topic, None, True) + self._topic, None, True) self._pub_task = None async def _read(self): diff --git a/pysmartnode/components/switches/buzzer.py b/pysmartnode/components/switches/buzzer.py index ae21026..7854e3a 100644 --- a/pysmartnode/components/switches/buzzer.py +++ b/pysmartnode/components/switches/buzzer.py @@ -13,14 +13,13 @@ # on_time: 500 #optional, defaults to 500ms, time buzzer stays at one pwm duty # iters: 1 #optional, iterations done, defaults to 1 # freq: 1000 #optional, defaults to 1000 - # mqtt_topic: null #optional, defaults to //Buzzer/set - # friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery } } +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ -__updated__ = "2020-03-29" -__version__ = "3.3" +__updated__ = "2020-04-03" +__version__ = "3.31" import gc from machine import Pin, PWM @@ -45,8 +44,7 @@ class Buzzer(ComponentButton): - def __init__(self, pin, pwm_values, on_time=500, iters=1, freq=1000, mqtt_topic=None, - friendly_name=None, discover=True, **kwargs): + def __init__(self, pin, pwm_values, on_time=500, iters=1, freq=1000, **kwargs): self.pin = PyPin(pin, Pin.OUT) self.on_time = on_time self.values = pwm_values @@ -56,9 +54,7 @@ def __init__(self, pin, pwm_values, on_time=500, iters=1, freq=1000, mqtt_topic= # This makes it possible to use multiple instances of Buzzer global _unit_index _unit_index += 1 - super().__init__(COMPONENT_NAME, __version__, _unit_index, mqtt_topic, discover=discover, - **kwargs) - self._frn = friendly_name + super().__init__(COMPONENT_NAME, __version__, _unit_index, **kwargs) gc.collect() async def _on(self): diff --git a/pysmartnode/components/switches/generic_switch.py b/pysmartnode/components/switches/generic_switch.py index 79a9dde..2877402 100644 --- a/pysmartnode/components/switches/generic_switch.py +++ b/pysmartnode/components/switches/generic_switch.py @@ -7,18 +7,15 @@ { package: .switches.generic_switch component: GenSwitch - constructor_args: { - # mqtt_topic: null # optional, defaults to //Switch<_unit_index>/set - # friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery - # discover: true # optional, if false no discovery message for homeassistant will be sent. - } + constructor_args: {} } This generic switch does absolutely nothing except publishing its state and receiving state changes. Can be used to represent (for example) the long-press state of a physical button. +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ -__updated__ = "2020-03-29" -__version__ = "1.1" +__updated__ = "2020-04-03" +__version__ = "1.11" from pysmartnode import config from pysmartnode.utils.component.switch import ComponentSwitch @@ -30,12 +27,11 @@ class GenSwitch(ComponentSwitch): - def __init__(self, mqtt_topic=None, friendly_name=None, discover=True, **kwargs): + def __init__(self, **kwargs): global _unit_index _unit_index += 1 initial_state = False - super().__init__(COMPONENT_NAME, __version__, _unit_index, mqtt_topic, instance_name=None, - wait_for_lock=True, discover=discover, friendly_name=friendly_name, + super().__init__(COMPONENT_NAME, __version__, _unit_index, wait_for_lock=True, initial_state=initial_state, **kwargs) @staticmethod diff --git a/pysmartnode/components/switches/gpio.py b/pysmartnode/components/switches/gpio.py index c855966..8fe6bb6 100644 --- a/pysmartnode/components/switches/gpio.py +++ b/pysmartnode/components/switches/gpio.py @@ -11,13 +11,14 @@ pin: D5 active_high: true #optional, defaults to active high # mqtt_topic: sometopic #optional, topic needs to have /set at the end, defaults to //GPIO/ - # friendly_name: "led" #optional, custom name for the pin in homeassistant, defaults to "GPIO_" + # instance_name: name #optional, name of the gpio instance, will be generated automatically } } +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ -__updated__ = "2020-03-29" -__version__ = "1.1" +__updated__ = "2020-04-03" +__version__ = "1.11" import gc import machine @@ -35,18 +36,16 @@ class GPIO(ComponentSwitch): - def __init__(self, pin, active_high=True, mqtt_topic=None, friendly_name=None, discover=True, - **kwargs): + 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, - instance_name="{!s}_{!s}".format(COMPONENT_NAME, pin), discover=discover, + 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._frn = friendly_name self._active_high = active_high async def _on(self): diff --git a/pysmartnode/components/switches/led.py b/pysmartnode/components/switches/led.py index f3d740c..75edbfa 100644 --- a/pysmartnode/components/switches/led.py +++ b/pysmartnode/components/switches/led.py @@ -12,14 +12,13 @@ #on_time: 50 #optional, time led is on, defaults to 50ms #off_time: 50 #optional, time led is off, defaults to 50ms #iters: 20 #optional, iterations done, defaults to 20 - #mqtt_topic: null #optional, topic needs to have /set at the end - # friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery } } +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ -__updated__ = "2020-03-29" -__version__ = "3.3" +__updated__ = "2020-04-03" +__version__ = "3.31" import gc @@ -44,8 +43,7 @@ class LEDNotification(ComponentButton): - def __init__(self, pin, on_time=50, off_time=50, iters=20, mqtt_topic=None, - friendly_name=None, discover=True, **kwargs): + 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 @@ -53,9 +51,7 @@ def __init__(self, pin, on_time=50, off_time=50, iters=20, mqtt_topic=None, # This makes it possible to use multiple instances of LED global _unit_index _unit_index += 1 - super().__init__(COMPONENT_NAME, __version__, _unit_index, mqtt_topic, discover=discover, - **kwargs) - self._frn = friendly_name + super().__init__(COMPONENT_NAME, __version__, _unit_index, **kwargs) gc.collect() async def _on(self): diff --git a/pysmartnode/components/switches/remote433mhz.py b/pysmartnode/components/switches/remote433mhz.py index e54d757..f75e593 100644 --- a/pysmartnode/components/switches/remote433mhz.py +++ b/pysmartnode/components/switches/remote433mhz.py @@ -12,10 +12,7 @@ file: "filename" # filename where the captured sequences are stored. Has to be uploaded manually! name_on: "on_a" # name of the sequence for turning the device on name_off: "off_a" # name of the sequence for turning the device off - # reps: 10 # optional, amount of times a frame is being sent - # mqtt_topic: null # optional, defaults to //Switch<_unit_index>/set - # friendly_name: null # optional, friendly name shown in homeassistant gui with mqtt discovery - # discover: true # optional, if false no discovery message for homeassistant will be sent. + # reps: 5 # optional, amount of times a frame is being sent } } Control 433Mhz devices (e.g. power sockets) with a cheap 433Mhz transmitter. @@ -23,10 +20,13 @@ For this to work you need to have sequences captured and stores on the device. How to do that is described in his repository. Note: This component only works on the devices supported by Peter Hinch's library! -(esp32, pyboards but not esp8266) +(esp32, pyboards but not esp8266). +Be careful with "reps", the amount of repitions as this currently uses a lot of RAM. + +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ -__updated__ = "2020-03-31" +__updated__ = "2020-04-03" __version__ = "0.2" from pysmartnode import config @@ -49,14 +49,11 @@ class Switch433Mhz(ComponentSwitch): - def __init__(self, pin, file: str, name_on: str, name_off: str, reps: int = 10, - mqtt_topic=None, friendly_name=None, discover=True, **kwargs): + def __init__(self, pin, file: str, name_on: str, name_off: str, reps: int = 5, **kwargs): global _unit_index _unit_index += 1 - initial_state = False global _tx if file not in _remotes and _tx is None: - print("Creating TX for pin", pin, file, name_on) pin = Pin(pin, machine.Pin.OUT) _tx = TX(pin, file, reps) _remotes[file] = _tx._data @@ -70,9 +67,8 @@ def __init__(self, pin, file: str, name_on: str, name_off: str, reps: int = 10, if name_off not in _remotes[file]: raise AttributeError("name_off {!r} not in file {!s}".format(name_off, file)) - super().__init__(COMPONENT_NAME, __version__, _unit_index, mqtt_topic, instance_name=None, - wait_for_lock=True, discover=discover, friendly_name=friendly_name, - initial_state=initial_state, **kwargs) + super().__init__(COMPONENT_NAME, __version__, _unit_index, wait_for_lock=True, + initial_state=False, **kwargs) self._reps = reps self._file = file @@ -82,15 +78,13 @@ def __init__(self, pin, file: str, name_on: str, name_off: str, reps: int = 10, self._name_off = name_off # one lock for all switches, overrides lock created by the base class - self.lock = _lock + self._lock = _lock ##################### # Change these methods according to your device. ##################### async def _on(self): """Turn device on.""" - import time - print("on", time.ticks_ms()) _tx._data = _remotes[self._file] reps = _tx._reps _tx._reps = self._reps @@ -103,8 +97,6 @@ async def _on(self): async def _off(self): """Turn device off. """ - import time - print("off", time.ticks_ms()) _tx._data = _remotes[self._file] reps = _tx._reps _tx._reps = self._reps diff --git a/pysmartnode/components/switches/remoteSwitch.py b/pysmartnode/components/switches/remoteSwitch.py index 9ada27c..1bb26ba 100644 --- a/pysmartnode/components/switches/remoteSwitch.py +++ b/pysmartnode/components/switches/remoteSwitch.py @@ -13,6 +13,7 @@ # timeout: 10 # optional, defaults to 10s, timeout for receiving an answer } } +NOTE: additional constructor arguments are available from base classes, check COMPONENTS.md! """ # TODO: implement possibility to set sensor topics through mqtt, similar to RemoteSensor implementation @@ -23,7 +24,7 @@ COMPONENT_NAME = "RemoteSwitch" -from pysmartnode.utils.component import Component +from pysmartnode.utils.component import ComponentBase from pysmartnode import config import uasyncio as asyncio import time @@ -34,7 +35,7 @@ _unit_index = -1 -class RemoteSwitch(Component): +class RemoteSwitch(ComponentBase): """ Generic Switch class. Use it according to the template. diff --git a/pysmartnode/components/switches/switch_extension/__init__.py b/pysmartnode/components/switches/switch_extension/__init__.py index aefca1b..b59ddd6 100644 --- a/pysmartnode/components/switches/switch_extension/__init__.py +++ b/pysmartnode/components/switches/switch_extension/__init__.py @@ -35,7 +35,7 @@ from pysmartnode import config from pysmartnode import logging import gc -from pysmartnode.utils.component.switch import Component, ComponentSwitch, DISCOVERY_SWITCH +from pysmartnode.utils.component.switch import ComponentBase, ComponentSwitch, DISCOVERY_SWITCH from uasyncio import Lock #################### @@ -85,12 +85,12 @@ def __str__(self): Mode = BaseMode() -class Switch(Component): +class Switch(ComponentBase): def __init__(self, component: ComponentSwitch, modes_enabled: list, - mqtt_topic_mode=None, friendly_name_mode=None, discover=True, **kwargs): + mqtt_topic_mode=None, friendly_name_mode=None, **kwargs): global _unit_index _unit_index += 1 - super().__init__(COMPONENT_NAME, __version__, _unit_index, discover, **kwargs) + super().__init__(COMPONENT_NAME, __version__, _unit_index, logger=_log, **kwargs) if type(component) == str: self._component = config.getComponent(component) if self._component is None: diff --git a/pysmartnode/config.py b/pysmartnode/config.py index 5a32b46..59fb87a 100644 --- a/pysmartnode/config.py +++ b/pysmartnode/config.py @@ -6,7 +6,7 @@ # Configuration management file ## -__updated__ = "2018-11-15" +__updated__ = "2020-04-02" from .config_base import * from sys import platform @@ -36,8 +36,6 @@ def __printRAM(start, info=""): import uasyncio as asyncio -loop = asyncio.get_event_loop() - gc.collect() __printRAM(_mem, "Imported uasyncio") @@ -114,7 +112,7 @@ def getMQTT(): return _mqtt -from pysmartnode.utils.component import Component +from pysmartnode.utils.component import ComponentBase __printRAM(_mem, "Imported Component base class") diff --git a/pysmartnode/utils/component/__init__.py b/pysmartnode/utils/component/__init__.py index 1143f7f..60afab9 100644 --- a/pysmartnode/utils/component/__init__.py +++ b/pysmartnode/utils/component/__init__.py @@ -2,14 +2,15 @@ # Copyright Kevin Köck 2019-2020 Released under the MIT license # Created on 2019-04-26 -__updated__ = "2020-03-29" -__version__ = "1.5" +__updated__ = "2020-04-01" +__version__ = "1.6" from pysmartnode import config import uasyncio as asyncio from pysmartnode.utils import sys_vars from .definitions import * import gc +from pysmartnode import logging # This module is used to create components. # This could be sensors, switches, binary_sensors etc. @@ -27,12 +28,20 @@ _components = None # pointer list of all registered components, used for mqtt etc -class Component: +class ComponentBase: """ Use this class as a base for components. Subclass to extend. See the template for examples. """ - def __init__(self, component_name, version, unit_index: int, discover=True, **kwargs): + def __init__(self, component_name, version, unit_index: int, discover=True, logger=None): + """ + Base component class + :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 discover: if the component should send a discovery message, used in Home-Assistant. + :param logger: optional logger instance. If not provided, one will be created with the component name + """ self._next_component = None # needed to keep a list of registered components global _components if _components is None: @@ -45,7 +54,7 @@ def __init__(self, component_name, version, unit_index: int, discover=True, **kw break c = c._next_component # Workaround to prevent every component object from creating a new asyncio task for - # network oriented initialization as this would lead to an asyncio queue overflow. + # network oriented initialization as this would cause a big RAM demand. global _init_queue_start if _init_queue_start is None: _init_queue_start = self @@ -54,12 +63,13 @@ def __init__(self, component_name, version, unit_index: int, discover=True, **kw self.VERSION = version self._count = unit_index self.__discover = discover + self._log = logger or logging.getLogger("{!s}{!s}".format(component_name, self._count)) @staticmethod async def removeComponent(component): if type(component) == str: component = config.getComponent(component) - if isinstance(component, Component) is False: + if not isinstance(component, ComponentBase): config._log.error( "Can't remove a component that is not an instance of pysmartnode.utils.component.Component") return False @@ -79,7 +89,11 @@ async def removeComponent(component): c = c._next_component async def _remove(self): - """Cleanup method. Stop all loops and unsubscribe all topics.""" + """ + Cleanup method. + Stop all loops and unsubscribe all topics. + Also removes the component from Home-Assistant if discovery is enabled. + """ await _mqtt.unsubscribe(None, self) await config._log.asyncLog("info", "Removed component", config.getComponentName(self), "module", self.COMPONENT_NAME, "version", self.VERSION, @@ -119,16 +133,16 @@ async def _discovery(self, register=True): @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) + topic = ComponentBase._getDiscoveryTopic(component_type, unique_name) + msg = ComponentBase._composeDiscoveryMsg(component_topic, unique_name, discovery_type, + friendly_name) await _mqtt.publish(topic, msg, qos=1, retain=True) del msg, topic gc.collect() @staticmethod async def _deleteDiscovery(component_type, unique_name): - topic = Component._getDiscoveryTopic(component_type, unique_name) + topic = ComponentBase._getDiscoveryTopic(component_type, unique_name) await _mqtt.publish(topic, "", qos=1, retain=True) @staticmethod @@ -155,14 +169,15 @@ def _composeDiscoveryMsg(component_topic, name, component_type_discovery, friend return DISCOVERY_BASE.format(component_topic, # "~" component state topic friendly_name, # name sys_vars.getDeviceID(), name, # unique_id - "" if no_avail else Component._composeAvailability(), + "" if no_avail else ComponentBase._composeAvailability(), component_type_discovery, # component type specific values sys_vars.getDeviceDiscovery()) # device @staticmethod - def _composeSensorType(device_class, unit_of_measurement, value_template): + def _composeSensorType(device_class, unit_of_measurement, value_template, expire_after=0): """Just to make it easier for component developers.""" - return DISCOVERY_SENSOR.format(device_class, unit_of_measurement, value_template) + return DISCOVERY_SENSOR.format(device_class, unit_of_measurement, value_template, + int(expire_after)) @staticmethod def _getDiscoveryTopic(component_type, name): diff --git a/pysmartnode/utils/component/button.py b/pysmartnode/utils/component/button.py index a168383..3288f03 100644 --- a/pysmartnode/utils/component/button.py +++ b/pysmartnode/utils/component/button.py @@ -2,8 +2,8 @@ # Copyright Kevin Köck 2019-2020 Released under the MIT license # Created on 2019-09-10 -__updated__ = "2020-03-29" -__version__ = "0.9" +__updated__ = "2020-04-02" +__version__ = "0.91" from .switch import ComponentSwitch from pysmartnode import config @@ -20,14 +20,13 @@ class ComponentButton(ComponentSwitch): Otherwise it would be a Switch. """ - def __init__(self, component_name, version, unit_index: int, command_topic=None, - instance_name=None, wait_for_lock=False, discover=True, friendly_name=None, + def __init__(self, component_name, version, unit_index: int, wait_for_lock=False, initial_state=False, **kwargs): """ :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 mqtt_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 :param wait_for_lock: if True then every request waits for the lock to become available, meaning the previous device request has to finish before the new one is started. @@ -36,28 +35,24 @@ def __init__(self, component_name, version, unit_index: int, command_topic=None, :param friendly_name: friendly name for homeassistant gui :param initial_state: the initial state of the button, typically False ("OFF") for Pushbutton """ - super().__init__(component_name, version, unit_index, command_topic, instance_name, - wait_for_lock, discover, restore_state=False, friendly_name=friendly_name, - initial_state=initial_state, **kwargs) - # 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 + super().__init__(component_name, version, unit_index, wait_for_lock=wait_for_lock, + restore_state=False, initial_state=initial_state, **kwargs) 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: + """Turn Button 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: - _mqtt.schedulePublish(self._topic[:-4], "ON", qos=1, retain=True, timeout=1, - await_connection=False) + async with self._lock: + if self._pub_task: + self._pub_task.cancel() # cancel if not finished, e.g. if activated quickly again + self._pub_task = asyncio.create_task(self._publish("ON")) # so device gets activated as quickly as possible self._state = True await self._on() self._state = False - await asyncio.sleep(0) - # to ensure first publish will be done before new publish in case _on() is fast - await _mqtt.publish(self._topic[:-4], "OFF", qos=1, retain=True, timeout=1, - await_connection=False) + if self._pub_task: + self._pub_task.cancel() # cancel if not finished, e.g. if _on() is very fast + self._pub_task = asyncio.create_task(self._publish("OFF")) return True async def off(self): @@ -67,7 +62,3 @@ async def off(self): async def toggle(self): """Just for compatibility reasons, will always activate single-shot action""" return await self.on() - - async def _off(self): - """Only for compatibility as single-shot action has no _off()""" - return True diff --git a/pysmartnode/utils/registerComponents.py b/pysmartnode/utils/registerComponents.py index dbd82c7..e910773 100644 --- a/pysmartnode/utils/registerComponents.py +++ b/pysmartnode/utils/registerComponents.py @@ -69,7 +69,8 @@ def registerComponent(componentname, component, _log): s = io.StringIO() sys.print_exception(e, s) _log.critical( - "Error importing package {!s}, error: {!s}".format(component["package"], s)) + "Error importing package {!s}, error: {!s}".format(component["package"], + s.getvalue())) module = None gc.collect() err = False @@ -93,7 +94,7 @@ def registerComponent(componentname, component, _log): sys.print_exception(e, s) _log.error( "Error during creation of object {!r}, {!r}, version {!s}: {!s}".format( - component["component"], componentname, version, s)) + component["component"], componentname, version, s.getvalue())) obj = None err = True if obj is not None: