File Coverage

blib/lib/Power/Outlet/MQTT.pm
Criterion Covered Total %
statement 42 128 32.8
branch 8 90 8.8
condition 0 32 0.0
subroutine 12 32 37.5
pod 18 18 100.0
total 80 300 26.6


line stmt bran cond sub pod time code
1             package Power::Outlet::MQTT;
2 1     1   1273 use strict;
  1         2  
  1         28  
3 1     1   5 use warnings;
  1         2  
  1         29  
4 1     1   527 use Net::MQTT::Simple 1.24 qw{}; #1.21 subscribe broken, 1.22 added login method
  1         33568  
  1         28  
5 1     1   507 use Net::MQTT::Simple::SSL qw{};
  1         49382  
  1         29  
6 1     1   548 use Net::MQTT::Simple::One_Shot_Loader;
  1         585  
  1         34  
7 1     1   7 use base qw{Power::Outlet::Common::IP};
  1         2  
  1         441  
8              
9             our $VERSION = '0.50';
10              
11             =head1 NAME
12              
13             Power::Outlet::MQTT - Control and query an outlet or relay via MQTT
14              
15             =head1 SYNOPSIS
16              
17             Tasmota defaults
18              
19             my $outlet = Power::Outlet::MQTT->new(
20             host => "mqtt",
21             name => "my_device",
22             relay => "POWER1",
23             );
24              
25             or topic defaults
26              
27             my $outlet = Power::Outlet::MQTT->new(
28             host => "mqtt",
29             publish_topic => "cmnd/my_device/POWER1",
30             subscribe_topic => "stat/my_device/POWER1",
31             );
32              
33             or explicit definitions with no defaults
34              
35             my $outlet = Power::Outlet::MQTT->new(
36             host => "mqtt",
37             publish_on => "cmnd/my_device/POWER1+ON", #plus sign delimited topic and message
38             publish_off => "cmnd/my_device/POWER1+OFF",
39             publish_switch => "cmnd/my_device/POWER1+TOGGLE",
40             publish_query => "cmnd/my_device/POWER1+",
41             subscribe_topic => "stat/my_device/POWER1",
42             subscribe_value_on => 'ON' #or qr/\A(?:ON|1)\Z/i,
43             subscribe_value_off => 'OFF, #or qr/\A(?:OFF|0)\Z/i,
44             );
45             print $outlet->query, "\n";
46             print $outlet->on, "\n";
47             print $outlet->off, "\n";
48              
49             =head1 DESCRIPTION
50              
51             Power::Outlet::MQTT is a package for controlling and querying an outlet or relay via MQTT
52              
53             Examples:
54              
55             $ mosquitto_pub -h mqtt -t "cmnd/my_device/POWER1" -m ON
56             $ mosquitto_pub -h mqtt -t "cmnd/my_device/POWER1" -m OFF
57             $ mosquitto_sub -h mqtt -t "stat/my_device/POWER1" -v
58              
59             =head1 USAGE
60              
61             use Power::Outlet::MQTT;
62             my $outlet = Power::Outlet::MQTT->new(host=>"mqtt", name=>"my_device");
63             print $outlet->on, "\n";
64              
65             =head1 CONSTRUCTOR
66              
67             =head2 new
68              
69             my $outlet = Power::Outlet->new(type=>"MQTT", host=>"mqtt");
70             my $outlet = Power::Outlet::MQTT->new(host=>"mqtt");
71              
72             =head1 PROPERTIES
73              
74             =head2 host
75              
76             Sets and returns the host name of the MQTT broker.
77              
78             Default: mqtt
79              
80             =cut
81              
82 0     0   0 sub _host_default {'mqtt'};
83              
84             =head2 port
85              
86             Sets and returns the port number of the MQTT broker.
87              
88             Default: 1883
89              
90             =cut
91              
92 1     1   3 sub _port_default {'1883'};
93              
94             =head2 secure
95              
96             Sets and returns a boolean property to use secure MQTT protocol or not.
97              
98             Default: if port=8883 then 1 else 0
99              
100             =cut
101              
102             sub secure {
103 1     1 1 2 my $self = shift;
104 1 50       3 $self->{'secure'} = shift if @_;
105 1 50       5 unless (defined $self->{'secure'}) {
106 1 50       4 $self->{'secure'} = $self->port eq '8883' ? 1 : 0;
107             }
108 1         4 return $self->{'secure'};
109             }
110              
111             =head2 device
112              
113             Sets and returns the device name of the MQTT topic.
114              
115             Note: Only used when topics are autogenerated for devices that support the Tasmota MQTT topic conventions.
116              
117             =cut
118              
119             sub device {
120 0     0 1 0 my $self = shift;
121 0 0       0 $self->{'device'} = shift if @_;
122 0 0       0 die("Error: either device or individual topics are required to be set") unless $self->{'device'};
123 0         0 return $self->{'device'};
124             }
125              
126             =head2 relay
127              
128             Sets and returns the relay of the device. Only used when name is used to define default publish and subscribe topics.
129              
130             Default: POWER1
131              
132             =cut
133              
134             sub relay {
135 0     0 1 0 my $self = shift;
136 0 0       0 $self->{'relay'} = shift if @_;
137 0 0       0 $self->{'relay'} = 'POWER1' unless $self->{'relay'};
138 0         0 return $self->{'relay'};
139             }
140              
141             =head2 publish_topic
142              
143             MQTT topic to publish to control the relay
144              
145             Default: "cmnd/$device/$relay"
146              
147             =cut
148              
149             sub publish_topic {
150 0     0 1 0 my $self = shift;
151 0 0       0 $self->{'publish_topic'} = shift if @_;
152 0 0 0     0 $self->{'publish_topic'} = join('/', 'cmnd', $self->device, $self->relay) if (!$self->{'publish_topic'} and defined $self->device);
153 0 0       0 die('Error: publish_topic required') unless $self->{'publish_topic'};
154 0         0 return $self->{'publish_topic'};
155             }
156              
157             =head2 publish_on
158              
159             MQTT topic and message payload to publish to turn the relay on (plus sign delimited)
160              
161             Default: "cmnd/$device/$relay+ON"
162              
163             =cut
164              
165             sub publish_on {
166 0     0 1 0 my $self = shift;
167 0 0       0 $self->{'publish_on'} = shift if @_;
168 0 0 0     0 $self->{'publish_on'} = join('+', $self->publish_topic, 'ON') if (!$self->{'publish_on'} and defined $self->publish_topic);
169 0 0       0 die('Error: publish_on required') unless $self->{'publish_on'};
170 0         0 return $self->{'publish_on'};
171             }
172              
173             =head2 publish_off
174              
175             MQTT topic and message payload to turn the relay off (plus sign delimited)
176              
177             Default: "cmnd/$device/$relay+OFF"
178              
179             =cut
180              
181             sub publish_off {
182 0     0 1 0 my $self = shift;
183 0 0       0 $self->{'publish_off'} = shift if @_;
184 0 0 0     0 $self->{'publish_off'} = join('+', $self->publish_topic, 'OFF') if (!$self->{'publish_off'} and defined $self->publish_topic);
185 0 0       0 die('Error: publish_off required') unless $self->{'publish_off'};
186 0         0 return $self->{'publish_off'};
187             }
188              
189             =head2 publish_switch
190              
191             MQTT topic and message payload to toggle the relay (plus sign delimited)
192              
193             Default: "cmnd/$device/$relay+TOGGLE"
194              
195             =cut
196              
197             sub publish_switch {
198 0     0 1 0 my $self = shift;
199 0 0       0 $self->{'publish_switch'} = shift if @_;
200 0 0 0     0 $self->{'publish_switch'} = join('+', $self->publish_topic, 'TOGGLE') if (!$self->{'publish_switch'} and defined $self->publish_topic);
201 0 0       0 die('Error: publish_switch required') unless $self->{'publish_switch'};
202 0         0 return $self->{'publish_switch'};
203             }
204              
205             =head2 publish_query
206              
207             MQTT topic and message payload to request the turn the current state of the relay (plus sign delimited)
208              
209             Default: "cmnd/$device/$relay+"
210              
211             =cut
212              
213             sub publish_query {
214 0     0 1 0 my $self = shift;
215 0 0       0 $self->{'publish_query'} = shift if @_;
216 0 0 0     0 $self->{'publish_query'} = join('+', $self->publish_topic, '') if (!$self->{'publish_query'} and defined $self->publish_topic);
217 0 0       0 die('Error: publish_query required') unless $self->{'publish_query'};
218 0         0 return $self->{'publish_query'};
219             }
220              
221             =head2 subscribe_topic
222              
223             MQTT topic which indicates the current state of the relay
224              
225             Default: "stat/$device/$relay+"
226              
227             =cut
228              
229             sub subscribe_topic {
230 0     0 1 0 my $self = shift;
231 0 0       0 $self->{'subscribe_topic'} = shift if @_;
232 0 0 0     0 $self->{'subscribe_topic'} = join('/', 'stat', $self->device, $self->relay) if (!$self->{'subscribe_topic'} and defined $self->device);
233 0 0       0 die('Error: subscribe_topic required') unless $self->{'subscribe_topic'};
234 0         0 return $self->{'subscribe_topic'};
235             }
236              
237             =head2 subscribe_value_on
238              
239             MQTT message payload to indicate the current state of the relay as on
240              
241             Default: "ON" or 1
242              
243             =cut
244              
245             sub subscribe_value_on {
246 0     0 1 0 my $self = shift;
247 0 0       0 $self->{'subscribe_value_on'} = shift if @_;
248 0 0       0 $self->{'subscribe_value_on'} = qr/\A(?:ON|1)\Z/i unless defined $self->{'subscribe_value_on'};
249 0         0 return $self->{'subscribe_value_on'};
250             }
251              
252             =head2 subscribe_value_off
253              
254             MQTT message payload to indicate the current state of the relay as off
255              
256             Default: "OFF" or 0
257              
258             =cut
259              
260             sub subscribe_value_off {
261 0     0 1 0 my $self = shift;
262 0 0       0 $self->{'subscribe_value_off'} = shift if @_;
263 0 0       0 $self->{'subscribe_value_off'} = qr/\A(?:OFF|0)\Z/i unless defined $self->{'subscribe_value_off'};
264 0         0 return $self->{'subscribe_value_off'};
265             }
266              
267             =head2 user
268              
269             Sets and returns the authentication user for the MQTT broker.
270              
271             Default: undef
272              
273             =cut
274              
275             sub user {
276 1     1 1 3 my $self = shift;
277 1 50       3 $self->{'user'} = shift if @_;
278 1 50       16 $self->{'user'} = $self->_user_default unless defined $self->{'user'};
279 1         4 return $self->{'user'};
280             }
281              
282 1     1   4 sub _user_default {undef};
283              
284             =head2 password
285              
286             Sets and returns the password used for authentication with the MQTT broker
287              
288             Default: ""
289              
290             =cut
291              
292             sub password {
293 0     0 1 0 my $self = shift;
294 0 0       0 $self->{'password'} = shift if @_;
295 0 0       0 $self->{'password'} = $self->_password_default unless defined $self->{'password'};
296 0         0 return $self->{'password'};
297             }
298              
299 0     0   0 sub _password_default {''};
300              
301              
302             =head1 METHODS
303              
304             =head2 name
305              
306             Sets and returns a user friendly name of this device relay.
307              
308             =head2 query
309              
310             Sends an HTTP message to the device to query the current state
311              
312             =cut
313              
314             sub query {
315 0     0 1 0 my $self = shift;
316 0         0 return $self->_mqtt_one_shot_value($self->subscribe_topic, $self->publish_query);
317             }
318              
319             =head2 on
320              
321             Sends a message to the device to Turn Power ON
322              
323             =cut
324              
325             sub on {
326 0     0 1 0 my $self = shift;
327 0 0       0 return defined(wantarray()) ? $self->_mqtt_one_shot_value($self->subscribe_topic, $self->publish_on)
328             : $self->_publish($self->publish_on);
329             }
330              
331             =head2 off
332              
333             Sends a message to the device to Turn Power OFF
334              
335             =cut
336              
337             sub off {
338 0     0 1 0 my $self = shift;
339 0 0       0 return defined(wantarray()) ? $self->_mqtt_one_shot_value($self->subscribe_topic, $self->publish_off)
340             : $self->_publish($self->publish_off);
341             }
342              
343             =head2 switch
344              
345             =cut
346              
347             sub switch {
348 0     0 1 0 my $self = shift;
349 0 0       0 return defined(wantarray()) ? $self->_mqtt_one_shot_value($self->subscribe_topic, $self->publish_switch)
350             : $self->_publish($self->publish_switch);
351             }
352              
353             =head2 cycle
354              
355             =cut
356              
357             #see Power::Outlet::Common->cycle
358              
359             sub _topic_message_split {
360 0     0   0 my $self = shift;
361 0         0 my $topic_message = shift;
362 0         0 my ($topic, $message);
363 0 0       0 if ($topic_message =~ m/\A([^+]+)\+(.*)\Z/) {
364 0         0 $topic = $1;
365 0         0 $message = $2;
366             } else {
367 0         0 $topic = $topic_message;
368 0         0 $message = undef;
369             }
370 0         0 return($topic, $message);
371             }
372              
373             sub _publish {
374 0     0   0 my $self = shift;
375 0         0 my $topic_message = shift;
376 0         0 my $epoch = $self->mqtt->publish($self->_topic_message_split($topic_message));
377 0         0 return $epoch;
378             }
379              
380             sub _mqtt_one_shot_value {
381 0     0   0 my $self = shift;
382 0         0 my $topic_subscribe = shift;
383 0         0 my $topic_message_publish = shift;
384 0         0 my ($topic_publish, $message_publish) = $self->_topic_message_split($topic_message_publish);
385 0   0     0 my $timeout = shift || 1.5;
386 0         0 my $return = $self->mqtt->one_shot($topic_subscribe, $topic_publish, $message_publish, $timeout);
387 0         0 my $error = $return->error;
388 0 0       0 warn(qq{MQTT Error: $error, topic: $topic_subscribe\n}) if $error;
389 0         0 my $message = $return->message;
390 0         0 my $on = $self->subscribe_value_on;
391 0         0 my $off = $self->subscribe_value_off;
392 0 0 0     0 return (!$error and ref($on) eq 'Regexp' and $message =~ m/$on/ ) ? 'ON'
    0 0        
    0 0        
    0 0        
393             : (!$error and ref($off) eq 'Regexp' and $message =~ m/$off/) ? 'OFF'
394             : (!$error and !ref($on) and $message eq $on ) ? 'ON'
395             : (!$error and !ref($off) and $message eq $off ) ? 'OFF'
396             : $message;
397             }
398              
399             =head1 ACCESSORS
400              
401             =head2 mqtt
402              
403             Returns a cached connected L or L object.
404              
405             =cut
406              
407             our $MQTT_CLASS = 'Net::MQTT::Simple';
408             our $MQTT_CLASS_SSL = 'Net::MQTT::Simple::SSL';
409              
410             sub mqtt {
411 1     1 1 6446 my $self = shift;
412 1 50       5 unless ($self->{'mqtt'}) {
413 1         5 my $host = join(':', $self->host, $self->port);
414 1 50       4 my $class = $self->secure ? $MQTT_CLASS_SSL : $MQTT_CLASS;
415 1         10 $self->{'mqtt'} = $class->new($host);
416 1 50       46 $self->{'mqtt'}->login($self->user, $self->password) if $self->user;
417             {
418             #Test connection with die instead of warn
419 1         3 local $SIG{__WARN__} = sub {
420 1     1   20833 my $msg = shift;
421 1         6 chomp $msg;
422 1         20 my (undef, undef, $text) = split /\s*:\s*/, $msg;
423 1         26 die(qq{MQTT Error: connection: $text\n});
424 1         9 };
425 1         7 $self->{'mqtt'}->_connect; #why private method?
426             }
427             }
428 0           return $self->{'mqtt'};
429             }
430              
431             =head1 BUGS
432              
433             Please log on GitHub
434              
435             =head1 AUTHOR
436              
437             Michael R. Davis
438              
439             =head1 COPYRIGHT
440              
441             Copyright (c) 2023 Michael R. Davis
442              
443             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
444              
445             The full text of the license can be found in the LICENSE file included with this module.
446              
447             =head1 SEE ALSO
448              
449             =cut
450              
451             1;