2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-02-26 10:34:52 +00:00

37_NINJA, 37:NinjaPiCrust: Add preliminary modules to contrib

git-svn-id: https://svn.fhem.de/fhem/trunk@8564 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
gandy92 2015-05-11 06:28:55 +00:00
parent 5d4ac02590
commit f07849bc1a
2 changed files with 1492 additions and 0 deletions

670
fhem/contrib/37_NINJA.pm Normal file
View File

@ -0,0 +1,670 @@
# $Id: $
#
# TODO:
package main;
my $dl = 4;
##########################
# This block is only needed when FileLog is checked outside fhem
#
sub Log3($$$);
sub Log($$);
sub RemoveInternalTimer($);
use vars qw(%attr);
use vars qw(%defs);
use vars qw(%modules);
use vars qw($readingFnAttributes);
use vars qw($reread_active);
##########################
use strict;
use warnings;
use SetExtensions;
use vars qw(%ninjaDevice);
use vars qw(%ninjaTypes);
use vars qw(%ninjaGroups);
sub NINJA_Parse($$);
sub NINJA_Send($$@);
sub
NINJA_Initialize($)
{
my ($hash) = @_;
$hash->{Match} = "^.+";
$hash->{SetFn} = "NINJA_Set";
#$hash->{GetFn} = "NINJA_Get";
$hash->{DefFn} = "NINJA_Define";
$hash->{UndefFn} = "NINJA_Undef";
$hash->{FingerprintFn} = "NINJA_Fingerprint";
$hash->{ParseFn} = "NINJA_Parse";
$hash->{AttrFn} = "NINJA_Attr";
$hash->{AttrList} = "IODev"
." readonly:1"
." forceOn:1"
." $readingFnAttributes";
}
sub
NINJA_Define__($$)
{
my ($hash, $def) = @_;
my @a = split("[ \t][ \t]*", $def);
if(@a != 4 ) {
my $msg = "wrong syntax: define <name> NINJA <g> <vendor>";
Log3 undef, 2, $msg;
return $msg;
}
$a[2] =~ m/^([\da-f]{6})$/i;
return "$a[2] is not a valid NINJA address" if( !defined($1) );
$a[3] =~ m/^([\da-f]{2})$/i;
return "$a[3] is not a valid NINJA channel" if( !defined($1) );
my $name = $a[0];
my $addr = $a[2];
my $channel = $a[3];
#return "$addr is not a 1 byte hex value" if( $addr !~ /^[\da-f]{2}$/i );
#return "$addr is not an allowed address" if( $addr eq "00" );
return "NINJA device $addr already used for $modules{NINJA}{defptr}{$addr}->{NAME}." if( $modules{NINJA}{defptr}{$addr}
&& $modules{NINJA}{defptr}{$addr}->{NAME} ne $name );
$hash->{addr} = $addr;
$hash->{channel} = $channel;
$modules{NINJA}{defptr}{$addr} = $hash;
AssignIoPort($hash);
if(defined($hash->{IODev}->{NAME})) {
Log3 $name, 3, "$name: I/O device is " . $hash->{IODev}->{NAME};
} else {
Log3 $name, 1, "$name: no I/O device";
}
#$attr{$name}{devStateIcon} = 'on:on:toggle off:off:toggle set.*:light_question:off' if( !defined( $attr{$name}{devStateIcon} ) );
#$attr{$name}{webCmd} = 'on:off:toggle:statusRequest' if( !defined( $attr{$name}{webCmd} ) );
#CommandAttr( undef, "$name userReadings consumptionTotal:consumption monotonic {ReadingsVal(\$name,'consumption',0)}" ) if( !defined( $attr{$name}{userReadings} ) );
#NINJA_Send($hash, $addr, "00" );
return undef;
}
sub
NINJA_Define($$)
{
my ($hash, $def) = @_;
my @a = split("[ \t][ \t]*", $def);
Log3 undef, 0, "NINJA_define: $def";
if(@a != 3 ) {
my $msg = "wrong syntax: define <name> NINJA <addr>";
Log3 undef, 2, $msg;
return $msg;
}
$a[2] =~ m/^(.+)$/i;
return "$a[2] is not a valid NINJA address" if( !defined($1) );
my $name = $a[0];
my $addr = $a[2];
#return "$addr is not a 1 byte hex value" if( $addr !~ /^[\da-f]{2}$/i );
return "$addr is not an allowed address" if( $addr eq "0" );
return "NINJA device $addr already used for $modules{NINJA}{defptr}{$addr}->{NAME}."
if( $modules{NINJA}{defptr}{$addr} && $modules{NINJA}{defptr}{$addr}->{NAME} ne $name );
$hash->{addr} = $addr;
$modules{NINJA}{defptr}{$addr} = $hash;
AssignIoPort($hash);
if(defined($hash->{IODev}->{NAME})) {
Log3 $name, 3, "$name: I/O device is " . $hash->{IODev}->{NAME};
} else {
Log3 $name, 1, "$name: no I/O device";
}
#$attr{$name}{devStateIcon} = 'on:on:toggle off:off:toggle *.:light_question:off' if( !defined( $attr{$name}{devStateIcon} ) );
#$attr{$name}{webCmd} = 'on:off:toggle:statusRequest' if( !defined( $attr{$name}{webCmd} ) );
#CommandAttr( undef, "$name userReadings consumptionTotal:consumption monotonic {ReadingsVal(\$name,'consumption',0)}" ) if( !defined( $attr{$name}{userReadings} ) );
return undef;
}
#####################################
sub
NINJA_Undef($$)
{
my ($hash, $arg) = @_;
my $name = $hash->{NAME};
my $addr = $hash->{addr};
delete( $modules{NINJA}{defptr}{$addr} );
return undef;
}
#####################################
sub
NINJA_Set($@)
{
my ($hash, $name, @aa) = @_;
my $cnt = @aa;
return "\"set $name\" needs at least one parameter" if($cnt < 1);
my $cmd = $aa[0];
my $arg = $aa[1];
my $arg2 = $aa[2];
my $arg3 = $aa[3];
my $readonly = AttrVal($name, "readonly", "0" );
#my $list = "identify:noArg reset:noArg statusRequest:noArg";
#$list .= " off:noArg on:noArg toggle:noArg" if( !$readonly );
my $list = "";
if( $cmd eq 'toggle' ) {
$cmd = ReadingsVal($name,"state","on") eq "off" ? "on" :"off";
}
if( !$readonly && $cmd eq 'off' ) {
readingsSingleUpdate($hash, "state", "set-$cmd", 1);
#NINJA_Send( $hash, 0x05, 0x00 );
} elsif( !$readonly && $cmd eq 'on' ) {
readingsSingleUpdate($hash, "state", "set-$cmd", 1);
#NINJA_Send( $hash, 0x05, 0x01 );
} elsif( $cmd eq 'statusRequest' ) {
readingsSingleUpdate($hash, "state", "set-$cmd", 1);
#NINJA_Send( $hash, 0x04, 0x00 );
} elsif( $cmd eq 'reset' ) {
readingsSingleUpdate($hash, "state", "set-$cmd", 1);
#NINJA_Send( $hash, 0x04, 0x01 );
#} elsif( $cmd eq 'identify' ) {
# NINJA_Send( $hash, 0x06, 0x00 );
} elsif ($cmd eq 'offset' ) {
if (defined $arg2) {
readingsSingleUpdate($hash, ".offset.$arg", $arg2, 0);
} else {
delete $hash->{READINGS}{".offset.$arg"};
}
} else {
#TODO: understand
return SetExtensions($hash, $list, $name, @aa);
}
return undef;
}
#####################################
sub
NINJA_Get($@)
{
my ($hash, $name, $cmd, @args) = @_;
return "\"get $name\" needs at least one parameter" if(@_ < 3);
my $list = "";
return "Unknown argument $cmd, choose one of $list";
}
sub
NINJA_Fingerprint($$)
{
my ($name, $msg) = @_;
return ( "", $msg );
}
sub
NINJA_ForceOn($)
{
my ($hash) = @_;
#NINJA_Send( $hash, 0x05, 0x01 );
}
sub
NINJA_Parse($$)
{
my ($hash, $msg) = @_;
my $name = $hash->{NAME};
#return undef if( $msg !~ m/^[\dA-F]{12,}$/ );
if (0) { ##---------------------------------------------------
if( $msg =~ m/^L/ ) {
my @parts = split( ' ', substr($msg, 5), 4 );
$msg = "OK 24 $parts[3]";
}
my( @bytes, $channel,$cmd,$addr,$data,$power,$consumption );
if( $msg =~ m/^OK/ ) {
@bytes = split( ' ', substr($msg, 6) );
$channel = sprintf( "%02X", $bytes[0] );
$cmd = $bytes[1];
$addr = sprintf( "%02X%02X%02X", $bytes[2], $bytes[3], $bytes[4] );
$data = $bytes[5];
return "" if( $cmd == 0x04 && $bytes[6] == 170 && $bytes[7] == 170 && $bytes[8] == 170 && $bytes[9] == 170 ); # ignore commands from display unit
return "" if( $cmd == 0x05 && ( $bytes[6] != 170 || $bytes[7] != 170 || $bytes[8] != 170 || $bytes[9] != 170 ) ); # ignore commands not from the plug
} elsif ( $msg =~ m/^TX/ ) {
# ignore TX
return "";
} else {
DoTrigger($name, "UNKNOWNCODE $msg");
Log3 $name, 3, "$name: Unknown code $msg, help me!";
return "";
}
} #------------------------------------------------
my $jsonref = NinjaPiCrust_ParseJSON($msg);
my %datagram = %$jsonref;
#Log3 $name, $dl, "NinjaPiCrust_Parse: \%datagram is @{[%datagram]}";
my $msgtype = (keys %datagram)[0];
Log3 $name, $dl, "$name: got message type '$msgtype'";
my %data = %{$datagram{$msgtype}[0]};
$data{MSGTYPE} = $msgtype;
Log3 $name, $dl, "$name: Got $msgtype $data{G} $data{V} $data{D} $data{DA} from $msg"
if (defined $data{G} and defined $data{V} and defined $data{D} and defined $data{DA});
$hash->{"${name}_MSGCNT"}++;
$hash->{"${name}_TIME"} = TimeNow();
$hash->{RAWMSG} = $msg;
# from here on, adhere to PCA301 logic for now:
my $raddr = $data{G};
my $rhash = $modules{NINJA}{defptr}{$raddr};
my $rname = $rhash?$rhash->{NAME}:$raddr;
if ( !$modules{NINJA}{defptr}{$raddr} ) {
Log3 $name, 3, "NINJA Unknown device $rname, please define it";
return "UNDEFINED NINJA_$rname NINJA $raddr";# $channel";
}
#CommandAttr( undef, "$rname userReadings consumptionTotal:consumption monotonic {ReadingsVal($rname,'consumption',0)}" ) if( !defined( $attr{$rname}{userReadings} ) );
my @list;
push(@list, $rname);
$rhash->{NINJA_lastRcv} = TimeNow();
Log3 $rhash, $dl, "$rname: identified module, commencing";
#if( $rhash->{channel} ne $channel ) {
# Log3 $rname, 3, "NINJA $rname, channel changed from $rhash->{channel} to $channel";
#
# $rhash->{channel} = $channel;
# $rhash->{DEF} = "$rhash->{addr} $rhash->{channel}";
# CommandSave(undef,undef) if( AttrVal( "autocreate", "autosave", 1 ) );
#}
my $readonly = AttrVal($rname, "readonly", "0" );
my $state = "";
#if( $cmd eq 0x04 ) {
# $state = $data==0x00?"off":"on";
# my $power = ($bytes[6]*256 + $bytes[7]) / 10.0;
# my $consumption = ($bytes[8]*256 + $bytes[9]) / 100.0;
# my $state = $state; $state = $power if( $readonly );
# readingsBeginUpdate($rhash);
# readingsBulkUpdate($rhash, "power", $power) if( $power != ReadingsVal($rname,"power",0) );
# readingsBulkUpdate($rhash, "consumption", $consumption) if( $consumption != ReadingsVal($rname,"consumption",0) );
# readingsBulkUpdate($rhash, "state", $state) if( $state ne ReadingsVal($rname,"state","") );
# readingsEndUpdate($rhash,1);
#} elsif( $cmd eq 0x05 ) {
# $state = $data==0x00?"off":"on";
#
# readingsSingleUpdate($rhash, "state", $state, 1)
#}
if( AttrVal($rname, "forceOn", 0 ) == 1
&& $state eq "off" ) {
readingsSingleUpdate($rhash, "state", "set-forceOn", 1);
InternalTimer(gettimeofday()+3, "NINJA_ForceOn", $rhash, 0);
}
my $key = "$data{V}:$data{D}";
unless (exists $ninjaDevice{$key}) {
Log3 $rname, 0, "$rname: unknown VID:DID '$key'";
return;
}
my %ndev = %{$ninjaDevice{$key}};
unless ($ndev{sens}) {
Log3 $rname, 0, "$rname: Not a sensor: VID:DID '$key' ($ndev{hint})";
return;
}
if (!exists $ninjaTypes{$ndev{type}}) {
Log3 $rname, 0, "$rname: Unsupported sensor VID:DID '$key' ($ndev{hint})";
readingsSingleUpdate($rhash, $ndev{hint}, $data{DA}, 1);
return;
} else {
# figure out which reding we are actually dealing with
# and create update event.
my %ntype = %{$ninjaTypes{$ndev{type}}};
my $ntname = $ntype{name};
my $now = gettimeofday();
$rhash->{".reading.timestamp.$ntname"} = $now;
my $val = $data{DA};
my $offset = ReadingsVal($rname,".offset.$ntname",0.0);
$val = $val + $offset unless ($offset == 0);
my $fmt = $ntype{format};
$val = sprintf($fmt,$val) if (defined $fmt);
readingsSingleUpdate($rhash, $ntname, $val, 1);
# if we expect more than one reading as defined by a group,
# check if we have enough information to compile the group reading
#
# TODO: In this case we may want to deferr the individual updates
# and use readingsBulkUpdate, instead
if (exists $ntype{group} and exists $ninjaGroups{$ntype{group}}) {
my %ngroup = %{$ninjaGroups{$ntype{group}}};
Log3 $rname, $dl, "$rname: reading '$ntype{name}' is of group '$ntype{group}'";
my $format = $ngroup{format};
my $reading = $format;
my $valid = 1;
while ($format =~ /\{([^\}]+)\}/g) {
my $rn = $1;
Log3 $rname, $dl, "$rname: found in template: $rn";
if (exists $rhash->{".reading.timestamp.$rn"}) {
my $rval = ReadingsVal($rname,$rn,undef);
$reading =~ s/\{$rn\}/$rval/;
$valid = 0 unless (($now - $rhash->{".reading.timestamp.$rn"}) < 0.5);
} else {
$valid = 0;
}
}
Log3 $rname, $dl, "$rname: got '$reading' from '$format'";
readingsSingleUpdate($rhash, $ngroup{name}, $reading, 1) if ($valid);
}
}
return @list;
}
%ninjaTypes = (
"temperature" => { name=>"temperature", group=>"TD", format=>"%.1f" },
"humidity" => { name=>"humidity", group=>"TD" }
);
%ninjaGroups = (
"TD" => { name=>"state", format=>"T: {temperature} H: {humidity}" }
);
#
# from devids.cvs :
# VID;DID;Device Type;Default Name;State;Actuator;Sensor;Silent;Has Sub Device;Time Series data
# cat ~/ninja/devids.csv | tr "; \r" "\t_ " |while read a b c d e f g h i j k; do key="\"$a:$b\" " key=${key:0:10}; type="\"$c\" "; type=${type:0:25}; echo "$key => {st=>$e, act=>$f, sens=>$g, sil=>$h, sub=>$i, tds=>$j, type=>$type, hint=>\"$d\" },"; done
#
# key is VID:DID
# type:Device Type
# hint:Default Name
# st:State
# act:Actuator
# sens:Sensor
# sil:Silent
# sub:Has Sub Device
# tsd:Time Series data
%ninjaDevice = (
"0:1" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"temperature" , hint=>"Block_Temperature" },
"0:2" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"linear_acceleration" , hint=>"Block_Accelerometer" },
"0:3" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"jiggle" , hint=>"Block_Jiggle" },
"0:5" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"button" , hint=>"Push_Button" },
"0:6" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"light_level" , hint=>"Light_Sensor" },
"0:7" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"pir" , hint=>"PIR_Motion_Sensor" },
"0:8" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"humidity" , hint=>"Humidity" },
"0:9" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"temperature" , hint=>"Temperature" },
"0:11" => {st=>0, act=>1, sens=>1, sil=>0, sub=>1, tds=>0, type=>"rf433" , hint=>"RF_433Mhz" },
"0:12" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"sound" , hint=>"Sound_Sensor" },
"0:13" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"temperature" , hint=>"La_Crosse_Temp_TX3/6" },
"0:14" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"hid" , hint=>"Unknown_HID_Device" },
"0:20" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"temperature" , hint=>"La_Crosse_Temp_WS2355" },
"0:21" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"humidity" , hint=>"La_Crosse_Humidity_WS2355" },
"0:22" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"rainfall" , hint=>"La_Crosse_Rainfall_WS2355" },
"0:23" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"direction" , hint=>"La_Crosse_Wind_Direction" },
"0:24" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"speed" , hint=>"La_Crosse_Wind_Speed" },
"0:30" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"humidity" , hint=>"Humidity" },
"0:31" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"temperature" , hint=>"Temperature" },
"0:101" => {st=>0, act=>1, sens=>1, sil=>1, sub=>0, tds=>0, type=>"twitter" , hint=>"Twitter" },
"0:102" => {st=>0, act=>1, sens=>1, sil=>1, sub=>0, tds=>0, type=>"facebook" , hint=>"Facebook" },
"0:103" => {st=>0, act=>1, sens=>1, sil=>1, sub=>0, tds=>0, type=>"sms" , hint=>"SMS" },
"0:104" => {st=>0, act=>1, sens=>1, sil=>1, sub=>0, tds=>0, type=>"dropbox" , hint=>"Dropbox" },
"0:105" => {st=>0, act=>1, sens=>1, sil=>1, sub=>0, tds=>0, type=>"googledrive" , hint=>"Google_Drive" },
"0:106" => {st=>0, act=>1, sens=>1, sil=>1, sub=>0, tds=>0, type=>"email" , hint=>"Email" },
"0:107" => {st=>0, act=>1, sens=>1, sil=>1, sub=>0, tds=>0, type=>"salesforce" , hint=>"Salesforce" },
"0:108" => {st=>0, act=>1, sens=>1, sil=>1, sub=>1, tds=>0, type=>"webhook" , hint=>"Webhook" },
"0:200" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"button" , hint=>"Push_Button" },
"0:201" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"momentary_switch" , hint=>"Reed_Switch" },
"0:202" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"temperature" , hint=>"Temperature" },
"0:203" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"humidity" , hint=>"Humidity" },
"0:204" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"pir" , hint=>"PIR_Motion_Sensor" },
"0:205" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"switch_sensor" , hint=>"Switch_Sensor" },
"0:206" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"switch_actuator" , hint=>"Switch_Actuator" },
"0:207" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>1, type=>"switch" , hint=>"Switch" },
"0:208" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"orientation" , hint=>"Orientation" },
"0:209" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"jiggle" , hint=>"Jiggle" },
"0:210" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"gesture" , hint=>"Gesture" },
"0:211" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"keyboard" , hint=>"Keyboard" },
"0:212" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"code_reader" , hint=>"Barcode_Scanner" },
"0:213" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"code_reader" , hint=>"QR_code_Scanner" },
"0:214" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"distance" , hint=>"Distance" },
"0:215" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"sound" , hint=>"Sound" },
"0:216" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"microphone" , hint=>"Microphone" },
"0:217" => {st=>0, act=>0, sens=>1, sil=>0, sub=>1, tds=>0, type=>"hid" , hint=>"HID_Device" },
"0:218" => {st=>0, act=>0, sens=>1, sil=>0, sub=>1, tds=>0, type=>"rfid" , hint=>"RFID_Reader" },
"0:219" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"proximity" , hint=>"Proximity_Sensor" },
"0:220" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"camera_still" , hint=>"Camera" },
"0:221" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"camera_video" , hint=>"Video_Camera" },
"0:222" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"compass" , hint=>"Compass" },
"0:223" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"location" , hint=>"Location" },
"0:224" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"light" , hint=>"Light" },
"0:225" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"moisture" , hint=>"Moisture" },
"0:226" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"ph" , hint=>"pH_Sensor" },
"0:227" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"geiger" , hint=>"Geiger_Counter" },
"0:228" => {st=>0, act=>1, sens=>1, sil=>0, sub=>1, tds=>0, type=>"rf" , hint=>"RF_Transceiver" },
"0:229" => {st=>0, act=>1, sens=>1, sil=>0, sub=>1, tds=>0, type=>"zigbee" , hint=>"Zigbee_Transceiver" },
"0:230" => {st=>0, act=>1, sens=>1, sil=>0, sub=>1, tds=>0, type=>"zwave" , hint=>"Z-wave_Transceiver" },
"0:231" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"alarm" , hint=>"Alarm" },
"0:232" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"speaker" , hint=>"Speaker" },
"0:233" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"light_onoff" , hint=>"Light" },
"0:233" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"light_switch" , hint=>"Light" },
"0:234" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"light_dim" , hint=>"Light_(Dimmable)" },
"0:235" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"rgbled8" , hint=>"RGB_Light_(Basic)" },
"0:236" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"rgbled" , hint=>"RGB_Light" },
"0:237" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"servo" , hint=>"Servo" },
"0:238" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"relay" , hint=>"Relay" },
"0:239" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"stepper" , hint=>"Stepper" },
"0:240" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"display_text" , hint=>"Text_Display" },
"0:241" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"display_image" , hint=>"Image_Display" },
"0:242" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"energy" , hint=>"Energy" },
"0:243" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"power" , hint=>"Power" },
"0:244" => {st=>1, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"state" , hint=>"Generic_State_Device" },
"0:255" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"screen_capture" , hint=>"Screen_Capture" },
"0:256" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"screen_capture" , hint=>"Mac_Screen_Capture" },
"0:260" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"presence" , hint=>"Presence" },
"0:261" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"presence" , hint=>"Presence_-_Wifi_AP" },
"0:262" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"presence" , hint=>"Presence_-_Wifi_Client" },
"0:263" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"presence" , hint=>"Presence_-_Bluetooth" },
"0:264" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"presence" , hint=>"Presence_-_USB" },
"0:265" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"presence" , hint=>"Presence_-_IP" },
"0:266" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"presence" , hint=>"Presence_-_UPNP" },
"0:267" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"presence" , hint=>"Presence_-_Zeroconf" },
"0:268" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"presence" , hint=>"Presence_-_MAC" },
"0:269" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"presence" , hint=>"Presence_-_Xbox_Live" },
"0:280" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"mediaplayer" , hint=>"Media_Player" },
"0:281" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"mediaplayer" , hint=>"Media_Player_-_Xbmc" },
"0:282" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"mediaplayer" , hint=>"Media_Player_-_VLC" },
"0:283" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"mediaplayer" , hint=>"Media_Player_-_iTunes" },
"0:284" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"mediaplayer" , hint=>"Media_Player_-_Spotify" },
"0:300" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"openurl" , hint=>"Open_URL" },
"0:310" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"notification" , hint=>"Notification" },
"0:311" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"notification" , hint=>"Mac_Notification" },
"0:320" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"lock-screen" , hint=>"Lock_Screen" },
# "0:500" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"location" , hint=>"Browser_GPS" },
# "0:500" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"cpu" , hint=>"CPU_Usage" },
"0:501" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"cpu" , hint=>"NinjaBlock_CPU_Usage" },
"0:502" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"cpu" , hint=>"Mac_CPU_Usage" },
"0:503" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"cpu" , hint=>"Raspberry_Pi_CPU_Usage" },
"0:510" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"temperature" , hint=>"CPU_Temperature" },
"0:511" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"temperature" , hint=>"Raspberry_Pi_CPU_Temperature" },
"0:512" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"temperature" , hint=>"NinjaBlock_CPU_Temperature" },
"0:513" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"temperature" , hint=>"Mac_CPU_Temperature" },
"0:520" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"ram" , hint=>"RAM_Usage" },
"0:521" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"ram" , hint=>"NinjaBlock_RAM_Usage" },
"0:522" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"ram" , hint=>"Mac_RAM_Usage" },
"0:523" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"ram" , hint=>"Raspberry_Pi_RAM_Usage" },
"0:530" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"network-activity" , hint=>"Incoming_Network_Activity" },
"0:531" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"network-activity" , hint=>"NinjaBlock_Incoming_Network_Activity" },
"0:532" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"network-activity" , hint=>"Mac_Incoming_Network_Activity" },
"0:533" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"network-activity" , hint=>"Raspberry_Pi_Incoming_Network_Activity" },
"0:540" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"network-activity" , hint=>"Outgoing_Network_Activity" },
"0:541" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"network-activity" , hint=>"NinjaBlock_Outgoing_Network_Activity" },
"0:542" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"network-activity" , hint=>"Mac_Outgoing_Network_Activity" },
"0:543" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"network-activity" , hint=>"Raspberry_Pi_Outgoing_Network_Activity" },
"0:550" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"battery" , hint=>"Battery" },
"0:551" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"battery" , hint=>"Mac_Battery" },
"0:600" => {st=>1, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"ias_zone" , hint=>"IAS_Zone" },
"0:999" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"rgbled" , hint=>"On_Board_RGB_LED_v2" },
# "0:999" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"rgbled8" , hint=>"Status_Light" },
"0:1000" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"rgbled8" , hint=>"On_Board_RGB_LED" },
"0:1002" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>1, type=>"relay" , hint=>"Relay_Board" },
"0:1003" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"system" , hint=>"Arduino_Version" },
"0:1004" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"webcam" , hint=>"Web_Cam" },
"0:1005" => {st=>0, act=>1, sens=>1, sil=>1, sub=>0, tds=>0, type=>"network" , hint=>"Network" },
"0:1006" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"speech" , hint=>"USB_Text_to_Speech" },
"0:1007" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"rgbled" , hint=>"Nina's_Eyes" },
"0:1008" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"light" , hint=>"Philips_Hue" },
"0:1009" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"relay" , hint=>"Belkin_WeMo_Socket" },
"0:1010" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"light" , hint=>"ZigBee_Light" },
"0:1011" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"light" , hint=>"Limitless_LED_RGB" },
"0:1012" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"light" , hint=>"Limitless_LED_White" },
"0:1020" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"speech" , hint=>"Text-to-Speech" },
"0:1021" => {st=>0, act=>1, sens=>0, sil=>0, sub=>0, tds=>0, type=>"speech" , hint=>"Mac_Text-to-Speech" },
"0:2000" => {st=>0, act=>1, sens=>1, sil=>0, sub=>1, tds=>1, type=>"sandbox" , hint=>"Sandbox" },
"0:3680" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"html" , hint=>"HTML" },
"0:7000" => {st=>0, act=>1, sens=>0, sil=>1, sub=>0, tds=>0, type=>"matrix_display" , hint=>"LED_Board" },
"0:9001" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"metric" , hint=>"Connected_Blocks" },
"0:9002" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"metric" , hint=>"Redis_Response_Time" },
"0:9003" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"metric" , hint=>"MySQL_Response_Time" },
"0:10000" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"led" , hint=>"Browser_LED" },
"2:9714" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"airconditioner" , hint=>"Air_Conditioner" },
"3:1" => {st=>0, act=>1, sens=>1, sil=>0, sub=>0, tds=>0, type=>"relay" , hint=>"Power_Socket_Switch" },
"3:2" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"power" , hint=>"Power_Usage" },
"3:3" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"switch_sensor" , hint=>"NetVox_Switch" },
"3:11" => {st=>0, act=>1, sens=>1, sil=>0, sub=>1, tds=>0, type=>"rf433" , hint=>"Camera_Control" },
"4:2" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"meeting_length" , hint=>"Meeting_Length" },
"4:3" => {st=>0, act=>0, sens=>1, sil=>0, sub=>0, tds=>1, type=>"room_utilisation" , hint=>"Room_Utilisation" },
"4:4" => {st=>1, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"battery_alarm" , hint=>"Battery_Alarm" },
"4:5" => {st=>1, act=>0, sens=>1, sil=>0, sub=>0, tds=>0, type=>"alarm" , hint=>"Zone_Alarm" }
);
sub
NINJA_Send($$@)
{
my ($hash, $cmd, $data) = @_;
$hash->{NINJA_lastSend} = TimeNow();
my $msg = sprintf( "%i,%i,%i,%i,%i,%i,255,255,255,255s", hex($hash->{channel}),
$cmd,
hex(substr($hash->{addr},0,2)), hex(substr($hash->{addr},2,2)), hex(substr($hash->{addr},4,2)),
$data );
IOWrite( $hash, $msg );
}
sub
NINJA_Attr(@)
{
my ($cmd, $name, $attrName, $attrVal) = @_;
return undef;
}
1;
=pod
=begin html
<a name="NINJA"></a>
<h3>NINJA</h3>
<ul>
<tr><td>
The NINJA is a RF controlled AC mains plug with integrated power meter functionality from ELV.<br><br>
It can be integrated in to FHEM via a <a href="#JeeLink">JeeLink</a> as the IODevice.<br><br>
The JeeNode sketch required for this module can be found in .../contrib/arduino/36_NINJA-pcaSerial.zip.<br><br>
<a name="NINJADefine"></a>
<b>Define</b>
<ul>
<code>define &lt;name&gt; NINJA &lt;addr&gt; &lt;channel&gt;</code> <br>
<br>
addr is a 6 digit hex number to identify the NINJA device.
channel is a 2 digit hex number to identify the NINJA device.<br><br>
Note: devices are autocreated on reception of the first message.<br>
</ul>
<br>
<a name="NINJA_Set"></a>
<b>Set</b>
<ul>
<li>on</li>
<li>off</li>
<li>identify<br>
Blink the status led for ~5 seconds.</li>
<li>reset<br>
Reset consumption counters</li>
<li>statusRequest<br>
Request device status update.</li>
<li><a href="#setExtensions"> set extensions</a> are supported.</li>
</ul><br>
<a name="NINJA_Get"></a>
<b>Get</b>
<ul>
</ul><br>
<a name="NINJA_Readings"></a>
<b>Readings</b>
<ul>
<li>power</li>
<li>consumption</li>
<li>consumptionTotal<br>
will be created as a default user reading to have a continous consumption value that is not influenced
by the regualar reset or overflow of the normal consumption reading</li>
</ul><br>
<a name="NINJA_Attr"></a>
<b>Attributes</b>
<ul>
<li>readonly<br>
if set to a value != 0 all switching commands (on, off, toggle, ...) will be disabled.</li>
<li>forceOn<br>
try to switch on the device whenever an off status is received.</li>
</ul><br>
</ul>
=end html
=cut

