Categories
Domoticz ESPEasy Home Automation software

Domoticz and ESPEasy – playing nice together

As soon as I completed the basics of my home automation system using Sonoff S20 switches reflashed with ESPEasy, it became clear that there were some shortcomings in the management the http based comms between Domoticz and the ESPEasy during a communications failure.

Here’s how I worked out and implemented an acceptable solution for the problem.

The Problem

The system works well for ‘normal’ operations. Domoticz switches the smart switches on/off from the dashboard and the activation through the local manual push button shows a changed state on the Domoticz dashboard (see this previous article).

The problem arises when the ESPEasy device is offline or one of the HTTP messages is missed. The Domoticz status shown for the the device is what the software thinks it should be rather than what it actually is (a ‘failed’ message).

An example case would be if the smart switch is powered off and is switched from the Domoticz dashboard. What I would expect to see is that the http message cannot be delivered (as the Wifi device is not available) and the icon/dashboard should show and error, or at least no change in status until it receives confirmation of the actual device status, which should never come, causing a timeout.

Instead, what actually happens an http comms error is logged error in the error log (message cannot be delivered) but the icon on the dashboard shows that the switch is activated. The software seems to be ignoring the failed message display and adopting an assumed state (ie, technically, open loop control rather than closed loop control).

Why this Happens

From what I can work out, the http communication from Domoticz to the ESPEasy device is initiated by the change of state within the configured virtual device. This means that, in the sequence of events, the device is already in the new state before sending the http message is sent (ie, setting the virtual device triggers the http message).

From there on there is no connection to the http and the switch and therefore no information in the failed http call to reveal who the originator of the call is. So, other than posting the error in the log, Domoticz does not know what action to take if a “cannot deliver” (or other) error occurs.

How to Solve?

There seems to be a couple of possible ways to implement a solution. In both cases this involves some form of scripting. Luckily this is well supported by Domoticz.

As a first pass solution, I also decided that when a device is detected as failed, it would simply be simply be switched off in Domoticz. In my situation this was the most likely real switch status. It would also be useful for the switch icon to show an error status, but I have not worked that one out yet!

Solution #1 – Switch in a Script

One possible solution is to send the switching command using a script that can check if the command makes it to the destination. This wiki article provides an example (in Perl) of a similar solution that replaces each http call in the Domoticz switch with a call to this script. My modified form is shown below. The rest of the setup (bash script, etc) follows the instructions in the wiki article.

This did not work as well as I expected. I also did not like that the configuration for each switch needed the IP address for the switch. In my system these were likely to change. The lock file technique in the script also did not seem to work when there were long network timeouts. Maybe it was just me …

#!/usr/bin/perl
use JSON::XS;
use LWP::UserAgent;
use URI::Escape;
no warnings 'uninitialized';
# Switches a Sonoff device on/off on behalf of Domoticz and then
# checks that it actually switched accordingly (in case of
# packet-loss)
# Autor: Marco Colli from original philchillbill script, 2018
# https://www.domoticz.com/wiki/Sonoff_-_failsafe_switching

($octet, $value) = @ARGV;
$tries = 0;
$domoticz = 'http://192.168.1.13:8080'; # full URL of DZ instance
$baseUrl = 'http://192.168.1.'; # most significant 3 octets of Sonoffs LAN IP addresses

sub postDomoticzLog
{
my $msg = shift;

$message = uri_escape($msg);
$msg_url = $domoticz.'/json.htm?type=command&param=addlogmessage&message='.$message;
$ua = LWP::UserAgent->new;
$ua->timeout(5);
$res = $ua->put($msg_url);
unless ($res->is_success) { warn "oops… ", $res->status_line };
}

$ua = LWP::UserAgent->new;
$ua->timeout(1);
$ua->env_proxy;
# first check we are not clashing with another process handling
# the same Sonoff device
while (-e "/tmp/.busy.$octet")
{
$secs++;
sleep 1;
$waited = 1;
if ($secs == 30)
{
unlink ("/tmp/.busy.$octet") or die $!;
&postDomoticzLog("Early exit of lockfile-wait loop with Sonoff '$octet'");
last;
}
}

if ($waited)
{
print "Waited for other process…\n";
sleep 5;
}

