diff --git a/climate b/climate new file mode 100755 index 0000000..a636c45 --- /dev/null +++ b/climate @@ -0,0 +1,44 @@ +#!/usr/bin/perl + +use lib 'lib'; +use strict; +use feature 'say'; +use CarBus; +use Getopt::Long; +use IO::File; +use IO::Socket::IP; +use IO::Termios; +use JSON; + +my $opt={ src=>'FakeSAM', dst=>'Thermostat', cmd=>'read', reg=>'0104', src_bus=>1, dst_bus=>1}; +GetOptions($opt, 'src=s', 'dst=s', 'reg=s', 'cmd=s' ); +$opt->{payload_raw} ||= pack("H*","00".$opt->{reg}); + +my $reqframe = CarBus::Frame->new($opt); +print STDERR $reqframe->frame_log."\n"; + +my $tcp = CarBus->new(IO::Socket::IP->new(PeerHost=>'192.168.1.23', PeerPort=>23)); #tcp +my $sam = CarBus->new(IO::Termios->open("/dev/cu.usbserial-A7039O5G", "38400,8,n,1")); #serial +#my $fil = CarBus->new(IO::File->new("new(buslist=>[$tcp,$sam]); +my $lastwrite=0; +my $tries=8; +while(1) { + foreach my $frame ($bridge->drive) { + next unless $frame; + next unless ( $frame->struct->{dst} eq $reqframe->struct->{src} + and $frame->struct->{src} eq $reqframe->struct->{dst} + and ( $frame->struct->{reg_string} eq $reqframe->struct->{reg_string} + or $frame->struct->{cmd} eq 'exception' ) + ); + print STDERR $frame->frame_log."\n"; + print encode_json($frame->frame_hash); + exit; + } + if (time>$lastwrite) { + $bridge->write($reqframe); + $lastwrite=time; + die 'timeout' unless $tries--; + } +} diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index d30c115..ad631bc 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -1,5 +1,3 @@ -version: "2.1" - services: infinitude: container_name: infinitude @@ -9,7 +7,8 @@ services: build: context: . dockerfile: Dockerfile - network_mode: host + ports: + - "3000:3000" volumes: - ./:/infinitude/ environment: diff --git a/docker-compose.yaml b/docker-compose.yaml index 96b9e46..8aef61f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: "2.1" - services: infinitude: container_name: infinitude @@ -9,8 +7,6 @@ services: context: . dockerfile: Dockerfile network_mode: host - ports: - - "3000:3000" volumes: - ./state:/infinitude/state environment: diff --git a/infinitude b/infinitude index 4731e91..a2af9bf 100755 --- a/infinitude +++ b/infinitude @@ -67,7 +67,6 @@ sub serial_init { require IO::Termios; warn "Using $config->{serial_tty} serial interface\n"; $handle ||= IO::Termios->open($config->{serial_tty},"38400,8,n,1"); - $handle->blocking(0); } else { warn "Can't find serial device: $config->{serial_tty}. Serial monitoring disabled.\n" if $config->{serial_tty}; delete $config->{serial_tty}; @@ -79,38 +78,11 @@ sub serial_init { $port //= 'telnet'; warn "Using $host port $port for serial interface\n"; $handle ||= IO::Socket::IP->new( PeerHost=>$host, PeerPort=>$port); - $handle->blocking(0); } - $carbus = CarBus->new(fh=>$handle) if $handle; + $carbus = CarBus->new($handle) if $handle; } serial_init(); -my @inbox = (); -if ($use_serial) { - my $last_frame_time = time; - my $frame_inbox_id = Mojo::IOLoop->recurring(0.0625 => sub { - my $loop = shift; - $loop->on(finish => sub { - warn "Frame filler loop finishing"; - }); - $loop->on(reset => sub { - warn "Frame filler loop resetting"; - }); - if ((time - $last_frame_time) < 10) { - return unless $carbus; - $carbus->fh_fill() if $carbus->fh; - my $frame = $carbus->get_frame; - if (not $frame->{error}) { - push(@inbox, $frame); - $last_frame_time = time; - } - shift @inbox if scalar(@inbox)>128; - } else { - serial_init(); - } - }); -} - app->secrets([$config->{app_secret}]); push (@{app->static->paths}, ('development' eq ($ENV{MOJO_MODE}//'')) ? 'public/app' : 'public/dist'); @@ -121,7 +93,7 @@ hook before_dispatch => sub { my $url = $c->req->url; - if ($url->to_abs->host =~ /(bryant|carrier|ioncomfort|infinitude|127.0.0.1)/i) { # request from stat or test harness + if ($url->to_abs->host =~ /(bryant|carrier|ioncomfort|infinitude)/i) { # request from stat or test harness my $nk = $url->path->to_string; $nk =~ s/\//-/g; $nk =~ s/^-//; @@ -439,26 +411,28 @@ any '/systems/:system_id/:part' => sub { my $scantab = 0; my $scanrow = 0; my $scansec = 0; +my $lastframe = 0; websocket '/serial' => sub { my $c = shift; unless ($use_serial) { - $c->app->log->info("Websocket opened, but no streaming source is available"); + $c->app->log->info("Websocket opened, but no streaming source is configured"); return; } - my $socketloop_id = Mojo::IOLoop->recurring(0.03125 => sub { + my $socketloop_id = Mojo::IOLoop->recurring(0.0625 => sub { my $loop = shift; - if (my $frame = shift @inbox) { + serial_init() if (!$carbus or time>($lastframe+10)); + if (my $frame = $carbus->get_frame) { return if (!$frame or !$frame->struct->{cmd}); my $fstruc = $frame->frame_hash; - $fstruc->{timestamp} = time; - - if ($fstruc->{Function} eq 'reply' and $fstruc->{type}) { - if (my $payf = $fstruc->{payload}) { + $fstruc->{timestamp} = $lastframe = time; + if ($fstruc->{cmd} eq 'reply') { + my $payf = $fstruc->{payload}; + if ($payf) { if ($payf->{rows} and $ENV{SCAN_THERMOSTAT}) { $scanrow = $payf->{rows}+1; warn sprintf(">>>>>>>>>>> table %02x has %d rows <<<<<<<<<<<<<<<<<<<", $scantab, $payf->{rows}); } - $fstruc->{field} = { $fstruc->{type}=>$fstruc->{payload} }; + $fstruc->{field} = { $fstruc->{reg_name}||'unknown' => $payf }; } } @@ -477,14 +451,16 @@ websocket '/serial' => sub { $carbus->samreq($scantab, $scanrow); } } + #warn $frame->frame_log; $c->send({json=>$fstruc}); } }); + $c->app->log->info("Websocket $socketloop_id Established"); $c->on('finish' => sub { my ($c, $code) = @_; Mojo::IOLoop->remove($socketloop_id); - $c->app->log->info("Websocket Closed: $code"); + $c->app->log->info("Websocket $socketloop_id Closed: $code"); }); }; diff --git a/lib/CarBus.pm b/lib/CarBus.pm index 37d34f8..88023c0 100644 --- a/lib/CarBus.pm +++ b/lib/CarBus.pm @@ -1,44 +1,50 @@ package CarBus; use Moo; use CarBus::Frame; +use Scalar::Util qw/blessed/; -has async => (is=>'ro', default=>sub{0}); -has fh => (is=>'ro'); +has fh => (is=>'ro', isa=>sub{ + die 'fh must be an IO::Handle or subclass thereof' unless + defined blessed($_[0]) and $_[0]->isa('IO::Handle'); +}); has buffer => (is=>'rw', default=>''); use constant MAX_BUFFER => 1024; +use constant MIN_FRAME => 10; sub BUILDARGS { my ( $class, @args ) = @_; unshift @args, "fh" if @args % 2 == 1; - return { @args }; + my $argref = { @args }; + $argref->{fh}->blocking(0); + return $argref; }; sub get_frame { - my $self = shift; + my $self = shift; - my $max_attempts = $self->async ? $self->buflen : MAX_BUFFER; - my $attempts = 0; - while ($attempts++<$max_attempts) { - my $data_len = $self->buflen>4 ? ord(substr($self->buffer,4,1)) : 0; - if ($data_len>0) { - my $frame_len = 10+$data_len; - if ($self->buflen>=$frame_len) { - my $frame_string = substr($self->buffer,0,$frame_len); - my $cbf = CarBus::Frame->new($frame_string); - if ($cbf->valid) { - $self->shift_stream($frame_len); - $self->handlers($cbf); - return $cbf; - } - $self->shift_stream(1); - } - } else { - $self->shift_stream(1); - } - $self->fh_fill(); - } - return { error=>'timed out or EOF' }; + my $max_attempts = $self->buflen>MAX_BUFFER ? $self->buflen : MAX_BUFFER; + my $attempts = 0; + while ($attempts++<$max_attempts) { + if ($self->buflen < MIN_FRAME) { + $self->fh_fill(); + next; + } + my $data_len = ord(substr($self->buffer,4,1)); + my $frame_len = MIN_FRAME+$data_len; + if ($self->buflen >= $frame_len) { + my $frame_string = substr($self->buffer,0,$frame_len); + my $cbf = CarBus::Frame->new($frame_string); + if ($cbf->valid) { + $self->shift_stream($frame_len); + $self->handlers($cbf); + return $cbf; + } + $self->shift_stream(1); + } + $self->fh_fill(); + } + return undef; } sub fh_fill { @@ -71,29 +77,58 @@ sub shift_stream { $self->buffer(substr($self->buffer,$byte_num)); } +sub write { + my $self = shift; + my $frame = shift; + $self->fh->syswrite($frame->frame); +} + sub samreq { my $self = shift; my ($table, $row, $frameopts) = @_; $frameopts //= {}; - my $samframe = CarBus::Frame->new(data=>pack("C*", 0, $table, $row), %$frameopts); - $self->fh->syswrite($samframe->frame); + my $samframe = CarBus::Frame->new( + src=>'FakeSAM', src_bus=>1, + dst=>'Thermostat', dst_bus=>1, + cmd=>'read', + payload_raw=>pack("C*", 0, $table, $row), + %$frameopts + ); + $self->write($samframe); return $samframe; } sub handlers { my $self = shift; my $frame = shift; - my $f = $frame->frame_hash; - #if ( - # $f->{DstClass} eq 'SAM' and $f->{Function} eq 'read' and - # $f->{table} == 1 and $f->{row} == 4 ) { - # my $nf = CarBus::Frame->new( Function=>'reply', - # DstClass=>$f->{SrcClass}, SrcClass=>'SAM', - # DstAddress=>$f->{SrcAddress}, SrcAddress=>1, - # data => pack("C*",0,1,4)."\0SYSTEM ACCESS MODULE\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0CESR131379-03 SYSTXCCSAM01\0\0\0\0\0\0\0\01009N182206-1009N182206-------------", - # ); - #$self->fh->syswrite($nf->frame); - #} + # mangle frame contents; +} + +package CarBus::Bridge; +use Moo; + +has buslist => (is=>'ro'); +has routes => (is=>'rw', default=>sub{{}}); + +sub drive { + my $self = shift; + my @frames = (); + foreach my $srcbus (@{$self->buslist}) { + if (my $frame = $srcbus->get_frame()) { + push(@frames,$frame); + foreach my $dstbus (@{$self->buslist}) { + next if $srcbus == $dstbus; + $dstbus->write($frame); + } + } + } + return @frames; +} + +sub write { + my $self = shift; + my $frame = shift; + $_->write($frame) for @{$self->buslist}; } 1; diff --git a/lib/CarBus/Frame.pm b/lib/CarBus/Frame.pm index 5c3b07d..b415058 100644 --- a/lib/CarBus/Frame.pm +++ b/lib/CarBus/Frame.pm @@ -6,8 +6,7 @@ use Try::Tiny; my %device_classes = ( SystemInit => 0x1F, - NIM => 0x80, - SAM => 0x92, + SAM=>0x92, FakeSAM => 0x93, Broadcast => 0xF1, _default_ => $DefaultPass @@ -19,6 +18,8 @@ my $classmap = { 4 => 'IndoorUnit', 5 => 'OutdoorUnit', 6 => 'ZoneControl', + 8 => 'NIM', + #9 => 'SAM' }; foreach my $pre (keys %$classmap) { @@ -26,114 +27,106 @@ foreach my $pre (keys %$classmap) { my $idx=0; while($idx<0xF) { my $addr = ($pre<<4) + $idx; - $device_classes{$label.($idx ? $idx : '')} = $addr; + $device_classes{$label.($idx ? $idx : '')} ||= $addr; $idx++; } } -my $frame_parser = Struct("CarFrame", - Enum(Byte("DstClass"), %device_classes), - Byte("DstAddress"), - Enum(Byte("SrcClass"), %device_classes), - Byte("SrcAddress"), - Byte("length"), - Padding(2), - Enum(Byte("Function"), - reply => 0x06, - read => 0x0B, - write => 0x0C, - exception => 0x15, - _default_ => $DefaultPass - ), - Field("data", sub { $_->ctx->{length} }), - ULInt16("checksum"), - Value("bus", sub { length($_->ctx->{data})>=3 ? ord(substr($_->ctx->{data}, 0,1)) : 0 }), - Value("table", sub { length($_->ctx->{data})>=3 ? ord(substr($_->ctx->{data}, 1,1)) : 0 }), - Value("row", sub { length($_->ctx->{data})>=3 ? ord(substr($_->ctx->{data}, 2,1)) : 0 }), +my $fp = Struct("CarFrame", + Enum(Byte("dst"),%device_classes), Byte("dst_bus"), + Enum(Byte("src"),%device_classes), Byte("src_bus"), + Byte("length"), + Byte('pid'), + Byte('ext'), + Enum(Byte("cmd"), + reply => 0x06, + read => 0x0B, + write => 0x0C, + exception => 0x15, + _default_ => $DefaultPass + ), + Field("payload_raw", sub { $_->ctx->{length} }), + ULInt16("checksum"), + + Value("raw", sub { ${$_->stream->{data}}; }), + Value("as_hex", sub { unpack("H*",$_->ctx->{raw}) }), + Value("reg_string", sub { length($_->ctx->{payload_raw})>=3 ? substr($_->ctx->{as_hex}, 18,4) : undef}), + Value("gensum", sub { crc16(substr($_->ctx->{raw},0,-2)) }), + Value("valid", sub { $_->ctx->{gensum} == $_->ctx->{checksum} }), + Value("payload", sub { length($_->ctx->{payload_raw})<=3 ? undef + : subparser($_->ctx->{reg_string})->parse(substr($_->ctx->{payload_raw},3)) }), + + Value("reg_name", sub { + my $fh = $_->ctx; + my $subp = subparser($fh->{reg_string}); + my $regname = $fh->{reg_string} // ''; + $regname = $subp->{Name}."($regname)" if $subp; + return $regname; + }) + + ); around BUILDARGS => sub { my ( $orig, $class, @args ) = @_; - - - my $defaults = { - DstClass => 'Thermostat', DstAddress=>1, - SrcClass => 'FakeSAM', SrcAddress=>1, - Function => 'read', - checksum => 0, - length => 0, - data => '', - }; - - if (@args == 1 && !ref $args[0]) { - my $parsed_frame = $frame_parser->parse($args[0]); - my $check_frame = __PACKAGE__->new($parsed_frame); - return { - struct => $check_frame->struct, - valid => ($args[0] eq $check_frame->frame) ? 1 : 0 - }; - } - (@args) = %{$args[0]} - if @args == 1 && ref $args[0]; + if @args == 1 && ref $args[0] eq 'HASH'; - my $struct = $defaults; - my %arghash = @args; - foreach my $key (keys %arghash) { - $struct->{$key} = delete $arghash{$key} if defined($defaults->{$key}); - } - - $arghash{struct} = $struct; + my $init_frame = chr(0)x10; + $init_frame = shift @args if (@args == 1 && !ref $args[0]); + $init_frame = pack("H*", $init_frame) if $init_frame =~ /^[0-9A-Fa-f]+$/; + my $struct = { valid=>0 }; + try { $struct = $fp->parse($init_frame); }; + $struct = {%$struct,@args}; - return $class->$orig(%arghash); + return $class->$orig({struct=>$struct}); }; -has parser => (is=>'ro', default=> sub { $frame_parser }); +has parser => (is=>'ro', default=> sub { $fp }); has struct => (is=>'rw'); -has valid => (is=>'ro', default=>sub{1}); + +sub valid { shift->struct->{valid} } sub frame { my $self = shift; - return undef unless $self->valid; - $self->struct->{length} = length($self->struct->{data}); - my $fstring = substr $self->parser->build($self->struct), 0, -2; - $self->struct->{checksum} = crc16($fstring); - return $fstring.pack("S",$self->struct->{checksum}); + return undef unless $self->struct->{valid}; + + my $struct = $self->struct; + $struct->{length} = length($struct->{payload_raw}); + $struct = $fp->parse($fp->build($struct)); + + if ($struct->{checksum} != $struct->{gensum}) { + $struct->{checksum} = $struct->{gensum}; + $struct = $fp->parse($fp->build($struct)); + } + $self->struct($struct); + + return $self->struct->{raw}; } sub frame_hex { my $self = shift; - return unpack("H*", $self->frame); + $self->frame; + return $self->struct->{as_hex}; } sub frame_hash { my $self = shift; - my $parsed = $self->parser->parse($self->frame); - return {} unless $parsed; - - my $register = sprintf("%02x%02x",$parsed->{table}, $parsed->{row}); - $parsed->{register} = $register; - if (my $regparser = $self->reg_parser($register)) { - $parsed->{type} = $regparser->{Name}; - try { - $parsed->{payload} = $regparser->parse($parsed->{data}); - }; - } - - return $parsed; + $self->frame; + return $self->struct; } sub frame_log { my $self = shift; + my $fh = $self->frame_hash; return join(' ', - $self->frame_hash->{SrcClass}, - $self->frame_hash->{Function}, - $self->frame_hash->{DstClass}, - $self->frame_hash->{register} + $fh->{src}, + $fh->{cmd}, + $fh->{dst}, + $fh->{reg_name} ); } -my @regdef = (Byte("bus"), Byte("table"), Byte("row")); my @schedchunk = ( Byte('min15s'), Enum(Byte('mode'), home=>0, away=>1, sleep=>2, wake=>3), Value('enabled', sub { $_->ctx->{min15s} == 0x60 ? 0 : 1 }), @@ -142,7 +135,6 @@ my @schedchunk = ( my $parsers = { '01' => Struct('tabledef', - @regdef, UBInt16('type'), String('name', 8), UBInt16('size'), @@ -156,7 +148,6 @@ my $parsers = { ), '0104' => Struct('device_info', - @regdef, PaddedString('device', 24, paddir=>'right'), PaddedString('location', 24, paddir=>'right'), PaddedString('software', 16, paddir=>'right'), @@ -165,12 +156,13 @@ my $parsers = { PaddedString('reference', 24, paddir=>'right'), ), - '0202' => Struct('time', @regdef, Byte('hour'), Byte('minute'), Byte('unknown')), - '0203' => Struct('date', @regdef, Byte('day'), Byte('month'), Byte('20xx'), Value('year', sub { 2000+int($_->ctx->{'20xx'}) })), + '0202' => Struct('time', Byte('hour'), Byte('minute'), Enum(Byte('weekday'), 0=>'Sunday', 1=>'Monday', 2=>'Tuesday', 3=>'Wednesday', 4=>'Thursday', 5=>'Friday', 6=>'Saturday')), + + '0203' => Struct('date', Byte('day'), Byte('month'), Byte('20xx'), Value('year', sub { 2000+int($_->ctx->{'20xx'}) })), # SAMINFO - '3B02' => Struct('sam_state', @regdef, + '3B02' => Struct('sam_state', Byte('active_zones'), Padding(2), Array(8, Byte('temperature')), @@ -181,18 +173,24 @@ my $parsers = { Flag('z8'), Flag('z7'), Flag('z6'), Flag('z5'), Flag('z4'), Flag('z3'), Flag('z2'), Flag('z1'), ), - - Nibble('stage'), - Nibble('mode'), - Array(5, Byte('unknown')), + BitStruct('stagmode', + Nibble('stage'), + Enum(Nibble('mode'), heat=>0, cool=>1, auto=>2, eheat=>3, off=>4) + ), + Array(2, Byte('unknown')), + Enum(Byte('weekday'), 0=>'Sunday', 1=>'Monday', 2=>'Tuesday', 3=>'Wednesday', 4=>'Thursday', 5=>'Friday', 6=>'Saturday'), + UBInt16('minutes_since_midnight'), Byte('displayed_zone') ), - '3B03' => Struct('sam_zones', @regdef, + '3B03' => Struct('sam_zones', Byte('active_zones'), Padding(2), - Array(8, Byte('fan_mode')), - Byte('zones_holding'), + Array(8, Enum(Byte('fan_mode'), high=>3, medium=>2, low=>1, auto=>0 )), + BitStruct('zones_holding', + Flag('z8'), Flag('z7'), Flag('z6'), Flag('z5'), + Flag('z4'), Flag('z3'), Flag('z2'), Flag('z1'), + ), Array(8, Byte('heat_setpoint')), Array(8, Byte('cool_setpoint')), Array(8, Byte('humidity_setpoint')), @@ -202,7 +200,7 @@ my $parsers = { Array(8, Field('zone_name', 12)) ), - '3B04' => Struct('sam_vacation', @regdef, + '3B04' => Struct('sam_vacation', Byte('active'), UBInt16('hours'), Byte('min_temp'), @@ -215,7 +213,7 @@ my $parsers = { #3B05 # contains: filterlevel,uvlevel,humidifierpadelvel, reminders for all - '3B05' => Struct('sam_accessories', @regdef, + '3B05' => Struct('sam_accessories', Padding(3), Byte('filter_consumption'), Byte('uv_consumption'), @@ -229,20 +227,25 @@ my $parsers = { #3B06 # contains: deadband, dealer name, dealer phone - '3B06' => Struct('sam_dealer', @regdef, + '3B06' => Struct('sam_dealer', + Byte('backlight'), + Byte('auto_mode'), + Padding(1), + Byte('deadband'), + Byte('cycles_per_hour'), + Byte('schedule_periods'), + Byte('programs_enabled'), + Byte('temp_units'), Pointer(15,CString('dealer_name')), Pointer(35,CString('dealer_phone')), ), - # zone 1 '4002' => Struct('schedule', - @regdef, Array(7, Array(5, Struct('chunk',@schedchunk))) ), # zone 1 '400A' => Struct('comfort_profile', - @regdef, Struct('home', Byte('heat'), Byte('cool'), Enum(Byte('fan'), off=>0, low=>1, med=>2, high=>3), Array(4,Byte('unknown'))), Struct('away', Byte('heat'), Byte('cool'), Enum(Byte('fan'), off=>0, low=>1, med=>2, high=>3), Array(4,Byte('unknown'))), Struct('sleep', Byte('heat'), Byte('cool'), Enum(Byte('fan'), off=>0, low=>1, med=>2, high=>3), Array(4,Byte('unknown'))), @@ -251,32 +254,32 @@ my $parsers = { ), # zone 1 '4012' => Struct('vacation_settings', - @regdef, Byte('min_temp'), Byte('max_temp'), Enum(Byte('fan'), off=>0, low=>1, med=>2, high=>3), Array(4,Byte('unknown')), ), # MISC1 - '4608' => Struct('insecurity', @regdef, + '4608' => Struct('insecurity', Pointer(7,CString('mac_address')), Pointer(27,CString('ssid')), Pointer(73,CString('password')), Pointer(142,CString('token?')), ), - '4609' => Struct('server', @regdef, + '4609' => Struct('server', Pointer(3,CString('cloud_host')), Pointer(70,CString('device_ip')), ) }; -sub reg_parser { - my $self = shift; +sub subparser { my $reg = shift; - $reg = uc($reg); + $reg = uc($reg//''); foreach my $key (keys %$parsers) { return $parsers->{$key} if $reg =~ /$key$/i; } + + return Value("unknown",undef); } 1; diff --git a/public/app/scripts/controllers/main.js b/public/app/scripts/controllers/main.js index 986661f..9780542 100644 --- a/public/app/scripts/controllers/main.js +++ b/public/app/scripts/controllers/main.js @@ -6,11 +6,19 @@ function wsu(s) { } var toHex = function (str) { - var hex = ''; - for(var i=0;i9) { $scope.state[id].history.pop(); } window.localStorage.setItem('infinitude-serial-state',angular.toJson($scope.state)); diff --git a/public/app/views/main.html b/public/app/views/main.html index 6fb09f5..b6cd2ea 100644 --- a/public/app/views/main.html +++ b/public/app/views/main.html @@ -4,6 +4,7 @@