View File

@ -0,0 +1,822 @@
# $Id: $
package main;
use strict;
use warnings;
use Time::HiRes qw(gettimeofday);
#use JSON;
##########################
# This block is only needed when FileLog is checked outside fhem
#
sub Log3($$$);
sub Log($$);
sub RemoveInternalTimer($);
use vars qw(%attr);
use vars qw(%defs);
use vars qw(%modules);
use vars qw($readingFnAttributes);
use vars qw($reread_active);
##########################
sub NinjaPiCrust_Attr(@);
sub NinjaPiCrust_Clear($);
sub NinjaPiCrust_HandleWriteQueue($);
sub NinjaPiCrust_Parse($$$$);
sub NinjaPiCrust_Read($);
sub NinjaPiCrust_ReadAnswer($$$$);
sub NinjaPiCrust_Ready($);
sub NinjaPiCrust_Write($$);
sub NinjaPiCrust_SimpleWrite(@);
my $dl = 4; # debug level for log - and yes, it's dirty..
my $clientsNinjaPiCrust = ":NINJA:";
my %matchListNinjaPiCrust = (
"1:NINJA" => "^.+"
);
my %RxListNinjaPiCrust = (
"HX2272" => "Or",
"FS20" => "Or",
"LaCrosse" => "Fr01",
);
sub
NinjaPiCrust_Initialize($)
{
my ($hash) = @_;
require "$attr{global}{modpath}/FHEM/DevIo.pm";
# Provider
$hash->{ReadFn} = "NinjaPiCrust_Read";
$hash->{WriteFn} = "NinjaPiCrust_Write";
$hash->{ReadyFn} = "NinjaPiCrust_Ready";
# Normal devices
$hash->{DefFn} = "NinjaPiCrust_Define";
$hash->{FingerprintFn} = "NinjaPiCrust_Fingerprint";
$hash->{UndefFn} = "NinjaPiCrust_Undef";
$hash->{GetFn} = "NinjaPiCrust_Get";
$hash->{SetFn} = "NinjaPiCrust_Set";
$hash->{AttrFn} = "NinjaPiCrust_Attr";
$hash->{AttrList} = "Clients MatchList"
." DebounceTime BeepLong BeepShort BeepDelay"
." tune " . join(" ", map { "tune_$_" } keys %RxListNinjaPiCrust)
." preferSketchReset:0,1 resetPulseWidth"
." $readingFnAttributes";
$hash->{ShutdownFn} = "NinjaPiCrust_Shutdown";
}
sub
NinjaPiCrust_Fingerprint($$)
{
}
#####################################
sub
NinjaPiCrust_Define($$)
{
my ($hash, $def) = @_;
my @a = split("[ \t][ \t]*", $def);
if(@a != 3) {
my $msg = "wrong syntax: define <name> NinjaPiCrust {devicename[\@baudrate] ".
"| devicename\@directio}";
Log3 undef, $dl, $msg;
return $msg;
}
DevIo_CloseDev($hash);
my $name = $a[0];
my $dev = $a[2];
$dev .= "\@9600" if( $dev !~ m/\@/ );
$hash->{Clients} = $clientsNinjaPiCrust;
$hash->{MatchList} = \%matchListNinjaPiCrust;
$hash->{DeviceName} = $dev;
my $ret = DevIo_OpenDev($hash, 0, "NinjaPiCrust_DoInit");
return $ret;
}
#####################################
sub
NinjaPiCrust_Undef($$)
{
my ($hash, $arg) = @_;
my $name = $hash->{NAME};
foreach my $d (sort keys %defs) {
if(defined($defs{$d}) &&
defined($defs{$d}{IODev}) &&
$defs{$d}{IODev} == $hash)
{
my $lev = ($reread_active ? 4 : 2);
Log3 $name, $lev, "deleting port for $d";
delete $defs{$d}{IODev};
}
}
NinjaPiCrust_Shutdown($hash);
DevIo_CloseDev($hash);
return undef;
}
#####################################
sub
NinjaPiCrust_Shutdown($)
{
my ($hash) = @_;
###NinjaPiCrust_SimpleWrite($hash, "X00");
return undef;
}
#sub
#NinjaPiCrust_RemoveLaCrossePair($)
#{
# my $hash = shift;
# delete($hash->{LaCrossePair});
#}
use JSON;
sub
NinjaPiCrust_ParseJSON($)
{
my ($str) = @_;
#Log 0, "NinjaPiCrust_ParseJSON('$str')";
return decode_json $str;
}
sub
NinjaPiCrust_encode($$$$)
{
my ($g,$v,$d,$da) = @_;
return '{"DEVICE":[{"G":"'.$g.'","V":'.$v.',"D":'.$d.',"DA":"'.$da.'"}]}';
}
#####################################
sub
NinjaPiCrust_Set($@)
{
my ($hash, @a) = @_;
my $name = shift @a;
my $cmd = shift @a;
my $arg = join(" ", @a);
#my $list = "led:on,off led-on-for-timer reset LaCrossePairForSec setReceiverMode:LaCrosse,HX2272,FS20";
my $list = "eyes:rgb led:on,off,red,green,blue,yellow,cyan,magenta";
return $list if( $cmd eq '?' || $cmd eq '');
my %rgb = (
on => "FFFFFF",
off => "000000",
red => "FF0000",
green => "00FF00",
blue => "0000FF",
cyan => "00FFFF",
magenta => "FF00FF",
yellow => "FFFF00"
);
if($cmd eq "raw") {
Log3 $name, 4, "set $name $cmd $arg";
NinjaPiCrust_SimpleWrite($hash, $arg);
} elsif ($cmd =~ m/^eyes$/i) {
return "Unknown argument $cmd, choose one of $list"
if($arg !~ m/^(on|off|red|green|blue|yellow|cyan|magenta|[0-9a-f]{6})$/i);
Log3 $name, 4, "set $name $cmd $arg";
NinjaPiCrust_Write($hash, (exists $rgb{$arg}) ? NinjaPiCrust_encode("0",0,1007,$rgb{$arg}) :
NinjaPiCrust_encode("0",0,1007,$arg) );
} elsif ($cmd =~ m/^led$/i) {
return "Unknown argument $cmd, choose one of $list"
if($arg !~ m/^(on|off|red|green|blue|yellow|cyan|magenta|[0-9a-f]{6})$/i);
Log3 $name, 4, "set $name $cmd $arg";
NinjaPiCrust_Write($hash, (exists $rgb{$arg}) ? NinjaPiCrust_encode("0",0,999,$rgb{$arg}) :
NinjaPiCrust_encode("0",0,999,$arg));
} elsif ($cmd =~ m/led-on-for-timer/i) {
return "Unknown argument $cmd, choose one of $list" if($arg !~ m/^[0-9]+$/i);
#remove timer if there is one active
if($modules{NinjaPiCrust}{ldata}{$name}) {
CommandDelete(undef, $name . "_timer");
delete $modules{NinjaPiCrust}{ldata}{$name};
}
Log3 $name, 4, "set $name on";
#TODO: NinjaPiCrust_Write($hash, "l" . "1");
my $to = sprintf("%02d:%02d:%02d", $arg/3600, ($arg%3600)/60, $arg%60);
$modules{NinjaPiCrust}{ldata}{$name} = $to;
Log3 $name, 4, "Follow: +$to setstate $name off";
CommandDefine(undef, $name."_timer at +$to {fhem(\"set $name led" ." off\")}");
} elsif ($cmd =~ m/reset/i) {
NinjaPiCrust_ResetDevice($hash);
} else {
return "Unknown argument $cmd, choose one of ".$list;
}
return undef;
}
#####################################
sub
NinjaPiCrust_Get($@)
{
my ($hash, $name, $cmd, @msg ) = @_;
my $arg = join(" ", @msg);
#my $list = "devices:noArg initNinjaPiCrust:noArg RFMconfig:noArg updateAvailRam:noArg raw";
my $list = "version";
if ($cmd eq "raw" ) {
return "raw => 01" if($arg =~ m/^Ir/); ## Needed for CUL_IR usage (IR-Receive is always on for NinjaPiCrusts
}
elsif ($cmd eq "version" ) {
NinjaPiCrust_SimpleWrite($hash, NinjaPiCrust_encode("0",0,1003,"VNO") );
}
#elsif ($cmd eq "updateAvailRam" ) {
# NinjaPiCrust_SimpleWrite($hash, "m");
#
#}
else {
return "Unknown argument $cmd, choose one of ".$list;
}
return undef;
}
sub
NinjaPiCrust_Clear($)
{
my $hash = shift;
return undef; #TODO: do we need this?
# Clear the pipe
$hash->{RA_Timeout} = 1;
for(;;) {
my ($err, undef) = NinjaPiCrust_ReadAnswer($hash, "Clear", 0, undef);
last if($err && $err =~ m/^Timeout/);
}
delete($hash->{RA_Timeout});
}
#####################################
sub
NinjaPiCrust_DoInit($)
{
my $hash = shift;
my $name = $hash->{NAME};
my $err;
my $msg = undef;
my $val;
NinjaPiCrust_Clear($hash);
$hash->{STATE} = "Opened";
# Reset the counter
delete($hash->{XMIT_TIME});
delete($hash->{NR_CMD_LAST_H});
return undef;
}
#####################################
# This is a direct read for commands like get
# Anydata is used by read file to get the filesize
sub
NinjaPiCrust_ReadAnswer($$$$)
{
my ($hash, $arg, $anydata, $regexp) = @_;
my $type = $hash->{TYPE};
return ("No FD", undef)
if(!$hash || ($^O !~ /Win/ && !defined($hash->{FD})));
my ($mpandata, $rin) = ("", '');
my $buf;
my $to = 3; # 3 seconds timeout
$to = $hash->{RA_Timeout} if($hash->{RA_Timeout}); # ...or less
for(;;) {
if($^O =~ m/Win/ && $hash->{USBDev}) {
$hash->{USBDev}->read_const_time($to*1000); # set timeout (ms)
# Read anstatt input sonst funzt read_const_time nicht.
$buf = $hash->{USBDev}->read(999);
return ("Timeout reading answer for get $arg", undef)
if(length($buf) == 0);
} else {
return ("Device lost when reading answer for get $arg", undef)
if(!$hash->{FD});
vec($rin, $hash->{FD}, 1) = 1;
my $nfound = select($rin, undef, undef, $to);
if($nfound < 0) {
next if ($! == EAGAIN() || $! == EINTR() || $! == 0);
my $err = $!;
DevIo_Disconnected($hash);
return("NinjaPiCrust_ReadAnswer $arg: $err", undef);
}
return ("Timeout reading answer for get $arg", undef)
if($nfound == 0);
$buf = DevIo_SimpleRead($hash);
return ("No data", undef) if(!defined($buf));
}
if($buf) {
#Log3 $hash->{NAME}, 5, "NinjaPiCrust/RAW (ReadAnswer): $buf";
$mpandata .= $buf;
}
chop($mpandata);
chop($mpandata);
return (undef, $mpandata)
}
}
#####################################
# Check if the 1% limit is reached and trigger notifies
sub
NinjaPiCrust_XmitLimitCheck($$)
{
my ($hash,$fn) = @_;
my $now = time();
if(!$hash->{XMIT_TIME}) {
$hash->{XMIT_TIME}[0] = $now;
$hash->{NR_CMD_LAST_H} = 1;
return;
}
my $nowM1h = $now-3600;
my @b = grep { $_ > $nowM1h } @{$hash->{XMIT_TIME}};
if(@b > 163) { # 163 comes from fs20. todo: verify if correct for NinjaPiCrust modulation
my $name = $hash->{NAME};
Log3 $name, 2, "NinjaPiCrust TRANSMIT LIMIT EXCEEDED";
DoTrigger($name, "TRANSMIT LIMIT EXCEEDED");
} else {
push(@b, $now);
}
$hash->{XMIT_TIME} = \@b;
$hash->{NR_CMD_LAST_H} = int(@b);
}
#####################################
sub
NinjaPiCrust_Write($$)
{
my ($hash, $cmd, $msg) = @_;
my $name = $hash->{NAME};
my $arg = $cmd;
#TODO: $arg .= " " . $msg if(defined($msg));
#Modify command for CUL_IR
#$arg =~ s/^\s+|\s+$//g;
#$arg =~ s/^Is/I/i; #SendIR command is "I" not "Is" for NinjaPiCrust devices
Log3 $name, 5, "$name sending $arg";
NinjaPiCrust_AddQueue($hash, $arg);
#TODO: NinjaPiCrust_SimpleWrite($hash, $msg);
}
sub
NinjaPiCrust_SendFromQueue($$)
{
my ($hash, $bstring) = @_;
my $name = $hash->{NAME};
my $to = 0.05;
if($bstring ne "") {
my $sp = AttrVal($name, "sendpool", undef);
if($sp) { # Is one of the NinjaPiCrust-fellows sending data?
my @fellows = split(",", $sp);
foreach my $f (@fellows) {
if($f ne $name &&
$defs{$f} &&
$defs{$f}{QUEUE} &&
$defs{$f}{QUEUE}->[0] ne "")
{
unshift(@{$hash->{QUEUE}}, "");
InternalTimer(gettimeofday()+$to, "NinjaPiCrust_HandleWriteQueue", $hash, 0);
return;
}
}
}
NinjaPiCrust_XmitLimitCheck($hash,$bstring);
NinjaPiCrust_SimpleWrite($hash, $bstring);
}
InternalTimer(gettimeofday()+$to, "NinjaPiCrust_HandleWriteQueue", $hash, 0);
}
sub
NinjaPiCrust_AddQueue($$)
{
my ($hash, $bstring) = @_;
if(!$hash->{QUEUE}) {
$hash->{QUEUE} = [ $bstring ];
NinjaPiCrust_SendFromQueue($hash, $bstring);
} else {
push(@{$hash->{QUEUE}}, $bstring);
}
}
#####################################
sub
NinjaPiCrust_HandleWriteQueue($)
{
my $hash = shift;
my $arr = $hash->{QUEUE};
if(defined($arr) && @{$arr} > 0) {
shift(@{$arr});
if(@{$arr} == 0) {
delete($hash->{QUEUE});
return;
}
my $bstring = $arr->[0];
if($bstring eq "") {
NinjaPiCrust_HandleWriteQueue($hash);
} else {
NinjaPiCrust_SendFromQueue($hash, $bstring);
}
}
}
#####################################
# called from the global loop, when the select for hash->{FD} reports data
sub
NinjaPiCrust_Read($)
{
my ($hash) = @_;
my $buf = DevIo_SimpleRead($hash);
return "" if(!defined($buf));
my $name = $hash->{NAME};
my $pandata = $hash->{PARTIAL};
#Log3 $name, $dl+2, "NinjaPiCrust/RAW: $pandata/$buf";
$pandata .= $buf;
while($pandata =~ m/\n/) {
my $rmsg;
($rmsg,$pandata) = split("\n", $pandata, 2);
$rmsg =~ s/\r//;
NinjaPiCrust_Parse($hash, $hash, $name, $rmsg) if($rmsg);
}
$hash->{PARTIAL} = $pandata;
}
sub
NinjaPiCrust_Parse($$$$)
{
my ($hash, $iohash, $name, $rmsg) = @_;
my $dmsg = $rmsg;
my $rssi = 0;
my $lqi = 0;
Log3 $hash, $dl, "$name: NinjaPiCrust_Parse '$dmsg'";
#next if(!$dmsg || length($dmsg) < 1); # Bogus messages
#return if($dmsg =~ m/^Available commands:/ ); # ignore startup messages
#return if($dmsg =~ m/^ / ); # ignore startup messages
#return if($dmsg =~ m/^-> ack/ ); # ignore send ack
my ($isdup, $idx) = CheckDuplicate("", "$name: $dmsg", undef);
return if ($isdup);
if($dmsg =~ m/^\[/ ) {
Log3 $name, 1, "NinjaPiCrust $name got special: $dmsg";
$hash->{model} = $dmsg;
if( $hash->{STATE} eq "Opened" ) {
if( $dmsg =~m /pcaSerial/ ) {
Log3 $hash, $dl, "nono";
}
$hash->{STATE} = "Initialized";
}
return;
}
readingsSingleUpdate($hash,"${name}_LASTMSG",$dmsg,1);
my $jsonref = NinjaPiCrust_ParseJSON($dmsg);
my %datagram = %$jsonref;
#Log3 $name, $dl, "NinjaPiCrust_Parse: \%datagram is @{[%datagram]}";
my %addvals;
my $msgtype = (keys %datagram)[0];
Log3 $name, $dl, "$name: got message type '$msgtype'";
my %data = %{$datagram{$msgtype}[0]};
$data{MSGTYPE} = $msgtype;
%addvals = %data;
Log3 $name, $dl, "$name: Got $msgtype $data{G} $data{V} $data{D} $data{DA} from $rmsg"
if (defined $data{G} and defined $data{V} and defined $data{D} and defined $data{DA});
$addvals{RAWMSG} = $rmsg;
$hash->{"${name}_MSGCNT"}++;
$hash->{"${name}_TIME"} = TimeNow();
$hash->{RAWMSG} = $rmsg;
#if(defined($rssi)) {
# $hash->{RSSI} = $rssi;
# $addvals{RSSI} = $rssi;
#}
#if(defined($lqi)) {
# $hash->{LQI} = $lqi;
# $addvals{LQI} = $lqi;
#}
if ($msgtype =~ m/ACK/i) {
my $omsg = $rmsg;
$omsg =~ s/ACK/DEVICE/;
Log3 $name, $dl, "$name: got ACK for command: $omsg";
# for now, do nothing...
return;
} elsif ($msgtype =~ m/ERROR/i) {
Log3 $name, 0, "$name: ERROR: got $rmsg from ".$hash->{RAWREQ};
$hash->{RAWREQ} = undef;
$hash->{RAWMSG} = undef;
return;
}
if (($data{G} eq "0") and ($data{V} == 0)) {
# message information pertains PiCrust hardware, so we handle it here:
my $D = int($data{D});
my $DA = $data{DA};
Log3 $name, $dl, "$name: Got shield related data $msgtype: $D => '$DA'";
if ($D == 1003) { # may be ACK (or even DEVICE?)
my $version = substr $DA, 1;
Log3 $name, $dl, "$name: Arduino version is $version";
$hash->{VERSION} = $version;
} elsif ($msgtype =~ m/DEVICE/) {
if ($D == 999) {
Log3 $name, $dl, "$name: led is '$DA'";
readingsSingleUpdate($hash,"led",$DA,1);
} elsif ($D == 1007) {
Log3 $name, $dl, "$name: eyes are '$DA'";
readingsSingleUpdate($hash,"eyes",$DA,1);
} else {
Log3 $name, 0, "$name: ERROR: got unsupported DID $D in '$rmsg'";
}
} else {
Log3 $name, $dl, "$name: ignoring $msgtype-type message '$rmsg'";
}
} else {
Log3 $hash, $dl, "$name: now dispatching '$dmsg'";
Dispatch($hash, $dmsg, \%addvals);
Log3 $hash, $dl, "$name: end dispatching '$dmsg'";
}
}
#my devinfo = (
# "0:0:999" => ( SENSE => 1 ),
# "0:0:1003 => ( SENSE => 1 )
#)
#####################################
sub
NinjaPiCrust_Ready($)
{
my ($hash) = @_;
return DevIo_OpenDev($hash, 1, "NinjaPiCrust_DoInit")
if($hash->{STATE} eq "disconnected");
# This is relevant for windows/USB only
my $po = $hash->{USBDev};
my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags);
if($po) {
($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status;
}
return ($InBytes && $InBytes>0);
}
########################
sub
NinjaPiCrust_ResetDevice($)
{
my ($hash) = @_;
return if(!$hash);
my $name = $hash->{NAME};
my $pulse = AttrVal($name, "resetPulseWidth", 0.5);
$pulse= 0.01 if ($pulse < 0.01);
$pulse= 2 if ($pulse > 2);
Log3 $name, 1, "NinjaPiCrust_ResetDevice with pulse with $pulse sec.";
#$hash->{USBDev}->pulse_dtr_on($pulse * 1000.0) if($hash->{USBDev});
return undef;
}
sub
NinjaPiCrust_SimpleWrite(@)
{
my ($hash, $msg, $nocr) = @_;
return if(!$hash);
my $name = $hash->{NAME};
Log3 $name, $dl, "$name: NinjaPiCrust_SW '$msg'";
$hash->{RAWREQ} = $msg;
$msg .= "\n" unless($nocr);
$hash->{USBDev}->write($msg) if($hash->{USBDev});
syswrite($hash->{DIODev}, $msg) if($hash->{DIODev});
# Some linux installations are broken with 0.001, T01 returns no answer
select(undef, undef, undef, 0.01);
}
sub
NinjaPiCrust_Attr(@)
{
my ($cmd,$name,$aName,$aVal) = @_;
my $hash = $defs{$name};
if( $aName eq "Clients" ) {
$hash->{Clients} = $aVal;
$hash->{Clients} = $clientsNinjaPiCrust if( !$hash->{Clients}) ;
} elsif( $aName eq "MatchList" ) {
$hash->{MatchList} = $aVal;
$hash->{MatchList} = \%matchListNinjaPiCrust if( !$hash->{MatchList} );
} elsif($aName =~ m/^tune/i) { #tune attribute freq / rx:bWidth / rx:rAmpl / rx:sens / tx:deviation / tx:power
# Frequenze: Fc =860+ F x0.0050MHz
# LNA Gain [dB] = MAX -6, -14, -20
# RX Bandwidth [kHz] = -, 400, 340, 270, 200, 134, 67
# DRSSI [dB] = -103, -97, -91, -85, -79, -73
# Deviation [kHz] = 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240
# OuputPower [dBm] = 0, -3, -6, -9, -12, -15, -18, -21
return "Usage: attr $name $aName <Frequence> <Rx:Bandwidth> <Rx:Amplitude> <Rx:Sens> <Tx:Deviation> <Tx:Power>"
if(!$aVal || $aVal !~ m/^(4|8)[\d]{2}.[\d]{3} (0|400|340|270|200|134|67) (0|\-6|\-14|\-20) (\-103|\-97|\-91|\-85|\-79|\-73) (15|30|45|60|75|90|105|120|135|150|165|180|195|210|225|240) (0|\-3|\-6|\-9|\-12|\-15|\-18|\-21)/ );
my $TuneStr = NinjaPiCrust_CalcTuneCmd($aVal);
NinjaPiCrust_Write($hash, "t" . $TuneStr);
} elsif ($aName eq "DebounceTime") {
return "Usage: attr $name $aName <OOK-Protocol-Number><DebounceTime>"
if($aVal !~ m/^[0-9]{3,5}$/);
#Log3 $name, 4, "set $name $cmd $arg";
NinjaPiCrust_Write($hash, "Od" . $aVal);
}
return undef;
}
sub NinjaPiCrust_CalcTuneCmd($) {
my ($str) = @_;
my ($freq, $rxbwidth, $rxampl, $rxsens, $txdev, $txpower) = split(' ', $str ,6);
my $sfreq;
if($freq < 800) {
$sfreq = sprintf("%03X", ($freq-430)/0.0025);
} else {
$sfreq = sprintf("%03X", ($freq-860)/0.0050);
}
my $sbwidth = sprintf("%01X", NinjaPiCrust_getIndexOfArray($rxbwidth,(0, 400, 340, 270, 200, 134, 67)));
my $sampl = sprintf("%01X", NinjaPiCrust_getIndexOfArray($rxampl,(0, -6, -14, -20)));
my $ssens = sprintf("%01X", NinjaPiCrust_getIndexOfArray($rxsens, (-103, -97, -91, -85, -79, -73)));
my $sdev = sprintf("%01X", NinjaPiCrust_getIndexOfArray($txdev, (15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240)));
my $soutpupower = sprintf("%01X", NinjaPiCrust_getIndexOfArray($txpower, (0, -3, -6, -9, -12, -15, -18, -21)));
return $sfreq . $sbwidth . $sampl . $ssens . $sdev . $soutpupower;
}
sub NinjaPiCrust_getIndexOfArray($@) {
my ($value, @array) = @_;
my ($ivalue) = grep { $array[$_] == $value } 0..$#array;
return $ivalue;
}
1;
=pod
=begin html
<a name="NinjaPiCrust"></a>
<h3>NinjaPiCrust</h3>
<ul>
The NinjaPiCrust is a family of RF devices sold by <a href="http://jeelabs.com">jeelabs.com</a>.
It is possible to attach more than one device in order to get better
reception, fhem will filter out duplicate messages.<br><br>
This module provides the IODevice for the <a href="#PCA301">PCA301</a> modules that implements the PCA301 protocoll.<br><br>
In the future other RF devices like the Energy Controll 3000, JeeLabs room nodes, fs20 or kaku devices will be supportet.<br><br>
Note: this module may require the Device::SerialPort or Win32::SerialPort
module if you attach the device via USB and the OS sets strange default
parameters for serial devices.
<br><br>
<a name="NinjaPiCrust_Define"></a>
<b>Define</b>
<ul>
<code>define &lt;name&gt; NinjaPiCrust &lt;device&gt;</code> <br>
<br>
USB-connected devices:<br><ul>
&lt;device&gt; specifies the serial port to communicate with the NinjaPiCrust.
The name of the serial-device depends on your distribution, under
linux the cdc_acm kernel module is responsible, and usually a
/dev/ttyACM0 device will be created. If your distribution does not have a
cdc_acm module, you can force usbserial to handle the NinjaPiCrust by the
following command:<ul>modprobe usbserial vendor=0x0403
product=0x6001</ul>In this case the device is most probably
/dev/ttyUSB0.<br><br>
You can also specify a baudrate if the device name contains the @
character, e.g.: /dev/ttyACM0@57600<br><br>
If the baudrate is "directio" (e.g.: /dev/ttyACM0@directio), then the
perl module Device::SerialPort is not needed, and fhem opens the device
with simple file io. This might work if the operating system uses sane
defaults for the serial parameters, e.g. some Linux distributions and
OSX. <br><br>
</ul>
<br>
</ul>
<br>
<a name="NinjaPiCrust_Set"></a>
<b>Set</b>
<ul>
<li>raw &lt;datar&gt;<br>
send &lt;data&gt; as a raw message to the NinjaPiCrust to be transmitted over the RF link.
</li><br>
<li>LaCrossePairForSec &lt;sec&gt; [ignore_battery]<br>
enable autocreate of new LaCrosse sensors for &lt;sec&gt; seconds. if ignore_battery is not given only sensors
sending the 'new battery' flag will be created.
</li>
</ul>
<a name="NinjaPiCrust_Get"></a>
<b>Get</b>
<ul>
</ul>
<a name="NinjaPiCrust_Attr"></a>
<b>Attributes</b>
<ul>
<li>Clients</li>
<li>MatchList</li>
</ul>
<br>
</ul>
=end html
=cut