# Place our own lockfile to hold off other processes
open(TMP, '>', "/tmp/.busy.$octet") or die $!;
do
{
# First send the command to the Sonoff
$url = $baseUrl.$octet."/control?cmd=event,".$value; # e.g. http://192.168.178.27/control?cmd=event,on
$c = 0;
do
{
$c++;
$response = $ua->get($url);
} until (($response->is_success) || ($c == 5));

# Now check if it reacted, using the /json endpoint
$url = $baseUrl.$octet."/json?view=sensorupdate&tasknr=2";
$d = 0;
do
{
sleep 1;
$d++;
$response = $ua->get($url);
} until (($response->is_success) || ($d == 10));

if ($response->is_success)
{
$tasks = $response->decoded_content;
$data =decode_json $tasks;
# handle both espeasy R120 / Mega JSON formats
if (exists $$data{State})
{
$state = $$data{State}
}
else
{
$state = $$data{TaskValues}[0]{Value}
}
}
else
{
&postDomoticzLog("No response from Sonoff '$octet'");
exit 0;
}
print "Value: $value State: $state\n";
$ok = (($value eq 'off') && ($state == 0)) ||
(($value eq 'on') && ($state == 1));
$tries++;
} until (($ok) || ($tries == 5));

if ( -e "/tmp/.busy.$octet" )
{
unlink ("/tmp/.busy.$octet") or die $!;
}

if (!$ok)
{
&postDomoticzLog("Error setting Sonoff '$octet' to state '$value'");
}
print "cmd_send: $c, resp_recv: $d, tries: $tries\n";
exit 0;

Solution #2 – Heartbeat and Timeout

A second obvious solution is to implement a heartbeat from each ESPEasy device and then check for comms timeouts at the Domoticz end using a script.

This is the solution implemented on my system. It takes advantage of the Domoticz events subsystem and the built-in Lua-based dzVents scripting.

This solution also holds promise for my ultimate goal of showing error conditions onto the dashboard as dzVents has access to most of the status information inside Domoticz – see here for more details on dzVents.

To implement the heartbeat component, each of the ESPEasy devices was set to sent its status periodically. This is done by setting the ESPEasy relay status device configuration ‘Send to Controller’ interval timer to the desired value in seconds (in this case 20), shown below. Be aware that the heartbeat status is logged to the log file. This means that each device will write to the log at least every ‘x’ seconds, creating large log files. The large number of writes could also have shorten the life of the Raspberry Pi’s micro SD card.

In DOmoticz, the devices that need checking include a JSON formatted text configuration in their Domoticz description block. As not every device needs checking, a side benefit of this approach is that the script is able to select the devices to check by the presence of the configuration block.

The important field is the offline_timeout period. The other configuration fields are used in the displayed error message and can be omitted if the message is not used. The full text of the JSON configuration is shown below.

 {
"offline_timeout": 20,
"ID": "Switch-12",
"maker": "Sonoff",
"type": "S20"
}

The dzVents script implemented (below) iterates through all the devices and check if they timed out, taking action if it finds they have. This script is executed every minute, so the device timeout period should be adjusted to be compatible with this cadence.

-- This script will run periodically to check if devices have not
-- communicated with DZ. This script arose from a need to provide
-- the correct status for Sonoff devices used with the HTTP
-- interface and configured as virtual devices. DZ does not know
-- the actual status of the device as the virtual device is not
-- directly connected to the hardware and any comms errors are
-- ignored.
-- The Sonoff devices are configured to periodically send their
-- status. If a device has not communicated within 3*this period
-- (seconds) it is assumed to be offline and turned 'off' in the
-- virtual device.
-- Each device to be checked can be individually configured by
-- putting json coded settings into the device's description field.
-- The setting required for this script is
-- * "offline_timeout" : <time in seconds>
-- * "ID": <id for the device>
-- * "maker": <maker's brand>
-- * "type": <maker's model or type>
-- If "offline_timeout" is not set, the device timeout is not
-- checked by this script.
-- If "offline_timeout" is set and is a valid number, the
-- device will be turned off when it the device's lastUpdate is
-- greater than 3*<time in seconds> seconds old.
-- All the other fields are used in the displayed message and can be omitted if message is
-- changed to not include this information.
return
{
on =
{
timer = { 'every minute' }
},
execute = function(dz, trigItem)
  local _ = dz.utils._
  dz.devices().forEach
  (
function(device)
  -- dz.log('Checking ' .. device.name .. '.', dz.LOG_INFO)
  local description = device.description
if (_.isString(description) and description ~= '') then
  -- dz.log('description = "' .. description .. '".', dz.LOG_INFO)
  local settings = dz.utils.fromJSON(description)
if (_.isTable(settings) and _.isNumber(settings.offline_timeout)) then
if (dz.time.compare(device.lastUpdate).secs >= (settings.offline_timeout * 3)) then
dz.log(device.name .. ': ' .. settings.ID .. ' (' .. settings.maker .. ' ' .. settings.type .. ') is offline.', dz.LOG_INFO)
device.switchOff().checkFirst().silent()
end
else
-- This next line used for testing only as we don't
  -- want to be told every pass what devices have incorrect
  -- description formats
-- dz.log(device.name ..' description not in correct format.', dz.LOG_INFO)
  end
end
  end
  )
end
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s