Developing a Python plugin
Overview
This page is aimed at developers wishing to improve Domoticz functionality via Python Plugins. If you simply want to use a plugin that a developer has already created for you, please see Using Python plugins. To see the Plugins already developed (for a reference) got to page Plugins.
The Domoticz Python Framework is not a general purpose Domoticz extension. It is designed to allow developers to easily create an interface between a piece of Hardware (or Virtual Hardware) and Domoticz. To that end it provides capabilities that seek to do the hard work for the developer leaving them to manage the messages that move backwards and forwards between the two.
The Framework provides the plugin with a full Python 3 instance that exists for as long as Domoticz is running. The plugin is called by Domoticz when relevant events occur so that it can manage connectivity to the hardware and state synchronisation.
Multiple copies of the same plugin can run for users that have more than one instance of a particular hardware type.
Plugin Framework types
There are current two variations of the Plugin Framework available, plugin authors can use either but not both at the same time:
- Legacy Framework: This has been stable for several years, invoked by importing 'Domoticz' into the plugin
- Extended Framework:
- Built on Legacy Framework but has a new object model and event localisation
- Invoked by importing 'DomoticzEx' into the plugin.
- Creates a two layer mapping over the DeviceStatus table of Devices and Units
- Plugins are not restricted to 256 Units (each Device can have 256 Units)
- Supports encapsulation and limited polymorphism
Sleeping and multi-threading details:
The following things should not be attempted using the Python Framework:
- Waiting or sleepingin Domoticz callbacks. Plugin callbacks are single threaded so the whole plugin system will wait.
The Python Framework has been uplifted to support multi-threaded plugins, key points:
- Use of asynchronous code or modules and callback functions are now supported. These should function as expected.
- All threads started within the plugin (either directly or by imported modules) must be terminated by the plugin prior to the plugin stopping ('onStop' is the recommended place for this). Failure to do this will result in Python aborting Domoticz during hardware 'Stops' &/or 'Updates'. The Plugin Framework cannot enumerate threads started by the plugin or stop them (Python limitation) and any active threads when the plugin interpreter is destroyed will cause Python to abort. YOU HAVE BEEN WARNED !
An example of a multi-threaded plugin including how to show running threads and thread shutdown can be found here on Github
Overall Structure
The plugin documentation is split into two distinct parts:
- Plugin Definition - Telling Domoticz about the plugin
- Runtime Structure - Interfaces and APIs to manage message flows between Domoticz and the hardware
Getting started
If you are writing a Python plugin from scratch, you may want to begin by using the Script Template in domoticz/plugins/examples/BaseTemplate.py, which is also available from the github source repo at https://github.com/domoticz/domoticz/blob/master/plugins/examples/BaseTemplate.py
Plugin Definition
To allow plugins to be added without the need for code changes to Domoticz itself requires that the plugins be exposed to Domoticz in a generic fashion. This is done by having the plugins live in a set location so they can be found and via an XML definition embedded in the Python script itself that describes the parameters that the plugin requires.
During Domoticz startup all directories directly under the 'plugins' directory are scanned for python files named plugin.py
and those that contain definitions are indexed by Domoticz. When the Hardware page is loaded the plugins defined in the index are merged into the list of available hardware and are indistinguishable from natively supported hardware. For the example below the Kodi plugin would be in a file domoticz/plugins/Kodi/plugin.py
. Some example Python scripts can be found in the domoticz/plugins/examples
directory.
Plugin definitions expose some basic details to Domoticz and a list of the parameters that users can configure. Each defined parameters will appear as an input on the Hardware page when the plugin is selected in the dropdown.
Definitions look like this:
"""
<plugin key="Kodi" name="Kodi Players" author="dnpwwo" version="1.0.0" wikilink="http://www.domoticz.com/wiki/plugins/Kodi.html" externallink="https://kodi.tv/">
<description>
<h2>Kodi Media Player Plugin</h2><br/>
<h3>Features</h3>
<ul style="list-style-type:square">
<li>Comes with three selectable icon sets: Default, Black and Round</li>
<li>Display Domoticz notifications on Kodi screen if a Notifier name is specified and events configured for that notifier</li>
<li>Multiple Shutdown action options</li>
<li>When network connectivity is lost the Domoticz UI will optionally show the device(s) with a Red banner</li>
</ul>
<h3>Devices</h3>
<ul style="list-style-type:square">
<li>Status - Basic status indicator, On/Off. Also has icon for Kodi Remote popup</li>
<li>Volume - Icon mutes/unmutes, slider shows/sets volume</li>
<li>Source - Selector switch for content source: Video, Music, TV Shows, Live TV, Photos, Weather</li>
<li>Playing - Icon Pauses/Resumes, slider shows/sets percentage through media</li>
</ul>
</description>
<params>
<param field="Address" label="IP Address" width="200px" required="true" default="127.0.0.1"/>
<param field="Port" label="Port" width="30px" required="true" default="9090"/>
<param field="Mode1" label="MAC Address" width="150px" required="false"/>
<param field="Mode2" label="Shutdown Command" width="100px">
<options>
<option label="Hibernate" value="Hibernate"/>
<option label="Suspend" value="Suspend"/>
<option label="Shutdown" value="Shutdown"/>
<option label="Ignore" value="Ignore" default="true" />
</options>
</param>
<param field="Mode3" label="Notifications" width="75px">
<options>
<option label="True" value="True"/>
<option label="False" value="False" default="true" />
</options>
</param>
<param field="Mode6" label="Debug" width="75px">
<description><h2>Debugging</h2>Select the desired level of debug messaging</description>
<options>
<option label="True" value="Debug"/>
<option label="False" value="Normal" default="true" />
</options>
</param>
</params>
</plugin>
"""
Definition format details:
Tag | Description/Attributes | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
<plugin> | Required.
| ||||||||||||||||
<description> | HTML description, contained within <plugin> and <param> tags.
This is displayed in the Hardware page when configuring a plugin. Embedded HTML tags are allowed and respected. | ||||||||||||||||
<params> | Simple wrapper for param tags. Contained within <plugin>. | ||||||||||||||||
<param> | Parameter definitions, Contained within <params>.
Parameters are used by the Hardware page during plugin addition and update operations. These are stored in the Hardware table in the database and are made available to the plugin at runtime.
| ||||||||||||||||
<options> | Simple wrapper for option tags. Contained within <param>.
Parameter definitions that contain this tag will be shown as drop down menus in the Hardware page. Available values are defined by the <option> tag. | ||||||||||||||||
<option> | Instance of a drop down option. Contained within <options>.
|
Runtime Structure
Domoticz exposes settings and device details through four Python dictionaries.
Settings
Contents of the Domoticz Settings page as found in the Preferences database table. These are always available and will be updated if the user changes any settings. The plugin is not restarted. They can be accessed by name for example: Settings["Language"]
Parameters
These are always available and remain static for the lifetime of the plugin. They can be accessed by name for example: Parameters["SerialPort"]
Description | |
---|---|
Key | Unique short name for the plugin, matches python filename. |
Name | Name assigned by the user to the hardware. |
HomeFolder | Folder or directory where the plugin was run from. |
Author | Plugin Author. |
Version | Plugin version. |
Address | IP Address, used during connection. |
Port | IP Port, used during connection. |
Username | Username. |
Password | Password. |
Mode1 | General Parameter 1 |
... | |
Mode6 | General Parameter 6 |
SerialPort | SerialPort, used when connecting to Serial Ports. |
DomoticzVersion | Domoticz version, for instance 4.11774 |
DomoticzHash | Domoticz hash, for instance a06400e05 |
DomoticzBuildTime | Domoticz build time, for instance 2020-03-03 15:41:00 |
Extended Plugin Framework
Available in Domoticz Stable 2022.1!
Devices
Description | |||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Members | DeviceID.
Devices are a container class designed to hold the Units that share a common DeviceID
| ||||||||||||
Methods | Per device calls into Domoticz to manipulate specific devices
| ||||||||||||
Callbacks | If defined, these callbacks will be sent to the Device object rather than to the module level function. Note that the function definitions are different because the API only passes the required parameters so no Device details are passed.
To define these callback plugin authors need to register a local object that extends the Domoticz default. See the Register function in the #C.2B.2B_Callable_API section for details
import DomoticzEx as Domoticz
...
class hvacDevice(Domoticz.Device):
def __init__(self, DeviceID):
super().__init__(DeviceID)
def onDeviceAdded(self, Unit):
Domoticz.Log("Device onDeviceAdded for Unit: "+str(self.Units[Unit]))
def onDeviceModified(self, Unit):
Domoticz.Log("Device onDeviceModified for Unit: "+str(self.Units[Unit]))
def onDeviceRemoved(self, Unit):
Domoticz.Log("Device onDeviceRemoved for Unit: "+str(self.Units[Unit]))
def onCommand(self, Unit, Command, Level, Hue):
Domoticz.Log("onCommand called for Device '" + DeviceID + "', Unit " + str(Unit) + ": Parameter '" + str(Command) + "', Level: " + str(Level))
Command = Command.strip()
action, sep, params = Command.partition(' ')
action = action.capitalize()
# Override the default Domoticz device object with custom one
Domoticz.Register(Device=hvacDevice)
def onDeviceModified(self, DeviceID, Unit):
# This will never be called because the Device specific version overrides it
Domoticz.Log("Device onDeviceModified")
|
Units
Description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Members | Unit.
Unit number for the device as specified in the Manifest.
Note: Units can be deleted in Domoticz so not all Units specified will necessarily still be present.
E.g:
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Methods | Per device calls into Domoticz to manipulate specific devices
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Callbacks |
|
Legacy Plugin Framework
Devices
Description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Key | Unit.
Unit number for the device as specified in the Manifest.
Note: Devices can be deleted in Domoticz so not all Units specified will necessarily still be present.
E.g:
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Methods | Per device calls into Domoticz to manipulate specific devices
|
Available Device Types
Filling is in progress, table doesn't contain full available list yet. Look at the Domoticz API/JSON page section Update Devices for more background information.
Type | Subtype | TypeName | Description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ID | Name | ID | Name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
17 | Lighting 2 | Behaves the same as Light/Switch, Preferable to use Type 244 instead | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
80 | Temp | 5 | LaCrosse TX3 | Temperature | Temperature sensor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
81 | Humidity | 1 | LaCrosse TX3 | Humidity | Humidity sensor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
82 | Temp+Hum | 1 | LaCrosse TX3 | Temp+Hum | Temperature + Humidity sensor | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
84 | Temp+Hum+Baro | 1 | THB1 - BTHR918, BTHGN129 | Temp+Hum+Baro | Temperature + Humidity + Barometer sensor Device.Update(nValue, sValue) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
2 | THB2 - BTHR918N, BTHR968 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
16 | Weather Station | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
85 | Rain | 1 | Rain | Rain sensor (sValue: "<RainLastHour_mm*100>;<Rain_mm>", Rain_mm is everincreasing counter) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
86 | Wind | 1 | Wind | Wind sensor (sValue: "<WindDirDegrees>;<WindDirText>;<WindAveMeterPerSecond*10>;<WindGustMeterPerSecond*10>;<Temp_c>;<WindChill_c>") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4 | Wind+Temp+Chill | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
87 | UV | 1 | UV | UV sensor (sValue: "<UV>;<Temp>") | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
89 | Current | 1 | Current/Ampere | Ampere (3 Phase) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
93 | Scale | 1 | Weight | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
113 | Counter | 0 | RFXMeter
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
241 | Color Switch | 1 | RGBW | WW | RGB + white, either RGB or white can be lit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
2 | RGB | RGB | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3 | White | White | Monochrome white | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4 | RGBWW | RGB_CW_WW | RGB + cold white + warm white, either RGB or white can be lit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
6 | RGBWZ | RGB_W_Z | Like RGBW, but allows combining RGB and white | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
7 | RGBWWZ | RGB_CW_WW_Z | Like RGBWW, but allows combining RGB and white | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
8 | Cold white + Warm white | CW_WW | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
242 | Thermostat (beta 2023.2: Setpoint) | 1 | Setpoint | Set Point | from Beta 2023/2 dd18-9-2023: name change to Setpoint and added Options.
Options={'ValueStep':'0.5', ' ValueMin':'-200', 'ValueMax':'200', 'ValueUnit':'°C'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
243 | General | 1 | Visibility | Visibility | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
2 | Solar Radiation | Solar Radiation | sValue: "float" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3 | Soil Moisture | Soil Moisture | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4 | Leaf Wetness | Leaf Wetness | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
6 | Percentage | Percentage | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
7 | Fan | Fan | Fan speed in rpm (from version 2024.3.16011) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
8 | Voltage | Voltage | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
9 | Pressure | Pressure | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
19 | Text | Text | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
22 | Alert | Alert | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
23 | Ampere (1 Phase) | Current (Single) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
24 | Sound Level | Sound Level | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
26 | Barometer | Barometer | nValue: 0, sValue: "pressure;forecast" Forecast: 0 - Stable 1 - Sunny 2 - Cloudy 3 - Unstable 4 - Thunderstorm 5 - Unknown 6 - Cloudy/Rain | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
27 | Distance | Distance | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
28 | Counter Incremental | Counter Incremental | Incremental counter used to measure energy (used or produced), gas, water. Does not compute the power or flow rate. nValue=0, sValue=INCREMENT_VALUE to increase counter by INCREMENT_VALUE, or sValue=NEGATIVE_VALUE to reset the counter.
A counter divider parameter can be specified: for example, for water flux meters, it's possible to set the divider to 1000 in case that the water flux sensor provides 1 pulse every liter, or 100000 if it sends 1 pulse every 0.01 liter. sValue parameter is float time, so it's possible for example to set divider to 1000 (1 liter) and update the device for a smaller quantity, for example sValue=0.002 to increase counter for 2 ml. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
29 | kWh | kWh | Electric (Instant+Counter) nValue should be zero sValue are two numbers separated by semicolon like "123;123456" The first number is the actual power in Watt, the second number is actual energy in kWh. When the option "EnergyMeterMode" is set to "Calculated", the second value is ignored When creating the device, set Switchtype=4 to get a device exporting energy (instead of importing) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
30 | Waterflow | Waterflow | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
31 | Custom Sensor | Custom | nValue: 0, sValue: "floatValue", Options: {'Custom': '1;<axisUnits>'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33 | Managed counter | nValue is always 0 sValue must be 2 semicolon separated values to update Dashboard, for instance "123456;78", 123456 being the absolute counter value, 78 being the usage (Wh). Set counter to -1 if you can't know the counter absolute value
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
244 | Light/Switch | 62 | Selector Switch | Selector Switch | Additional attribute Switchtype is applicable
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
73 | Switch | Check:
Additional attribute Switchtype | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
246 | Lux | 1 | Lux | Illumination | Illumination (sValue: "float") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
247 | Temp+Baro | 1 | LaCrosse TX3 | Temperature + Barometer sensor | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
248 | Usage | 1 | Electric | Usage | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
249 | Air Quality | 1 | Air Quality | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
250 | P1 Smart Meter | 1 | Energy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
251 | P1 Smart Meter | 2 | Gas | Gas |
Note on counters
Usually, counters are updated daily to feed week/month/year history log views. Starting version 4.11774, if you want to disable that behavior to control everything from an external script or a plugin (already the case for managed counter), you can set the "DisableLogAutoUpdate" device option to "true", for instance in a Python plugin:
Domoticz.Device(Name="MyCounter", Unit=1, Type=0xfa, Subtype=0x01, Options={"DisableLogAutoUpdate" : "true").Create()
Starting version 4.11774, you can too directly insert data in in history log. Set the "AddDBLogEntry" device option to "true", for instance in a Python plugin:
Domoticz.Device(Name="MyCounter", Unit=1, Type=0xfa, Subtype=0x01, Options={"AddDBLogEntry" : "true").Create()
Then, depending on counters, you can insert values in history log. For most counters:
Devices[Unit].Update(nValue=nValue, sValue="COUNTER;USAGE;DATE")
- COUNTER = absolute counter energy (Wh)
- USAGE = energy usage in Watt-hours (Wh)
- DATE = date with %Y-%m-%d format (for instance 2019-09-24) to put data in last week/month/year history log, or "%Y-%m-%d %H:%M:%S" format (for instance 2019-10-03 14:00:00) to put data in last days history log
For multi meters (P1 Smart Meter, CM113, Electrisave and CM180i):
Devices[Unit].Update(nValue=nValue, sValue="USAGE1;USAGE2;RETURN1;RETURN2;CONS;PROD;DATE")
- USAGE1= energy usage meter tariff 1, This is an incrementing counter
- USAGE2= energy usage meter tariff 2, This is an incrementing counter
- RETURN1= energy return meter tariff 1, This is an incrementing counter
- RETURN2= energy return meter tariff 2, This is an incrementing counter
- CONS= actual usage power (Watt)
- PROD= actual return power (Watt)
- DATE = date with %Y-%m-%d format (for instance 2019-09-24) to put data in last week/month/year history log, or "%Y-%m-%d %H:%M:%S" format (for instance 2019-10-03 14:00:00) to put data in last days history log
or
Devices[Unit].Update(nValue=nValue, sValue="USAGE1;USAGE2;RETURN1;RETURN2;CONS;PROD;COUNTER1;COUNTER2;COUNTER3;COUNTER4;DATE")
as previously, plus absolute counter values
for counters with custom units: Use RFXCom customer counter, e.g. Domoticz.Unit(Name=name, Unit=unit, Type=113, Switchtype=3, Options={"ValueQuantity": "Custom", "ValueUnits": "customunit"}).Create()
Connections
Connection objects allow plugin developers to connect to multiple external sources using a variety of transports and protocols simultaneously. By using the plugin framework to handle connectivity Domoticz will remain responsive no matter how many connections the plugin handles.
Connections remain active only while they are in scope in Python after that Domoticz will actively disconnect them so plugins should store Connections that they want to keep in global or class variables.
Function | Description/Attributes | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
__init__ | Defines the connection type that will be used by the object.
This allows Domoticz to make connections on behalf of the plugin. E.g: myConn = Domoticz.Connection(Name="JSON Connection", Transport="TCP/IP", Protocol="JSON", Address=Parameters["Address"], Port=Parameters["Port"])
myConn.Connect()
secureConn = Domoticz.Connection(Name="Secure Connection", Transport="TCP/IP", Protocol="HTTPS", Address=Parameters["Address"], Port="443")
secureConn .Connect()
mySerialConn = Domoticz.Connection(Name="Serial Connection", Transport="Serial", Protocol="XML", Address=Parameters["SerialPort"], Baud=115200)
mySerialConn.Connect()
Both positional and named parameters are supported. | ||||||||||||||
Name | Returns the Name of the Connection. | ||||||||||||||
Address | Return/Set the Address associated with the Connection. | ||||||||||||||
Port | Return/Set the Port associated with the Connection. | ||||||||||||||
Baud | Returns the Baud Rate of the Connection. | ||||||||||||||
Target | Get or Set the event target for the Connection. See Target parameter description on Connect and Listen for more detail. | ||||||||||||||
Parent | Normally 'None' but for incoming connections this will hold the Connection object that is 'Listening' for the connection. | ||||||||||||||
Connecting | Parameters: None.
Returns True if a connection has been requested but has yet to complete (or fail), otherwise False. | ||||||||||||||
Connected | Parameters: None.
Returns True if the connection is connected or listening, otherwise False. | ||||||||||||||
Connect | Initiate a connection to a external hardware using transport details.
Connect returns immediately and the results of the actual connection operation will be returned via the onConnect callback. If the address set via the Transport function translates to multiple endpoints they will all be tried in order until the connection succeeds or the list of endpoints is exhausted. | ||||||||||||||
Listen | Start listening on specifed Port using the specified TCP/IP, UDP/IP or ICMP/IP transport.
Connection objects will be created for each client that connects and onConnect will be called. self.httpServerConn = Domoticz.Connection(Name="WebServer", Transport="TCP/IP", Protocol="HTTP", Port=Parameters["Port"])
self.httpServerConn.Listen()
self.BeaconConn = Domoticz.Connection(Name="Beacon", Transport="UDP/IP", Address="239.255.255.250", Port="1900")
self.BeaconConn.Listen()
| ||||||||||||||
Send | Send the specified message to the external hardware.
Both positional and named parameters are supported. myConn.Send(Message=myMessage, Delay=4)
Headers = {"Connection": "keep-alive", "Accept": "Content-Type: text/html; charset=UTF-8"}
myConn.Send({"Verb":"GET", "URL":"/page.html", "Headers": Headers})
postData = "param1=value¶m2=other+value"
myHttpConn.Send({'Verb':'POST', 'URL':'/MediaRenderer/AVTransport/Control', 'Data': postData})
responseData = "<!doctype html><html><head></head><body><h1>Successful GET!!!</h1><body></html>"
self.httpClientConn.Send({"Status":"200 OK", "Headers": {"Connection": "keep-alive", "Accept": "Content-Type: text/html; charset=UTF-8"}, "Data": responseData })
udpBcastConn = Domoticz.Connection(Name="UDP Broadcast Connection", Transport="UDP/IP", Protocol="None", Address="224.0.0.1", Port="9777")
udpBcastConn.Send("Hello World!")
| ||||||||||||||
Disconnect | Parameters: None.
Terminate the connection to the external hardware for the connection. |
Images
Developers can ship custom images with plugins in the standard Domoticz format as described here: [1]. Resultant zip file(s) should be placed in the folder with the plugin itself
Description | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Key | Base.
The base value as specified in icons.txt file in custom image zip file.
| |||||||||||||||
Methods | Per image calls into Domoticz to manipulate specific images
|
Callbacks
Plugins are event driven. If a callback is not used by a plugin then it should not be present in the plugin.py file, Domoticz will not attempt to call callbacks that are not defined.
Domoticz will notify the plugin when certain events occur through a number of callbacks, these are:
Callback | Description |
---|---|
onStart | Parameters: None.
Called when the hardware is started, either after Domoticz start, hardware creation or update. |
onConnect | Parameters: Connection, Status, Description
Called when connection to remote device either succeeds or fails, or when a connection is made to a listening Address:Port. Connection is the Domoticz Connection object associated with the event. Zero Status indicates success.
If Status is not zero then the Description will describe the failure. |
onMessage | Parameters: Connection, Data.
Called when a single, complete message is received from the external hardware (as defined by the Protocol setting).
This callback should be used to interpret messages from the device and set the related Domoticz devices as required. |
onNotification | Parameters: Name, Subject, Text, Status, Priority, Sound, ImageFile.
Called when any Domoticz device generates a notification. Name parameter is the device that generated the notification, the other parameters contain the notification details. Hardware that can handle notifications should be notified as required. |
onCommand | Legacy Plugin Framework ('import Domoticz')
Extended Plugin Framework ('import DomoticzEx')
ColorMode {
ColorModeNone = 0, // Illegal
ColorModeWhite = 1, // White. Valid fields: none
ColorModeTemp = 2, // White with color temperature. Valid fields: t
ColorModeRGB = 3, // Color. Valid fields: r, g, b.
ColorModeCustom = 4, // Custom (color + white). Valid fields: r, g, b, cw, ww, depending on device capabilities
ColorModeLast = ColorModeCustom,
};
Color {
ColorMode m;
uint8_t t; // Range:0..255, Color temperature (warm / cold ratio, 0 is coldest, 255 is warmest)
uint8_t r; // Range:0..255, Red level
uint8_t g; // Range:0..255, Green level
uint8_t b; // Range:0..255, Blue level
uint8_t cw; // Range:0..255, Cold white level
uint8_t ww; // Range:0..255, Warm white level (also used as level for monochrome white)
}
|
onHeartbeat | Called every 'heartbeat' seconds (default 10) regardless of connection status.
Heartbeat interval can be modified by the Heartbeat command: Domoticz.Heartbeat(30)
Allows the Plugin to do periodic tasks including request reconnection if the connection has failed.
|
onTimeout | Parameters: Connection
Called in response to a connection or read timeout.
|
onDisconnect | Parameters: Connection
Called after the remote device is disconnected, Connection is the Domoticz Connection object associated with the event |
onStop | Called when the hardware is stopped or deleted from Domoticz. |
onDeviceAdded | Legacy Plugin Framework ('import Domoticz')
Extended Plugin Framework ('import DomoticzEx')
|
onDeviceModified | Legacy Plugin Framework ('import Domoticz')
Extended Plugin Framework ('import DomoticzEx')
|
onDeviceRemoved | Legacy Plugin Framework ('import Domoticz')
Extended Plugin Framework ('import DomoticzEx')
|
onSecurityEvent | Legacy Plugin Framework ('import Domoticz')
Extended Plugin Framework ('import DomoticzEx')
|
C++ Callable API
Importing the ‘Domoticz’ module in the Python code exposes functions that plugins can call to perform specific functions. All functions are non-blocking and return immediately.
Function | Description/Attributes | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Debug | Parameters: String
Write a message to Domoticz log only if verbose logging is turned on. | ||||||||||||||||||||
Log | Parameters: String.
Write a message to Domoticz log | ||||||||||||||||||||
Status | Parameters: String.
Write a status message to Domoticz log | ||||||||||||||||||||
Error | Parameters: String
Write an error message to Domoticz log | ||||||||||||||||||||
Debugging | Parameters: Integer.
Set logging level and type for debugging.
Mask values can be added together, for example to see debugging details around the plugin and its objects use: | ||||||||||||||||||||
Heartbeat | Parameters: Integer (Optional).
Set the heartbeat interval in seconds if parameter supplied (initial value 10 seconds). Values greater than 30 seconds will cause a message to be regularly logged about the plugin not responding. The plugin will actually function correctly with values greater than 30 though. Returns current Heartbeat value in seconds. | ||||||||||||||||||||
Notifier | Parameters: Name, String.
Informs the plugin framework that the plugin's external hardware can comsume Domoticz Notifications. | ||||||||||||||||||||
Trace | Parameters: N/A, Boolean. Default: False.
When True, Domoticz will log line numbers of the lines being executed by the plugin. Calling Trace again with False will suppress line level logging. Usage: def onHeartBeat():
Domoticz.Trace(True)
Domoticz.Log("onHeartBeat called")
...
Domoticz.Trace(False)
| ||||||||||||||||||||
Configuration | Parameters: Dictionary (Optional).
Returns a dictionary containing the plugin's configuration data that was previously stored. If a Dictionary paramater is supplied the database will be updated with the new configuration data. # Configuration Helpers
def getConfigItem(Key=None, Default={}):
Value = Default
try:
Config = Domoticz.Configuration()
if (Key != None):
Value = Config[Key] # only return requested key if there was one
else:
Value = Config # return the whole configuration if no key
except KeyError:
Value = Default
except Exception as inst:
Domoticz.Error("Domoticz.Configuration read failed: '"+str(inst)+"'")
return Value
def setConfigItem(Key=None, Value=None):
Config = {}
try:
Config = Domoticz.Configuration()
if (Key != None):
Config[Key] = Value
else:
Config = Value # set whole configuration if no key specified
Config = Domoticz.Configuration(Config)
except Exception as inst:
Domoticz.Error("Domoticz.Configuration operation failed: '"+str(inst)+"'")
return Config
| ||||||||||||||||||||
Register | Parameters: Device (Class Object), Optional Unit (Class Object)
Must be positioned outside of any class code so that the Plugin Framework can process it during the module import. Changes the object type that Domoticz uses to represent Devices (and Units where applicable) to a user defined type. The specified class must inherit (directly or indirectly) from the default class type and the underlying object initialisation should be invoked (via super().__init__) to ensure there are no unexpected behaviours.
import Domoticz
from Domoticz import Device
class myDevice(Domoticz.Device):
def __init__(self, Name, Unit, TypeName="", Type=0, Subtype=0, Switchtype=0, Image=0, Options="", Used=0, DeviceID="", Description=""):
super().__init__(Name=Name, Unit=Unit, TypeName=TypeName, Type=Type, Subtype=Subtype, Switchtype=Switchtype, Image=Image, Options=Options, Used=Used, DeviceID=DeviceID, Description=Description)
# This code will run prior to onStart for existing data
self.localVariable = "Hello"
def localFunction(self):
Domoticz.Log("Function called")
# Callbacks cannot be overridden
Domoticz.Register(Device=myDevice)
import DomoticzEx as Domoticz
from DomoticzEx import Device, Unit
class myUnit(Domoticz.Unit):
def __init__(self, Name, DeviceID, Unit, TypeName="", Type=0, Subtype=0, Switchtype=0, Image=0, Options="", Used=0, Description=""):
super().__init__(Name, DeviceID, Unit, TypeName, Type, Subtype, Switchtype, Image, Options, Used, Description)
# NOTE: This code will run prior to onStart for existing data
self.Refresh() # Only DeviceID and Unit will be populated this early in the initialisation process so force a refresh
if (self.SubType == 73) and (self.SwitchType == 0):
self.__class__ = myOtherUnit # Run time polymorphism Python style
self.myVar = 0
# Optionally override onDeviceAdded, onDeviceModified, onDeviceRemoved or onCommand callbacks
def onDeviceModified(self):
Domoticz.Log("Unit onDeviceModified")
def onCommand(self, Command, Level, Hue):
Domoticz.Log("myUnit onCommand")
class myOtherUnit(myUnit):
# onCommand will be called directly from Domoticz
def onCommand(self, Command, Level, Hue):
Domoticz.Log("myOtherUnit onCommand")
class myDevice(Domoticz.Device):
def __init__(self, DeviceID):
super().__init__(DeviceID)
# This code will run prior to onStart for existing data
self.localVariable = "Hello"
def localFunction(self):
Domoticz.Log("Function called")
# Optionally override onDeviceAdded, onDeviceModified, onDeviceRemoved or onCommand callbacks
def onDeviceModified(self, Unit):
Domoticz.Log("Device onDeviceModified")
Domoticz.Register(Device=myDevice, Unit=myUnit)
| ||||||||||||||||||||
Dump | Parameters: None.
Dumps current values to the Domoticz log, useful during debugging or exception handling. Dumped values:
import DomoticzEx as Domoticz
from DomoticzEx import Device, Unit
import sys
import datetime
import json
modeCoolCmd = 'N000001{"SYST": {"OSS": {"MD": "C" } } }'
modeEvapCmd = 'N000001{"SYST": {"OSS": {"MD": "E" } } }'
modeHeatCmd = 'N000001{"SYST": {"OSS": {"MD": "H" } } }'
# Heater, Cooling and Evaporate common functionality
class hvacBase(Domoticz.Connection):
def __init__(self, DeviceID, Name, Image, Address, Port):
super().__init__(Name=Name, Transport="TCP/IP", Protocol="None", Address=Address, Port=Port)
self.isActive = False
self.DeviceID = DeviceID
self.DeviceImage = Image
self.Device = None
self.reportedMode = DeviceID
def onConnect(self, Connection, Status, Description):
if (self != Connection):
Domoticz.Error("Connection '"+Connection.Name+"' is not the same as '"+self.Name+"'")
if (Status == 0):
Domoticz.Log(self.Name+" connected successfully to: "+Connection.Address+":"+Connection.Port)
for device in Devices:
Devices[device].TimedOut = 0
else:
Domoticz.Log(self.Name+" failed to connect ("+str(Status)+") to: "+Connection.Address+":"+Connection.Port)
for device in Devices:
Devices[device].TimedOut = 1
class Heating(hvacBase):
def __init__(self, DeviceID, Name, Image, Address, Port):
super().__init__(DeviceID, Name, Image, Address, Port)
def onMessage(self, Connection, Response):
if (len(Response) > 10): # filter out 'HELLO' message
self.Disconnect()
jsonPayload = json.loads(Response[7:])
Domoticz.Dump()
2021-07-06 15:18:34.296 Rinnai: (Rinnai) Context dump:
2021-07-06 15:18:34.296 Rinnai: (Rinnai) ----> 'Address' '192.168.999.999'
2021-07-06 15:18:34.297 Rinnai: (Rinnai) ----> 'Baud' '-1'
2021-07-06 15:18:34.297 Rinnai: (Rinnai) ----> 'Device' 'DeviceID: 'HGOM', Units: 1'
2021-07-06 15:18:34.298 Rinnai: (Rinnai) ----> 'DeviceID' 'HGOM'
2021-07-06 15:18:34.298 Rinnai: (Rinnai) ----> 'DeviceImage' '15'
2021-07-06 15:18:34.298 Rinnai: (Rinnai) ----> 'Name' 'Heating'
2021-07-06 15:18:34.299 Rinnai: (Rinnai) ----> 'Parent' 'None'
2021-07-06 15:18:34.299 Rinnai: (Rinnai) ----> 'Port' '27847'
2021-07-06 15:18:34.299 Rinnai: (Rinnai) ----> 'Target' 'Name: 'Heating', Transport: 'TCP/IP', Protocol: 'None', Address: '192.168.999.999', Port: '27847', Baud: -1, Timeout: 0, Bytes: 1071, Connected: True, Last Seen: 2021-07-06 15:18:34, Parent: 'None''
2021-07-06 15:18:34.300 Rinnai: (Rinnai) ----> 'isActive' 'True'
2021-07-06 15:18:34.300 Rinnai: (Rinnai) ----> 'reportedMode' 'HGOM'
2021-07-06 15:18:34.300 Rinnai: (Rinnai) Locals dump:
2021-07-06 15:18:34.300 Rinnai: (Rinnai) ----> 'self' 'Name: 'Heating', Transport: 'TCP/IP', Protocol: 'None', Address: '192.168.999.999', Port: '27847', Baud: -1, Timeout: 0, Bytes: 1071, Connected: True, Last Seen: 2021-07-06 15:18:34, Parent: 'None''
2021-07-06 15:18:34.301 Rinnai: (Rinnai) ----> 'Connection' 'Name: 'Heating', Transport: 'TCP/IP', Protocol: 'None', Address: '192.168.999.999', Port: '27847', Baud: -1, Timeout: 0, Bytes: 1071, Connected: True, Last Seen: 2021-07-06 15:18:34, Parent: 'None''
2021-07-06 15:18:34.301 Rinnai: (Rinnai) ----> 'Response' 'b'N000000[{"SYST": {"CFG": {"MTSP": ... "MT": "231" } } }]''
2021-07-06 15:18:34.302 Rinnai: (Rinnai) ----> 'jsonPayload' '[{'SYST': {'CFG': {'MTSP': ... 'MT': '231'}}}]'
2021-07-06 15:18:34.303 Rinnai: (Rinnai) Globals dump:
2021-07-06 15:18:34.304 Rinnai: (Rinnai) ----> 'Domoticz' '<module 'DomoticzEx' (built-in)>'
2021-07-06 15:18:34.304 Rinnai: (Rinnai) ----> 'sys' '<module 'sys' (built-in)>'
2021-07-06 15:18:34.304 Rinnai: (Rinnai) ----> 'datetime' '<module 'datetime' from 'C:\\Program Files (x86)\\Python39-32\\Lib\\datetime.py'>'
2021-07-06 15:18:34.304 Rinnai: (Rinnai) ----> 'json' '<module 'json' from 'C:\\Program Files (x86)\\Python39-32\\Lib\\json\\__init__.py'>'
2021-07-06 15:18:34.305 Rinnai: (Rinnai) ----> 'modeCoolCmd' 'N000001{"SYST": {"OSS": {"MD": "C" } } }'
2021-07-06 15:18:34.305 Rinnai: (Rinnai) ----> 'modeEvapCmd' 'N000001{"SYST": {"OSS": {"MD": "E" } } }'
2021-07-06 15:18:34.306 Rinnai: (Rinnai) ----> 'modeHeatCmd' 'N000001{"SYST": {"OSS": {"MD": "H" } } }'
2021-07-06 15:18:34.306 Rinnai: (Rinnai) ----> '_plugin' '<plugin.BasePlugin object at 0x008544E0>'
2021-07-06 15:18:34.306 Rinnai: (Rinnai) ----> 'Parameters' '{'HardwareID': 2, 'HomeFolder': 'plugins\\Domoticz-RinnaiTouch-Plugin\\', 'StartupFolder': '', 'UserDataFolder': '', 'WebRoot': '', 'Database': 'domoticz.db', 'Language': 'en', 'Version': '1.0.0', 'Author': 'dnpwwo', 'Name': 'Rinnai', 'Address': '', 'Port': '27847', 'SerialPort': '', 'Username': '', 'Password': '', 'Key': 'RinnaiTouch', 'Mode1': '', 'Mode2': '', 'Mode3': '', 'Mode4': '', 'Mode5': 'True', 'Mode6': '0', 'DomoticzVersion': '2021.1 (build 2107)', 'DomoticzHash': '2107', 'DomoticzBuildTime': '1970-01-01 11:35:07'}'
2021-07-06 15:18:34.307 Rinnai: (Rinnai) ----> 'Devices' '{'HGOM': <plugin.hvacDevice object at 0x008574E0>, 'SYST': <plugin.hvacDevice object at 0x00857570>}'
2021-07-06 15:18:34.307 Rinnai: (Rinnai) ----> 'Images' '{}'
2021-07-06 15:18:34.308 Rinnai: (Rinnai) ----> 'Settings' '{'DB_Version': '148', 'Title': 'Domoticz', 'LightHistoryDays': '30', ... 'MaxElectricPower': '6000'}'
|
Protocol Details
Protocol | Details | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP/HTTPS | HTTP and HTTPS are handled the same from a protocol perspective. HTTPS signals the underlying transport to use SSL/TLS, see Connections.
The HTTP protocol handles incoming and outgoing messages for both client connections requesting content from remote websites and server side listeners waiting incoming requests. Data is passed to the protocol (Connection.Send()) and passed back to the plugin (onMessage) using dictionaries. Key values in these directories are described below with an emphasis on the impact they have to what Domoticz transmits.
| ||||||||||||||||||||||||||||
WS/WSS | WebSockets (WS) and Secure WebSockets (WSS) are handled the same from a protocol perspective. WSS signals the underlying transport to use SSL/TLS.
WebSocket connections are initially made over HTTP and then 'upgraded' to be Web Sockets. This means that the message parameters (and matching response) use the protocol details for HTTP until the Server switches to the WebSocket protocol. Successful upgrades to the WebSocket protocol are signalled by an HTTP response with a "Status" of "101".
def onConnect(self, Connection, Status, Description):
if Status == 0:
Domoticz.Log("Connected successfully to: " + Connection.Address + ":" + Connection.Port)
# End point is now connected so request the upgrade to Web Sockets
send_data = {'URL': Parameters["Mode1"],
'Headers': {'Host': Parameters["Address"],
'Origin': 'https://' + Parameters["Address"],
'Sec-WebSocket-Key': base64.b64encode(secrets.token_bytes(16)).decode("utf-8")}}
Connection.Send(send_data)
|
UI
There is no built-in capabilities to create UI for plugin but you can use workaround with Domoticz Custom Page
Example of complex UI implementation you can find in this plugin
Setup custom page within plugin
- Create custom HTML file in your plugins' folder, let's assume it is index.html
- General idea is to install custom page when your plugin starts and remove it when plugin stops. To do so you need to modify your plugin's onStart and onStop methods. Base folder for Python will be Domoticz root (not your plugin root)
... from shutil import copy2 import os ... class Plugin: def onStart(self): ... copy2('./plugins/myplugin/index.html', './www/templates/myplugin.html') def onStop(self): ... if (os.path.exists('./www/templates/myplugin.html')): os.remove('./www/templates/myplugin.html')
Connect your page to Domoticz UI core
Domoticz uses AngularJS framework for its UI. Additionally it uses requirejs library for lazy loading. It is not required to follow the same patterns in your custom pages but following them will allow to access built-in classes and components. Every custom page would be loaded and rendered as template for AngularJS framework with access to $rootScope and other global variables.
Example of custom page with access to AngularJS and domoticzApi service:
<!-- Placeholder for page content -->
<div id="plugin-view"></div>
<!-- Template for custom component -->
<script type="text/ng-template" id="app/myplugin/sampleComponent.html">
<div class="container">
<a class="btnstylerev" back-button>{{ ::'Back' | translate }}</a>
<h2 class="page-header">My Plugin UI</h2>
<p>{{ $ctrl.message }}</p>
</div>
</script>
<script>
require(['app'], function(app) {
// Custom component definition
app.component('myPluginRoot', {
templateUrl: 'app/myplugin/sampleComponent.html',
controller: function($scope, domoticzApi) {
var $ctrl = this;
$ctrl.message = 'This is my plugin'
}
});
// This piece triggers Angular to re-render page with custom components support
angular.element(document).injector().invoke(function($compile) {
var $div = angular.element('<my-plugin-root />');
angular.element('#plugin-view').append($div);
var scope = angular.element($div).scope();
$compile($div)(scope);
});
});
</script>
Examples
There are a number of examples that are available that show potential usages and patterns that may be useful that can be found on Github:
Example | Description |
---|---|
Base Template | A good starting point for developing a plugin. |
Denon/Marantz | Support for Denon AVR amplifiers.
|
DLink DSP-W215 | TCP/IP connectivity using HTTP protocol.
|
HTTP | Shows how to do HTTP connections and handle redirects. |
HTTP Listener | Shows how to listen for incoming connections.
|
Kodi | Alternate Kodi plugin to the built in one.
|
RAVEn | Alternate RAVEn Energy Monitor plugin to the built in one.
|
UDP Discovery | Logs incoming UDP Broadcast messages for either Dynamic Device Discovery (DDD) or Simple Service Discovery Protocol (SSDP) protocols
|
Mutli-Threaded | Starts a Queue on a thread to write log messages and shuts down properly |
Web Socket Client | Establishes a Web Socket connection and shows how to send and recieve messages |
Troubleshooting
Importing Modules Fails
Python 'import' that are unsuccessful will stop the entrie plugin from importing and running unless they are wrapped within a 'try'/'except' pair. If a module is not found the Plugin Framework will provide as much information as it gets from Python. For example, an attempt to import fakemodule
in a plugin called 'Google Devices' will be reported like this in the Domoticz log:
2019-03-23 17:17:44.483 Error: (GoogleDevs) failed to load 'plugin.py', Python Path used was '/home/domoticz/plugins/Domoticz-Google-Plugin/:/usr/lib/python36.zip:/usr/lib/python3.6:/usr/lib/python3.6:/usr/lib/python3.6/lib-dynload:/usr/local/lib/python3.6/dist-packages:/usr/lib/python3/dist-packages:/usr/lib/python3.6/dist-packages'. 2019-03-23 17:17:44.483 Error: (Google Devices) Module Import failed, exception: 'ModuleNotFoundError' 2019-03-23 17:17:44.483 Error: (Google Devices) Module Import failed: ' Name: fakemodule' 2019-03-23 17:17:44.483 Error: (Google Devices) Error Line details not available.
the framework shows the directories it searches. Broadly these are: the plugin directory, the existing Python path as picked up when Domoticz started and the 'site' directories for system level modules installed with pip3. Local 'site' directories are not searched by defaul although plugin developers can add any directory the like to the path inside the plugin. Pip3 installs packages in system directories when sudo is used on linux platforms.
Plugin Crashes Domoticz
Yes, this is unfortunately possible. Python3 itself is vulnerable to segmentation faults and Domoticz just inherits this. If you don't believe me try typing python3 -c "import ctypes; ctypes.string_at(0)"
at a linux command line with Python installed.
To help troubleshoot these issues the Plugin Framework attempts to load the standard 'faulthandler' module for each plugin just before the plugin itself is loaded. If the plugin triggers any signals a thread by thread stack trace will be printed. To see this you will need to run Domoticz from the command line or redirect sys.stderr to a file (how would I do that?)
Output for a multi-threaded plugin will look something like this, a Python stack trace followed by a normal Domoticz one. The offending line in the plugin is shown in bold:
Fatal Python error: Segmentation fault Thread 0xa8d0eb40 (most recent call first): File "/usr/local/lib/python3.6/dist-packages/pychromecast/socket_client.py", line 361 in run_once File "/usr/local/lib/python3.6/dist-packages/pychromecast/socket_client.py", line 341 in run File "/usr/lib/python3.6/threading.py", line 916 in _bootstrap_inner File "/usr/lib/python3.6/threading.py", line 884 in _bootstrap Current thread 0xab553b40 (most recent call first): .... File "<frozen importlib._bootstrap>", line 219 in _call_with_frames_removed File "<frozen importlib._bootstrap_external>", line 678 in exec_module File "<frozen importlib._bootstrap>", line 665 in _load_unlocked File "<frozen importlib._bootstrap>", line 955 in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 971 in _find_and_load File "/home/domoticz/plugins/Domoticz-Google-Plugin/plugin.py", line 322 in handleMessage File "/usr/lib/python3.6/threading.py", line 864 in run File "/usr/lib/python3.6/threading.py", line 916 in _bootstrap_inner File "/usr/lib/python3.6/threading.py", line 884 in _bootstrap Thread 0xad597b40 (most recent call first): File "/usr/lib/python3.6/threading.py", line 299 in wait File "/usr/local/lib/python3.6/dist-packages/zeroconf.py", line 1794 in wait File "/usr/local/lib/python3.6/dist-packages/zeroconf.py", line 1365 in run File "/usr/lib/python3.6/threading.py", line 916 in _bootstrap_inner File "/usr/lib/python3.6/threading.py", line 884 in _bootstrap Thread 0xb57cfb40 (most recent call first): 2019-03-23 11:21:04.003 Error: Domoticz(pid:24259, tid:24500('PluginMgr')) received fatal signal 11 (Segmentation fault) 2019-03-23 11:21:04.003 Error: siginfo address=0x5ec3, address=0xb7f51d09 2019-03-23 11:21:04.741 Error: Thread 19 (Thread 0xab553b40 (LWP 24500)): 2019-03-23 11:21:04.741 Error: #0 0xb7f51d09 in __kernel_vsyscall () ...
Debugging
Debugging embedded Python can be done using the standard pdb functionality that comes with Python using the rpdb
(or 'Remote PDB') Python library.
The only restriction is that it can not be done from a debug instance of Domoticz running in Visual Studio on Windows.
Download the rpdb
library from https://pypi.python.org/pypi/rpdb/ and drop the 'rpdb' directory into the directory of the plugin to be debugged (or anywhere in the Python path). Alternatively, if pip3 is installed (sudo apt install python3-pip
if on Raspian or a Debian distro) then install the debugger using sudo pip3 install rpdb
Import the library using something like:
def onStart(self): if Parameters["Mode6"] == "Debug": Domoticz.Debugging(1) Domoticz.Log("Debugger started, use 'telnet 0.0.0.0 4444' to connect") import rpdb rpdb.set_trace() else: Domoticz.Log("onStart called")
The rpdb.set_trace()
command causes the Python Framework to be halted and a debugger session to be started on port 4444. For the code above the Domoticz log will show something like:
2017-04-17 15:39:25.448 (MyPlugin) Initialized version 1.0.0, author 'dnpwwo' 2017-04-17 15:39:25.448 (MyPlugin) Debug log level set to: 'true'. 2017-04-17 15:39:25.448 (MyPlugin) Debugger started, use 'telnet 0.0.0.0 4444' to connect pdb is running on 127.0.0.1:4444
Connect to the debugger using a command line tool such as Telnet. Attaching to the debugger looks like this if you start the session on the same machine as Domoticz, otherwise supply the Domoticz IP address instead of '0.0.0.0':
pi@bob:~$ telnet 0.0.0.0 4444 Trying 0.0.0.0... Connected to 0.0.0.0. Escape character is '^]'. --Return-- > /home/pi/domoticz/plugins/MyPlugin/plugin.py(30)onStart()->None -> rpdb.set_trace() (Pdb)
enter commands at the prompt such as:
(Pdb) l 25 def onStart(self): 26 if Parameters["Mode6"] == "Debug": 27 Domoticz.Debugging(1) 28 Domoticz.Log("Debugger started, use 'telnet 0.0.0.0 4444' to connect") 29 import rpdb 30 -> rpdb.set_trace() 31 else: 32 Domoticz.Log("onStart called") 33 34 def onStop(self): 35 Domoticz.Log("onStop called") (Pdb) pp Parameters {'Address': , 'Author': 'dnpwwo', 'HardwareID': 7, 'HomeFolder': '/home/pi/domoticz/plugins/MyPlugin/', 'Key': 'Debug', 'Mode1': , 'Mode2': , 'Mode3': , 'Mode4': , 'Mode5': , 'Mode6': 'Debug', 'Name': 'MyPlugin', 'Password': , 'Port': '4444', 'SerialPort': , 'Username': , 'Version': '1.0.0'} (Pdb) b 53 Breakpoint 1 at /home/pi/domoticz/plugins/MyPlugin/plugin.py:53 (Pdb) continue > /home/pi/domoticz/plugins/MyPlugin/plugin.py(53)onHeartbeat() -> Domoticz.Log("onHeartbeat called") (Pdb) ll 52 def onHeartbeat(self): 53 B-> Domoticz.Log("onHeartbeat called") (Pdb)
Details of debugging commands can be found here: https://docs.python.org/3/library/pdb.html#debugger-commands