├── LICENSE ├── README.md ├── evok.conf ├── unipi_mqtt.py ├── unipi_mqtt.service ├── unipi_mqtt_config.json └── unipipython.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Matthijs van den Berg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniPi MQTT 2 | 3 | This is a script that creates MQTT messages based on the events that happen on the UNIPI device ans switches UniPi outputs based on received MQTT messages. Worked with a Unipi 513 here. Main goal is to get MQTT messages to/from Home Assistant. 4 | 5 | Script creates a websocket connection to EVOK and based on those websocket messages and a config file MQTT messages are published. 6 | 7 | Also creates a MQTT listener to listen to incomming MQTT topics and switch UniPi outputs based on those messages. I created the system in such a way that info to switch an output must be in the MQTT message. See the Hass examples below. 8 | 9 | WARNING: I am not a programmer, so this code kinda works, but it ain't pretty ;-) (I think...). So there is a big chance you need to tinker a bit in the scripts. It's also a version 1 that I build specifically for my home assistant setup, so it's quite tailored to my personal need and way of working. 10 | 11 | Update July 2020, I have a 'unipi friend' in Belgium now that has a setup too, and changes this to be a bit more generic, so perhaps a bit broader applicable. 12 | 13 | Be sure to use the 2 python scripts (python3) and the json config file. 14 | 15 | ## Setup 16 | 17 | I put the files in a directory and create a service to automatically start and stop the service on system start. 18 | 19 | Prereq: 20 | - A UniPi system with EVOK (opensource software from EVOK, works both on a self build as wel as on a UniPi provided Evok image from here (https://kb.unipi.technology/en:files:software:os-images:00-start). Don't forget to install Evok afterwards per documentation in that site. 21 | - MQTT Broker somewhere 22 | 23 | Setup: 24 | - Install python 3 and pip (sudo apt install `python3-pip`) 25 | - Install the required python packes with pip (`pip3 install paho-mqtt threaded websocket-client statistics requests`) 26 | - Copy the 3 scripts into a dir (I use /scripts in my unipi user dir, can be anything) 27 | - Adjust the vars in the script to your needs, like IP, etc. 28 | - Adjust the unipi_mqtt_config.json file to refelxt your unipi and the connected devices to it (see below for more details) 29 | - optional; Create a service based on this script (example to do so here; https://github.com/MydKnight/PiClasses/wiki/Making-a-script-run-as-daemon-on-boot) Example file in this github (unipi_mqtt.service) 30 | - Start the service or script and see what happens (sudo service start unipi_mqtt) 31 | - Logging goes to /var/log/unipi_mqtt.log 32 | 33 | ## UniPi unipi_mqtt_config.json 34 | 35 | A config file is used to describe to inputs on the UniPi so the script knows what to send out when a change on a input is detected. An example config file is in the repo, here an example entry. It's JSON, so make sure it's valid. 36 | 37 | ### Digital In as binary sensor in HA 38 | 39 | Example PIR sensor for motion detection in unipi_mqtt_config.json: 40 | ```json 41 | { 42 | "circuit":"1_04", 43 | "description":"Kantoor PIR", 44 | "dev":"input", 45 | "device_delay":120, 46 | "device_normal":"no", 47 | "unipi_value":0, 48 | "unipi_prev_value":0, 49 | "unipi_prev_value_timstamp":0, 50 | "state_topic": "unipi/bgg/kantoor/motion" 51 | }, 52 | ``` 53 | 54 | The HA part for this is a binary sensor: 55 | ``` 56 | - platform: mqtt 57 | name: "Kantoor Motion" 58 | unique_id: "kantoor_motion" 59 | state_topic: "unipi/bgg/kantoor/motion" 60 | payload_on: "ON" 61 | payload_off: "OFF" 62 | availability_topic: "unipi/bgg/kantoor/motion/available" 63 | payload_available: "online" 64 | payload_not_available: "offline" 65 | qos: 0 66 | device_class: presence 67 | ``` 68 | 69 | ### Handle local options (3 types) 70 | Example unipi_mqtt_config.json with "handle local" function to handle a local "critical" function within the script so it works without HA or other MQTT connections. This also sends MQTT messages to inform HA about the state change. 71 | 72 | #### 1 - Handle Local Bel (on AND off switch of relay in 1 action) 73 | This example rings a bel 3 time (or switches a realy 3 x on and off, so 6 total actions). The bel I use is a Friendland bel on 12 or 24 volt. It rings once on power on and power off (power on pulls the ring stick, power off launches it to ring quite loud). 74 | 75 | ```json 76 | { 77 | "circuit":"2_05", 78 | "description":"Voordeur Beldrukker", 79 | "dev":"input", 80 | "handle_local": 81 | { 82 | "type": "bel", 83 | "trigger":"on", 84 | "rings": 3, 85 | "output_dev": "output", 86 | "output_circuit": "2_01" 87 | }, 88 | "device_delay":1, 89 | "device_normal":"no", 90 | "unipi_value":0, 91 | "unipi_prev_value":0, 92 | "unipi_prev_value_timstamp":0, 93 | "state_topic": "unipi/bgg/voordeur/beldrukker" 94 | } 95 | ``` 96 | 97 | I trigger this in HA from a automation where the action part is; 98 | ``` 99 | action: 100 | - service: mqtt.publish 101 | data: 102 | topic: 'homeassistant/bgg/hal/bel/set' 103 | payload: '{"circuit": "2_01", "dev": "relay", "repeat": "1", "state": "pulse"}' 104 | ``` 105 | 106 | #### 2 - Handle Local Light Dimmer (Analog output 0-10 volt). 107 | Example unipi_mqtt_config.json of a handle local switch with dimmer (analog output 0-10 volt is used to dimm led source). I user 0-10 volt (not 1-10!) led dimmers. Works flawlessly. Note that the Level in unipi = 0-10 and in HA 0-255 for 0-100%. Not that handle local sets a value, but it's static. Things like holding the sensor to dimm are not implemented. 108 | 109 | unipi_mqtt_config.json: 110 | 111 | ```{ 112 | "circuit":"3_02", 113 | "description":"Schakelaar Bijkeuken Licht", 114 | "dev":"input", 115 | "handle_local": 116 | { 117 | "type": "dimmer", 118 | "output_dev": "analogoutput", 119 | "output_circuit": "2_03", 120 | "level": 10 121 | }, 122 | "device_normal":"no", 123 | "state_topic": "homeassistant/bgg/bijkeuken/licht" 124 | } 125 | ``` 126 | 127 | The HA part can look like: 128 | 129 | ``` 130 | - platform: mqtt 131 | schema: template 132 | name: "Woonkamer Nis light" 133 | unique_id: "woonkamer_nis_licht" 134 | state_topic: "homeassistant/bgg/woonkamer/nis/licht" 135 | command_topic: "homeassistant/bgg/woonkamer/nis/licht/set" 136 | availability_topic: "homeassistant/bgg/woonkamer/nis/licht/available" 137 | payload_available: "online" 138 | payload_not_available: "offline" 139 | command_on_template: > 140 | {"state": "on" 141 | , "circuit": "2_04" 142 | , "dev": "analogoutput" 143 | {%- if brightness is defined -%} 144 | , "brightness": {{ brightness }} 145 | {%- elif brightness is undefined -%} 146 | , "brightness": 100 147 | {%- endif -%} 148 | {%- if transition is defined -%} 149 | , "transition": {{ transition }} 150 | {%- endif -%} 151 | } 152 | command_off_template: '{"state": "off", "circuit": "2_04", "dev": "analogoutput"}' 153 | state_template: '{{ value_json.state }}' 154 | brightness_template: '{{ value_json.brightness }}' 155 | qos: 0 156 | ``` 157 | 158 | #### 3 - Handle Local Switch (output or relayoutput toggle) 159 | Example unipi_mqtt_config.json of handle local switch (on / off only, relay or digital output used to switch a device or powersource to a device). 160 | It will poll the unipi box and toggle the output to the other state. So on becomes off and visa versa. A MQTT message reflecting this is send. HA need to have the some topic and payload to recognise a change in the HA GUI. 161 | 162 | ```{ 163 | "circuit":"UART_4_4_04", 164 | "description":"TEST IN FUTURE Schakelaar Woonkamer Eker Licht", 165 | "dev":"input", 166 | "handle_local": 167 | { 168 | "type": "switch", 169 | "output_dev": "output", 170 | "output_circuit": "2_02" 171 | }, 172 | "device_normal":"no", 173 | "state_topic": "homeassistant/bgg/meterkast/testrelay" 174 | } 175 | ``` 176 | 177 | The HA part of this switch looks like (for me under lights in YAML): 178 | ``` 179 | - platform: mqtt 180 | schema: template 181 | name: "Test Relay 2_02" 182 | unique_id: "test_relay_2_02" 183 | state_topic: "homeassistant/bgg/meterkast/testrelay" 184 | command_topic: "homeassistant/bgg/meterkast/testrelay/set" 185 | availability_topic: "homeassistant/bgg/meterkast/testrelay/available" 186 | payload_available: "online" 187 | payload_not_available: "offline" 188 | command_on_template: '{"state": "on", "circuit": "2_02", "dev": "output"}' 189 | command_off_template: '{"state": "off", "circuit": "2_02", "dev": "output"}' 190 | state_template: '{{ value_json.state }}' 191 | qos: 0 192 | ``` 193 | 194 | ### 1-Wire sensors 195 | You can connect 1-wire sensors to the Unipi (16 cascaded sensors to 1 1-wire port). The sensors allow you to measure things like temperature and humidity. The implementation currently support sensors with model `DS2438` and `DS18B20`. Other might work, but I just don't have them and the script hard-checks for those models. So let me know if you need a change / add here. This info can be found in the UniPi API (exmp. http://192.168.1.125:8080/rest/sensor/28D1EFB708025352 ) The value for "circuit" can be found in the web GUI of the UNiPi. 196 | 197 | Config in unipi_mqtt the config file 198 | ``` 199 | "circuit":"28D1EFB708025352", 200 | "description":"Temperatuur Sensor buiten", 201 | "dev":"temp", 202 | "interval":19, 203 | "state_topic":"unipi/buiten/voordeur/temperatuur" 204 | ``` 205 | 206 | "dev" value options are `"temp"`, `"humidity"` or `"light"`. 207 | 208 | Config in HA sensors part: 209 | ``` 210 | - platform: mqtt 211 | name: "Buiten Temperatuur" 212 | unique_id: "buiten_temperatuur" 213 | state_topic: "unipi/buiten/voordeur/temperatuur" 214 | unit_of_measurement: "°C" 215 | value_template: "{{value_json.temperature}}" 216 | force_update: true 217 | ``` 218 | 219 | NOTE: the `force_update: true` is used to always update the sensor. Home Assistant by default does NOT updates sensor values if they, compared to the latest value, are unchanged. This is optional. Since I run a script to monitor my unipi device based on a regular update (if > 10 min no update = alart) of this value I want it to always update. 220 | 221 | ## Description of the fields: 222 | - dev: The input device type on the UniPi 223 | - circuit: The input circuit on the UniPi 224 | - description: Description of what you do with this input 225 | - device_delay: delay to turn device off automatically (used for PIR sensors that work pulse based) 226 | - device_normal: is device normal open or normal closed 227 | - unipi_value: what is the current value, used as a "global var" 228 | - unipi_prev_value: what is the previous value, used as a "global var" to calculate average of multiple values ver time 229 | - unipi_prev_value_timstamp: when was the last status change. Used for delay based off messages, for exmpl. for PIR pulse 230 | - state_topic: MQTT state topic to send message on 231 | - handle_local: Use to switch outputs based on a input directly. So no dependency on MQTT broker or HASSIO. Use this for bel and light switches. Does send a MQTT update message to status can change in Home Assistant. 232 | - interval: value for 1-wire sensors and analog inputs to create an avarage based on this number of readings and send this avg out. 233 | 234 | 235 | ## MQTT messages to change UniPi Outputs 236 | You can send MQTT message to the Unipi box over MQTT to switch outputs. This does not require a config entry on the unipi since we're sending the device and circuit information in the MQTT message that is handled by the script. 237 | 238 | Example for dimmable light (publish from HASS to UniPi to turn on an output) 239 | ``` 240 | - platform: mqtt 241 | schema: template 242 | name: "Voordeur light" 243 | state_topic: "homeassistant/buiten/voordeur/licht" 244 | command_topic: "homeassistant/buiten/voordeur/licht/set" 245 | availability_topic: "homeassistant/buiten/voordeur/licht/available" 246 | payload_available: "online" 247 | payload_not_available: "offline" 248 | command_on_template: > 249 | {"state": "on" 250 | , "circuit": "2_02" 251 | , "dev": "analogoutput" 252 | {%- if brightness is defined -%} 253 | , "brightness": {{ brightness }} 254 | {%- elif brightness is undefined -%} 255 | , "brightness": 100 256 | {%- endif -%} 257 | {%- if effect is defined -%} 258 | , "effect": "{{ effect }}" 259 | {%- endif -%} 260 | {%- if transition is defined -%} 261 | , "transition": {{ transition }} 262 | {%- endif -%} 263 | } 264 | command_off_template: '{"state": "off", "circuit": "2_02", "dev": "analogoutput"}' 265 | state_template: '{{ value_json.state }}' 266 | brightness_template: '{{ value_json.brightness }}' 267 | qos: 0 268 | ``` 269 | 270 | Switch a relay: 271 | ``` 272 | - platform: mqtt 273 | schema: template 274 | name: "Test Relay 2_02" 275 | unique_id: "test_relay_2_02" 276 | state_topic: "unipi1/bgg/meterkast/testrelay" 277 | command_topic: "unipi1/bgg/meterkast/testrelay/set" 278 | payload_available: "online" 279 | payload_not_available: "offline" 280 | command_on_template: '{"state": "on", "circuit": "2_02", "dev": "output"}' 281 | command_off_template: '{"state": "off", "circuit": "2_02", "dev": "output"}' 282 | state_template: "{{ value_json.state }}" 283 | qos: 0 284 | ``` 285 | 286 | I the device remains offline you need to take out the availability topic line to let HA not check that. 287 | 288 | 289 | # Change log 290 | 291 | ### version 02.2021.1 ("the average" release) 292 | Changes: 293 | - the 1-wire implementation did not use the average setting. Rewrote the 1-wire part for temp and humidity sensors. You can now add a "interval" variable that counts creates an average. "interval" is the number or readings. Note that 1-wire readings are approx. once every 3 seconds and 0 is the first value so a seeeting of 19 = 20 values. This allows you to greatly reduce the number of updates HA has to handle when the sensors count builds. :-) 294 | - The same average sytems is used for LUX value based on an anolog input (I know, rather specific). 295 | 296 | ### version 11.2020 (the "Stijn" release) 297 | Changes: 298 | - New version numbering since that's really cool 299 | - Made WebSockets listener re-connect on disconnect (like a service interruption of evok) every 5 seconds 300 | - implemented authentication for MQTT since that is a requirement for HA now 301 | - adjusted some timeouts to make a external realy work via the bel function 302 | - Changed the unipi_mqtt.service file to restart a service if the script fails for enhanced resillience 303 | 304 | ### Version 0.4 305 | Changes: 306 | - Added authentication for MQTT with username and password variable since the standard MQTT broker in HA requires this from now on. 307 | - Added a counter function to count pulses coming in on a digital input. Counter totals and counter delta for X time can be send via MQTT. Personally use this for a water flow meter that procuces pulse for every X ML. 308 | - Changed the time based interval to a clock instead of imconning messages to be a bit more precise. 309 | - Changes handle local for swithes. Was sending back a wrong MQTT topic for my HA config to work (MIGHT BE BREAKING CHANGE). 310 | - Changed a bug in unipython.py where switch status for on / off was the wrong way around. 311 | - 0.41 has a small fix to honor the "level" information in unipi_mqtt_config for handle local dimmmers. 312 | 313 | ### Version 0.3 314 | Changes: 315 | - Changed the thread part so threading and especially the stop thread part now works correctly 316 | - Changed the MQTT send part to make sure that on a handle local action only 1 message is send (was 4). Now works nicely 317 | - Revamped the threaded function like duration and transition to be interuptable 318 | - Changed code for the 1 wire devices. Upgrade of Evok changed the naming convention for those devices from "temp" to "1wire". Now handled again. 319 | 320 | ### Version 0.2 321 | Changes: 322 | - Changed handling if DI devices with delay to no longer use previous state for rest of devices, cleaned up json config file. Should fix a bug that crashed the script on certain ON / OFF actions. 323 | - Implemented a "frist run" part to set MQTT messages at script start to reflect actual status of inputs, not last known status maintained in MQTT broker or no status at al. 324 | - tested UART (extension module) and that works. Changed config file with example 325 | 326 | ### Version 0.1 327 | Initial release and documentation in this readme file 328 | 329 | ## ToDo 330 | - Something with certificates 331 | - Use config file for client part too? 332 | - clean up code more 333 | - many other yet to discover things. 334 | 335 | # Test info 336 | 337 | Tested on a UniPi 513 with Extensio xS30 running Evok 2.x and Home Assistant 0.102 338 | Used: 339 | - 0-10v inputs and outputs 340 | - relay outputs 341 | - Digital inputs and outputs 342 | - 1 wire for temp, humidity and light 343 | - UART Extention module 30 344 | 345 | -------------------------------------------------------------------------------- /evok.conf: -------------------------------------------------------------------------------- 1 | ; Example config to het the xS30 to communicate with the main unit. Dipswitches are END: ON, ADDRESS 4: ON AND E/N: ON, rest OFF. 2 | ; Communication settings made on Serial Port 1.01 3 | 4 | [EXTENSION_1] 5 | global_id = 4 ; Mandatory, REQUIRED TO BE UNIQUE 6 | device_name = xS30 ; Mandatory 7 | modbus_uart_port = /dev/extcomm/0/0 ; Mandatory 8 | neuron_uart_circuit = 1_01 ; Optional, allows associating extensions with specific Neuron UART-over-Modbus ports (not possible for n$ 9 | ;allow_register_access = True ; Optional, False default, is mandatory with third-party devices 10 | address = 4 ; Optional, 1 default 11 | ;scan_frequency = 10 ; Optional, 10 default, scanning frequency in [Hz] 12 | ;scan_enabled = True ; Optional, True default 13 | ; Note that the following settings will be inherited by other devices sharing the same port, i.e. /dev/extcomm/0/0 14 | ;baud_rate = 19200 ; Optional, NEEDS UNIPI IMAGE TO WORK WITH UNIPI SERIAL PORTS! USE API TO CONFIGURE UART MANUALLY$ 15 | ;parity = E ; Optional, NEEDS UNIPI IMAGE TO WORK WITH UNIPI SERIAL PORTS! USE API TO CONFIGURE UART $ 16 | ;stop_bits = 1 ; Optional, NEEDS UNIPI IMAGE TO WORK WITH UNIPI SERIAL PORTS! USE API TO CONFIGURE UART MANUALLY$ 17 | 18 | -------------------------------------------------------------------------------- /unipi_mqtt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Script to turn on / set level of UNIPI s based on MQTT messages that come in. 4 | # No fancy coding to see here, please move on (Build by a complete amateur ;-) ) 5 | # Matthijs van den Berg / https://github.com/matthijsberg/unipi-mqtt 6 | # MIT License 7 | # version 02.2021.1 (new version numbering since that's really face these days) 8 | 9 | # resources used besides google; 10 | # - http://jasonbrazeal.com/blog/how-to-build-a-simple-iot-system-with-python/ 11 | # - http://www.diegoacuna.me/how-to-run-a-script-as-a-service-in-raspberry-pi-raspbian-jessie/ TO run this as service 12 | # - https://gist.github.com/beugley/e37dd3e36fd8654553df for stopable thread part, ### Class and functions to create threads that can be stopped, so that when a lights is still dimming (in a thread since it blocking) but motion is detected and lights need to turn on during dimming we kill the thread and start the new action. WARNING. If you dont need threading, dont use it. Its not fun ;-). 13 | 14 | import paho.mqtt.client as mqtt 15 | import sys 16 | import json 17 | import logging 18 | import datetime 19 | from unipipython import unipython 20 | import os 21 | import time, traceback 22 | import threading 23 | import websocket 24 | from websocket import create_connection 25 | import traceback 26 | from collections import OrderedDict 27 | import statistics 28 | import math 29 | # from hanging_threads import start_monitoring 30 | 31 | ######################################################################################################################## 32 | # Variables used in the system ### 33 | ######################################################################################################################## 34 | 35 | # MQTT Connection Variables 36 | mqtt_address = "192.168.1.x" 37 | mqtt_subscr_topic = "homeassistant/#" #to what channel to listen for MQTT updates to switch stuff. I use a "send by" topic here as example. 38 | mqtt_client_name = "UNIPI2-MQTT" 39 | mqtt_user = "unipi02" 40 | mqtt_pass = "abc123ABC!@#" 41 | # Websocket Connection Variables 42 | ws_server = "192.168.1.x" 43 | ws_user = "none" #not implemented auth for ws yet 44 | ws_pass = "none" 45 | # Generic Variables 46 | logging_path = "/var/log/unipi_mqtt.log" 47 | dThreads = {} #keeping track of all threads running 48 | intervals_average = {} #dict that we fill with and array per sensors that needs an average value. Number of values in array is based on "interval" var in config file 49 | intervals_counter = {} #counter to use per device in dict so we know when to stop. :-) global since it iterates and this was the best I could come up with. 50 | 51 | ######################################################################################################################## 52 | ### Some housekeeping functions to handle threads, logging, etc. ### 53 | ### NO CHANGES AFTER THIS REQUIRED FOR NORMAL USE! ### 54 | ######################################################################################################################## 55 | 56 | class StoppableThread(threading.Thread): # Implements a thread that can be stopped. 57 | def __init__(self, name, target, args=()): 58 | super(StoppableThread, self).__init__(name=name, target=target, args=args) 59 | self._status = 'running' 60 | def stop_me(self): 61 | if (self._status == 'running'): 62 | #logging.debug('{}: Changing thread "{}" status to "stopping".'.format(get_function_name(),dThreads[thread_id])) 63 | self._status = 'stopping' 64 | def running(self): 65 | self._status = 'running' 66 | #logging.debug('{}: Changing thread "{}" status to "running".'.format(get_function_name(),dThreads[thread_id])) 67 | def stopped(self): 68 | self._status = 'stopped' 69 | #logging.debug('{}: Changing thread "{}" status to "stopped".'.format(get_function_name(),dThreads[thread_id])) 70 | def is_running(self): 71 | return (self._status == 'running') 72 | def is_stopping(self): 73 | return (self._status == 'stopping') 74 | def is_stopped(self): 75 | return (self._status == 'stopped') 76 | 77 | def StopThread(thread_id): 78 | # Stops a thread and removes its entry from the global dThreads dictionary. 79 | logging.warning('{}: STOPthread ID {} .'.format(get_function_name(),thread_id)) 80 | global dThreads 81 | if thread_id in str(dThreads): 82 | logging.warning('{}: Thread {} found in thread list: {} , checking if running or started...'.format(get_function_name(),dThreads[thread_id],dThreads)) 83 | thread = dThreads[thread_id] 84 | if (thread.is_stopping()): 85 | logging.warning('{}: Thread {} IS found STOPPING in running threads: {} , waiting till stop complete for thread {}.'.format(get_function_name(),thread_id,dThreads,dThreads[thread_id])) 86 | if (thread.is_running()): 87 | logging.warning('{}: Thread {} IS found active in running threads: {} , proceeding to stop {}.'.format(get_function_name(),thread_id,dThreads,dThreads[thread_id])) 88 | logging.warning('{}: Stopping thread "{}"'.format(get_function_name(),thread_id)) 89 | thread.stop_me() 90 | logging.warning('{}: thread.stop_me finished. Now running threads and status: {} .'.format(get_function_name(),dThreads)) 91 | thread.join(10) #implemented a timeout of 10 since the join here is blocking and halts the complete script. this allows the main function to continue, but is an ERROR since a thread is not joining. Most likely an function that hangs (function needs to end before a join is succesfull)! 92 | thread.stopped() 93 | logging.warning('{}: Stopped thread "{}"'.format(get_function_name(),thread_id)) 94 | del dThreads[thread_id] 95 | logging.warning('{}: Remaining running threads are "{}".'.format(get_function_name(),dThreads)) 96 | else: 97 | logging.warning('{}: Thread {} not running or started.'.format(get_function_name(),dThreads[thread_id])) 98 | else: 99 | logging.warning('{}: Thread {} not found in global thread var: {}.'.format(get_function_name(),thread_id,dThreads)) 100 | 101 | 102 | def get_function_name(): 103 | return traceback.extract_stack(None, 2)[0][2] 104 | 105 | def every(delay, task): 106 | next_time = time.time() + delay 107 | while True: 108 | time.sleep(max(0, next_time - time.time())) 109 | try: 110 | task() 111 | except Exception: 112 | logging.warning('{}: Problem while executing repetitive task.'.format(get_function_name())) 113 | # skip tasks if we are behind schedule: 114 | next_time += (time.time() - next_time) // delay * delay + delay 115 | 116 | ######################################################################################################################## 117 | ### Functions to handle the incomming MQTT messages, filter, sort, and kick off the action functions to switch. ### 118 | ######################################################################################################################## 119 | 120 | def on_mqtt_message(mqttc, userdata, msg): 121 | #print(msg.topic+" "+str(msg.payload)) 122 | if "set" in msg.topic: 123 | mqtt_msg=str(msg.payload.decode("utf-8","ignore")) 124 | logging.debug('{}: Message "{}" on input.'.format(get_function_name(),mqtt_msg)) 125 | mqtt_msg_history = mqtt_msg 126 | if mqtt_msg.startswith("{"): 127 | try: 128 | mqtt_msg_json = json.loads(mqtt_msg, object_pairs_hook=OrderedDict) #need the orderedDict here otherwise the order of the mQTT message is changed, that will bnreak the return message and than the device won't turn on in HASSIO 129 | except ValueError as e: 130 | logging.error('{}: Message "{}" not a valid JSON - message not processed, error is "{}".'.format(get_function_name(),mqtt_msg,e)) 131 | else: 132 | logging.debug('{}: Message "{}" is a valid JSON, processing json in handle_json.'.format(get_function_name(),mqtt_msg_json)) 133 | handle_json(msg.topic,mqtt_msg_json) 134 | else: 135 | logging.debug("{}: Message \"{}\" not JSON format, processing other format.".format(get_function_name(),mqtt_msg)) 136 | handle_other(msg.topic,mqtt_msg) 137 | 138 | # Main function to handle incomming MQTT messages, check content en start the correct function to handle the request. All time consuming, and thus blocking actions, are threaded. 139 | def handle_json(ms_topic,message): 140 | global dThreads 141 | try: 142 | # We NEED a dev in the message as this targets a circuit type (analog / digital inputs, etc.) on the UniPi 143 | dev_presence = 'dev' in message 144 | if dev_presence == True: dev_value = message['dev'] 145 | # We also NEED a circuit in the message to be able to target a circuit on the UniPi 146 | circuit_presence = 'circuit' in message 147 | if circuit_presence == True: circuit_value = message['circuit'] 148 | # state, what do we need to do 149 | state_presence = 'state' in message 150 | if state_presence == True: state_value = message['state'] 151 | # Transition, optional. You can fade anolog outputs slowly. Transition is the amount of seconds you want to fade to take (seconds always applied to 0-100%, so 0-25% = 25% of seconds) 152 | transition_presence = 'transition' in message 153 | if transition_presence == True: transition_value = message['transition'] 154 | # Brightness, if you switch lights with 0-10 volt we translate the input value (0-255) to 0-10 and consider this brightness 155 | brightness_presence = 'brightness' in message 156 | if brightness_presence == True: brightness_value = message['brightness'] 157 | # Repeat, if present this will trigger an on - off action x amout of times. I use this to trigger a relay multiple times to let a bel ring x amount of times. 158 | repeat_presence = 'repeat' in message 159 | if repeat_presence == True: repeat_value = message['repeat'] 160 | # Duration is used to switch a output on for x seconds. IN my case used to open electrical windows. 161 | duration_presence = 'duration' in message 162 | if duration_presence == True: duration_value = message['duration'] 163 | # Effect, not activly used yet, for future reference. 164 | effect_presence = 'effect' in message 165 | if effect_presence == True: effect_value = message['effect'] 166 | logging.debug('Device: {} - {}, Circuit: {} - {}, State: {} - {}, Transition: {} , Brightness: {} , Repeat: {} , Duration: {} , Effect: {} .'.format(dev_presence,dev_value,circuit_presence,circuit_value,state_presence,state_value,transition_presence,brightness_presence,repeat_presence,duration_presence,effect_presence)) 167 | except: 168 | logging.error('{}: Unhandled exception. Looks like input is not valid dict / json. Message data is: "{}".'.format(get_function_name(),message)) 169 | #id = circuit_value 170 | thread_id = dev_value + circuit_value 171 | if dev_presence and circuit_presence and state_presence: # these are the minimal required arguments for this function to work 172 | logging.debug('{}: Valid WebSocket input received, processing message "{}"'.format(get_function_name(),message)) 173 | if transition_presence: 174 | if brightness_presence: 175 | logging.warning('{}: starting "transition" message handling for dev "{}" circuit "{}" to value "{}" in {} s. time.'.format(get_function_name(),circuit_value,state_value,brightness_value,transition_value)) 176 | if(brightness_value > 255): logging.error('{}: Brightness input is greater than 255, 255 is max value! Setting Brightness to 255.'.format(get_function_name())); brightness_value = 255 177 | StopThread(thread_id) 178 | dThreads[thread_id] = StoppableThread(name=thread_id, target=transition_brightness, args=(brightness_value,transition_value,dev_value,circuit_value,ms_topic,message)) 179 | dThreads[thread_id].start() 180 | logging.warning('TEMP threads {}:'.format(dThreads)) 181 | logging.warning('{}: started thread "{}" for "transition" of dev "{}" circuit "{}".'.format(get_function_name(),dThreads[thread_id],circuit_value,state_value)) 182 | else: 183 | logging.error('{}: Processing "transition", but missing argument "brightness", aborting. Message data is "{}".'.format(get_function_name(),message)) 184 | elif brightness_presence: 185 | logging.debug('{}: starting "brightness" message handling for dev "{}" circuit "{}" to value "{}" (not in thread).'.format(get_function_name(),circuit_value,state_value,brightness_value)) 186 | if(brightness_value > 255): logging.error('{}: Brightness input is greater than 255, 255 is max value! Setting Brightness to 255.'.format(get_function_name())); brightness_value = 255 187 | StopThread(thread_id) 188 | set_brightness(brightness_value,circuit_value,ms_topic,message) # not in thread as this is not blocking 189 | elif effect_presence: 190 | logging.error('{}: Processing "effect", but not yet implemented, aborting. Message data is "{}"'.format(get_function_name(),message)) 191 | elif duration_presence: 192 | logging.debug('{}: starting "duration" message handling for dev "{}" circuit "{}" to value "{}" for {} sec.'.format(get_function_name(),circuit_value,state_value,state_value,duration_value)) 193 | StopThread(thread_id) 194 | dThreads[thread_id] = StoppableThread(name=thread_id, target=set_duration, args=(dev_value,circuit_value,state_value,duration_value,ms_topic,message)) 195 | dThreads[thread_id].start() 196 | logging.debug('{}: started thread "{}" for "duration" of dev "{}" circuit "{}".'.format(get_function_name(),dThreads[thread_id],circuit_value,state_value)) 197 | elif repeat_presence: 198 | logging.debug('{}: starting "repeat" message handling for dev "{}" circuit "{}" for {} time'.format(get_function_name(),circuit_value,state_value,int(repeat_value))) 199 | StopThread(thread_id) 200 | dThreads[thread_id] = StoppableThread(name=thread_id, target=set_repeat, args=(dev_value,circuit_value,int(repeat_value),ms_topic,message)) 201 | dThreads[thread_id].start() 202 | logging.debug('{}: started thread "{}" for "repeat" of dev "{}" circuit "{}".'.format(get_function_name(),dThreads[thread_id],circuit_value,state_value)) 203 | elif (state_value == "on" or state_value == "off"): 204 | logging.debug('{}: starting "state value" message handling for dev "{}" circuit "{}" to value "{}" (not in thread).'.format(get_function_name(),circuit_value,state_value,state_value)) 205 | StopThread(thread_id) 206 | set_state(dev_value,circuit_value,state_value,ms_topic,message) #not in thread, not blocking 207 | else: 208 | logging.error('{}: No valid actionable item found!') 209 | else: 210 | logging.error('{}: Not all required arguments found in received MQTT message "{}". Need "dev", "circuit" and "state" minimal.'.format(get_function_name(),message)) 211 | 212 | def handle_other(ms_topic,message): #TODO, initialy started to handle ON and OFF messages, but since we require dev and circuit this doesn't work. Maybe for future ref. and use config file? 213 | logging.warning('"{}": function not yet implemented! Received message "{}" here.'.format(get_function_name(),message)) 214 | 215 | ######################################################################################################################## 216 | # Functions to handle WebSockets (UniPi) inputs to filter, sort, and kick off the actions via MQTT Publish. # 217 | ######################################################################################################################## 218 | 219 | def ws_sanity_check(message): 220 | # Function to handle all messaging from Websocket Connection and do input validation 221 | # MEMO TO SELF - print("{}. {} appears {} times.".format(i, key, wordBank[key])) 222 | tijd = time.time() 223 | # Check if message is list or dict (Unipi sends most as list in dics, but modbus sensors as dict 224 | mesdata = json.loads(message) 225 | if type(mesdata) is dict: 226 | message_sort(mesdata) 227 | logging.debug('DICT message without converting (will be processed): {}'.format(message)) 228 | else: 229 | for message_dev in mesdata: # Check if there are updates over websocket and run functions to see if we need to update anything 230 | if type(message_dev) is dict: 231 | message_sort(message_dev) 232 | else: 233 | logging.debug('Ignoring received data, it is not a dict: {}'.format(device)) 234 | # Check if we need to switch off something. This is handled here since this function triggers every second (analoge input update freq.). 235 | # off_commands() 236 | # fire a function that checks things based on time (off_commands does that too, but to switch devices off) 237 | #timed_updates() #for now integrated in off_commands since it uses the same logic. 238 | 239 | def message_sort(message_dev): 240 | # Function to sort to different websocket messages for processing based on device type (dev) 241 | if message_dev['dev'] == "input": 242 | dev_di(message_dev) 243 | elif message_dev['dev'] == "ai": 244 | dev_ai(message_dev) 245 | elif message_dev['dev'] == "temp": #temp is being used for the modbus temp only sensors, multi sensors in modbus use dev: 1wdevice since latest evok version 246 | dev_modbus(message_dev) 247 | elif message_dev['dev'] == "1wdevice": # modules I tested so far as indicator (U1WTVS, U1WTD) that also report humidity and light intensity. 248 | dev_modbus(message_dev) 249 | elif message_dev['dev'] == "relay": # not sure what this does yet, not worked with it much. 250 | dev_relay(message_dev) 251 | elif message_dev['dev'] == "wd": #Watchdog notices, ignoring and only show in debug logging level (std off) 252 | logging.debug('{}: UNIPI WatchDog Notice: {}'.format(get_function_name(),message_dev)) 253 | elif message_dev['dev'] == "ao": 254 | logging.debug('{}: Received and AO message in web-socket input, most likely a result from a switch action that also triggers this. ignoring'.format(get_function_name(),message_dev)) 255 | else: 256 | logging.warning('{}: Message has no "dev" type of "input", "ai", "relay" or string "DS". Received input is : {} .'.format(get_function_name(),message_dev)) 257 | 258 | def dev_di(message_dev): 259 | # Function to handle Digital Inputs from WebSocket (UniPi) 260 | logging.debug('{}: SOF'.format(get_function_name())) 261 | tijd = time.time() 262 | in_list_cntr = 0 263 | for config_dev in devdes: 264 | if (config_dev['circuit'] == message_dev['circuit'] and config_dev['dev'] == 'input'): # To check if device switch is in config file and is an input 265 | raw_mode_presence = 'raw_mode' in config_dev # becomes True is "raw_mode" is found in config 266 | device_type_presence = 'device_type' in config_dev # becomes True is "device_type" is found in config 267 | handle_local_presence = 'handle_local' in config_dev # becomes True is "handle local" is found in config 268 | device_delay_presence = 'device_delay' in config_dev # becomes True is "device_delay" is found in config 269 | if device_delay_presence == True: 270 | if config_dev['device_delay'] == 0: device_delay_presence = False 271 | unipi_value_presence = 'unipi_value' in config_dev 272 | # If raw mode is selected a WEbdav message will only be transformed into a MQTT message, nothing else. 273 | if (raw_mode_presence == True): # RAW modes just pushes all fields in the websocket message out via MQTT TODO 274 | logging.error(' {}: TO BE IMPLEMENTED".'.format(get_function_name()))#todo. Can we just add input iD and raw to make this work? stop the loop here? 275 | device_type_presence = 'device_type' in config_dev # becomes True is "device_type" is found in config 276 | # Implemented device types per 2020 to filter counter for pulse based counter devices like water meters. Just count counter on NO devices that turn on. Since WebDav does not send a trigger for every update we calculate the delta betwee this and the previous update. 277 | elif (device_type_presence == True): 278 | if (config_dev['device_type'] == 'counter'): 279 | if (('max_delay_value' in config_dev) or ('device_delay' in config_dev)) is False: 280 | logging.error('{}: Error in Config file, missing fields'.format(get_function_name())) 281 | else: 282 | config_dev['counter_value'] = message_dev['counter'] 283 | if (config_dev["counter_value"] == 0): #for bootup of script to set initial value 284 | config_dev["counter_value"] = message_dev["counter"] 285 | if (config_dev["unipi_value"] == 0): 286 | config_dev["unipi_value"] = (message_dev["counter"] -1) 287 | else: logging.error('{}: Unknown device type "{}", breaking.'.format(get_function_name(),config_dev['device_type'])) 288 | elif (device_delay_presence == True): 289 | # Running devices with delay to reswitch (like pulse bsed motion sensors that pulse on presence ever 10 sec to on) Using no / nc and delay to switch 290 | # We should only see "ON" here! Off messages are handled in function off_commands 291 | logging.debug('{}: Loop with delay with message: {}'.format(get_function_name(),message_dev)) 292 | if tijd >= (config_dev['unipi_prev_value_timstamp'] + config_dev['device_delay']): 293 | if (message_dev['value'] == 1): 294 | if (config_dev['unipi_value'] == 1): 295 | logging.debug('{}: received status 1 is actual status: {}'.format(get_function_name(),message_dev)) #nothing to do, since there is not status change. First in condition to easy load ;-) 296 | elif(config_dev['device_normal'] == 'no'): 297 | dev_switch_on(config_dev['state_topic']) # check if device is normal status is OPEN or CLOSED loop to turn ON / OFF 298 | if handle_local_presence == True: handle_local_switch_on_or_toggle(message_dev,config_dev) 299 | config_dev['unipi_value'] = message_dev['value'] 300 | config_dev['unipi_prev_value_timstamp'] = tijd 301 | elif(config_dev['device_normal'] == 'nc'): #should never run! 302 | #should not do anything since and off commands are handled in off_commands def. 303 | logging.debug('{}: This should do nothing since off commands are not handled here. Config: {}, Received message: {}'.format(get_function_name(),message_dev,config_dev)) 304 | else: 305 | logging.error('{}: Unhandled Exception 1, config: {}, status: {}, normal_config: {}, {}, {}'.format(get_function_name(),config_dev['unipi_value'],message_dev['value'],config_dev['device_normal'],message_dev['circuit'],config_dev['state_topic'])) 306 | elif (message_dev['value'] == 0): 307 | if (config_dev['unipi_value'] == 0): 308 | logging.debug('{}: received status 0 is actual status: {}'.format(get_function_name(),message_dev)) #nothing to do, since there is not status change. First in condition to easy load ;-) 309 | elif(config_dev['device_normal'] == 'no'): #should never run! 310 | #should not do anything since and off commands are handled in off_commands def. 311 | logging.debug('{}: This should do nothing since off commands are not handled here. Config: {}, Received message: {}'.format(get_function_name(),message_dev,config_dev)) 312 | elif(config_dev['device_normal'] == 'nc'): 313 | dev_switch_on(config_dev['state_topic']) 314 | if handle_local_presence == True: handle_local_switch_on_or_toggle(message_dev,config_dev) 315 | config_dev['unipi_value'] = message_dev['value'] 316 | config_dev['unipi_prev_value_timstamp'] = tijd 317 | else: 318 | logging.error('{}: Unhandled Exception 2, config: {}, status: {}, normal_config: {}, {}, {}'.format(get_function_name(),config_dev['unipi_value'],message_dev['value'],config_dev['device_normal'],message_dev['circuit'],config_dev['state_topic'])) 319 | else: 320 | logging.error('{}: Device value not 0 or 1 as expected for Digital Input. Message is: {}'.format(get_function_name(),message_dev)) 321 | else: 322 | config_dev['unipi_prev_value_timstamp'] = tijd 323 | else: 324 | # Running devices without delay, always switching on / of based on UniPi Digital Input 325 | logging.debug('{}: Loop without delay with message: {}'.format(get_function_name(),message_dev)) 326 | if (message_dev['value'] == 1): 327 | if(config_dev['device_normal'] == 'no'): 328 | if (device_type_presence == True): 329 | if (config_dev['device_type'] == 'counter'): mqtt_set_counter(message_dev,config_dev) 330 | else: logging.error('{}: Unknown device type "{}", breaking.'.format(get_function_name(),config_dev['device_type'])) # check if device is normal status is OPEN or CLOSED loop to turn ON / OFF 331 | elif handle_local_presence == True: handle_local_switch_on_or_toggle(message_dev,config_dev) 332 | else: dev_switch_on(config_dev['state_topic']) # sends MQTT command, removed as test since this is done in handle_local_switch_toggle too 333 | elif(config_dev['device_normal'] == 'nc'): # Turn off devices that switch to their normal mode and have no delay configured! Delayed devices will be turned off somewhere else 334 | if handle_local_presence == True: pass # OLD: handle_local_switch_toggle(message_dev,config_dev) # we do a pass since a pulse based switch sends a ON and OFF in 1 action, we only need 1 action to happen! 335 | else: dev_switch_off(config_dev['state_topic']) # sends MQTT command, removed as test since this is done in handle_local_switch_toggle too 336 | else: 337 | logging.debug('{}: ERROR 1, config: {}, normal_config: {}, {}, {}'.format(get_function_name(),message_dev['value'],config_dev['device_normal'],message_dev['circuit'],config_dev['state_topic'])) 338 | elif (message_dev['value'] == 0): 339 | if(config_dev['device_normal'] == 'no'): 340 | if handle_local_presence == True: pass #- OLD:handle_local_switch_toggle(message_dev,config_dev) 341 | else: dev_switch_off(config_dev['state_topic']) # Turn off devices that switch to their normal mode and have no delay configured! Delayed devices will be turned off somewhere else 342 | elif(config_dev['device_normal'] == 'nc'): 343 | if (device_type_presence == True): 344 | if (config_dev['device_type'] == 'counter'): mqtt_set_counter(message_dev,config_dev) 345 | else: logging.error('{}: Unknown device type "{}", breaking.'.format(get_function_name(),config_dev['device_type'])) 346 | elif handle_local_presence == True: handle_local_switch_on_or_toggle(message_dev,config_dev) 347 | else: dev_switch_on(config_dev['state_topic']) 348 | else: 349 | logging.debug('{}: ERROR 2, config: {}, normal_config: {}, {}, {}'.format(get_function_name(),message_dev['value'],config_dev['device_normal'],message_dev['circuit'],config_dev['state_topic'])) 350 | else: 351 | logging.error('{}: Device value not 0 or 1 as expected for Digital Input. Message is: {}'.format(get_function_name(),message_dev)) 352 | 353 | def dev_ai(message_dev): 354 | # Function to handle Analoge Inputs from WebSocket (UniPi), mainly focussed on LUX from analoge input now. using a sample rate to reduce rest calls to domotics 355 | for config_dev in devdes: 356 | if config_dev['circuit'] == message_dev['circuit'] and config_dev['dev'] == "ai": 357 | int_presence = 'interval' in config_dev #check to see if "interval" in config. If not throw an error. If you want to disable average, set to 0. 358 | if (int_presence == True): 359 | cntr = intervals_counter[config_dev['dev']+config_dev['circuit']] 360 | if cntr <= config_dev['interval']: 361 | intervals_average[config_dev['dev']+config_dev['circuit']][cntr] = float(round(message_dev['value'],3)) 362 | intervals_counter[config_dev['dev']+config_dev['circuit']] += 1 363 | else: 364 | # write LUX to MQTT here. 365 | lux = int(round((statistics.mean(intervals_average[config_dev['dev']+config_dev['circuit']])*200),0)) 366 | mqtt_set_lux(config_dev['state_topic'],lux) 367 | config_dev['unipi_avg_cntr'] = 0 368 | logging.debug('PING Received WebSocket data and collected 30 samples of lux data : {}'.format(message_dev)) #we're loosing websocket connection, debug 369 | intervals_counter[config_dev['dev']+config_dev['circuit']] = 0 370 | else: 371 | logging.error('{}: CONFIG ERROR : 1-WIRE sensor "{}" is missing "interval" in config file. Set to 0 to disable or set sampling rate with a higher value.'.format(get_function_name(),message_dev)) 372 | 373 | 374 | def dev_relay(message_dev): 375 | pass #still need to figure out what to do with this. RELAYS ARE HANDLED AS OUTPUT. 376 | 377 | def dev_modbus(message_dev): 378 | # Function to handle Analoge Inputs from WebSocket (UniPi), mainly focussed on LUX from analoge input now. using a sample rate to reduce MQTT massages. TODO needs to be improved! 379 | for config_dev in devdes: 380 | try: 381 | if (config_dev['circuit'] == message_dev['circuit'] and (config_dev['dev'] == "temp" or config_dev['dev'] == "humidity" or config_dev['dev'] == "light")): 382 | int_presence = 'interval' in config_dev #check to see if "interval" in config. If not throw an error. If you want to disable average, set to 0. 383 | if int_presence == True: 384 | cntr = intervals_counter[config_dev['dev']+config_dev['circuit']] 385 | #config for 1-wire temperature sensors intervals_average[config_dev['dev']+config_dev['circuit']] 386 | if config_dev['dev'] == "temp": 387 | if cntr <= config_dev['interval']: 388 | if message_dev['typ'] == "DS18B20": 389 | if -55 <= float(message_dev['value']) <= 125: #sensor should be able to do -55 to +125 celcius 390 | intervals_average[config_dev['dev']+config_dev['circuit']][cntr] = float(message_dev['value']) 391 | intervals_counter[config_dev['dev']+config_dev['circuit']] += 1 392 | else: 393 | logging.error('{}: Message "{}" is out of range, temp smaller than -55 or larger than 125.'.format(get_function_name(),message_dev)) 394 | elif message_dev['typ'] == "DS2438": 395 | if -55 <= float(message_dev['temp']) <= 125: #sensor should be able to do -55 to +125 celcius 396 | intervals_average[config_dev['dev']+config_dev['circuit']][cntr] = float(message_dev['temp']) 397 | intervals_counter[config_dev['dev']+config_dev['circuit']] += 1 398 | else: 399 | logging.error('{}: Message "{}" is out of range, temp smaller than -55 or larger than 125.'.format(get_function_name(),message_dev)) 400 | else: 401 | logging.error('{}: Unknown Device sensor type {} in config'.format(get_function_name(),message_dev['typ'])) 402 | else: 403 | avg_temperature = statistics.mean(intervals_average[config_dev['dev']+config_dev['circuit']]) 404 | avg_temperature = round(avg_temperature,1) 405 | mqtt_set_temp(config_dev['state_topic'],avg_temperature) 406 | intervals_counter[config_dev['dev']+config_dev['circuit']] = 0 407 | #config for 1-wire humidity sensors 408 | elif config_dev['dev'] == "humidity": 409 | if cntr <= config_dev['interval']: 410 | if message_dev['typ'] == "DS2438": 411 | if 0 <= float(message_dev['humidity']) <= 100: 412 | intervals_average[config_dev['dev']+config_dev['circuit']][cntr] = float(round(message_dev['humidity'],1)) 413 | intervals_counter[config_dev['dev']+config_dev['circuit']] += 1 414 | else: 415 | logging.error('{}: Message "{}" is out of range, humidity smaller or larger than 100.'.format(get_function_name(),message_dev)) 416 | else: 417 | logging.error('{}: Unknown Device sensor type {} in config'.format(get_function_name(),message_dev['typ'])) 418 | else: 419 | avg_humidity = float(statistics.mean(intervals_average[config_dev['dev']+config_dev['circuit']])) 420 | avg_humidity = round(avg_humidity,1) 421 | mqtt_set_humi(config_dev['state_topic'],avg_humidity) 422 | intervals_counter[config_dev['dev']+config_dev['circuit']] = 0 423 | #config for 1-wire light / lux sensors 424 | elif config_dev['dev'] == "light": 425 | if cntr <= config_dev['interval']: 426 | if message_dev['typ'] == "DS2438": 427 | if 0 <= float(message_dev['vis']) <= 0.25: 428 | intervals_average[config_dev['dev']+config_dev['circuit']][cntr] = float(round(message_dev['vis'],1)) 429 | intervals_counter[config_dev['dev']+config_dev['circuit']] += 1 430 | else: 431 | logging.error('{}: Message "{}" is out of range, humidity smaller or larger than 100.'.format(get_function_name(),message_dev)) 432 | else: 433 | logging.error('{}: Unknown Device sensor type {} in config'.format(get_function_name(),message_dev['typ'])) 434 | else: 435 | avg_illumination = float(statistics.mean(intervals_average[config_dev['dev']+config_dev['circuit']])) 436 | if avg_illumination < 0: 437 | avg_illumination = 0 # sometimes I see negative values that would make no sense, make that a 0 438 | # try to match this with LUX from other sensors, 0 to 2000 LUX so need to calculate from 0 to 0.25 volt to match that. TODO is 2000 LUX = 0.25 or more? 439 | avg_illumination = avg_illumination*8000 440 | avg_illumination = round(avg_illumination,0) 441 | mqtt_set_lux(config_dev['state_topic'],avg_illumination) 442 | intervals_counter[config_dev['dev']+config_dev['circuit']] = 0 443 | else: 444 | logging.error('{}: CONFIG ERROR : 1-WIRE sensor "{}" is missing "interval" in config file. Set to 0 to disable or set sampling rate with a higher value.'.format(get_function_name(),message_dev)) 445 | except ValueError as e: 446 | logging.error('Message "{}" not a valid JSON - message not processed, error is "{}".'.format(message_dev,e)) 447 | 448 | ### Functions to switch outputs on the UniPi 449 | ### Used for incomming messages from MQTT and switches UniPi outputs conform the message received 450 | 451 | def set_repeat(dev,circuit,repeat,topic,message): 452 | logging.debug(' {}: SOF with message "{}".'.format(get_function_name(),message)) 453 | global dThreads 454 | thread_id = dev + circuit 455 | thread = dThreads[thread_id] 456 | ctr = 0 457 | while repeat > ctr and thread.is_running(): 458 | stat_code_on = (unipy.set_on(dev,circuit)) 459 | time.sleep(0.1) # time for output on 460 | stat_code_off = (unipy.set_off(dev,circuit)) 461 | if ctr == 0: #set MQTT responce on so icon turn ON while loop runs 462 | mqtt_ack(topic,message) 463 | ctr += 1 464 | time.sleep(0.25) #sleep between output, maybe put this in var one day. 465 | else: 466 | if thread.is_stopping(): 467 | logging.warning(' {}: Thread {} was given stop signal and stop before finish. Leaving the cleaning of thread information to "def StopThread". NOT sending final MQTT messages'.format(get_function_name(),thread_id)) 468 | unipy.set_off(dev,circuit) # extra off since we need to make sure my bel is off, or it will burn out. :-( 469 | else: 470 | if (int(stat_code_off) == 200 or int(stat_code_on) == 200): 471 | # Need to disable switch in HASS with message like {"circuit": "2_01", "dev": "relay", "state": "off"} where org message is {"circuit": "2_01", "dev": "relay", "repeat": "2", "state": "pulse"}. 472 | message.pop("repeat") #remove repeat from final mqtt ack with orderd dict action 473 | message.update({"state":"off"}) #replace state "pulse" with "off" with orderd dict action 474 | mqtt_ack(topic,message) 475 | logging.info(' {}: Successful ran function on dev {} circuit {} for {} times.'.format(get_function_name(),dev,circuit,repeat)) 476 | else: 477 | logging.error(' {}: Error setting device {} circuit {} on UniPi, got error "{}" back when posting via rest.'.format(get_function_name(),dev,circuit,stat_code_off)) 478 | logging.info(' {}: Successful finished thread {}, now deleting thread information from global thread var'.format(get_function_name(),thread_id)) 479 | del dThreads[thread_id] 480 | logging.debug(' {}: EOF.'.format(get_function_name())) 481 | 482 | # SET A DEVICE STATE, NOTE: json keys are put in order somewhere, and for the ack message to hassio to work it needs to be in the same order (for switches as template is not available, only on / off) 483 | def set_state(dev,circuit,state,topic,message): 484 | logging.debug(' {}: SOF with message "{}".'.format(get_function_name(),message)) 485 | if (dev == "analogoutput" and state == "on"): 486 | logging.error(' {}: We can not switch an analog output on since we don not maintain last value, not sure to witch value to set output. Send brightness along to fix this'.format(get_function_name())) 487 | elif (dev == "relay" or dev == "output" or (dev == "analogoutput" and state == "off")): 488 | if state == 'on': 489 | stat_code = (unipy.set_on(dev,circuit)) 490 | elif state == 'off': 491 | stat_code = (unipy.set_off(dev,circuit)) 492 | else: 493 | stat_code = '999' 494 | if int(stat_code) == 200: 495 | mqtt_ack(topic,message) 496 | logging.info(' {}: Successful ran function on device {} circuit {} to state {}.'.format(get_function_name(),dev,circuit,state)) 497 | else: 498 | logging.error(' {}: Error setting device {} circuit {} on UniPi, got error "{}" back when posting via rest.'.format(get_function_name(),dev,circuit,stat_code.status_code)) 499 | else: 500 | logging.error(' {}: Unhandled exception in function.'.format(get_function_name())) 501 | del dThreads[thread_id] 502 | logging.debug(' {}: EOF.'.format(get_function_name())) 503 | 504 | def set_duration(dev,circuit,state,duration,topic,message): #Set to switch on for a certain amount of time, I use this to open a rooftop window so for example 30 = 30 seconds 505 | logging.debug(' {}: SOF with message "{}".'.format(get_function_name(),message)) 506 | global dThreads 507 | thread_id = dev + circuit 508 | thread = dThreads[thread_id] 509 | counter = int(duration) 510 | if (dev == "analogoutput" and state == "on"): 511 | logging.error(' {}: We can not switch an analog output on since we don not maintain last value, not sure to witch value to set output. Send brightness along to fix this'.format(get_function_name())) 512 | elif (dev == "relay" or dev == "output" or (dev == "analogoutput" and state == "off")): 513 | logging.info(' {}: Setting {} device {} to state {} for {} seconds.'.format(get_function_name(),dev,circuit,state,time)) 514 | if state == 'on': 515 | rev_state = "off" 516 | stat_code = (unipy.set_on(dev,circuit)) 517 | elif state == 'off': 518 | rev_state = "on" 519 | stat_code = (unipy.set_off(dev,circuit)) 520 | if int(stat_code) == 200: # sending return message straight away otherwise the swithc will only turn on after delay time 521 | mqtt_ack(topic,message) 522 | logging.info(' {}: Set {} for circuit "{}".'.format(get_function_name(),state,circuit)) 523 | else: 524 | logging.error(' {}: error switching device {} on UniPi {}.'.format(get_function_name(),circuit,stat_code)) 525 | while counter > 0 and thread.is_running(): 526 | time.sleep(1) 527 | counter -= 1 528 | else: #handled when thread finishes by completion or external stop signal (StopThread function) #time.sleep(int(duration)) #old depriciated for stoppable thread 529 | if state == 'on': 530 | stat_code = (unipy.set_off(dev,circuit)) 531 | message.update({"state":"off"}) #need to change on to off in mqtt message 532 | elif state == 'off': 533 | stat_code = (unipy.set_on(dev,circuit)) 534 | message.update({"state":"on"}) #need to change on to off in mqtt message 535 | if int(stat_code) == 200: # sending return message straight away otherwise the swithc will only turn on after delay time 536 | mqtt_ack(topic,message) 537 | logging.info(' {}: Set {} for circuit "{}".'.format(get_function_name(),rev_state,circuit)) 538 | else: 539 | logging.error(' {}: error switching device {} to {} on UniPi {}.'.format(get_function_name(),circuit,rev_state,stat_code)) 540 | if thread.is_stopping(): 541 | logging.warning(' {}: Thread {} was given stop signal and stop before finish. Leaving the cleaning of thread information to "def StopThread". NOT sending final MQTT messages'.format(get_function_name(),thread_id)) 542 | else: 543 | logging.info(' {}: Successful Finished thread {}, now deleting thread information from global thread var'.format(get_function_name(),thread_id)) 544 | del dThreads[thread_id] 545 | logging.debug(' {}: EOF.'.format(get_function_name())) 546 | 547 | def set_brightness(desired_brightness,circuit,topic,message): 548 | logging.debug(' {}: Starting with message "{}".'.format(get_function_name(), message)) 549 | brightness_volt=round(int(desired_brightness)/25.5,2) 550 | stat_code = (unipy.set_level(circuit, brightness_volt)) 551 | if stat_code == 200: 552 | mqtt_ack(topic,message) 553 | logging.info(' {}: Set {} for circuit "{}".'.format(get_function_name(),state,circuit)) 554 | else: 555 | logging.error("Error switching on device on UniPi: %s ", stat_code.status_code) 556 | logging.debug(' {}: EOF.'.format(get_function_name())) 557 | 558 | def transition_brightness(desired_brightness,trans_time,dev,circuit,topic,message): 559 | logging.debug(' {}: Starting function with message "{}".'.format(get_function_name(), message)) 560 | global dThreads 561 | thread_id = dev + circuit 562 | thread = dThreads[thread_id] 563 | logging.info(' {}:thread information from global thread var {}'.format(get_function_name(),dThreads)) 564 | trans_step = round(float(trans_time)/100,3) # determine time per step for 100 steps. Fix for 100 so dimming is always the same speed, independent of from and to levels 565 | current_level = unipy.get_circuit(dev,circuit) # get current circuit level from unipi REST 566 | desired_level = round(float(desired_brightness) / 25.5,1) # calc desired level to 1/100 in stead of 256 steps for 0-10 volts 567 | print(current_level['value']) 568 | delta_level = (desired_level - current_level['value']) # determine delta based on from and to levels 569 | number_steps = abs(round(delta_level*10,0)) # determine number of steps based on from and to level 570 | new_level = current_level['value'] 571 | execution_error = 2 # start with debugging to based return message on 572 | id = circuit 573 | logging.debug(' {}: Running with Current Level: {} and Desired Level: {} resulting in a delta of {} and {} number of steps to get there'.format(get_function_name(),current_level['value'],desired_level,delta_level,number_steps)) 574 | if (number_steps != 0): 575 | if (delta_level != number_steps): 576 | # we need to set a start level via MQTT here as otherwise the device won't show as on when stating transition. Do not include in loop, too slow. 577 | step_increase = float(delta_level / number_steps) 578 | #logging.debug('TRANSITION DEBUG 2; number of steps: {} and tread.is_running: {}'.format(number_steps,thread_status)) 579 | short_lived_ws = create_connection("ws://" + ws_server + "/ws") #setting up a websocket connect here to send the change commands over. Cannot go to global WS since that is in a function and that won't accept commands from here. Maybe one day change to asyncio websocket? 580 | ### Using the stop_thread function to interrupt when needed. Thread.is_running makes sure we listen to external stop signals ### 581 | while int(number_steps) > 0 and thread.is_running(): 582 | new_level = round(new_level + step_increase,1) 583 | stat_code = 1 #(unipy.set_level(circuit, new_level)) 584 | short_lived_ws.send('{"cmd":"set","dev":"' + dev + '","circuit":"' + circuit + '","value":' + str(new_level) + '}') 585 | #Test, send mqtt message to switch device on on every change (maybe throttle in future/). If we don't HA will still thinks it's off while the loop turned it on. With long times this can mess up automations 586 | temp_level = math.ceil(new_level * 25.5) 587 | message.update({"brightness":temp_level}) #replace requested level with actual level in orderd dict action 588 | mqtt_ack(topic,message) 589 | number_steps -= 1 590 | if number_steps > 0: 591 | time.sleep(trans_step) 592 | elif number_steps == 0: 593 | logging.info(' {}: Done setting brightness via WebSocket.'.format(get_function_name())) 594 | #NEXT CODE IS TO CHECK IS COMMAND WAS SUCCESFULL 595 | time.sleep(1.5) # need a sleep here since getting actual value back is slow sometimes, it takes about a second to get the final value. 596 | actual_level = unipy.get_circuit(dev,circuit) 597 | logging.info(' {}: Got actual level of "{}" back from function unipy.get_circuit.'.format(get_function_name(),actual_level)) 598 | if (round(actual_level['value'],1) != desired_level): 599 | execution_error == 1 # TOT Need to changed this to 0 so i always send back actual status of lamp via MQTT (had issue that mqtt was not updating while lamp was on) 600 | logging.error(" {}: Return value \"{}\" not matching requested value \"{}\". Unipi might not be responding or in error. Retuning mqtt message with actual level, not requested".format(get_function_name(),round(actual_level['value'],1),desired_level)) 601 | temp_level = math.ceil(actual_level['value'] * 25.5) 602 | message.update({"brightness":temp_level}) #replace requested level with actual level in orderd dict action 603 | mqtt_ack(topic,message) 604 | else: 605 | execution_error == 0 606 | logging.info(' {}: Return value "{}" IS matching requested value "{}". Proceeding in compiling the MQTT message to ack that.'.format(get_function_name(),round(actual_level['value'],1),desired_level)) 607 | if execution_error != 1: 608 | # COMPILE THE MQTT ACK MESSAGE TO HASSIO 609 | mqtt_ack(topic,message) 610 | logging.info(' {}: Finished Set brightness for dev "{}" circuit "{}" to "{}" in "{}" seconds.'.format(get_function_name(),dev,circuit,desired_brightness,trans_time)) 611 | else: 612 | logging.error(' {}: Unhandled Condition'.format(get_function_name())) 613 | else: #handled when thread finishes by completion or external stop signal (StopThread function) 614 | if thread.is_stopping(): 615 | logging.info(' {}: Thread {} was given stop signal and stop before finish. Leaving the cleaning of thread information to "def StopThread". NOT sending final MQTT messages'.format(get_function_name(),thread_id)) 616 | else: 617 | logging.warning(' {}: Successful Finished thread {}, now deleting thread information from global thread var'.format(get_function_name(),thread_id)) 618 | del dThreads[thread_id] 619 | logging.debug(' {}: EOF.'.format(get_function_name())) 620 | short_lived_ws.close() # Closing the websocket connection for this function and interation. 621 | else: 622 | logging.error(' {}: delta_level != number_steps.'.format(get_function_name(),dev,circuit)) 623 | else: 624 | logging.info(' {}: Actual UniPi status for device {} circuit {} is matching desired state, not changing anything.'.format(get_function_name(),dev,circuit)) 625 | 626 | ### UniPi outputs Switch Commands 627 | ### Used to switch outputs on the UniPi device based on the websocket message received 628 | 629 | def off_commands(): 630 | # Function to handle delayed off for devices based on config file. use to switch motion sensors off (get a pulse update every 10 sec) 631 | tijd = time.time() 632 | for config_dev in devdes: 633 | device_type_presence = 'device_type' in config_dev 634 | handle_local_presence = 'handle_local' in config_dev 635 | if (device_type_presence == True): 636 | if (config_dev['device_type'] == 'counter'): #need this to set counter to 0 via MQTT otherwise only messages with a value are send 637 | if (tijd >= (config_dev['unipi_prev_value_timstamp'] + config_dev['device_delay'])): 638 | if ((config_dev['counter_value'] >= config_dev['unipi_value']) and (config_dev['counter_value'] > 0)): 639 | counter = config_dev["counter_value"] 640 | delta = config_dev["counter_value"] - config_dev["unipi_value"] #abuse of unipi value, but since we dont use this for counter devices... 641 | config_dev["unipi_value"] = config_dev["counter_value"] 642 | config_dev['unipi_prev_value_timstamp'] = tijd 643 | if counter != delta: 644 | mqtt_set_counter(config_dev["state_topic"],counter,delta) 645 | else: 646 | logging.warning('{}: counter ({}) has the same value as ({}), not sending MQTT as this is startup error that I need to fix.'.format(get_function_name(),counter,delta)) 647 | elif config_dev['counter_value'] == 0: 648 | pass #this happens on boot with 0 as value untill the first counter values come in. 649 | else: 650 | logging.error('{}: Negative value!.'.format(get_function_name())) 651 | logging.error('{}: - config: {}'.format(get_function_name(),config_dev)) 652 | else: logging.error('{}: Unknown device type "{}", breaking.'.format(get_function_name(),config_dev['device_type'])) 653 | elif 'device_delay' in config_dev: #Only switch devices off that have a delay > 0. Devices with no delay or delay '0' do not need to turned off or are turned off bij a new status (like door sensor) 654 | if config_dev['device_delay'] > 0 and tijd >= (config_dev['unipi_prev_value_timstamp'] + config_dev['device_delay']): 655 | #dev_switch_off(config_dev['state_topic']) #device uit zetten 656 | #if config_dev['unipi_value'] == 1 and config_dev['device_normal'] == 'no': 657 | if config_dev['unipi_value'] == 1 and config_dev['device_normal'] == 'no': 658 | dev_switch_off(config_dev['state_topic']) #device uit zetten 659 | if handle_local_presence == True: handle_local_switch_toggle(message_dev,config_dev) 660 | config_dev['unipi_value'] = 0 # Set var in config file to off 661 | logging.info('{}: Triggered delayed OFF after {} sec for "no" device "{}" for MQTT topic: "{}" .'.format(get_function_name(),config_dev['device_delay'],config_dev['description'],config_dev['state_topic'])) 662 | elif config_dev['unipi_value'] == 0 and config_dev['device_normal'] == 'nc': 663 | dev_switch_off(config_dev['state_topic']) #device uit zetten 664 | if handle_local_presence == True: handle_local_switch_toggle(message_dev,config_dev) 665 | config_dev['unipi_value'] = 1 # Set var in config file to on 666 | logging.info('{}: Triggered delayed OFF after {} sec for "nc" device "{}" for MQTT topic: "{}" .'.format(get_function_name(),config_dev['device_delay'],config_dev['description'],config_dev['state_topic'])) 667 | #else: 668 | # logging.debug('{}: unhandled exception in device switch off'.format(get_function_name())) 669 | logging.debug(' {}: EOF.'.format(get_function_name())) 670 | 671 | def dev_switch_on(mqtt_topic): 672 | # Set via MQTT 673 | mqttc.publish(mqtt_topic, payload='ON', qos=1, retain=True) 674 | logging.info('{}: Set ON for MQTT topic: "{}".'.format(get_function_name(),mqtt_topic)) 675 | 676 | def dev_switch_off(mqtt_topic): 677 | # Set via MQTT 678 | mqttc.publish(mqtt_topic, payload='OFF', qos=1, retain=True) 679 | logging.info('{}: Set OFF for MQTT topic: "{}".'.format(get_function_name(),mqtt_topic)) 680 | 681 | def mqtt_set_lux(mqtt_topic, lux): 682 | try: 683 | send_msg = { 684 | "lux": lux 685 | } 686 | mqttc.publish(mqtt_topic, payload=json.dumps(send_msg), qos=1, retain=False) 687 | logging.info('{}: Set LUX: {} for MQTT topic: "{}" .'.format(get_function_name(),lux,mqtt_topic)) 688 | except: 689 | logging.error('{}: An error has occurred sending "{}" C for MQTT topic: "{}" .'.format(get_function_name(),temp,mqtt_topic)) 690 | 691 | def mqtt_set_temp(mqtt_topic, temp): 692 | try: 693 | send_msg = { 694 | "temperature": temp 695 | } 696 | mqttc.publish(mqtt_topic, payload=json.dumps(send_msg), qos=1, retain=False) 697 | logging.info('{}: Set temperature: {} C for MQTT topic: "{}" .'.format(get_function_name(),temp,mqtt_topic)) 698 | except: 699 | logging.error('{}: An error has occurred sending "{}" C for MQTT topic: "{}" .'.format(get_function_name(),temp,mqtt_topic)) 700 | 701 | def mqtt_set_humi(mqtt_topic, humi): 702 | try: 703 | send_msg = { 704 | "humidity": humi 705 | } 706 | mqttc.publish(mqtt_topic, payload=json.dumps(send_msg), qos=1, retain=False) 707 | logging.info('{}: Set humidity: {} for MQTT topic: "{}" .'.format(get_function_name(),humi,mqtt_topic)) 708 | except: 709 | logging.error('{}: An error has occurred sending "{}" C for MQTT topic: "{}" .'.format(get_function_name(),temp,mqtt_topic)) 710 | 711 | def mqtt_set_counter(mqtt_topic,counter,delta): #published an MQTT message with a counter delta based on the interval defined or between de messages received. Messages from webdav might not trigger every pulse. 712 | logging.debug('Hit Functions {}'.format(get_function_name())) 713 | send_msg = { 714 | "counter_delta": delta, 715 | "counter":counter 716 | } 717 | mqttc.publish(mqtt_topic, payload=json.dumps(send_msg), qos=1, retain=False) 718 | logging.info('{}: Set counter {} and delta: {} for topic "{}" .'.format(get_function_name(),counter,delta,mqtt_topic)) 719 | 720 | def mqtt_topic_ack(mqtt_topic, mqtt_message): 721 | mqttc.publish(mqtt_topic, payload=mqtt_message, qos=1, retain=False) 722 | logging.info('{}: Send MQTT message: "{}" for MQTT topic: "{}" .'.format(get_function_name(),mqtt_message,mqtt_topic)) 723 | 724 | def mqtt_topic_set(mqtt_topic, mqtt_message): 725 | mqtt_topic = mqtt_topic+"/set" 726 | mqttc.publish(mqtt_topic, payload=mqtt_message, qos=1, retain=True) #changed retain to true as HASS does a retain true for most messages. Meaning actual state is not maintained to last resort. 727 | logging.info('{}: Send MQTT message: "{}" for MQTT topic: "{}" .'.format(get_function_name(),mqtt_message,mqtt_topic)) 728 | 729 | ### Handle Local Switch Commands 730 | ### Used to switch local outputs based on the websock input with some basic logic so some stuff still works when we do not have a working MQTT / Home Assistant 731 | 732 | def handle_local_switch_on_or_toggle(message_dev,config_dev): 733 | logging.debug('{}: Handle Local ON for message: {} and handle_local_config {}.'.format(get_function_name(),message_dev,config_dev["handle_local"])) 734 | if config_dev["handle_local"]["type"] == 'bel': 735 | unipy.ring_bel(config_dev["handle_local"]["rings"],"relay",config_dev["handle_local"]["output_circuit"]) 736 | logging.info('{}: Handle Local is ringing the bel {} times'.format(get_function_name(),config_dev["handle_local"]["rings"])) 737 | mqtt_message = 'ON' 738 | mqtt_topic_ack(config_dev["state_topic"], mqtt_message) #(we send a set too, to maks sure we stop threads in mqtt_client) 739 | mqtt_message = 'OFF' 740 | mqtt_topic_ack(config_dev["state_topic"], mqtt_message) #(we send a set too, to maks sure we stop threads in mqtt_client) 741 | else: 742 | handle_local_switch_toggle(message_dev,config_dev) 743 | 744 | def handle_local_switch_toggle(message_dev,config_dev): 745 | logging.debug('{}: Starting function with message "{}"'.format(get_function_name(),message_dev)) 746 | if config_dev["handle_local"]["type"] == 'dimmer': 747 | logging.debug('{}: Dimmer Toggle Running.'.format(get_function_name())) 748 | status,success=(unipy.toggle_dimmer("analogoutput",config_dev["handle_local"]["output_circuit"],config_dev["handle_local"]["level"])) 749 | # unipy.toggle_dimmer('analogoutput', '2_03', 7) 750 | if success == 200: # I know, mixing up status and succes here from the unipython class... some day ill fix it. 751 | if status == 0: 752 | mqtt_message = '{"state": "off", "circuit": "' + config_dev["handle_local"]["output_circuit"] + '", "dev": "analogoutput"}' 753 | mqtt_topic_set(config_dev["state_topic"], mqtt_message) #(we send a set too, to maks sure we stop threads in mqtt_client) 754 | logging.info('{}: Handle Local toggled analogoutput {} to OFF'.format(get_function_name(),config_dev["handle_local"]["output_circuit"])) 755 | elif status == 1: 756 | brightness = math.ceil(config_dev["handle_local"]["level"] * 25.5) 757 | mqtt_message = '{"state": "on", "circuit": "' + config_dev["handle_local"]["output_circuit"] + '", "dev": "analogoutput", "brightness": ' + str(brightness) + '}' 758 | mqtt_topic_set(config_dev["state_topic"], mqtt_message) #(we send a set too, to maks sure we stop threads in mqtt_client) 759 | logging.info('{}: Handle Local toggled analogoutput {} to ON'.format(get_function_name(),config_dev["handle_local"]["output_circuit"])) 760 | elif (status == 666 or status == 667): 761 | logging.error('{}: Received error from rest call with code "{}" on analogoutput {}.'.format(get_function_name(),status,config_dev["handle_local"]["output_circuit"])) 762 | else: 763 | logging.error('{}: "status" not 0,1,666 or 667 while running "dimmer loop"."'.format(get_function_name())) 764 | else: 765 | logging.error('{}: Tried to toggle analogoutput {} but failed with http return code "{}" .'.format(get_function_name(),config_dev["handle_local"]["output_circuit"],success)) 766 | elif config_dev["handle_local"]["type"] == 'switch': 767 | logging.debug('Switch Toggle Running function : "{}"'.format(get_function_name())) 768 | status,success=(unipy.toggle_switch("output",config_dev["handle_local"]["output_circuit"])) 769 | if success == 200: 770 | if status == 0: 771 | #mqtt_message = 'OFF' #used this for simple MQTT ack message, but looks like I don't use this, so changing to more advanced json MQTT message. This mist match payload_on / off messages! 772 | mqtt_message = '{"state": "off", "circuit": "' + config_dev["handle_local"]["output_circuit"] + '", "dev": "output"}' 773 | mqtt_topic_set(config_dev["state_topic"], mqtt_message) #(we send a set too, to maks sure we stop threads in mqtt_client) 774 | logging.info('{}: Handle Local toggled output {} to OFF'.format(get_function_name(),config_dev["handle_local"]["output_circuit"])) 775 | elif status == 1: 776 | #mqtt_message = 'ON' #used this for simple MQTT ack message, but looks like I don't use this, so changing to more advanced json MQTT message. This mist match payload_on / off messages at HA to work / show status there. 777 | mqtt_message = '{"state": "on", "circuit": "' + config_dev["handle_local"]["output_circuit"] + '", "dev": "output"}' 778 | mqtt_topic_set(config_dev["state_topic"], mqtt_message) #(we send a set too, to maks sure we stop threads in mqtt_client) 779 | logging.info('{}: Handle Local toggled output {} to ON'.format(get_function_name(),config_dev["handle_local"]["output_circuit"])) 780 | elif (status == 666 or status == 667): 781 | logging.error('{}: Received error from rest call with code "{}" on output {}.'.format(get_function_name(),status,config_dev["handle_local"]["output_circuit"])) 782 | else: 783 | logging.error('{}: "status" not found while running "switch loop"'.format(get_function_name())) 784 | else: 785 | logging.error("{}: Tried to toggle device {} but failed with http return code '{}' .".format(get_function_name(),config_dev["handle_local"]["output_circuit"],success)) 786 | else: 787 | logging.error('{}: Unhandled exception in function config type: {}'.format(get_function_name(),config_dev["handle_local"]["type"])) 788 | logging.debug('{}: EOF.'.format(get_function_name())) 789 | 790 | ### MQTT CONNECTION FUNCTIONS ### 791 | 792 | def mqtt_ack(topic,message): 793 | #Function to adjust MQTT message / topic to return to sender. 794 | logging.debug(' {}: Starting function on topic "{}" with message "{}".'.format(get_function_name(),topic,message)) 795 | if topic.endswith('/set'): 796 | topic = topic[:-4] 797 | logging.debug(' {}: Removed "set" from state topic, is now "{}" .'.format(get_function_name(),topic)) 798 | if topic.endswith('/brightness'): 799 | topic = topic[:-11] 800 | logging.debug(' {}: Removed "/brightness" from state topic, is now "{}" .'.format(get_function_name(),topic)) 801 | # Adjusting Message to be returned 802 | if 'mqtt_reply_message' in message: 803 | #this is currently unused, not a clue why i build it once... 804 | logging.debug(' {}:Found "mqtt_reply_message" key in message "{}", changing reply message.'.format(get_function_name(),message)) 805 | for key,value in message.items(): 806 | if key=='mqtt_reply_message': 807 | message = value 808 | logging.debug(' {}:Message set to: "{}".'.format(get_function_name(),message)) 809 | else: 810 | logging.debug(' {}:UNchanged return message, remains "{}" .'.format(get_function_name(),message)) 811 | #returnmessage = message 812 | return_message = json.dumps(message) # we need this due to the fact that some MQTT message need a retun value of ON or OFF instead of original message 813 | mqttc.publish(topic, return_message, qos=0, retain=True) # You need to confirm light status to leave it on in HASSIO 814 | logging.debug(' {}: Returned topic is "{}" and message is "{}".'.format(get_function_name(),return_message, topic)) 815 | logging.debug(' {}: EOF.'.format(get_function_name())) 816 | 817 | # The callback for when the client receives a CONNACK response from the server. 818 | def on_mqtt_connect(mqttc, userdata, flags, rc): 819 | logging.info('{}: MQTT Connected with result code {}.'.format(get_function_name(),str(rc))) 820 | mqttc.subscribe(mqtt_subscr_topic) # Subscribing in on_connect() means that if we lose the connection and reconnect then subscriptions will be renewed. 821 | mqtt_online() 822 | 823 | def mqtt_online(): #function to bring MQTT devices online to broker 824 | for dd in devdes: 825 | mqtt_topic_online = (dd['state_topic'] + "/available") 826 | mqttc.publish(mqtt_topic_online, payload='online', qos=2, retain=True) 827 | logging.info('{}: MQTT "online" command to topic "{}" send.'.format(get_function_name(),mqtt_topic_online)) 828 | 829 | def on_mqtt_subscribe(mqttc, userdata, mid, granted_qos): 830 | logging.info('{}: Subscribed with details: mqttc: {}, userdata: {}, mid: {}, granted_qos: {}.'.format(get_function_name(),mqttc,userdata,mid,granted_qos)) 831 | 832 | 833 | def on_mqtt_disconnect(mqttc, userdata, rc): 834 | logging.critical('{}: MQTT DISConnected from MQTT broker with reason: {}.'.format(get_function_name(),str(rc))) # Return Code (rc)- Indication of disconnect reason. 0 is normal all other values indicate abnormal disconnection 835 | if str(rc) == 0: 836 | mqttc.unsubscribe(mqtt_subscr_topic) 837 | mqtt_offline() 838 | 839 | def mqtt_offline(): #function to bring MQTT devices offline to broker 840 | for dd in devdes: 841 | #print("debug2") 842 | mqtt_topic_offline = (dd['state_topic'] + "/available") 843 | mqttc.publish(mqtt_topic_offline, payload='offline', qos=0, retain=True) 844 | logging.warning('{}: MQTT "offline" command to topic "{}" send.'.format(get_function_name(),mqtt_topic_offline)) 845 | mqttc.disconnect() 846 | 847 | def on_mqtt_unsubscribe(mqttc, userdata, mid, granted_qos): 848 | logging.info('{}: Unsubscribed with details: mqttc: {}, userdata: {}, mid: {}, granted_qos: {}.'.format(get_function_name(),mqttc,userdata,mid,granted_qos)) 849 | 850 | def on_mqtt_close(ws): 851 | logging.warning('{}: MQTT on_close function called.'.format(get_function_name())) 852 | 853 | def on_mqtt_log(client, userdata, level, buf): 854 | logging.debug('{}: {}'.format(get_function_name(),buf)) 855 | 856 | ### WEBSOCKET CONNECTION FUNCTIONS ### 857 | 858 | def create_ws(): 859 | while True: 860 | try: 861 | websocket.enableTrace(False) 862 | ws = websocket.WebSocketApp("ws://" + ws_server + "/ws",# header=ws_header, 863 | on_open = on_ws_open, 864 | on_message = on_ws_message, 865 | on_error = on_ws_error, 866 | on_close = on_ws_close) 867 | ws.run_forever(skip_utf8_validation=True,ping_interval=10,ping_timeout=8) # open websocket connection 868 | except Exception as e: 869 | gc.collect() 870 | logging.error("Websocket connection Error : {0}".format(e)) 871 | logging.error("Reconnecting websocket after 5 sec") 872 | time.sleep(5) #sleep to prevent setting up many connections / sec. 873 | 874 | def on_ws_open(ws): 875 | logging.error('{}: WebSockets connection is starting in a separate thread!'.format(get_function_name())) 876 | firstrun() 877 | #TODO, Build a first run function to set ACTUAL states of UniPi inputs as MQTT message and in config file! 878 | 879 | def on_ws_message(ws, message): 880 | ws_sanity_check(message) #This is starting the main message handling for UniPi originating messages 881 | #print(ws) 882 | #print(message) 883 | 884 | def on_ws_close(ws): 885 | logging.critical('{}: WEBSOCKETS CONNECTION CLOSED - THIS WILL PREVENT UNIPI INITIATED ACTIONS FROM RUNNING!'.format(get_function_name())) 886 | if t_ws.isAlive(): 887 | t_ws.join() 888 | logging.error('{}: Joined websocket thread into main thread to cleanup thread.'.format(get_function_name())) 889 | else: 890 | logging.error('{}: WebSockets thread was not foundrunning, in reconnect loop?'.format(get_function_name())) 891 | 892 | def on_ws_error(ws, errors): 893 | logging.error('{}: WebSocket Error; "{}"'.format(get_function_name(),errors)) 894 | 895 | ### First Run Function to set initial state of Inputs 896 | def firstrun(): 897 | for config_dev in devdes: 898 | message = unipy.get_circuit(config_dev['dev'],config_dev['circuit']) 899 | try: 900 | message = json.dumps(message) 901 | logging.info('{}: Set status for dev: {}, circuit: {} to message and values: {}'.format(get_function_name(),config_dev['dev'],config_dev['circuit'],message)) 902 | ws_sanity_check(message) 903 | except: 904 | logging.error('{}: Input error in first run, message received is ERROR {} on dev: {} and circuit: {}. Please ignore if dev humidity or light'.format(get_function_name(),message,config_dev['dev'],config_dev['circuit'])) 905 | #Note first run will also find dev = humidity, etc. but cannot match that to a get to unipi and the creates arror 500, however the humidity is already handled on topic "temp" as humidity is not a device class. Maybe oneday clean this up by changing dev types and something like sub_dev, but works like a charm this way too. 906 | # Pre-empt the dicts with values and an array to fill in the counter values and sensor values to calculate an average value for sensors. 907 | # We only do this for sensors where we find "interval" in the configuration file. Since we start with 0, 0=1, 1=2, etc. 908 | int_presence = 'interval' in config_dev 909 | if (int_presence == True): 910 | global intervals_average 911 | global intervals_counter 912 | intervals_average[(config_dev['dev']+config_dev['circuit'])] = [0.0] * (config_dev['interval'] + 1) 913 | intervals_counter[(config_dev['dev']+config_dev['circuit'])] = 0 914 | 915 | ### MAIN FUNCTION 916 | 917 | if __name__ == "__main__": 918 | ### setting some housekeeping functions and globel vars 919 | logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s',filename=logging_path,level=logging.ERROR,datefmt='%Y-%m-%d %H:%M:%S') #DEBUG,INFO,WARNING,ERROR,CRITICAL 920 | urllib3_log = logging.getLogger("urllib3") #ignoring informational logging from called modules (rest calls in this case) https://stackoverflow.com/questions/24344045/how-can-i-completely-remove-any-logging-from-requests-module-in-python 921 | urllib3_log.setLevel(logging.CRITICAL) 922 | unipy = unipython(ws_server, ws_user, ws_pass) 923 | 924 | ### Loading the JSON settingsfile 925 | dirname = os.path.dirname(__file__) #set relative path for loading files 926 | dev_des_file = os.path.join(dirname, 'unipi_mqtt_config.json') 927 | devdes = json.load(open(dev_des_file)) 928 | 929 | ### MQTT Connection. 930 | mqttc = mqtt.Client(mqtt_client_name) # If you want to use a specific client id, use this, otherwise a randon is autogenerated. 931 | mqttc.on_connect = on_mqtt_connect 932 | mqttc.on_log = on_mqtt_log # set client logging 933 | mqttc.on_disconnect = on_mqtt_disconnect 934 | mqttc.on_subscribe = on_mqtt_subscribe 935 | mqttc.on_unsubscribe = on_mqtt_unsubscribe 936 | mqttc.on_message = on_mqtt_message 937 | mqttc.username_pw_set(username=mqtt_user,password=mqtt_pass) 938 | mqttc.connect(mqtt_address, 1883, 600,) #define MQTT server settings 939 | t_mqtt = threading.Thread(target=mqttc.loop_forever) #define a thread to run MQTT connection 940 | t_mqtt.start() #Start connection to MQTT in thread so non-blocking 941 | 942 | ### WebSocket listener Connection. Must be in main to be referenced from other functions like ws.send, so we handle this differently since I moved this to a function. 943 | # start a function so we can reconnect on disconnect (like EVOK upgrade or network outage) every 5 seconds 944 | # starts in a seperate thread to not block anything 945 | t_websocket = threading.Thread(target=create_ws) #define a thread to run MQTT connection 946 | t_websocket.start() #Start connection to MQTT in thread so non-blocking 947 | 948 | ### Time function so we're not dependent of incomming commands to trigger things 949 | ### https://stackoverflow.com/questions/474528/what-is-the-best-way-to-repeatedly-execute-a-function-every-x-seconds 950 | threading.Thread(target=lambda: every(1, off_commands)).start() 951 | -------------------------------------------------------------------------------- /unipi_mqtt.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=UniPi to MQTT Message Service 3 | After=multi-user.target 4 | StartLimitInterval=200 5 | StartLimitBurst=8 6 | 7 | [Service] 8 | Type=simple 9 | ExecStart=/usr/bin/python3 /home/pi/scripts/unipi_mqtt.py 10 | Restart=always 11 | RestartSec=30 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /unipi_mqtt_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "circuit":"1_01", 4 | "description":"Hal PIR", 5 | "dev":"input", 6 | "device_delay":20, 7 | "device_normal":"no", 8 | "unipi_value":0, 9 | "unipi_prev_value_timstamp":0, 10 | "state_topic": "unipi/bgg/hal/motion" 11 | }, 12 | { 13 | "circuit":"2_02", 14 | "description":"Achterdeur Beldrukker", 15 | "dev":"input", 16 | "handle_local": 17 | { 18 | "type": "bel", 19 | "trigger":"on", 20 | "rings": 2, 21 | "output_dev": "output", 22 | "output_circuit": "2_01" 23 | }, 24 | "device_delay":0, 25 | "device_normal":"no", 26 | "state_topic": "unipi/bgg/achterdeur/beldrukker" 27 | }, 28 | { 29 | "circuit":"2_03", 30 | "description":"Achterdeur Contact", 31 | "dev":"input", 32 | "device_normal":"nc", 33 | "state_topic": "unipi/bgg/achterdeur/contact" 34 | }, 35 | { 36 | "circuit":"3_05", 37 | "description":"Schakelaar Achterdeur Licht", 38 | "dev":"input", 39 | "handle_local": 40 | { 41 | "type": "dimmer", 42 | "output_dev": "analogoutput", 43 | "output_circuit": "2_01", 44 | "level": 10 45 | }, 46 | "device_normal":"no", 47 | "state_topic": "unipi1/buiten/achterdeur/licht" 48 | }, 49 | { 50 | "circuit":"3_06", 51 | "description":"Schakelaar Achter Buitenlicht", 52 | "dev":"input", 53 | "device_normal":"no", 54 | "state_topic": "unipi/bgg/achter/buitenlichtschakelaar" 55 | }, 56 | { 57 | "circuit":"UART_4_4_02", 58 | "description":"Watermeter Pulse Counter", 59 | "dev":"input", 60 | "device_type":"counter", 61 | "unipi_value":0, 62 | "counter_value":0, 63 | "device_delay":10, 64 | "device_normal":"no", 65 | "unipi_prev_value_timstamp":0, 66 | "state_topic": "unipi/huis/watermeter" 67 | }, 68 | { 69 | "circuit":"UART_4_4_04", 70 | "description":"Schakelaar Woonkamer Eker Licht", 71 | "dev":"input", 72 | "handle_local": 73 | { 74 | "type": "dimmer", 75 | "output_dev": "analogoutput", 76 | "output_circuit": "3_01", 77 | "level": 5 78 | }, 79 | "device_normal":"no", 80 | "state_topic": "unipi1/bgg/woonkamer/erker/licht" 81 | }, 82 | { 83 | "circuit":"1_01", 84 | "description":"Hal LUX", 85 | "dev":"ai", 86 | "device_normal":"no", 87 | "interval":60, 88 | "state_topic": "unipi/bgg/hal/lux" 89 | }, 90 | 91 | { 92 | "circuit":"28D1EFA708000052", 93 | "description":"Temperatuur Sensor buiten", 94 | "dev":"temp", 95 | "interval":19, 96 | "state_topic":"unipi/buiten/voordeur/temperatuur" 97 | }, 98 | { 99 | "circuit":"26729616020000C2", 100 | "description":"Temperatuur Sensor bijkeuken", 101 | "dev":"temp", 102 | "interval":19, 103 | "state_topic":"unipi/bbg/bijkeuken/temperatuur" 104 | }, 105 | { 106 | "circuit":"26729616020000C2", 107 | "description":"Luchtvochtigheid Sensor bijkeuken", 108 | "dev":"humidity", 109 | "interval":19, 110 | "state_topic":"unipi/bbg/bijkeuken/luchtvochtigheid" 111 | } 112 | ] 113 | -------------------------------------------------------------------------------- /unipipython.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Title: unipipython.py 3 | # Author: Matthijs van den Berg 4 | # Date: 2018-2020 somewhere 5 | # Version 0.2 beta 6 | # Information from https://evok.api-docs.io/1.0/rest 7 | 8 | # VAN VOORBEELD SCRIPT BOVENSTAANDE SITE 9 | # payload = "{}" 10 | # conn.request("POST", "/rest/analogoutput/%7Bcircuit%7D?mode=Voltage", payload) 11 | 12 | 13 | import urllib.request 14 | import requests 15 | import json 16 | import datetime 17 | import time 18 | import datetime 19 | 20 | # Remove in production - just for test and dev in https URI's 21 | requests.packages.urllib3.disable_warnings() 22 | 23 | def ErrorHandling(e): 24 | errdatetime = ('Timestamp: {:%Y-%m-%d %H:%M:%S}'.format(datetime.datetime.now())) 25 | print("### Oops, some weird error occurred on that we still need to report properly ###") 26 | # MEMO TO SELF - print("{}. {} appears {} times.".format(i, key, wordBank[key])) 27 | print("### Timestamp: {} ###".format(errdatetime)) 28 | print("### Error dump:") 29 | print(e) 30 | print("### --------------END-------------- ###") 31 | 32 | class unipython(object): 33 | 34 | def __init__(self, host, username, password): 35 | self.base_url = 'http://%s:8080/rest/' % (host) 36 | #self.api = requests.Session() 37 | #self.api.auth = (username, password) 38 | #self.api.headers.update({'Content-Type': 'application/json; charset=utf-8'}) 39 | 40 | # Turn a device OFF 41 | def set_off(self, dev, circuit): 42 | url=(self.base_url + dev + "/" + circuit + "") 43 | payload = {"value" : 0} # voltage toe te passen 0-10 volt NOG LEVEL VAR MAKEN 44 | headers = {"source-system": "unipipython"} 45 | try: 46 | r = requests.post(url, data=payload, headers=headers) 47 | except Exception as e: 48 | return (e) 49 | else: 50 | return(r.status_code) 51 | 52 | # Turn a device ON 53 | # only works for DO / Relay devices? 54 | def set_on(self, dev, circuit): 55 | url=(self.base_url + dev + "/" + circuit + "/") 56 | payload = {"value" : 1} # voltage toe te passen NOG LEVEL VAR MAKEN 57 | headers = {"source-system": "unipipython"} 58 | try: 59 | r = requests.post(url, data=payload, headers=headers) 60 | except Exception as e: 61 | return (e) 62 | else: 63 | return(r.status_code) 64 | 65 | # Set device level (http://your-ip-goes-here:8080/rest/analogoutput/{circuit}?mode=Voltage) 66 | def set_level(self, circuit, level): 67 | url=(self.base_url + "analogoutput/" + circuit + "?mode=Voltage") 68 | payload = {"value" : level} 69 | headers = {"source-system": "unipipython"} 70 | try: 71 | r = requests.post(url, data=payload, headers=headers) 72 | except Exception as e: 73 | ErrorHandling(e) 74 | else: 75 | r = requests.post(url, data=payload, headers=headers) 76 | return(r.status_code) 77 | 78 | # Get device information from Unipi and return to calling function. Json format. 79 | def get_circuit(self, dev, circuit): 80 | url=(self.base_url + dev + "/" + circuit + "/") 81 | headers = {"source-system": "unipipython"} 82 | r = requests.get(url, headers=headers) 83 | if(r.status_code == 200): 84 | return(r.json()) 85 | else: 86 | return(r.status_code) 87 | 88 | # Toggle a device (when on, off, etc.) 89 | def toggle_switch(self, dev, circuit): #source_dev is switch, dev / circuit is to be switched 90 | #print('toggle_switch dev started') 91 | url=(self.base_url + dev + "/" + circuit + "/") 92 | #print(url) 93 | headers = {"source-system": "unipipython"} 94 | r = requests.get(url, headers=headers) 95 | #print(r) 96 | if(r.status_code == 200): 97 | #return(r.json()) 98 | status=r.json() 99 | if(status['value'] == 0): 100 | unipython.set_on(self, dev, circuit) 101 | return(1,r.status_code) 102 | elif(status['value'] == 1): 103 | unipython.set_off(self, dev, circuit) 104 | return(0,r.status_code) 105 | else: 106 | return(667,r.status_code)#print('Geen matchende status gevonden van 0 of 1') 107 | else: 108 | return(666,r.status_code) 109 | 110 | # Toggle a dimm device (when on, off, etc.) 111 | def toggle_dimmer(self, dev, circuit, level): #source_dev is switch, dev / circuit is to be switched 112 | url=(self.base_url + dev + "/" + circuit + "/") 113 | headers = {"source-system": "unipipython"} 114 | r = requests.get(url, headers=headers) 115 | if(r.status_code == 200): 116 | status=r.json() 117 | if(status['value'] == 0): 118 | unipython.set_level(self, circuit, level) 119 | return(1,r.status_code) 120 | elif(status['value'] > 0): 121 | unipython.set_level(self, circuit, 0) 122 | return(0,r.status_code) 123 | else: 124 | return(667,r.status_code)#print('Geen matchende status gevonden van 0 of 1') 125 | else: 126 | return(666,r.status_code) 127 | 128 | #Since the bel NEEDS to be turned on and off to ring, we script on and off here. 129 | def ring_bel(self, times, dev, circuit): 130 | ctr = 0 131 | while (ctr < times): 132 | url=(self.base_url + dev + "/" + circuit + "/") 133 | #print(url) 134 | payload = {"value" : 1} # voltage toe te passen 0-10 volt NOG LEVEL VAR MAKEN 135 | headers = {"source-system": "unipipython"} 136 | try: 137 | result = requests.post(url, data=payload, headers=headers) 138 | except Exception as e: 139 | ErrorHandling(e) 140 | else: 141 | response = requests.post(url, data=payload, headers=headers) 142 | time.sleep(0.1) 143 | payload = {"value" : 0} 144 | try: 145 | result = requests.post(url, data=payload, headers=headers) 146 | except Exception as e: 147 | ErrorHandling(e) 148 | else: 149 | response = requests.post(url, data=payload, headers=headers) 150 | time.sleep(0.3) 151 | ctr += 1 152 | #print(response) 153 | return (response) #assuming that last responce is representative for all s-: 154 | --------------------------------------------------------------------------------