From aea5880631991a45ddfd202600fc1e95a747e5fe Mon Sep 17 00:00:00 2001 From: DaveNeudoerffer Date: Tue, 23 Feb 2021 17:00:13 -0500 Subject: [PATCH] bug fixes, cleaned up topic handling --- lib/mqtt_items.pm | 171 +++++++++++++++++++++++++++++----------------- 1 file changed, 108 insertions(+), 63 deletions(-) diff --git a/lib/mqtt_items.pm b/lib/mqtt_items.pm index aeb1174a6..57f198eef 100644 --- a/lib/mqtt_items.pm +++ b/lib/mqtt_items.pm @@ -165,22 +165,24 @@ Usage: # MQTT_BROKER, name, subscribe topic, host/ip, port, user, pwd, keepalive MQTT_BROKER, mqtt_1, , localhost, 1883, , , 121 - # MQTT_INSTMQTT, name, groups, broker, type, topicprefix, discoverable Friendly Name - MQTT_INSTMQTT, bootroom_switch, Lights, mqtt_1, switch, insteoncottage/bootroom, 1, Bootroom Light + # Used to define mqtt items as published by insteon-mqtt project + # TopicPattern should be of the form "//+". + # MQTT_INSTMQTT, name, groups, broker, type, topicpattern, discoverable Friendly Name + MQTT_INSTMQTT, bootroom_switch, Lights, mqtt_1, switch, insteon/bootroom/+, 1, Bootroom Light - # Define a Tasmota item. Note that the topicprefix must be in the order that the device will - # send. This is configured in the Tasmota MQTT configuration. The prefix listed can - # be any of stat/tele/cmnd. - # MQTT_REMOTEITEM, name, groups, broker, type, topicprefix, discoverable Friendly Name - MQTT_REMOTEITEM, tas_outdoor_plug, , mqtt_1, switch, tasmota_outdoor_plug/stat, 0, Tasmota Outdoor Plug + # Define a Tasmota item. Note that the topicpattern must be in the order that the device will + # send. This is configured in the Tasmota MQTT configuration. + # MQTT_REMOTEITEM, name, groups, broker, type, topicpattern, discoverable Friendly Name + MQTT_REMOTEITEM, tas_outdoor_plug, , mqtt_1, switch, tasmota_outdoor_plug/+/+, 0, Tasmota Outdoor Plug # Say you have a local INSTEON item (could be any kind of misterhouse item) INSTEON_SWITCHLINC, 52.9E.DD, shed_light, Lights|Outside # - # then you can create an mqtt item to publish its state and receive mqtt commands - # MQTT_LOCALITEM, name, local item, broker, type, topicprefix, discoverable Friendly Name - MQTT_LOCALITEM, bootroom_switch, shed_light, mqtt_1, switch, insteoncottage/bootroom, 1, Bootroom Light + # Then you can create an mqtt item to publish its state and receive mqtt commands + # TopicPattern should be of the form "//+". + # MQTT_LOCALITEM, name, local item, broker, type, topicpattern, discoverable Friendly Name + MQTT_LOCALITEM, bootroom_switch, shed_light, mqtt_1, switch, insteon/bootroom/+, 1, Bootroom Light # @@ -488,13 +490,14 @@ sub decode_mqtt_payload { if( $topic eq $self->{disc_info}->{command_topic} ) { $value = $payload; } + $self->debug( 3, "payload '$payload' decoded to: '$value'" ); - if( $$self{mqtt_type} eq 'binary_sensor' || $$self{mqtt_type} eq 'sensor' ) { - if( $retained ) { - $self->debug( 1, "Retained message ignored for $$self{mqtt_type}:$$self{mqtt_name} device" ); - return; - } - } + # if( $$self{mqtt_type} eq 'binary_sensor' || $$self{mqtt_type} eq 'sensor' ) { + # if( $retained ) { + # $self->debug( 1, "Retained message ignored for $$self{mqtt_type}:$$self{mqtt_name} device" ); + # return; + # } + # } if( $$self{mqtt_type} eq 'light' ) { if( $self->{disc_info}->{schema} eq 'json' ) { @@ -868,7 +871,7 @@ use Data::Dumper; =cut sub new { ### mqtt_LocalItem - my ( $class, $interface, $name, $type, $local_object, $topicprefix, $discoverable, $friendly_name ) = @_; + my ( $class, $interface, $name, $type, $local_object, $topicpattern, $discoverable, $friendly_name ) = @_; my ($base_type, $device_class) = $type =~ m/^([^:]*):?(.*)$/; @@ -882,8 +885,20 @@ sub new { ### mqtt_LocalItem return; } - my ($realm, $mqtt_name) = $topicprefix =~ m|^([^/]+)/([^/]+)$|; - my $listen_topic = "$topicprefix/+"; + my (@topic_parts) = split( "/", $topicpattern ); + my $realm = $topic_parts[0]; + my $mqtt_name = $topic_parts[1]; + my $topic_prefix = "$realm/$mqtt_name"; + my $listen_topic; + if( $#topic_parts == 1 ) { + $listen_topic = "$topic_prefix/+"; + } else { + $listen_topic = $topicpattern; + } + if( !$mqtt_name ) { + $interface->error( "Invalid topic pattern '$topicpattern' on object '$name'" ); + return; + } my $self = new mqtt_BaseItem( $interface, $mqtt_name, $base_type, $listen_topic, $discoverable ); return @@ -892,7 +907,7 @@ sub new { ### mqtt_LocalItem bless $self, $class; $self->{realm} = $realm; - $self->debug( 1, "New mqtt_LocalItem( $interface->{instance}, '$mqtt_name', '$type', '$local_object', '$topicprefix', $discoverable, '$friendly_name' )" ); + $self->debug( 1, "New mqtt_LocalItem( $interface->{instance}, '$mqtt_name', '$type', '$local_object', '$topicpattern', $discoverable, '$friendly_name' )" ); $self->{disc_info} = {}; if( !$friendly_name ) { @@ -900,21 +915,25 @@ sub new { ### mqtt_LocalItem $friendly_name =~ s/_/ /g; } $self->{disc_info}->{name} = $friendly_name; - $self->{disc_info}->{state_topic} = "$topicprefix/state"; + $self->{disc_info}->{state_topic} = "$topic_prefix/state"; if( $base_type eq 'light' ) { - $self->{disc_info}->{command_topic} = "$topicprefix/level"; + $self->{disc_info}->{command_topic} = "$topic_prefix/level"; $self->{disc_info}->{schema} = 'json'; $self->{disc_info}->{brightness} = "true"; $self->{disc_info}->{brightness_scale} = 100; } elsif( $base_type eq 'switch' ) { - $self->{disc_info}->{command_topic} = "$topicprefix/set"; + $self->{disc_info}->{command_topic} = "$topic_prefix/set"; } elsif( $base_type eq 'scene' ) { - $self->{disc_info}->{command_topic} = "$topicprefix/set"; + $self->{disc_info}->{command_topic} = "$topic_prefix/set"; delete $self->{disc_info}->{state_topic}; } elsif( $base_type eq 'binary_sensor' ) { - $self->{disc_info}->{device_class} = $device_class; + if( $device_class ) { + $self->{disc_info}->{device_class} = $device_class; + } } elsif( $base_type eq 'sensor' ) { - $self->{disc_info}->{device_class} = $device_class; + if( $device_class ) { + $self->{disc_info}->{device_class} = $device_class; + } if( $device_class eq 'temperature' ) { $self->{disc_info}->{unit_of_measurement} = 'C'; } @@ -1287,43 +1306,55 @@ use Data::Dumper; @mqtt_RemoteItem::ISA = ( 'mqtt_BaseRemoteItem' ); -=item C +=item C Creates a MQTT RemoteItem/object that will mirror the state of the object, and send commands to it. =cut sub make_topic { - my ( $prefixfirst, $prefix, $topic, $command ) = @_; - if( $prefixfirst ) { - return "$prefix/$topic/$command"; - } else { - return "$topic/$prefix/$command"; + my ( $topicpattern, @parms ) = @_; + + my (@topic_parts) = split( "/", $topicpattern ); + my $wildcard_count = 0; + for( my $i=0; $i <= $#topic_parts; $i += 1 ) { + my $part = $topic_parts[$i]; + if( $part eq '+' ) { + $topic_parts[$i] = $parms[$wildcard_count]; + $wildcard_count += 1; + } } + return join( '/', @topic_parts ); } sub new { ### mqtt_RemoteItem - my ( $class, $interface, $type, $topicprefix, $discoverable, $friendly_name ) = @_; + my ( $class, $interface, $type, $topicpattern, $discoverable, $friendly_name ) = @_; my ($base_type, $device_class) = $type =~ m/^([^:]*):?(.*)$/; - if( !grep( /$base_type/, ('light','switch') ) ) { + if( !grep( /$base_type/, ('light','switch','sensor','binary_sensor') ) ) { $interface->error( "Invalid InstMqttItem type '$type'" ); return; } - my $prefixfirst = 1; - my ($prefix, $mqtt_name) = $topicprefix =~ m|^([^/]+)/([^/]+)$|; - $prefix = lc( $prefix ); - if( $prefix && !grep( /$prefix/, ('tele', 'stat', 'cmnd') ) ) { - ($mqtt_name, $prefix) = $topicprefix =~ m|^([^/]+)/([^/]+)$|; - $prefixfirst = 0; + my $mqtt_name; + my (@topic_parts) = split( "/", $topicpattern ); + my $wildcard_count = 0; + for( my $i=0; $i <= $#topic_parts; $i += 1 ) { + my $part = $topic_parts[$i]; + if( grep( /^$part$/, ('tele', 'stat', 'cmnd') ) ) { + $topic_parts[$i] = '+'; + } elsif( $part eq '+' ) { + $wildcard_count += 1; + } else { + $mqtt_name = $part; + } } if( !$mqtt_name ) { - $interface->error( "Unrecognized topic prefix '$topicprefix'" ); + $interface->error( "Unrecognized topic pattern '$topicpattern' for device '$friendly_name'" ); } - my $listen_topic = make_topic( $prefixfirst, '+', $mqtt_name, '+' ); + my $listen_topic = join( '/', @topic_parts ); my $self = new mqtt_BaseRemoteItem( $interface, $mqtt_name, $base_type, $listen_topic, $discoverable ); @@ -1332,7 +1363,7 @@ sub new { ### mqtt_RemoteItem bless $self, $class; - $self->debug( 1, "New mqtt_RemoteItem( $interface->{instance}, '$mqtt_name', '$type', '$topicprefix', $discoverable, '$friendly_name' )" ); + $self->debug( 1, "New mqtt_RemoteItem( $interface->{instance}, '$mqtt_name', '$type', '$topicpattern', $discoverable, '$friendly_name' )" ); $self->{discovered} = 0; @@ -1342,30 +1373,38 @@ sub new { ### mqtt_RemoteItem $friendly_name =~ s/_/ /g; } $self->{disc_info}->{name} = $friendly_name; - $self->{disc_info}->{availability_topic} = make_topic( $prefixfirst, 'tele', $mqtt_name, 'LWT' ); - $self->{disc_info}->{payload_available} = 'Online'; - $self->{disc_info}->{payload_not_available} = 'Offline'; + if( $wildcard_count == 2 ) { + $self->{disc_info}->{availability_topic} = make_topic( $listen_topic, 'tele', 'LWT' ); + $self->{disc_info}->{payload_available} = 'Online'; + $self->{disc_info}->{payload_not_available} = 'Offline'; + } if( $base_type eq 'switch' ) { - $self->{disc_info}->{command_topic} = make_topic( $prefixfirst, 'cmnd', $mqtt_name, 'POWER' ); - $self->{disc_info}->{state_topic} = make_topic( $prefixfirst, 'stat', $mqtt_name, 'POWER' ); + if( $wildcard_count != 2 ) { + $self->error( "Don't know how to create switch topics for '$friendly_name'" ); + } + $self->{disc_info}->{command_topic} = make_topic( $listen_topic, 'cmnd', 'POWER' ); + $self->{disc_info}->{state_topic} = make_topic( $listen_topic, 'stat', 'POWER' ); } elsif( $base_type eq 'light' ) { - $self->{disc_info}->{command_topic} = make_topic( $prefixfirst, 'cmnd', $mqtt_name, 'POWER' ); - $self->{disc_info}->{state_topic} = make_topic( $prefixfirst, 'tele', $mqtt_name, 'STATE' ); + if( $wildcard_count != 2 ) { + $self->error( "Don't know how to create light topics for '$friendly_name'" ); + } + $self->{disc_info}->{command_topic} = make_topic( $listen_topic, 'cmnd', 'POWER' ); + $self->{disc_info}->{state_topic} = make_topic( $listen_topic, 'tele', 'STATE' ); $self->{disc_info}->{state_value_template} = '{{value_json.POWER}}'; - $self->{disc_info}->{brightness_state_topic} = make_topic( $prefixfirst, 'tele', $mqtt_name, 'STATE' ); + $self->{disc_info}->{brightness_state_topic} = make_topic( $listen_topic, 'tele', 'STATE' ); $self->{disc_info}->{brightness_value_template} = '{{value_json.Dimmer}}'; $self->{disc_info}->{brightness_scale} = 100; - $self->{disc_info}->{brightness_command_topic} = make_topic( $prefixfirst, 'cmnd', $mqtt_name, 'Dimmer' ); + $self->{disc_info}->{brightness_command_topic} = make_topic( $listen_topic, 'cmnd', 'Dimmer' ); $self->{disc_info}->{on_command_type} = 'brightness'; } elsif( $base_type eq 'binary_sensor' ) { # Motion sensor config as defined here: https://blakadder.com/pir-in-tasmota/ - $self->{disc_info}->{state_topic} = make_topic( $prefixfirst, 'tele', $mqtt_name, 'MOTION' ); + $self->{disc_info}->{state_topic} = make_topic( $listen_topic, 'tele', 'MOTION' ); $self->{disc_info}->{payload_on} = 1; $self->{disc_info}->{device_class} = $device_class; $self->{disc_info}->{force_update} = 'true'; $self->{disc_info}->{off_delay} = 30; } elsif( $base_type eq 'sensor' ) { - $self->{disc_info}->{state_topic} = make_topic( $prefixfirst, 'tele', $mqtt_name, 'STATE' ); + $self->{disc_info}->{state_topic} = make_topic( $listen_topic, 'tele', 'STATE' ); $self->{disc_info}->{device_class} = $device_class; $self->{disc_info}->{force_update} = 'true'; } else { @@ -1384,7 +1423,7 @@ sub new { ### mqtt_RemoteItem return $self; } -# -[ Fini - mqtt_TasmotaItem ]--------------------------------------------------------- +# -[ Fini - mqtt_RemoteItem ]--------------------------------------------------------- # ------------------------------------------------------------------------------ @@ -1397,14 +1436,14 @@ use Data::Dumper; @mqtt_InstMqttItem::ISA = ( 'mqtt_BaseRemoteItem' ); -=item C +=item C Creates a MQTT BaseRemoteItem/object that will mirror the state of the object, and send commands to it. =cut sub new { ### mqtt_InstMqttItem - my ( $class, $interface, $type, $topicprefix, $discoverable, $friendly_name ) = @_; + my ( $class, $interface, $type, $topicpattern, $discoverable, $friendly_name ) = @_; my ($base_type, $device_class) = $type =~ m/^([^:]*):?(.*)$/; @@ -1413,8 +1452,14 @@ sub new { ### mqtt_InstMqttItem return; } - my ($realm, $mqtt_name) = $topicprefix =~ m|^([^/]+)/([^/]+)$|; - my $listen_topic = "$topicprefix/+"; + my (@topic_parts) = split( "/", $topicpattern ); + my $realm = $topic_parts[0]; + my $mqtt_name = $topic_parts[1]; + my $topic_prefix = "$realm/$mqtt_name"; + if( !$mqtt_name ) { + $interface->error( "Unrecognized topic pattern '$topicpattern' for device '$friendly_name'" ); + } + my $listen_topic = "$topic_prefix/+"; my $self = new mqtt_BaseRemoteItem( $interface, $mqtt_name, $base_type, $listen_topic, $discoverable ); @@ -1423,7 +1468,7 @@ sub new { ### mqtt_InstMqttItem bless $self, $class; - $self->debug( 1, "New mqtt_InstMqttItem( $interface->{instance}, '$mqtt_name', '$type', '$topicprefix', $discoverable, '$friendly_name' )" ); + $self->debug( 1, "New mqtt_InstMqttItem( $interface->{instance}, '$mqtt_name', '$type', '$topicpattern', $discoverable, '$friendly_name' )" ); $self->{realm} = $realm; $self->{discovered} = 0; @@ -1441,10 +1486,10 @@ sub new { ### mqtt_InstMqttItem $self->{disc_info}->{payload_on} = "{ \"cmd\" : \"ON\", \"name\" : \"$mqtt_name\" }"; $self->{disc_info}->{payload_off} = "{ \"cmd\" : \"OFF\", \"name\" : \"$mqtt_name\" }"; } else { - $self->{disc_info}->{state_topic} = "$topicprefix/state"; - $self->{disc_info}->{command_topic} = "$topicprefix/set"; + $self->{disc_info}->{state_topic} = "$topic_prefix/state"; + $self->{disc_info}->{command_topic} = "$topic_prefix/set"; if( $base_type eq 'light' ) { - $self->{disc_info}->{command_topic} = "$topicprefix/level"; + $self->{disc_info}->{command_topic} = "$topic_prefix/level"; $self->{disc_info}->{schema} = 'json'; $self->{disc_info}->{brightness} = "true"; } elsif( $base_type eq 'binary_sensor' ) {