Status

{{ notifications.notification[0].timestamp[0] | date:"EEE yyyy-MM-dd 'at' h:mma" }} +

Global

Operating Mode: {{status.mode[0]}} @@ -62,7 +63,6 @@

{{zone.name[0]}}

-
{{ status.zones[0].zone[selectedZone].name[0] }}
diff --git a/public/app/views/node_tree.html b/public/app/views/node_tree.html new file mode 100644 index 0000000..5dda821 --- /dev/null +++ b/public/app/views/node_tree.html @@ -0,0 +1,21 @@ +
+{{ depth = typeof(depth) == 'number' ? depth : 0 }} +
+ + + + + + + + + + + + + +
{{title}}
{{ k }} + {{v}} + +
+
diff --git a/public/app/views/serial.html b/public/app/views/serial.html index 72191ee..f803b3b 100644 --- a/public/app/views/serial.html +++ b/public/app/views/serial.html @@ -9,22 +9,22 @@

Virtual SAM Request

Stream

- +
- - - - + + + + - - - - - - + + + + + +
srcdstfunctionaddresssrc dst cmd reg
{{ frame.SrcClass }}{{ frame.DstClass }}{{ frame.Function }}{{ frame.data | limitTo:3 | toHex }}
{{ frame.src }}{{ frame.dst }}{{ frame.cmd }}{{ frame.payload_raw | limitTo:3 | toHex }}
@@ -40,26 +40,29 @@

