Skip to content
Snippets Groups Projects
Commit 3a320799 authored by Benji's avatar Benji
Browse files

Adding sensor interface and LM75 i2c sensor support

parent 5b7b8330
Branches kegan/alias-settings
Tags
No related merge requests found
......@@ -90,3 +90,6 @@ ENV/
# Rope project settings
.ropeproject
#Kate
*.kate-swp
PI MQTT GPIO
============
Expose the Raspberry Pi GPIO pins and/or external IO modules to an MQTT server. This allows pins to be read and switched by reading or writing messages to MQTT topics.
Expose the Raspberry Pi GPIO pins, external IO modules and I2C sensors to an MQTT server. This allows pins to be read and switched by reading or writing messages to MQTT topics. The I2C sensors will be read periodically and publish their values.
Modules
-------
GPIO Modules
------------
- Raspberry Pi GPIO (`raspberrypi`)
- PCF8574 IO chip (`pcf8574`)
- PiFaceDigital 2 IO board (`piface2`)
I2C Sensors
-----------
- LM75 i2c temperature sensor (`lm75`)
Installation
------------
......@@ -82,6 +87,32 @@ digital_inputs:
pulldown: no
```
### Sensors
Receive updates on the value of a sensor by subscribing to the `home/sensor/temperature` topic:
```yaml
mqtt:
host: test.mosquitto.org
port: 1883
user: ""
password: ""
topic_prefix: home
sensor_modules:
- name: lm75
module: lm75
i2c_bus_num: 1
chip_addr: 0x48
cleanup: no # This optional boolean value sets whether the module's `cleanup()` function will be called when the software exits.
sensor_inputs:
- name: temperature
module: lm75
interval: 15 #interval in seconds, that a value is read from the sensor and a update is published
digits: 4 # number of digits to be round
```
#### SSL/TLS
You may want to connect to a remote server, in which case it's a good idea to use an encrypted connection. If the server supports this, then you can supply the relevant config values for the [tls_set()](https://github.com/eclipse/paho.mqtt.python#tls_set) command.
......
......@@ -18,6 +18,12 @@ gpio_modules:
module: stdio
cleanup: no
sensor_modules:
- name: lm75
module: lm75
i2c_bus_num: 1
chip_addr: 0x48
digital_inputs:
- name: button
module: raspberrypi
......@@ -40,3 +46,10 @@ digital_outputs:
pin: 1
on_payload: "ON"
off_payload: "OFF"
sensor_inputs:
- name: temperature
module: lm75
interval: 15
digits: 4
......@@ -92,7 +92,27 @@ mqtt:
gpio_modules:
type: list
required: no
schema:
type: dict
allow_unknown: yes
schema:
name:
type: string
required: yes
empty: no
module:
type: string
required: yes
empty: no
cleanup:
type: boolean
required: no
default: yes
sensor_modules:
type: list
required: no
schema:
type: dict
allow_unknown: yes
......@@ -189,3 +209,33 @@ digital_outputs:
type: boolean
required: no
default: no
sensor_inputs:
type: list
required: no
default: []
schema:
type: dict
schema:
name:
type: string
required: yes
empty: no
module:
type: string
required: yes
empty: no
retain:
type: boolean
required: no
default: no
interval:
type: integer
required: no
default: 60
min: 1
digits:
type: integer
required: no
default: 2
min: 0
......@@ -95,7 +95,27 @@ mqtt:
gpio_modules:
type: list
required: no
schema:
type: dict
allow_unknown: yes
schema:
name:
type: string
required: yes
empty: no
module:
type: string
required: yes
empty: no
cleanup:
type: boolean
required: no
default: yes
sensor_modules:
type: list
required: no
schema:
type: dict
allow_unknown: yes
......@@ -193,4 +213,34 @@ digital_outputs:
required: no
default: no
sensor_inputs:
type: list
required: no
default: []
schema:
type: dict
schema:
name:
type: string
required: yes
empty: no
module:
type: string
required: yes
empty: no
retain:
type: boolean
required: no
default: no
interval:
type: integer
required: no
default: 60
min: 1
digits:
type: integer
required: no
default: 2
min: 0
""")
......@@ -55,3 +55,25 @@ class GenericGPIO(object):
Called when closing the program to handle any cleanup operations.
"""
pass
class GenericSensor(object):
"""
Abstracts a generic sensor interface to be implemented
by the modules in this directory.
"""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def setup_sensor(self, config):
pass
@abc.abstractmethod
def get_value(self, sensor):
pass
def cleanup(self):
"""
Called when closing the program to handle any cleanup operations.
"""
pass
from pi_mqtt_gpio.modules import GenericSensor
"""REQUIREMENTS = ("smbus",)"""
CONFIG_SCHEMA = {
"i2c_bus_num": {
"type": "integer",
"required": True,
"empty": False
},
"chip_addr": {
"type": "integer",
"required": True,
"empty": False
}
}
LM75_TEMP_REGISTER = 0
class Sensor(GenericSensor):
"""
Implementation of GPIO class for the PCF8574 IO expander chip.
"""
def __init__(self, config):
import smbus
self.bus = smbus.SMBus(config["i2c_bus_num"])
self.address = config["chip_addr"]
def setup_sensor(self, config):
return True # nothing to do here
def get_value(self, sensor):
"""get the temperature value from the sensor"""
value = self.bus.read_word_data(self._address,
LM75_TEMP_REGISTER) & 0xFFFF
value = ((value << 8) & 0xFF00) + (value >> 8)
return self.convert_to_celsius(value)
def convert_to_celsius(self, value):
return (value / 32.0) / 8.0
......@@ -8,6 +8,10 @@ from time import sleep, time
from importlib import import_module
from hashlib import sha1
import threading # For callback functions
from fractions import gcd # for calculating the callback periodic time
from functools import reduce
import paho.mqtt.client as mqtt
import cerberus
......@@ -23,15 +27,19 @@ LOG_LEVEL_MAP = {
mqtt.MQTT_LOG_ERR: logging.ERROR,
mqtt.MQTT_LOG_DEBUG: logging.DEBUG
}
RECONNECT_DELAY_SECS = 5
GPIO_MODULES = {}
GPIO_CONFIGS = {}
GPIO_MODULES = {} # storage for gpio modules
SENSOR_MODULES = {} # storage for sensor modules
GPIO_CONFIGS = {} # storage for gpios
SENSOR_CONFIGS = {} # storage for sensors
LAST_STATES = {}
SET_TOPIC = "set"
SET_ON_MS_TOPIC = "set_on_ms"
SET_OFF_MS_TOPIC = "set_off_ms"
OUTPUT_TOPIC = "output"
INPUT_TOPIC = "input"
SENSOR_TOPIC = "sensor"
_LOG = logging.getLogger(__name__)
_LOG.addHandler(logging.StreamHandler())
......@@ -414,6 +422,29 @@ def configure_gpio_module(gpio_config):
return gpio_module.GPIO(gpio_config)
def configure_sensor_module(sensor_config):
"""
Imports sensor module, validates its config and returns an instance of it.
:param sensor_config: Module configuration values
:type sensor_config: dict
:return: Configured instance of the sensor module
:rtype: pi_mqtt_gpio.modules.GenericSensor
"""
sensor_module = import_module(
"pi_mqtt_gpio.modules.%s" % sensor_config["module"])
# Doesn't need to be a deep copy because we won't modify the base
# validation rules, just add more of them.
module_config_schema = BASE_SCHEMA.copy()
module_config_schema.update(
getattr(sensor_module, "CONFIG_SCHEMA", {}))
module_validator = cerberus.Validator(module_config_schema)
if not module_validator.validate(sensor_config):
raise ModuleConfigInvalid(module_validator.errors)
sensor_config = module_validator.normalized(sensor_config)
install_missing_requirements(sensor_module)
return sensor_module.Sensor(sensor_config)
def initialise_digital_input(in_conf, gpio):
"""
Initialises digital input.
......@@ -446,6 +477,85 @@ def initialise_digital_output(out_conf, gpio):
gpio.setup_pin(out_conf["pin"], PinDirection.OUTPUT, None, out_conf)
def initialise_sensor_input(sens_conf, sensor):
"""
Initialises sensor input.
:param in_conf: Sensor config
:type in_conf: dict
:param sensor: Instance of GenericSensor to use
:type sensor: pi_mqtt_gpio.modules.GenericSensor
:return: None
:rtype: NoneType
"""
sensor.setup_sensor(sens_conf)
def sensor_timer_thread(SENSOR_MODULES, sensor_inputs, topic_prefix):
"""
Timer thread for the sensors
To reduce cpu usage, there is only one cyclic thread for all sensors.
At the beginning the cycle time is calculated (ggT) to match all intervals.
For each sensor interval, the reduction value is calculated, that triggers
the read, round and publish for the sensor, when loop_count is a multiple
of it. In worst case, the cycle_time is 1 second, in best case, e.g., when
there is only one sensor, cycle_time is its interval.
"""
# calculate the min time
arr = []
for sens_conf in sensor_inputs:
arr.append(sens_conf.get("interval", 60))
# get the greates common divisor (gcd) for the list of interval times
cycle_time = reduce(lambda x, y: gcd(x, y), arr)
_LOG.info(
"sensor_timer_thread: calculated cycle_time will be %d seconds",
cycle_time)
for sens_conf in sensor_inputs:
sens_conf["interval_reduction"] = sens_conf.get("interval",
60) / cycle_time
# Start the cyclic thread
loop_count = 0
next_call = time()
while True:
loop_count = loop_count + 1
for sens_conf in sensor_inputs:
if(loop_count % sens_conf["interval_reduction"] == 0):
sensor = SENSOR_MODULES[sens_conf["module"]]
try:
value = round(sensor.get_value(sensor),
sens_conf["digits"])
_LOG.info(
"sensor_timer_thread: reading sensor '%s' value %r",
sens_conf["name"],
value
)
# publish each value
client.publish(
"%s/%s/%s" % (
topic_prefix, SENSOR_TOPIC, sens_conf["name"]
),
payload=value,
retain=sens_conf["retain"]
)
except ModuleConfigInvalid as exc:
_LOG.error(
"sensor_timer_thread: failed to read sensor '%s': %s",
sens_conf["name"],
exc
)
# schedule next call
next_call = next_call+cycle_time # every cycle_time sec
sleep(next_call - time())
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument("config")
......@@ -463,9 +573,11 @@ if __name__ == "__main__":
digital_inputs = config["digital_inputs"]
digital_outputs = config["digital_outputs"]
sensor_inputs = config["sensor_inputs"]
client = init_mqtt(config["mqtt"], config["digital_outputs"])
# Install modules for GPIOs
for gpio_config in config["gpio_modules"]:
GPIO_CONFIGS[gpio_config["name"]] = gpio_config
try:
......@@ -478,6 +590,20 @@ if __name__ == "__main__":
gpio_config["name"],
yaml.dump(exc.errors)
)
# Install modules for Sensors
for sensor_config in config["sensor_modules"]:
SENSOR_CONFIGS[sensor_config["name"]] = sensor_config
try:
SENSOR_MODULES[sensor_config["name"]] = configure_sensor_module(
sensor_config)
except ModuleConfigInvalid as exc:
_LOG.error(
"Config for %r module named %r did not validate:\n%s",
sensor_config["module"],
sensor_config["name"],
yaml.dump(exc.errors)
)
sys.exit(1)
for in_conf in digital_inputs:
......@@ -487,6 +613,9 @@ if __name__ == "__main__":
for out_conf in digital_outputs:
initialise_digital_output(out_conf, GPIO_MODULES[out_conf["module"]])
for sens_conf in sensor_inputs:
initialise_sensor_input(sens_conf, SENSOR_MODULES[sens_conf["module"]])
try:
client.connect(config["mqtt"]["host"], config["mqtt"]["port"], 60)
except socket.error as err:
......@@ -498,6 +627,20 @@ if __name__ == "__main__":
topic_prefix = config["mqtt"]["topic_prefix"]
try:
# Starting the sensor thread (if there are sensors configured)
if(len(sensor_inputs) > 0):
sensor_thread = threading.Thread(target=sensor_timer_thread,
kwargs={'SENSOR_MODULES':
SENSOR_MODULES,
'sensor_inputs':
sensor_inputs,
'topic_prefix':
topic_prefix})
sensor_thread.name = 'pi-mqtt-gpio_SensorReader'
# stops the thread, when main program terminates
sensor_thread.daemon = True
sensor_thread.start()
while True:
for in_conf in digital_inputs:
gpio = GPIO_MODULES[in_conf["module"]]
......@@ -524,6 +667,7 @@ if __name__ == "__main__":
except KeyboardInterrupt:
print("")
finally:
client.publish(
"%s/%s" % (topic_prefix, config["mqtt"]["status_topic"]),
config["mqtt"]["status_payload_stopped"], qos=1, retain=True)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment