use v5.32.1; use strict; use warnings; use Test2::V0; use JSON::PP qw(decode_json); use FindBin; use lib "${FindBin::Bin}/../../../lib/"; use lib "${FindBin::Bin}/../lib/"; use FAPG::DAQ::MQTT qw( publish_mqtt publish_status_mqtt mqtt_payload_for_reading mqtt_payload_for_status mqtt_topic_for_reading mqtt_topic_for_status mqtt_broker_endpoint ); { package Local::FakeMQTT; use v5.32.1; use warnings; sub new { my ($class) = @_; return bless { published => [] }, $class; } sub publish { my ( $self, $topic, $payload ) = @_; push $self->{published}->@*, [ $topic, $payload ]; return 1; } sub published { my ($self) = @_; return $self->{published}; } } subtest 'topic shape' => sub { my $topic = mqtt_topic_for_reading( { probe => 'pH', value => 7.12, unit => 'pH' }, node => 'fapg-daq-zero-01', ); is $topic, 'fapg/daq/ph/fapg-daq-zero-01/reading'; my $status_topic = mqtt_topic_for_status( { probe => 'pH', status => 'ok' }, node => 'fapg-daq-zero-01', ); is $status_topic, 'fapg/daq/ph/fapg-daq-zero-01/status'; }; subtest 'payload shape' => sub { my $payload = mqtt_payload_for_reading( { probe => 'pH', value => 7.12, unit => 'pH', raw => '7.12', timestamp => '2026-07-06T10:00:00Z', }, node => 'fapg-daq-zero-01', ); my $decoded = decode_json($payload); is $decoded->{schema}, 'fapg.daq.reading.v1'; is $decoded->{timestamp}, '2026-07-06T10:00:00Z'; is $decoded->{node}, 'fapg-daq-zero-01'; is $decoded->{probe}, 'ph'; is $decoded->{value}, 7.12; is $decoded->{unit}, 'pH'; is $decoded->{raw}, '7.12'; is $decoded->{source}, 'fapg-daq-node'; }; subtest 'status payload shape' => sub { my $payload = mqtt_payload_for_status( { probe => 'pH', status => 'probe_error', error => 'timeout waiting for EZO response', device => '/dev/serial/by-id/usb-FTDI_USB_UART', timestamp => '2026-07-06T10:00:05Z', }, node => 'fapg-daq-zero-01', ); my $decoded = decode_json($payload); is $decoded->{schema}, 'fapg.daq.status.v1'; is $decoded->{timestamp}, '2026-07-06T10:00:05Z'; is $decoded->{node}, 'fapg-daq-zero-01'; is $decoded->{probe}, 'ph'; is $decoded->{status}, 'probe_error'; is $decoded->{error}, 'timeout waiting for EZO response'; is $decoded->{device}, '/dev/serial/by-id/usb-FTDI_USB_UART'; is $decoded->{source}, 'fapg-daq-node'; }; subtest 'hub status shape' => sub { my $topic = mqtt_topic_for_status( { probe => 'hub', status => 'ok', }, node => 'fapg-daq-five-01', ); is $topic, 'fapg/daq/hub/fapg-daq-five-01/status'; my $payload = mqtt_payload_for_status( { probe => 'hub', status => 'ok', message => 'DAQ hub MQTT broker reachable', timestamp => '2026-07-06T10:00:10Z', }, node => 'fapg-daq-five-01', source => 'fapg-daq-hub', ); my $decoded = decode_json($payload); is $decoded->{schema}, 'fapg.daq.status.v1'; is $decoded->{timestamp}, '2026-07-06T10:00:10Z'; is $decoded->{node}, 'fapg-daq-five-01'; is $decoded->{probe}, 'hub'; is $decoded->{status}, 'ok'; is $decoded->{message}, 'DAQ hub MQTT broker reachable'; is $decoded->{source}, 'fapg-daq-hub'; }; subtest 'publish uses injected client, so unit tests need no broker' => sub { my $fake = Local::FakeMQTT->new; my $result = publish_mqtt( { probe => 'DO', value => 8.34, unit => 'mg/L', timestamp => '2026-07-06T10:00:00Z', }, node => 'fapg-daq-zero-02', client => $fake, ); is $result->{topic}, 'fapg/daq/do/fapg-daq-zero-02/reading'; is scalar $fake->published->@*, 1; is $fake->published->[0][0], 'fapg/daq/do/fapg-daq-zero-02/reading'; my $decoded = decode_json( $fake->published->[0][1] ); is $decoded->{probe}, 'do'; is $decoded->{value}, 8.34; my $status = publish_status_mqtt( { probe => 'DO', status => 'ok', }, node => 'fapg-daq-zero-02', client => $fake, ); is $status->{topic}, 'fapg/daq/do/fapg-daq-zero-02/status'; is scalar $fake->published->@*, 2; is $fake->published->[1][0], 'fapg/daq/do/fapg-daq-zero-02/status'; }; subtest 'broker endpoint defaults to the Pi 5 broker' => sub { local $ENV{FAPG_MQTT_HOST}; local $ENV{MQTT_HOST}; local $ENV{FAPG_MQTT_PORT}; local $ENV{MQTT_PORT}; is mqtt_broker_endpoint(), 'fapg-daq-five-01:1883'; }; done_testing;