diff --git a/fhem/contrib/00_ZWDongle.pm b/fhem/contrib/00_ZWDongle.pm index b10666890..e854a282e 100755 --- a/fhem/contrib/00_ZWDongle.pm +++ b/fhem/contrib/00_ZWDongle.pm @@ -1,6 +1,8 @@ ############################################## # $Id: 00_ZWDongle.pm 1721 2012-07-11 14:48:24Z rudolfkoenig $ # Handling a ZWave (USB) Dongle +# TODO: +# - get list of command classes for a device. package main; use strict; @@ -19,23 +21,28 @@ sub ZWDongle_Write($$$); # http://open-zwave.googlecode.com/svn-history/r426/trunk/cpp/src/Driver.cpp # http://buzzdavidson.com/?p=68 my %sets = ( - "learnMode" => "50%02x", # ZW_SET_LEARN_MODE - "sendData" => "13%02x%s", # ZW_SEND_DATA - # 032001000500/032001FF0500 (off/on) + "addNode" => { cmd => "4a%02x@", # ZW_ADD_NODE_TO_NETWORK', + param => {on=>0x81, off=>0x05 } }, + "removeNode"=> { cmd => "4b%02x@", # ZW_REMOVE_NODE_FROM_NETWORK', + param => {on=>0x81, off=>0x05 } }, + "createNode"=> { cmd => "60%02x" }, # ZW_REQUEST_NODE_INFO', ); my %gets = ( "caps" => "07", # SERIAL_API_GET_CAPABILITIES "ctrlCaps" => "05", # ZW_GET_CONTROLLER_CAPS - "devInfo" => "41%02x", # ZW_GET_NODE_PROTOCOL_INFO - "devList" => "02", # SERIAL_API_GET_INIT_DATA + "nodeInfo" => "41%02x", # ZW_GET_NODE_PROTOCOL_INFO + "nodeList" => "02", # SERIAL_API_GET_INIT_DATA "homeId" => "20", # MEMORY_GET_ID "version" => "15", # ZW_GET_VERSION + "raw" => "%s", ); # Known controller function. # Note: Known != implemented, see %sets & %gets for the implemented ones. -my %func_id= ( +use vars qw(%zw_func_id); +use vars qw(%zw_type6); +%zw_func_id= ( '02' => 'SERIAL_API_GET_INIT_DATA', '03' => 'SERIAL_API_APPL_NODE_INFORMATION', '04' => 'APPLICATION_COMMAND_HANDLER', @@ -114,6 +121,20 @@ my %func_id= ( 'd0' => 'ZW_SET_PROMISCUOUS_MODE', ); +%zw_type6 = ( + '01' => 'GENERIC_CONTROLLER', '12' => 'SWITCH_REMOTE', + '02' => 'STATIC_CONTROLLER', '13' => 'SWITCH_TOGGLE', + '03' => 'AV_CONTROL_POINT', '20' => 'SENSOR_BINARY', + '06' => 'DISPLAY', '21' => 'SENSOR_MULTILEVEL', + '07' => 'GARAGE_DOOR', '22' => 'WATER_CONTROL', + '08' => 'THERMOSTAT', '30' => 'METER_PULSE', + '09' => 'WINDOW_COVERING', '40' => 'ENTRY_CONTROL', + '0F' => 'REPEATER_SLAVE', '50' => 'SEMI_INTEROPERABLE', + '10' => 'SWITCH_BINARY', 'ff' => 'NON_INTEROPERABLE', + '11' => 'SWITCH_MULTILEVEL', +); + + sub ZWDongle_Initialize($) @@ -126,6 +147,7 @@ ZWDongle_Initialize($) $hash->{ReadFn} = "ZWDongle_Read"; $hash->{WriteFn} = "ZWDongle_Write"; $hash->{ReadyFn} = "ZWDongle_Ready"; + $hash->{ReadAnswerFn} = "ZWDongle_ReadAnswer"; # Normal devices $hash->{DefFn} = "ZWDongle_Define"; @@ -156,7 +178,7 @@ ZWDongle_Define($$) my $dev = $a[2]; $hash->{Clients} = ":ZWave:"; - my %matchList = ( "1:ZWave" => "^........ ...*" ); + my %matchList = ( "1:ZWave" => ".*" ); $hash->{MatchList} = \%matchList; if($dev eq "none") { @@ -170,6 +192,7 @@ ZWDongle_Define($$) } $hash->{DeviceName} = $dev; + $hash->{CallbackNr} = 0; my $ret = DevIo_OpenDev($hash, 0, "ZWDongle_DoInit"); return $ret; } @@ -185,12 +208,32 @@ ZWDongle_Set($@) return "\"set ZWDongle\" needs at least one parameter" if(@a < 1); my $type = shift @a; - return "Unknown argument $type, choose one of " . join(" ", sort keys %sets) - if(!defined($sets{$type})); - my $nargs = int(split("%", $sets{$type}, -1))-1; + if(!defined($sets{$type})) { + my @r; + map { my $p = $sets{$_}{param}; + push @r,($p ? "$_:".join(",",sort keys %{$p}) : $_)} sort keys %sets; + return "Unknown argument $type, choose one of " . join(" ",@r); + } + my $cmd = $sets{$type}{cmd}; + my $par = $sets{$type}{param}; + if($par) { + return "Unknown argument for $type, choose one of ".join(" ",keys %{$par}) + if(!defined($par->{$a[0]})); + $a[0] = $par->{$a[0]}; + } + + if($cmd =~ m/\@/) { + my $c = $hash->{CallbackNr}+1; + $c = 1 if($c > 255); + $hash->{CallbackNr} = $c; + $c = sprintf("%02x", $c); + $cmd =~ s/\@/$c/g; + } + + my $nargs = int(split("%", $cmd, -1))-1; return "set $name $type needs $nargs arguments" if($nargs != int(@a)); - ZWDongle_Write($hash, "00", sprintf($sets{$type}, @a)); + ZWDongle_Write($hash, "00", sprintf($cmd, @a)); return undef; } @@ -215,13 +258,14 @@ ZWDongle_Get($@) ZWDongle_Write($hash, "00", sprintf($gets{$type}, @a)); my $re = "^01".substr($gets{$type},0,2); # Start with <01><01> - my $ret = ZWDongle_ReadAnswer($hash, "get $name $type", $re); + my ($err, $ret) = ZWDongle_ReadAnswer($hash, "get $name $type", $re); + return $err if($err); my $msg=""; $msg = $ret if($ret); my @r = map { ord($_) } split("", pack('H*', $ret)) if(defined($ret)); - if($type eq "devList") { ############################ + if($type eq "nodeList") { ############################ return "$name: Bogus data received" if(int(@r) != 36); my @list; for my $byte (0..28) { @@ -241,7 +285,7 @@ ZWDongle_Get($@) for my $byte (0..31) { my $bits = $r[10+$byte]; for my $bit (0..7) { - my $fn = $func_id{sprintf("%02x", $byte*8+$bit)}; + my $fn = $zw_func_id{sprintf("%02x", $byte*8+$bit)}; push @list, $fn if(($bits & (1<<$bit)) && $fn); } } @@ -250,7 +294,7 @@ ZWDongle_Get($@) } elsif($type eq "homeId") { ############################ $msg = sprintf("HomeId:%s CtrlNodeId:%s", substr($ret,4,8), substr($ret,12,2)); - $hash->{HomeId} = substr($ret,4,8); + $hash->{homeId} = substr($ret,4,8); } elsif($type eq "version") { ############################ $msg = join("", map { chr($_) } @r[2..13]); @@ -267,28 +311,15 @@ ZWDongle_Get($@) } $msg = join(" ", @list); - } elsif($type eq "devInfo") { ############################ + } elsif($type eq "nodeInfo") { ############################ my $id = sprintf("%02x", $r[6]); if($id eq "00") { $msg = "node $a[0] is not present"; } else { my @list; my @type5 = qw( CONTROLLER STATIC_CONTROLLER SLAVE ROUTING_SLAVE); - my %type6 = ( - '01' => 'GENERIC_CONTROLLER', '12' => 'SWITCH_REMOTE', - '02' => 'STATIC_CONTROLLER', '13' => 'SWITCH_TOGGLE', - '03' => 'AV_CONTROL_POINT', '20' => 'SENSOR_BINARY', - '06' => 'DISPLAY', '21' => 'SENSOR_MULTILEVEL', - '07' => 'GARAGE_DOOR', '22' => 'WATER_CONTROL', - '08' => 'THERMOSTAT', '30' => 'METER_PULSE', - '09' => 'WINDOW_COVERING', '40' => 'ENTRY_CONTROL', - '0F' => 'REPEATER_SLAVE', '50' => 'SEMI_INTEROPERABLE', - '10' => 'SWITCH_BINARY', 'ff' => 'NON_INTEROPERABLE', - '11' => 'SWITCH_MULTILEVEL', - ); - push @list, $type5[$r[5]-1] if($r[5]>0 && $r[5] <= @type5); - push @list, $type6{$id} if($type6{$id}); + push @list, $zw_type6{$id} if($zw_type6{$id}); push @list, ($r[2] & 0x80) ? "listening" : "sleeping"; push @list, "routing" if($r[2] & 0x40); push @list, "40kBaud" if(($r[2] & 0x38) == 0x10); @@ -374,8 +405,8 @@ ZWDongle_Read($@) $buf = unpack('H*', $buf); - # The dongle looses data over USB(?), and dropping the old buffer after a - # timeout is my only idea of solving this problem. + # The dongle looses data over USB for some commands(?), and dropping the old + # buffer after a timeout is my only idea of solving this problem. my $ts = gettimeofday(); my $data = ($hash->{READ_TS} && $ts-$hash->{READ_TS} > 1) ? "" : $hash->{PARTIAL}; @@ -420,6 +451,7 @@ ZWDongle_Read($@) Log $ll5, "ZWDongle_Read $name: $msg"; last if(defined($local) && (!defined($regexp) || ($msg =~ m/$regexp/))); ZWDongle_Parse($hash, $name, $msg); + $msg = undef; } $hash->{PARTIAL} = $data; return $msg if(defined($local)); @@ -432,9 +464,8 @@ sub ZWDongle_ReadAnswer($$$) { my ($hash, $arg, $regexp) = @_; - return ("No FD", undef) + return ("No FD (dummy device?)", undef) if(!$hash || ($^O !~ /Win/ && !defined($hash->{FD}))); - my $to = ($hash->{RA_Timeout} ? $hash->{RA_Timeout} : 3); for(;;) { @@ -467,7 +498,7 @@ ZWDongle_ReadAnswer($$$) } my $ret = ZWDongle_Read($hash, $buf, $regexp); - return $ret if(defined($ret)); + return (undef, $ret) if(defined($ret)); } } @@ -482,13 +513,7 @@ ZWDongle_Parse($$$) $hash->{RAWMSG} = $rmsg; my %addvals = (RAWMSG => $rmsg); - my $homeId = $hash->{HomeId}; - if(!$homeId) { - Log 1, "ERROR: $name HomeId is not set!" if(!$hash->{errReported}); - $hash->{errReported} = 1; - return; - } - Dispatch($hash, "$homeId $rmsg", \%addvals); + Dispatch($hash, $rmsg, \%addvals); } diff --git a/fhem/contrib/10_ZWave.pm b/fhem/contrib/10_ZWave.pm index 9a90bb341..3d5ad276d 100755 --- a/fhem/contrib/10_ZWave.pm +++ b/fhem/contrib/10_ZWave.pm @@ -1,176 +1,470 @@ ############################################## +# See ZWDongle.pm for inspiration package main; use strict; use warnings; +sub ZWave_Parse($$@); +sub ZWave_Set($@); +sub ZWave_Get($@); +sub ZWave_Cmd($$@); +sub ZWave_ParseMeter($); +sub ZWave_SetClasses($$$$); + +use vars qw(%zw_func_id); + my @zwave_models = qw( - Ever + Everspring_AN1582 Everspring_AN1583 ); -sub +my %zwave_id2class; +my %zwave_class = ( + NO_OPERATION => { id => '00', }, + BASIC => { id => '20', + set => { basicValue => "01%02x", }, + get => { basicStatus => "02", }, + parse => { "..2003(.*)" => '"basicReport:$1"',}, }, + CONTROLLER_REPLICATION => { id => '21', }, + APPLICATION_STATUS => { id => '22', }, + ZIP_SERVICES => { id => '23', }, + ZIP_SERVER => { id => '24', }, + SWITCH_BINARY => { id => '25', + set => { off => "0100", + on => "01FF", + reportOn => "03FF", + reportOff => "0300", }, + get => { swbStatus => "02", }, + parse => { "03250300" => "state:off", + "032503ff" => "state:on", }, } , + SWITCH_MULTILEVEL => { id => '26', }, + SWITCH_ALL => { id => '27', }, + SWITCH_TOGGLE_BINARY => { id => '28', }, + SWITCH_TOGGLE_MULTILEVEL => { id => '29', }, + CHIMNEY_FAN => { id => '2A', }, + SCENE_ACTUATOR_CONF => { id => '2C', }, + SCENE_CONTROLLER_CONF => { id => '2D', }, + ZIP_ADV_SERVICES => { id => '2F', }, + SCENE_ACTIVATION => { id => '2b', }, + ZIP_CLIENT => { id => '2e', }, + SENSOR_BINARY => { id => '30', + get => { sbStatus => "02", }, + parse => { "03300300" => "state:closed", + "033003ff" => "state:open", },}, + SENSOR_MULTILEVEL => { id => '31', }, + METER => { id => '32', + parse => { "..3202(.*)"=> 'ZWave_ParseMeter($1)' }, }, + ZIP_ADV_SERVER => { id => '33', }, + ZIP_ADV_CLIENT => { id => '34', }, + METER_PULSE => { id => '35', }, + THERMOSTAT_HEATING => { id => '38', }, + THERMOSTAT_MODE => { id => '40', }, + THERMOSTAT_OPERATING_STATE => { id => '42', }, + THERMOSTAT_SETPOINT => { id => '43', }, + THERMOSTAT_FAN_MODE => { id => '44', }, + THERMOSTAT_FAN_STATE => { id => '45', }, + CLIMATE_CONTROL_SCHEDULE => { id => '46', }, + THERMOSTAT_SETBACK => { id => '47', }, + BASIC_WINDOW_COVERING => { id => '50', }, + MTP_WINDOW_COVERING => { id => '51', }, + MULTI_INSTANCE => { id => '60', }, + DOOR_LOCK => { id => '62', }, + USER_CODE => { id => '63', }, + CONFIGURATION => { id => '70', + set => { configDefault=>"04%02x80", + configByte => "04%02x01%02x", + configWord => "04%02x02%04x", + configLong => "04%02x04%08x", }, + get => { config => "05%02x", }, + parse => { "..7006(..)..(.*)" => '"config_$1:".hex($2)',}, }, + ALARM => { id => '71', + get => { alarm => "04%02x", }, + parse => { "..7105(..)(..)" => '"alarm_type_$1:level $2"',}, }, + MANUFACTURER_SPECIFIC => { id => '72', }, + POWERLEVEL => { id => '73', }, + PROTECTION => { id => '75', }, + LOCK => { id => '76', }, + NODE_NAMING => { id => '77', }, + GROUPING_NAME => { id => '7B', }, + FIRMWARE_UPDATE_MD => { id => '7a', }, + REMOTE_ASSOCIATION_ACTIVATE => { id => '7c', }, + REMOTE_ASSOCIATION => { id => '7d', }, + BATTERY => { id => '80', + get => { battery => "02" }, + parse => { "038003(..)"=> '"battery:".hex($1)." %"' }, }, + CLOCK => { id => '81', }, + HAIL => { id => '82', }, + WAKE_UP => { id => '84', + set => { wakeupInterval => "04%06x%02x" }, + get => { wakeupInterval => "05" }, + parse => { "028407" => 'wakeup:notification', + "..8406(......)(..)" => + '"wakeupReport:interval:".hex($1)." target:".hex($2)',}, }, + ASSOCIATION => { id => '85', + set => { associationAdd => "01%02x%02x*", + associationDel => "04%02x%02x*", }, + get => { association => "02%02x", }, + parse => { "..8503(..)(..)..(.*)" => '"assocGroup_$1:Max:$2 Nodes:$3"',}, }, + VERSION => { id => '86', + get => { version => "11", }, + parse => { "078612(..)(..)(..)(..)(..)" => + 'sprintf("Lib:%d Prot:%d.%d App:%d.%d",hex($1),hex($2),hex($3),hex($4),hex($5))', } }, + INDICATOR => { id => '87', }, + PROPRIETARY => { id => '88', }, + LANGUAGE => { id => '89', }, + TIME_PARAMETERS => { id => '8B', }, + GEOGRAPHIC_LOCATION => { id => '8C', }, + COMPOSITE => { id => '8D', }, + MULTI_CMD => { id => '8F', }, + TIME => { id => '8a', }, + MULTI_INSTANCE_ASSOCIATION => { id => '8E', }, + ENERGY_PRODUCTION => { id => '90', }, + MANUFACTURER_PROPRIETARY => { id => '91', }, + SCREEN_MD => { id => '92', }, + SCREEN_ATTRIBUTES => { id => '93', }, + SIMPLE_AV_CONTROL => { id => '94', }, + AV_CONTENT_DIRECTORY_MD => { id => '95', }, + AV_RENDERER_STATUS => { id => '96', }, + AV_CONTENT_SEARCH_MD => { id => '97', }, + SECURITY => { id => '98', }, + AV_TAGGING_MD => { id => '99', }, + IP_CONFIGURATION => { id => '9A', }, + ASSOCIATION_COMMAND_CONFIGURATION => { id => '9B', }, + SENSOR_ALARM => { id => '9C', }, + SENSOR_CONFIGURATION => { id => '9E', }, + SILENCE_ALARM => { id => '9d', }, + MARK => { id => 'ef', }, + NON_INTEROPERABLE => { id => 'f0', }, + ); + + + sub ZWave_Initialize($) { my ($hash) = @_; - $hash->{Match} = "^........ ...*"; + $hash->{Match} = ".*"; $hash->{SetFn} = "ZWave_Set"; + $hash->{GetFn} = "ZWave_Get"; $hash->{DefFn} = "ZWave_Define"; + $hash->{UndefFn} = "ZWave_Undef"; $hash->{ParseFn} = "ZWave_Parse"; $hash->{AttrList} = "IODev do_not_notify:1,0 ". - "ignore:1,0 dummy:1,0 showtime:1,0 ". - "loglevel:0,1,2,3,4,5,6 " . - "model:".join(",", sort @zwave_models); + "ignore:1,0 dummy:1,0 showtime:1,0 classes ". + "loglevel:0,1,2,3,4,5,6 " . + "model:".join(",", sort @zwave_models); + map { $zwave_id2class{$zwave_class{$_}{id}} = $_ } keys %zwave_class; } -my %zwave_classes = ( - 'AV_CONTROL_POINT' => {} , - 'DISPLAY' => {} , - 'GARAGE_DOOR' => {} , - 'THERMOSTAT' => {} , - 'WINDOW_COVERING' => {} , - 'REPEATER_SLAVE' => {} , - 'SWITCH_BINARY' => { - set => { "off" => "13%02x0320010005", - "on" => "13%02x032001FF05", }, - parse => { "03250300" => "state:off", - "032503ff" => "state:on" }, } , - 'SWITCH_MULTILEVEL' => {} , - 'SWITCH_REMOTE' => {} , - 'SWITCH_TOGGLE' => {} , - 'SENSOR_BINARY' => {} , - 'SENSOR_MULTILEVEL' => { - parse => { "0832022112(....)0000" => '"power:".hex($1)." W"' }, }, - 'WATER_CONTROL' => {} , - 'METER_PULSE' => {} , - 'ENTRY_CONTROL' => {} , -); - ############################# -sub + sub ZWave_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); - - my $u = "wrong syntax: define ZWave homeId id class [class...]"; - return $u if(int(@a) < 4); - my $name = shift @a; my $type = shift(@a); # always ZWave + + my $u = "wrong syntax for $name: define ZWave homeId id [classes]"; + return $u if(int(@a) < 2 || int(@a) > 3); + my $homeId = lc(shift @a); my $id = shift @a; return "define $name: wrong homeId ($homeId): need an 8 digit hex value" - if( ($homeId !~ m/^[a-f0-9]{8}$/i) ); + if( ($homeId !~ m/^[a-f0-9]{8}$/i) ); return "define $name: wrong id ($id): need a number" - if( ($id !~ m/^\d+$/i) ); - foreach my $cl (@a) { - return "define $name: unknown class $cl" if(!$zwave_classes{uc($cl)}); - } + if( ($id !~ m/^\d+$/i) ); $id = sprintf("%02x", $id); - $hash->{HomeId} = $homeId; - $hash->{Id} = $id; - $hash->{Classes} = uc(join(" ", @a)); + $hash->{homeId} = $homeId; + $hash->{id} = $id; $modules{ZWave}{defptr}{"$homeId $id"} = $hash; AssignIoPort($hash); # FIXME: should take homeId into account + + if(@a) { + ZWave_SetClasses($homeId, $id, undef, $a[0]); + + if($attr{$name}{classes} =~ m/ASSOCIATION/) { + my $iodev = $hash->{IODev}; + my $homeReading = ReadingsVal($iodev->{NAME}, "homeId", "") if($iodev); + my $ctrlId = $1 if($homeReading && $homeReading =~ m/CtrlNodeId:(..)/); + + if($ctrlId) { + Log 1, "Adding the controller $ctrlId to association group 1"; + IOWrite($hash, "00", "130a04850101${ctrlId}05"); + + } else { + Log 1, "Cannot associate $name, missing controller id"; + } + } + } + return undef; } ################################### sub -ZWave_Set($@) +ZWave_Cmd($$@) { - my ($hash, @a) = @_; + my ($type, $hash, @a) = @_; my $ret = undef; - - return "no set value specified" if(int(@a) < 2); + return "no $type argument specified" if(int(@a) < 2); my $name = shift(@a); my $cmd = shift(@a); # Collect the commands from the distinct classes my %cmdList; - foreach my $cl (split(" ", $hash->{Classes})) { - my $ptr = $zwave_classes{$cl}{set} if($zwave_classes{$cl}{set}); + my $classes = AttrVal($name, "classes", ""); + foreach my $cl (split(" ", $classes)) { + my $ptr = $zwave_class{$cl}{$type} if($zwave_class{$cl}{$type}); next if(!$ptr); foreach my $k (keys %{$ptr}) { - $cmdList{$k} = $ptr->{$k}; + if(!$cmdList{$k}) { + $cmdList{$k}{fmt} = $ptr->{$k}; + $cmdList{$k}{id} = $zwave_class{$cl}{id}; + } } } - return ("Unknown argument $cmd, choose one of ".join(" ",sort keys %cmdList)) + return ("Unknown $type argument $cmd, choose one of " + . join(" ",sort keys %cmdList)) if(!$cmdList{$cmd}); - my $cmdOut = sprintf($cmdList{$cmd}, $hash->{Id}, @a); - IOWrite($hash, "00", $cmdOut); - $cmd .= " ".join(" ", @a) if(@a); + ################################ + # ZW_SEND_DATA,nodeId,CMD,ACK|AUTO_ROUTE + my $id = $hash->{id}; + my $cmdFmt = $cmdList{$cmd}{fmt}; + my $cmdId = $cmdList{$cmd}{id}; + + my $nArg = 0; + $nArg = int(split("%", $cmdFmt))-1 if($cmdFmt =~ m/%/); + my $parTxt = ($nArg == 0 ? "no parameter" : + ($nArg == 1 ? "one parameter" : + "$nArg parameters")); + if($cmdFmt =~ m/^(.*)\*$/) { + $cmdFmt = $1; + return "$type $cmd needs at least $parTxt" if($nArg > int(@a)); + $cmdFmt .= ("%02x" x (int(@a)-$nArg)); + + } else { + return "$type $cmd needs $parTxt" if($nArg != int(@a)); + } + $cmdFmt = sprintf($cmdFmt, @a) if($nArg); + my $len = sprintf("%02x", length($cmdFmt)/2+1); + + my $data = "13$id$len$cmdId${cmdFmt}05"; + if($classes =~ m/WAKE_UP/) { + if(!$hash->{WakeUp}) { + my @arr = (); + $hash->{WakeUp} = \@arr; + } + push @{$hash->{WakeUp}}, $data; + return ($type eq "get" ? "Scheduled for sending after WAKEUP" : undef); + } + IOWrite($hash, "00", $data); + + my $val; + if($type eq "get") { + no strict "refs"; + my $iohash = $hash->{IODev}; + my $fn = $modules{$iohash->{TYPE}}{ReadAnswerFn}; + my ($err, $data) = &{$fn}($iohash, $cmd, "^000400$id"); + use strict "refs"; + + return $err if($err); + $val = ZWave_Parse($iohash, $data, 1); + + } else { + $cmd .= " ".join(" ", @a) if(@a); + + } + my $tn = TimeNow(); + if($type eq "set") { + $hash->{CHANGED}[0] = $cmd; + $hash->{STATE} = $cmd; + $hash->{READINGS}{state}{TIME} = $tn; + $hash->{READINGS}{state}{VAL} = $cmd; - $hash->{CHANGED}[0] = $cmd; - $hash->{STATE} = $cmd; - $hash->{READINGS}{state}{TIME} = $tn; - $hash->{READINGS}{state}{VAL} = $cmd; + } else { + my $mval = $val; + ($cmd, $mval) = split(":", $val) if($val); + $hash->{READINGS}{$cmd}{TIME} = $tn; + $hash->{READINGS}{$cmd}{VAL} = $mval; - return undef; + } + return $val; } +sub ZWave_Set($@) { return ZWave_Cmd("set", shift, @_); } +sub ZWave_Get($@) { return ZWave_Cmd("get", shift, @_); } + sub -ZWave_Parse($$) +ZWave_ParseMeter($) { - my ($hash, $msg) = @_; + my ($val) = @_; + return if($val !~ m/^(..)(..)(.*)$/); + my ($v1, $v2, $v3) = (hex($1) & 0x1f, hex($2), $3); + my @prectab = (1,10,100,1000,10000,100000,1000000, 10000000); + my $prec = $prectab[($v2 >> 5) & 0x7]; + my $scale = ($v2 >> 3) & 0x3; + my $size = ($v2 >> 0) & 0x7; + my @txt = ("undef", "power", "gas", "water"); + my $txt = ($v1 > $#txt ? "undef" : $txt[$v1]); + my %unit = (power => ["kWh", "kVAh", "W", "pulseCount"], + gas => ["m3", "feet3", "undef", "pulseCount"], + water => ["m3", "feet3", "USgallons", "pulseCount"]); + my $unit = $txt eq "undef" ? "undef" : $unit{$txt}[$scale]; + $v3 = hex(substr($v3, 0, 2*$size))/$prec; + return "$txt:$v3 $unit"; +} - my ($homeId, $pmsg) = split(" ", $msg, 2); - return "" if($pmsg !~ m/^000400(..)(.*)$/); # Ignore unknown commands for now - my ($id, $p) = ($1, $2); +sub +ZWave_SetClasses($$$$) +{ + my ($homeId, $id, $type6, $classes) = @_; my $def = $modules{ZWave}{defptr}{"$homeId $id"}; - if($def) { - Log 1, "Got $p"; + if(!$def) { + $type6 = $zw_type6{$type6} if($type6 && $zw_type6{$type6}); + $id = hex($id); + return "UNDEFINED ZWave_${type6}_$id ZWave $homeId $id $classes" + } - my @event; - my @changed; - my $tn = TimeNow(); + my @classes; + for my $classId (grep /../, split(/(..)/, lc($classes))) { + push @classes, $zwave_id2class{$classId} if($zwave_id2class{$classId}); + } + my $name = $def->{NAME}; + $attr{$name}{classes} = join(" ", @classes) if(@classes); + $def->{DEF} = "$homeId ".hex($id); + return ""; +} - foreach my $cl (split(" ", $def->{Classes})) { - my $ptr = $zwave_classes{$cl}{parse} if($zwave_classes{$cl}{parse}); - next if(!$ptr); - foreach my $k (keys %{$ptr}) { - if($p =~ m/$k/) { - my $val = $ptr->{$k}; - $val = eval $val if(index($val, '$') >= 0); - push @event, $val; - } - } +sub +ZWave_Parse($$@) +{ + my ($iodev, $msg, $local) = @_; + my $homeId = $iodev->{homeId}; + my $ioName = $iodev->{NAME}; + if(!$homeId) { + Log 1, "ERROR: $ioName homeId is not set!" if(!$iodev->{errReported}); + $iodev->{errReported} = 1; + return; + } + my $ll4 = AttrVal($ioName, "loglevel", 4); + + return "" if($msg !~ m/00(..)(..)(..)(..*)/); # Ignore unknown commands + my ($cmd, $callbackid, $id, $arg) = ($1, $2, $3, $4); + $cmd = $zw_func_id{$cmd} if($zw_func_id{$cmd}); + + ##################################### + # Controller commands + my $evt; + + if($cmd eq 'ZW_ADD_NODE_TO_NETWORK' || + $cmd eq 'ZW_REMOVE_NODE_FROM_NETWORK') { + my @vals = ("learnReady", "nodeFound", "slave", + "controller", "", "done", "failed"); + $evt = ($id eq "00" || hex($id)>@vals+1) ? "unknownArg" : $vals[hex($id)-1]; + if($evt eq "slave" && + $arg =~ m/(..)....(..)..(.*)$/) { + my ($id,$type6,$classes) = ($1, $2, $3); + return ZWave_SetClasses($homeId, $id, $type6, $classes); } - return "" if(!@event); - - for(my $i = 0; $i < int(@event); $i++) { - next if($event[$i] eq ""); - my ($vn, $vv) = split(":", $event[$i], 2); - if($vn eq "state") { - $def->{STATE} = $vv; - push @changed, $vv; - - } else { - push @changed, "$vn: $vv"; - - } - $def->{READINGS}{$vn}{TIME} = $tn; - $def->{READINGS}{$vn}{VAL} = $vv; - } - $def->{CHANGED} = \@changed; - return $def->{NAME}; - - - } else { - Log 3, "ZWave unknown device $homeId $id, please define it"; + } elsif($cmd eq "ZW_APPLICATION_UPDATE" && $arg =~ m/....(..)..(.*)$/) { + my ($type6,$classes) = ($1, $2, $3); + return ZWave_SetClasses($homeId, $id, $type6, $classes); } - return ""; + if($evt) { + return "$cmd $evt" if($local); + DoTrigger($ioName, "$cmd $evt"); + Log $ll4, "$ioName $cmd $evt"; + return ""; + + } else { + Log $ll4, "$ioName $cmd $id ($arg)"; + + } + + + ###################################### + # device messages + return "" if($cmd ne "APPLICATION_COMMAND_HANDLER" || $arg !~ m/^..(..)/); + my $class = $1; + my $hash = $modules{ZWave}{defptr}{"$homeId $id"}; + if(!$hash) { + $id = hex($id); + Log 3, "Unknown ZWave device $homeId $id, please define it"; + return ""; + } + + + my $className = $zwave_id2class{$class} ? $zwave_id2class{$class} : "UNKNOWN"; + my $ptr = $zwave_class{$className}{parse} if($zwave_class{$className}{parse}); + if(!$ptr) { + Log $ll4, "$hash->{NAME}: Unknown message ($className $arg)"; + return ""; + } + + my @event; + my $wakeup; + foreach my $k (keys %{$ptr}) { + if($arg =~ m/$k/) { + my $val = $ptr->{$k}; + $val = eval $val if(index($val, '$') >= 0); + push @event, $val; +Log 1, ">$val<"; +# $wakeup = 1 if($val eq "wakeup:notification" && !$wakeup); + } + } + + return "" if(!@event); + return join(" ", @event) if($local); + + if($hash->{WakeUp} && @{$hash->{WakeUp}}) { +Log 1, "WU..."; + IOWrite($hash, "00", shift @{$hash->{WakeUp}}); + } + + + my @changed; + my $tn = TimeNow(); + for(my $i = 0; $i < int(@event); $i++) { + next if($event[$i] eq ""); + my ($vn, $vv) = split(":", $event[$i], 2); + if($vn eq "state") { + if($hash->{STATE} ne $vv) { + $hash->{STATE} = $vv; + push @changed, $vv; + } + push @changed, "deviceState:$vv"; + + } else { + push @changed, "$vn: $vv"; + + } + $hash->{READINGS}{$vn}{TIME} = $tn; + $hash->{READINGS}{$vn}{VAL} = $vv; + } + $hash->{CHANGED} = \@changed; + return $hash->{NAME}; +} + +##################################### +sub +ZWave_Undef($$) +{ + my ($hash, $arg) = @_; + my $homeId = $hash->{homeId}; + my $id = $hash->{id}; + delete $modules{ZWave}{defptr}{"$homeId $id"}; + return undef; } 1;