State

Updated - Device - Address + Src + Register Value - + {{ frame.timestamp*1000 | timeAgo }} {{ frame.Device }} - {{ frame.data | limitTo:3 | toHex }} + {{ frame.payload_raw | limitTo:3 | toHex }}

{{ frame.field.name }}

- -
{{ frame.data | strings }}
+ +
{{ frame.payload_raw | strings }}
+
-

+

+

{{ frame.field }}

+
diff --git a/public/dist/index.html b/public/dist/index.html index 78ce716..778d86b 100644 --- a/public/dist/index.html +++ b/public/dist/index.html @@ -22,4 +22,4 @@ stroke: black; fill: blue; font-weight: 100; - } \ No newline at end of file + } \ No newline at end of file diff --git a/public/dist/scripts/b4c9807f.scripts.js b/public/dist/scripts/b4c9807f.scripts.js new file mode 100644 index 0000000..00071c8 --- /dev/null +++ b/public/dist/scripts/b4c9807f.scripts.js @@ -0,0 +1 @@ +"use strict";function wsu(a){var b=window.location;return("https:"===b.protocol?"wss://":"ws://")+b.hostname+(80!==b.port&&443!==b.port?":"+b.port:"")+a}angular.module("infinitude",["ngCookies","ngResource","ngSanitize","ngRoute","yaru22.angular-timeago","angular-dialgauge","jkuri.timepicker","chart.js","angular-toArrayFilter"]).config(["$routeProvider","$locationProvider",function(a,b){a.when("/",{templateUrl:"views/main.html"}).when("/profiles",{templateUrl:"views/profiles.html"}).when("/schedules",{templateUrl:"views/schedules.html"}).when("/serial",{templateUrl:"views/serial.html"}).when("/energy",{templateUrl:"views/energy.html"}).when("/about",{templateUrl:"views/about.html"}).otherwise({redirectTo:"/"}),b.hashPrefix("")}]);var toHex=function(a){return"string"!=typeof a?"":a.split("").map(function(a){return a.charCodeAt(0).toString(16).padStart(2,"0")}).join(" ")},fromHex=function(a){return"string"!=typeof a?"":a.replace(" ","").split(/(\w\w)/g).filter(function(a){return!!a}).map(function(a){return String.fromCharCode(parseInt(a,16))}).join("")};angular.module("infinitude").filter("markDiff",function(){return function(a,b){if(!b)return a;for(var c=!1,d="",e=0;e'),a.charCodeAt(e)===b.charCodeAt(e)&&c===!0&&(c=!1,d+=""),d+=a.substr(e,1);return c&&(d+=""),d}}).filter("subStr",function(){return function(a,b,c){return a?a.substr(b,c):""}}).filter("strings",function(){return function(a,b){if(!a)return"";b=b||4;for(var c=0,d=!1,e="",f="",g=0;g=32&&a.charCodeAt(g)<=127?(e+=a.substr(g,1),c+=1,c>=b&&(d=!0)):(d&&(f+=e+"\n"),c=0,d=!1,e="");return d&&(f+=e),f}}).filter("toHex",function(){return toHex}).filter("fromHex",function(){return fromHex}).filter("toList",function(){return function(a){var b=[];return angular.forEach(a,function(a){b.push(a)}),b}}).controller("MainCtrl",["$scope","$http","$interval","$timeout","$location",function(a,b,c,d,e){const f="#16F",g="#44E",h="#F0F",i="#E44";a.selectedZone=0,a.systemsEdit=null,a.systemsEdited=null,a["typeof"]=function(a){return typeof a},a.mkTime=function(a){return angular.equals({},a)?"00:00":a};var j=angular.fromJson(window.localStorage.getItem("infinitude"))||{};if(a.history){var k=[],l=[[],[]];angular.forEach(a.history.coilTemp[0].values,function(b,c){var d=a.history.coilTemp[0].values.length,e=Math.round(d/20);c%e===0&&(k.push(new Date(1e3*b[0]).toLocaleString()),l[0].push(b[1]),l[1].push(a.history.outsideTemp[0].values[c][1]))}),a.oduLabels=k,a.oduSeries=["CoilTemp","OutsideTemp"],a.oduData=l}var m;a.reloadData=function(c){c&&a.systemsEdited&&confirm("This will erase your unsaved changes")&&(a.systemsEdited=null);var e=["systems","status","notifications","energy"];angular.forEach(e,function(c){a.systemsEdited!==!0&&(a.globeColor=f),b.get("/"+c+".json").then(function(b){var e=c;"systems"===e&&(e="system",(a.systemsEdited===!1||null===a.systemsEdited)&&(a.systemsEdit=angular.copy(b.data[e][0]))),a[c]=j[c]=b.data[e][0],a.systemsEdited!==!0&&(a.globeColor=g),d.cancel(m),m=d(function(){a.globeColor=i},24e4)},function(){a.globeColor=i,console.log("oh noes!",arguments)})}),window.localStorage.setItem("infinitude",angular.toJson(j))},a.$watch("systemsEdit",function(){null!==a.systemsEdit&&(null===a.systemsEdited||angular.equals(a.systems,a.systemsEdit)?(a.systemsEdited=!1,a.globeColor=g):a.systemsEdited===!1&&(a.systemsEdited=!0,a.globeColor=h))},!0),a.reloadData(!1),c(a.reloadData,18e4,0,!0,!1),a.isActive=function(a){return a===e.path()},a.save=function(){a.systems=angular.copy(a.systemsEdit);var c={system:[a.systems]};b.post("/systems/infinitude",c).then(function(){setTimeout(function(){a.reloadData(!1)},1e4),a.systemsEdited=!1,a.globeColor=g},function(){console.log("oh noes! save fail.")})},a.samreq=function(a){console.log(a),b.post("/api/samreq",{register:a}).then(function(a){console.log(a.data.frame_hex)})},a.selectZone=function(b){a.selectedZone=b},a.equals=function(a,b){return angular.equals(a,b)},a.rawSerial="Loading",a.frames=[],a.devices={},a.state=angular.fromJson(window.localStorage.getItem("infinitude-serial-state"))||{};var n=new WebSocket(wsu("/serial"));n.onopen=function(){console.log("Socket open")},n.onclose=function(){console.log("Socket closed"),window.location.reload()},n.onerror=function(a){console.log("Socket error",a)};var o;n.onmessage=function(b){var c=angular.fromJson(b.data);"string"!=typeof c.cmd&&console.log(c),a.transferColor="#4F4",d.cancel(o),d(function(){a.transferColor="#5E5"},2e3);var e=new jDataView(c.payload_raw);if("undefined"==typeof a.carbus&&(a.carbus={}),a.history=angular.fromJson(window.localStorage.getItem("tmpdat"))||{},c.cmd.match(/write|reply/)){var f=c.reg_string;f=f||"",f=f.toUpperCase();var g=c.cmd+c.src+c.dst+f;c.Device="reply"===c.cmd?c.src:c.dst,a.devices[c.Device]=1;var h=function(b,d){a.history[b]=a.history[b]||[{key:b,values:[]}],d=d||a.carbus[b],a.history[b][0].values.push([c.timestamp,d]),a.history[b][0].values.length>500&&a.history[b][0].values.shift(),window.localStorage.setItem("tmpdat",angular.toJson(a.history))};"reply"==c.cmd&&c.src.match(/IndoorUnit/)&&(f.match(/0306/)&&(a.carbus.blowerRPM=e.getInt16(4),h("blowerRPM",a.carbus.blowerRPM)),f.match(/0316/)&&(a.carbus.airflowCFM=e.getInt16(7),h("airflowCFM",a.carbus.airflowCFM))),"reply"==c.cmd&&c.src.match(/OutdoorUnit/)&&(f.match(/0302/)&&(a.carbus.outsideTemp=e.getInt16(5)/16),f.match(/3E01/)&&(a.carbus.outsideTemp=e.getInt16(3)/16,a.carbus.coilTemp=e.getInt16(5)/16,h("coilTemp",a.carbus.coilTem),h("outsideTemp",a.carbus.outsideTemp)));var i=a.state[g]||c;i=i.payload_raw,a.state[g]=a.state[g]||{},angular.extend(a.state[g],c),a.state[g].history=a.state[g].history||[],i!==c.payload_raw&&a.state[g].history.unshift(i),a.state[g].history.length>9&&a.state[g].history.pop(),window.localStorage.setItem("infinitude-serial-state",angular.toJson(a.state))}a.frames.push(c),a.frames.length>9&&a.frames.shift(),a.$apply()}}]); \ No newline at end of file diff --git a/public/dist/scripts/b5a2b1a8.scripts.js b/public/dist/scripts/b5a2b1a8.scripts.js deleted file mode 100644 index c893a2f..0000000 --- a/public/dist/scripts/b5a2b1a8.scripts.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";function wsu(a){var b=window.location;return("https:"===b.protocol?"wss://":"ws://")+b.hostname+(80!==b.port&&443!==b.port?":"+b.port:"")+a}angular.module("infinitude",["ngCookies","ngResource","ngSanitize","ngRoute","yaru22.angular-timeago","angular-dialgauge","jkuri.timepicker","chart.js","angular-toArrayFilter"]).config(["$routeProvider","$locationProvider",function(a,b){a.when("/",{templateUrl:"views/main.html"}).when("/profiles",{templateUrl:"views/profiles.html"}).when("/schedules",{templateUrl:"views/schedules.html"}).when("/serial",{templateUrl:"views/serial.html"}).when("/energy",{templateUrl:"views/energy.html"}).when("/about",{templateUrl:"views/about.html"}).otherwise({redirectTo:"/"}),b.hashPrefix("")}]);var toHex=function(a){for(var b="",c=0;c'),a.charCodeAt(e)===b.charCodeAt(e)&&c===!0&&(c=!1,d+=""),d+=a.substr(e,1);return c&&(d+=""),d}}).filter("subStr",function(){return function(a,b,c){return a?a.substr(b,c):""}}).filter("strings",function(){return function(a,b){b=b||4;for(var c=0,d=!1,e="",f="",g=0;g=32&&a.charCodeAt(g)<=127?(e+=a.substr(g,1),c+=1,c>=b&&(d=!0)):(d&&(f+=e+"\n"),c=0,d=!1,e="");return d&&(f+=e),f}}).filter("toHex",function(){return toHex}).filter("toList",function(){return function(a){var b=[];return angular.forEach(a,function(a){b.push(a)}),b}}).controller("MainCtrl",["$scope","$http","$interval","$timeout","$location",function(a,b,c,d,e){const f="#16F",g="#44E",h="#F0F",i="#E44";a.selectedZone=0,a.systemsEdit=null,a.systemsEdited=null,a.mkTime=function(a){return angular.equals({},a)?"00:00":a};var j=angular.fromJson(window.localStorage.getItem("infinitude"))||{};if(a.history){var k=[],l=[[],[]];angular.forEach(a.history.coilTemp[0].values,function(b,c){var d=a.history.coilTemp[0].values.length,e=Math.round(d/20);c%e===0&&(k.push(new Date(1e3*b[0]).toLocaleString()),l[0].push(b[1]),l[1].push(a.history.outsideTemp[0].values[c][1]))}),a.oduLabels=k,a.oduSeries=["CoilTemp","OutsideTemp"],a.oduData=l}var m;a.reloadData=function(c){c&&a.systemsEdited&&confirm("This will erase your unsaved changes")&&(a.systemsEdited=null);var e=["systems","status","notifications","energy"];angular.forEach(e,function(c){a.systemsEdited!==!0&&(a.globeColor=f),b.get("/"+c+".json").then(function(b){var e=c;"systems"===e&&(e="system",(a.systemsEdited===!1||null===a.systemsEdited)&&(a.systemsEdit=angular.copy(b.data[e][0]))),a[c]=j[c]=b.data[e][0],a.systemsEdited!==!0&&(a.globeColor=g),d.cancel(m),m=d(function(){a.globeColor=i},24e4)},function(){a.globeColor=i,console.log("oh noes!",arguments)})}),window.localStorage.setItem("infinitude",angular.toJson(j))},a.$watch("systemsEdit",function(){null!==a.systemsEdit&&(null===a.systemsEdited||angular.equals(a.systems,a.systemsEdit)?(a.systemsEdited=!1,a.globeColor=g):a.systemsEdited===!1&&(a.systemsEdited=!0,a.globeColor=h))},!0),a.reloadData(!1),c(a.reloadData,18e4,0,!0,!1),a.isActive=function(a){return a===e.path()},a.save=function(){a.systems=angular.copy(a.systemsEdit);var c={system:[a.systems]};b.post("/systems/infinitude",c).then(function(){setTimeout(function(){a.reloadData(!1)},1e4),a.systemsEdited=!1,a.globeColor=g},function(){console.log("oh noes! save fail.")})},a.samreq=function(a){console.log(a),b.post("/api/samreq",{register:a}).then(function(a){console.log(a.data.frame_hex)})},a.selectZone=function(b){a.selectedZone=b},a.equals=function(a,b){return angular.equals(a,b)},a.rawSerial="Loading",a.frames=[],a.devices={},a.state=angular.fromJson(window.localStorage.getItem("infinitude-serial-state"))||{};var n=new WebSocket(wsu("/serial"));n.onopen=function(){console.log("Socket open")},n.onclose=function(){console.log("Socket closed"),window.location.reload()},n.onerror=function(a){console.log("Socket error",a)};var o;n.onmessage=function(b){var c=angular.fromJson(b.data);a.transferColor="#4F4",d.cancel(o),d(function(){a.transferColor="#5E5"},2e3);var e=new jDataView(c.data);if("undefined"==typeof a.carbus&&(a.carbus={}),a.history=angular.fromJson(window.localStorage.getItem("tmpdat"))||{},c.Function.match(/write|reply/)){var f=toHex(c.data.substring(0,3)),g=c.Function+c.SrcClass+c.DstClass+f;c.Device="reply"===c.Function?c.SrcClass:c.DstClass,a.devices[c.Device]=1;var h=function(b,d){a.history[b]=a.history[b]||[{key:b,values:[]}],d=d||a.carbus[b],a.history[b][0].values.push([c.timestamp,d]),a.history[b][0].values.length>500&&a.history[b][0].values.shift(),window.localStorage.setItem("tmpdat",angular.toJson(a.history))};"reply"==c.Function&&c.SrcClass.match(/IndoorUnit/)&&(f.match(/00 03 06/)&&(a.carbus.blowerRPM=e.getInt16(4),h("blowerRPM",a.carbus.blowerRPM)),f.match(/00 03 16/)&&(a.carbus.airflowCFM=e.getInt16(7),h("airflowCFM",a.carbus.airflowCFM))),"reply"==c.Function&&c.SrcClass.match(/OutdoorUnit/)&&(f.match(/00 03 02/)&&(a.carbus.outsideTemp=e.getInt16(5)/16),f.match(/00 3E 01/)&&(a.carbus.outsideTemp=e.getInt16(3)/16,a.carbus.coilTemp=e.getInt16(5)/16,h("coilTemp",a.carbus.coilTem),h("outsideTemp",a.carbus.outsideTemp)));var i=a.state[g]||c;i=i.data,a.state[g]=a.state[g]||{},angular.extend(a.state[g],c),a.state[g].history=a.state[g].history||[],i!==c.data&&a.state[g].history.unshift(i),a.state[g].history.length>9&&a.state[g].history.pop(),window.localStorage.setItem("infinitude-serial-state",angular.toJson(a.state))}a.frames.push(c),a.frames.length>9&&a.frames.shift(),a.$apply()}}]); \ No newline at end of file diff --git a/public/dist/views/main.html b/public/dist/views/main.html index a0bd308..2a4a477 100644 --- a/public/dist/views/main.html +++ b/public/dist/views/main.html @@ -1 +1 @@ -

Status

{{ notifications.notification[0].message[0] }} {{ notifications.notification[0].timestamp[0] | date:"EEE yyyy-MM-dd 'at' h:mma" }}

Global

Operating Mode: {{status.mode[0]}}

{{zone.name[0]}}

{{ status.zones[0].zone[selectedZone].name[0] }}
{{key}}{{ value[0] }}
{{key}}{{ value[0] }}
\ No newline at end of file +

Status

{{ notifications.notification[0].message[0] }} {{ notifications.notification[0].timestamp[0] | date:"EEE yyyy-MM-dd 'at' h:mma" }}

Global

Operating Mode: {{status.mode[0]}}

{{zone.name[0]}}

{{key}}{{ value[0] }}
{{key}}{{ value[0] }}
\ No newline at end of file diff --git a/public/dist/views/node_tree.html b/public/dist/views/node_tree.html new file mode 100644 index 0000000..0145b68 --- /dev/null +++ b/public/dist/views/node_tree.html @@ -0,0 +1 @@ +
{{ depth = typeof(depth) == 'number' ? depth : 0 }}
{{title}}
{{ k }}{{v}}
\ No newline at end of file diff --git a/public/dist/views/serial.html b/public/dist/views/serial.html index 9782c9e..8d13a5c 100644 --- a/public/dist/views/serial.html +++ b/public/dist/views/serial.html @@ -1 +1 @@ -

Virtual SAM Request

Thermostat Register

Stream

srcdstfunctionaddress
{{ frame.SrcClass }}{{ frame.DstClass }}{{ frame.Function }}{{ frame.data | limitTo:3 | toHex }}

State

UpdatedDeviceAddressValue
{{ frame.timestamp*1000 | timeAgo }}{{ frame.Device }}{{ frame.data | limitTo:3 | toHex }}

{{ frame.field.name }}


{{ frame.data | strings }}

{{ frame.field }}

\ No newline at end of file +

Virtual SAM Request

Thermostat Register

Stream

srcdstcmdreg
{{ frame.src }}{{ frame.dst }}{{ frame.cmd }}{{ frame.payload_raw | limitTo:3 | toHex }}

State

UpdatedSrcRegisterValue
{{ frame.timestamp*1000 | timeAgo }}{{ frame.Device }}{{ frame.payload_raw | limitTo:3 | toHex }}

{{ frame.field.name }}


{{ frame.payload_raw | strings }}

{{ frame.field }}

\ No newline at end of file