Group
Extension

App-mqtt2job/lib/App/mqtt2job.pm

package App::mqtt2job;

use strict;
use warnings;

use Template;
use File::Temp;
use Exporter qw/import/;

# ABSTRACT: Helper module for mqtt2job

our @EXPORT_OK = qw/ helper_v1 ha_helper_cfg /;

sub helper_v1 {
    my ($obj) = @_;

    my $unlink = $obj->{rm} ? 1 : 0;

    # helper scripts need to be saved, so set up & prepare a filehandle
    my $fh = File::Temp->new( SUFFIX => "." . ($obj->{suffix} || "pl"), UNLINK => $unlink );
    $obj->{wrapper_location} = $fh->filename;
    
    my $tt = Template->new();
    my $ttout = undef;
    
    $tt->process( _helper_v1(), $obj, \$ttout );

    return _save($fh, $ttout, $obj->{rm});
}

sub ha_helper_cfg {
    my ($obj) = @_;

    my $tt = Template->new();
    my $ttout = undef;
    
    $tt->process( _ha_helper_cfg(), $obj, \$ttout );

    return $ttout;
}

sub _save {
    my ($fh, $output) = @_;
    print $fh $output;
    return $fh;
}

sub _helper_v1 {
    my $tpl = undef;
    $tpl = <<'_RUNNER_TPL';
#![% shebang %]

use strict;
use warnings;

use Net::MQTT::Simple;
use DateTime;
use JSON; 
use Capture::Tiny ':all';

my $dt_start = DateTime->now();
my $mqtt = Net::MQTT::Simple->new("[% mqtt_server %]:[% mqtt_port %]");

$mqtt->retain("[% base_topic %]/status/" . "[% task || "unknown" %]", encode_json({ status => "initiated", dt => "$dt_start", msg => "[% cmd %]" }) );
my $real_cmd = "[% job_dir %]/[% cmd %]";
my $real_args = "[% args %]";

print STDERR "$dt_start: [MQTT TASK] $real_cmd $real_args\n";

my ($output, $exit) = tee_merged {
    my @args = split(" ", $real_args);
    system($real_cmd, @args);
};

my $msg = ($exit == 0) ? "ok" : "failed";

my $dt_end = DateTime->now();
my $dt_elapsed_obj = $dt_end - $dt_start;
my $dt_elapsed = $dt_elapsed_obj->in_units("seconds");

my @split_output = split("\n", $output);
my $last_line = $split_output[$#split_output];

$output =~ s/\n//g;

$mqtt->retain("[% base_topic %]/status/" . "[% task || "unknown" %]", encode_json({ status => "completed", dt => "$dt_end", last_line => "$last_line", output => "$output", elapsed => "$dt_elapsed", msg => "$msg" }) );
print STDERR "$dt_end: [MQTT TASK] $real_cmd $real_args (${dt_elapsed}s) Exit: $exit\n";

$mqtt->disconnect;
[% UNLESS no_unlink %]unlink "[% wrapper_location %]";[% END %]
_RUNNER_TPL
    return \$tpl;
}

sub _ha_helper_cfg {
    my $tpl = undef;
$tpl = <<'_HA_CFG_TPL';
=====================================================================
TRIGGER: (automation)
=====================================================================
alias: mqtt2job [% task %]
description: ""
triggers:
  - seconds: "0"
    trigger: time_pattern
    enabled: true
conditions: []
actions:
  - action: mqtt.publish
    metadata: {}
    data:
      topic: [% base_topic %]
      payload: |-
        { 
          "cmd": "[% cmd %]",
          "args": "[% args %]", 
          "dt": "{{ now().strftime("%Y-%m-%d %H:%M:%S") }}",
          "task": "[% task %]",
          "status": "queue"
        }
mode: single

=====================================================================
SENSORS: (configuration.yaml)
=====================================================================
  sensor:
    - name: "[% task %] status"
      state_topic: "[% base_topic %]/status/[% task || "unknown" %]"
      value_template: "{{ value_json.status }}"
    - name: "[% task %] datetime"
      state_topic: "[% base_topic %]/status/[% task || "unknown" %]"
      value_template: "{{ value_json.dt }}"
    - name: "[% task %] message"
      state_topic: "[% base_topic %]/status/[% task || "unknown" %]"
      value_template: "{{ value_json.msg }}"
    - name: "[% task %] elapsed"
      state_topic: "[% base_topic %]/status/[% task || "unknown" %]"
      value_template: "{{ value_json.elapsed }}"
    - name: "[% task %] output"
      state_topic: "[% base_topic %]/status/[% task || "unknown" %]"
      value_template: "{{ value_json.last_line }}"

=====================================================================
CARD: (dashboard, uses button-card from HACS)
=====================================================================
type: custom:button-card
show_state: false
custom_fields:
  [% task %]_status:
    card:
      type: custom:button-card
      entity: sensor.[% task %]_message
      name: [% task %]
      show_icon: true
      icon: mdi:circle
      state:
        - value: ok
          color: green
          icon: mdi:check-circle
        - value: failed
          color: red
          icon: mdi:alert-circle
        - operator: default
          color: blue
          icon: mdi:progress-clock
      tap_action:
        action: navigate
        navigation_path: [% task %] 
styles:
  custom_fields:
    [% task %]_status:
      - padding: 5px
      - font-size: 12px
  grid:
    - grid-template-areas: "\"[% task %]_status\""
    - grid-template-columns: 1fr 1fr
    - grid-template-rows: auto
  card:
    - padding: 10px
    - width: auto
    - height: auto

=====================================================================
WIDGET: (hidden dashboard)
=====================================================================
title: [% task %]
path: [% task %]
icon: mdi:widgets
type: panel
cards:
  - type: entities
    entities:
      - entity: sensor.[% task %]_datetime
      - entity: sensor.[% task %]_elapsed
      - entity: sensor.[% task %]_message
      - entity: sensor.[% task %]_status
      - entity: sensor.[% task %]_output
subview: true

_HA_CFG_TPL
	return \$tpl;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

App::mqtt2job - Helper module for mqtt2job

=head1 VERSION

version 0.03

=head1 SYNOPSIS

  mqtt2job --mqtt_server mqtt.example.com --base_topic my/topic --job_dir /apps

=head1 DESCRIPTION

Subscribes to the my/topic/job mqtt topic and upon receiving a 
correctly formatted json message will fork and run the requested 
job in a wrapper script providing it is present and executable in 
the job_dir directory. 

This wrapper will generate two child mqtt messages under the base 
topic, at my/topic/status. Message one is sent when the job is 
initiated. The second is sent when the job has completed (or timed 
out). This second message will also include any output from the job 
amongst various other metadata (e.g. execution datetime, duration, 
timeout condition, etc.)

=head1 NAME

App::mqtt2job - Subscribe to an MQTT topic and trigger job execution

=head1 FOR THE LOVE OF ALL THAT IS SACRED, WHY?

This is part one of my "Cursed Solutions" series, URL to be added
later when I've uploaded it.

=head1 COPYRIGHT

Copyright 2024 -- Chris Carline

=head1 LICENSE

This software is licensed under the same terms as Perl.

=head1 NO WARRANTY

This software is provided "as is" without any express or implied
warranty. Using it for any reason whatsoever is probably an 
extremely bad idea and it should only ever be considered if you 
understand the potential consequences. In no event shall the 
author be held liable for any damages arising from the use of 
this software. It is provided for demonstration purposes only.

=head1 AUTHOR

Chris Carline <chris@carline.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2024 by Chris Carline.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.