###################################################################### # # 88_HMCCUCHN.pm # # $Id$ # # Version 4.3.011 # # (c) 2020 zap (zap01 t-online de) # ###################################################################### # Client device for Homematic channels. # Requires modules 88_HMCCU.pm, HMCCUConf.pm ###################################################################### package main; use strict; use warnings; use SetExtensions; sub HMCCUCHN_Initialize ($); sub HMCCUCHN_Define ($@); sub HMCCUCHN_InitDevice ($$); sub HMCCUCHN_Set ($@); sub HMCCUCHN_Get ($@); sub HMCCUCHN_Attr ($@); ###################################################################### # Initialize module ###################################################################### sub HMCCUCHN_Initialize ($) { my ($hash) = @_; $hash->{DefFn} = "HMCCUCHN_Define"; $hash->{SetFn} = "HMCCUCHN_Set"; $hash->{GetFn} = "HMCCUCHN_Get"; $hash->{AttrFn} = "HMCCUCHN_Attr"; $hash->{parseParams} = 1; $hash->{AttrList} = "IODev ccucalculate ". "ccuflags:multiple-strict,ackState,logCommand,nochn0,trace ccureadingfilter ". "ccureadingformat:name,namelc,address,addresslc,datapoint,datapointlc ". "ccureadingname:textField-long ccuSetOnChange ". "ccureadings:0,1 ccuscaleval ccuverify:0,1,2 ccuget:State,Value controldatapoint ". "disable:0,1 hmstatevals:textField-long statedatapoint statevals substitute:textField-long ". "substexcl stripnumber peer:textField-long ". $readingFnAttributes; } ###################################################################### # Define device ###################################################################### sub HMCCUCHN_Define ($@) { my ($hash, $a, $h) = @_; my $name = $hash->{NAME}; my $usage = "Usage: define $name HMCCUCHN {device} ['readonly'] ['defaults'] [iodev={iodevname}]"; return $usage if (@$a < 3); my $devname = shift @$a; my $devtype = shift @$a; my $devspec = shift @$a; my $hmccu_hash = undef; # Store some definitions for delayed initialization $hash->{hmccu}{devspec} = $devspec; # Defaults $hash->{channels} = 1; $hash->{statevals} = 'devstate'; # Parse optional command line parameters my $n = 0; my $arg = shift @$a; while (defined ($arg)) { return $usage if ($n == 3); if ($arg eq 'readonly') { $hash->{statevals} = $arg; } elsif ($arg eq 'defaults') { HMCCU_SetDefaults ($hash) if ($init_done); } else { return $usage; } $n++; $arg = shift @$a; } # IO device can be set by command line parameter iodev, otherwise try to detect IO device if (exists ($h->{iodev})) { return "Specified IO Device ".$h->{iodev}." does not exist" if (!exists ($defs{$h->{iodev}})); return "Specified IO Device ".$h->{iodev}." is not a HMCCU device" if ($defs{$h->{iodev}}->{TYPE} ne 'HMCCU'); $hmccu_hash = $defs{$h->{iodev}}; } else { # The following call will fail during FHEM start if CCU is not ready $hmccu_hash = HMCCU_FindIODevice ($devspec); } if ($init_done) { # Interactive define command while CCU not ready or no IO device defined if (!defined ($hmccu_hash)) { my ($ccuactive, $ccuinactive) = HMCCU_IODeviceStates (); if ($ccuinactive > 0) { return "CCU and/or IO device not ready. Please try again later"; } else { return "Cannot detect IO device"; } } } else { # CCU not ready during FHEM start if (!defined ($hmccu_hash) || $hmccu_hash->{ccustate} ne 'active') { Log3 $name, 2, "HMCCUCHN: [$devname] Cannot detect IO device, maybe CCU not ready. Trying later ..."; # readingsSingleUpdate ($hash, "state", "Pending", 1); $hash->{ccudevstate} = 'pending'; return undef; } } # Initialize FHEM device, set IO device my $rc = HMCCUCHN_InitDevice ($hmccu_hash, $hash); return "Invalid or unknown CCU channel name or address" if ($rc == 1); return "Can't assign I/O device ".$hmccu_hash->{NAME} if ($rc == 2); return undef; } ###################################################################### # Initialization of FHEM device. # Called during Define() or by HMCCU after CCU ready. # Return 0 on successful initialization or >0 on error: # 1 = Invalid channel name or address # 2 = Cannot assign IO device ###################################################################### sub HMCCUCHN_InitDevice ($$) { my ($hmccu_hash, $dev_hash) = @_; my $devspec = $dev_hash->{hmccu}{devspec}; return 1 if (!HMCCU_IsValidChannel ($hmccu_hash, $devspec, 7)); my ($di, $da, $dn, $dt, $dc) = HMCCU_GetCCUDeviceParam ($hmccu_hash, $devspec); return 1 if (!defined ($da)); # Inform HMCCU device about client device return 2 if (!HMCCU_AssignIODevice ($dev_hash, $hmccu_hash->{NAME}, undef)); $dev_hash->{ccuif} = $di; $dev_hash->{ccuaddr} = $da; $dev_hash->{ccuname} = $dn; $dev_hash->{ccutype} = $dt; # readingsSingleUpdate ($dev_hash, "state", "Initialized", 1); $dev_hash->{ccudevstate} = 'active'; return 0; } ###################################################################### # Set attribute ###################################################################### sub HMCCUCHN_Attr ($@) { my ($cmd, $name, $attrname, $attrval) = @_; my $hash = $defs{$name}; if ($cmd eq "set") { return "Missing attribute value" if (!defined ($attrval)); if ($attrname eq 'IODev') { $hash->{IODev} = $defs{$attrval}; } elsif ($attrname eq 'statevals') { return "Device is read only" if ($hash->{statevals} eq 'readonly'); $hash->{statevals} = "devstate"; my @states = split /,/,$attrval; foreach my $st (@states) { my @statesubs = split /:/,$st; return "value := text:substext[,...]" if (@statesubs != 2); $hash->{statevals} .= '|'.$statesubs[0]; } } } elsif ($cmd eq "del") { if ($attrname eq 'statevals') { $hash->{statevals} = "devstate"; } } return undef; } ###################################################################### # Set commands ###################################################################### sub HMCCUCHN_Set ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; my $opt = shift @$a; return "No set command specified" if (!defined ($opt)); my $rocmds = "clear defaults:noArg"; my $rwcmds = "clear config control datapoint defaults:noArg rpcparameter devstate"; # Get I/O device, check device state return undef if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending' || !defined ($hash->{IODev})); return undef if ($hash->{statevals} eq 'readonly' && $opt ne '?' && $opt !~ /^(clear|config|defaults)$/); my $disable = AttrVal ($name, "disable", 0); return undef if ($disable == 1); my $hmccu_hash = $hash->{IODev}; if (HMCCU_IsRPCStateBlocking ($hmccu_hash)) { return undef if ($opt eq '?'); return "HMCCUCHN: CCU busy"; } my $ccutype = $hash->{ccutype}; my $ccuaddr = $hash->{ccuaddr}; my $ccuif = $hash->{ccuif}; my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $statevals = AttrVal ($name, 'statevals', ''); my ($sc, $sd, $cc, $cd) = HMCCU_GetSpecialDatapoints ($hash, '', 'STATE', '', ''); my $result = ''; my $rc; # Log commands HMCCU_Log ($hash, 3, "set $name $opt ".join (' ', @$a)) if ($ccuflags =~ /logCommand/); if ($opt eq 'datapoint') { my $usage = "Usage: set $name datapoint {datapoint} {value} [...]"; my %dpval; my $i = 0; while (my $objname = shift @$a) { my $objvalue = shift @$a; $i += 1; return HMCCU_SetError ($hash, $usage) if (!defined ($objvalue)); return HMCCU_SetError ($hash, -8) if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $ccuaddr, $objname, 2)); my $no = sprintf ("%03d", $i); $objvalue =~ s/\\_/%20/g; $dpval{"$no.$ccuif.$ccuaddr.$objname"} = HMCCU_Substitute ($objvalue, $statevals, 1, undef, ''); } return HMCCU_SetError ($hash, $usage) if (scalar (keys %dpval) < 1); $rc = HMCCU_SetMultipleDatapoints ($hash, \%dpval); return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); } elsif ($opt eq 'control') { return HMCCU_SetError ($hash, -14) if ($cd eq ''); return HMCCU_SetError ($hash, -8) if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $cc, $cd, 2)); my $objvalue = shift @$a; return HMCCU_SetError ($hash, "Usage: set $name control {value}") if (!defined ($objvalue)); $objvalue =~ s/\\_/%20/g; $rc = HMCCU_SetMultipleDatapoints ($hash, { "001.$ccuif.$ccuaddr.$cd" => HMCCU_Substitute ($objvalue, $statevals, 1, undef, '') } ); return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); } elsif ($opt =~ /^($hash->{statevals})$/) { my $cmd = $1; my $objvalue = ($cmd ne 'devstate') ? $cmd : shift @$a; return HMCCU_SetError ($hash, -13) if ($sd eq ''); return HMCCU_SetError ($hash, -8) if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $ccuaddr, $sd, 2)); return HMCCU_SetError ($hash, "Usage: set $name devstate {value}") if (!defined ($objvalue)); $objvalue =~ s/\\_/%20/g; $rc = HMCCU_SetMultipleDatapoints ($hash, { "001.$ccuif.$ccuaddr.$sd" => HMCCU_Substitute ($objvalue, $statevals, 1, undef, '') } ); return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); } elsif ($opt eq 'toggle') { return HMCCU_SetError ($hash, -15) if ($statevals eq '' || !exists($hash->{statevals})); return HMCCU_SetError ($hash, -13) if ($sd eq ''); return HMCCU_SetError ($hash, -8) if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $ccuaddr, $sd, 2)); my $tstates = $hash->{statevals}; $tstates =~ s/devstate\|//; my @states = split /\|/, $tstates; my $stc = scalar (@states); my $objname = $ccuif.'.'.$ccuaddr.'.'.$sd; ($rc, $result) = HMCCU_GetDatapoint ($hash, $objname, 1); return HMCCU_SetError ($hash, $rc, $result) if ($rc < 0); my $objvalue = ''; my $st = 0; while ($st < $stc) { if ($states[$st] eq $result) { $objvalue = ($st == $stc-1) ? $states[0] : $states[$st+1]; last; } else { $st++; } } return HMCCU_SetError ($hash, "Current device state doesn't match statevals") if ($objvalue eq ''); $rc = HMCCU_SetMultipleDatapoints ($hash, { "001.$objname" => HMCCU_Substitute ($objvalue, $statevals, 1, undef, '') } ); return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); } elsif ($opt eq 'pct' || $opt eq 'up' || $opt eq 'down') { return HMCCU_SetError ($hash, "Can't find LEVEL datapoint for device type $ccutype") if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $ccuaddr, "LEVEL", 2)); if ($opt eq 'pct') { my $objname = ''; my $objvalue = shift @$a; return HMCCU_SetError ($hash, "Usage: set $name pct {value} [{ontime} [{ramptime}]]") if (!defined ($objvalue)); my $timespec = shift @$a; my $ramptime = shift @$a; my %dpval; # Set on time if (defined ($timespec)) { return HMCCU_SetError ($hash, "Can't find ON_TIME datapoint for device type $ccutype") if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $ccuaddr, "ON_TIME", 2)); if ($timespec =~ /^[0-9]{2}:[0-9]{2}/) { $timespec = HMCCU_GetTimeSpec ($timespec); return HMCCU_SetError ($hash, "Wrong time format. Use HH:MM[:SS]") if ($timespec < 0); } $dpval{"001.$ccuif.$ccuaddr.ON_TIME"} = $timespec if ($timespec > 0); } # Set ramp time $dpval{"002.$ccuif.$ccuaddr.RAMP_TIME"} = $ramptime if (defined ($ramptime)); # Set level $dpval{"003.$ccuif.$ccuaddr.LEVEL"} = $objvalue; $rc = HMCCU_SetMultipleDatapoints ($hash, \%dpval); } else { my $delta = shift @$a; $delta = 10 if (!defined ($delta)); $delta = -$delta if ($opt eq 'down'); my $objname = "$ccuif.$ccuaddr.LEVEL"; ($rc, $result) = HMCCU_GetDatapoint ($hash, $objname, 1); return HMCCU_SetError ($hash, $rc, $result) if ($rc < 0); # Set level my $objvalue = HMCCU_Min(HMCCU_Max($result+$delta,0),100); $rc = HMCCU_SetMultipleDatapoints ($hash, { "001.$objname" => $objvalue }); } return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); } elsif ($opt eq 'on-for-timer' || $opt eq 'on-till') { return HMCCU_SetError ($hash, -15) if ($statevals eq '' || !exists($hash->{statevals})); return HMCCU_SetError ($hash, "No state value for 'on' defined") if ("on" !~ /($hash->{statevals})/); return HMCCU_SetError ($hash, -13) if ($sd eq ''); return HMCCU_SetError ($hash, -8) if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $ccuaddr, $sd, 2)); return HMCCU_SetError ($hash, "Can't find ON_TIME datapoint for device type") if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $ccuaddr, "ON_TIME", 2)); my $timespec = shift @$a; return HMCCU_SetError ($hash, "Usage: set $name $opt {ontime-spec}") if (!defined ($timespec)); if ($opt eq 'on-till') { $timespec = HMCCU_GetTimeSpec ($timespec); return HMCCU_SetError ($hash, "Wrong time format. Use HH:MM[:SS]") if ($timespec < 0); } $rc = HMCCU_SetMultipleDatapoints ($hash, { "001.$ccuif.$ccuaddr.ON_TIME" => $timespec, "002.$ccuif.$ccuaddr.$sd" => HMCCU_Substitute ("on", $statevals, 1, undef, '') }); return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); } elsif ($opt eq 'clear') { my $rnexp = shift @$a; HMCCU_DeleteReadings ($hash, $rnexp); return HMCCU_SetState ($hash, "OK"); } elsif ($opt eq 'config') { return HMCCU_SetError ($hash, "Usage: set $name config [device] {parameter}={value} [...]") if ((scalar keys %{$h}) < 1); my $ccuobj = $ccuaddr; my $par = shift @$a; if (defined ($par) && $par eq 'device') { ($ccuobj, undef) = HMCCU_SplitChnAddr ($ccuaddr); } ($rc, $result) = HMCCU_RPCRequest ($hash, "putParamset", $ccuobj, "MASTER", $h); return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); } elsif ($opt eq 'rpcparameter') { return HMCCU_SetError ($hash, "Usage: set $name rpcparameter [MASTER|VALUES] {parameter}={value} [...]") if ((scalar keys %{$h}) < 1); my $key = shift @$a; $key = 'VALUES' if (!defined ($key)); if ($key eq 'VALUES') { ($rc, $result) = HMCCU_SetMultipleParameters ($hash, $ccuaddr, $h); } elsif ($key eq 'MASTER') { ($rc, $result) = HMCCU_RPCRequest ($hash, "putParamset", $ccuaddr, $key, $h); } else { return HMCCU_SetError ($hash, "Key must be MASTER or VALUES"); } return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); } elsif ($opt eq 'defaults') { $rc = HMCCU_SetDefaults ($hash); return HMCCU_SetError ($hash, $rc == 0 ? "No default attributes found" : "OK"); } else { my $retmsg = "clear defaults:noArg"; if ($hash->{statevals} ne 'readonly') { $retmsg .= " config control datapoint rpcparameter devstate"; if ($hash->{statevals} ne '') { my @cmdlist = split /\|/,$hash->{statevals}; shift @cmdlist; $retmsg .= ':'.join(',',@cmdlist) if (scalar(@cmdlist) > 0); foreach my $sv (@cmdlist) { $retmsg .= ' '.$sv.':noArg'; } $retmsg .= " toggle:noArg"; $retmsg .= " on-for-timer on-till" if (HMCCU_IsValidDatapoint ($hash, $hash->{ccutype}, $ccuaddr, "ON_TIME", 2)); $retmsg .= " pct up down" if (HMCCU_IsValidDatapoint ($hash, $hash->{ccutype}, $ccuaddr, "LEVEL", 2)); } } return AttrTemplate_Set ($hash, $retmsg, $name, $opt, @$a); } } ###################################################################### # Get commands ###################################################################### sub HMCCUCHN_Get ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; my $opt = shift @$a; return "No get command specified" if (!defined ($opt)); return undef if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending' || !defined ($hash->{IODev})); my $disable = AttrVal ($name, "disable", 0); return undef if ($disable == 1); my $hmccu_hash = $hash->{IODev}; if (HMCCU_IsRPCStateBlocking ($hmccu_hash)) { return undef if ($opt eq '?'); return "HMCCUCHN: CCU busy"; } my $ccutype = $hash->{ccutype}; my $ccuaddr = $hash->{ccuaddr}; my $ccuif = $hash->{ccuif}; my ($sc, $sd, $cc, $cd) = HMCCU_GetSpecialDatapoints ($hash, '', 'STATE', '', ''); my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $ccureadings = AttrVal ($name, "ccureadings", 1); my $result = ''; my $rc; # Log commands HMCCU_Log ($hash, 3, "set $name $opt ".join (' ', @$a)) if ($ccuflags =~ /logCommand/); if ($opt eq 'devstate') { return HMCCU_SetError ($hash, -13) if ($sd eq ''); return HMCCU_SetError ($hash, -8) if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $ccuaddr, $sd, 1)); my $objname = $ccuif.'.'.$ccuaddr.'.'.$sd; ($rc, $result) = HMCCU_GetDatapoint ($hash, $objname, 0); return HMCCU_SetError ($hash, $rc, $result) if ($rc < 0); return $ccureadings ? undef : $result; } elsif ($opt eq 'datapoint') { my $objname = shift @$a; return HMCCU_SetError ($hash, "Usage: get $name datapoint {datapoint}") if (!defined ($objname)); return HMCCU_SetError ($hash, -8) if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $ccuaddr, $objname, 1)); $objname = $ccuif.'.'.$ccuaddr.'.'.$objname; ($rc, $result) = HMCCU_GetDatapoint ($hash, $objname, 0); return HMCCU_SetError ($hash, $rc, $result) if ($rc < 0); return $ccureadings ? undef : $result; } elsif ($opt eq 'update') { my $ccuget = shift @$a; $ccuget = 'Attr' if (!defined ($ccuget)); if ($ccuget !~ /^(Attr|State|Value)$/) { return HMCCU_SetError ($hash, "Usage: get $name update [{'State'|'Value'}]"); } $rc = HMCCU_GetUpdate ($hash, $ccuaddr, $ccuget); return HMCCU_SetError ($hash, $rc) if ($rc < 0); return undef; } elsif ($opt eq 'deviceinfo') { my $ccuget = shift @$a; $ccuget = 'Attr' if (!defined ($ccuget)); if ($ccuget !~ /^(Attr|State|Value)$/) { return HMCCU_SetError ($hash, "Usage: get $name deviceinfo [{'State'|'Value'}]"); } my ($a, $c) = split(":", $hash->{ccuaddr}); $result = HMCCU_GetDeviceInfo ($hash, $a, $ccuget); return HMCCU_SetError ($hash, -2) if ($result eq ''); return HMCCU_FormatDeviceInfo ($result); } elsif ($opt eq 'config') { my $ccuobj = $ccuaddr; my $par = shift @$a; if (defined ($par)) { if ($par eq 'device') { ($ccuobj, undef) = HMCCU_SplitChnAddr ($ccuaddr); $par = shift @$a; } } $par = '.*' if (!defined ($par)); ($rc, $result) = HMCCU_RPCRequest ($hash, "getParamset", $ccuobj, "MASTER", undef, $par); return HMCCU_SetError ($hash, $rc, $result) if ($rc < 0); return $ccureadings ? undef : $result; } elsif ($opt eq 'configlist') { my $ccuobj = $ccuaddr; my $par = shift @$a; if (defined ($par)) { if ($par eq 'device') { ($ccuobj, undef) = HMCCU_SplitChnAddr ($ccuaddr); $par = shift @$a; } } $par = '.*' if (!defined ($par)); ($rc, $result) = HMCCU_RPCRequest ($hash, "listParamset", $ccuobj, "MASTER", undef, $par); return HMCCU_SetError ($hash, $rc, $result) if ($rc < 0); return $result; } elsif ($opt eq 'configdesc') { my $ccuobj = $ccuaddr; my $par = shift @$a; if (defined ($par) && $par eq 'device') { ($ccuobj, undef) = HMCCU_SplitChnAddr ($ccuaddr); } ($rc, $result) = HMCCU_RPCRequest ($hash, "getParamsetDescription", $ccuobj, "MASTER", undef); return HMCCU_SetError ($hash, $rc, $result) if ($rc < 0); return $result; } elsif ($opt eq 'defaults') { return HMCCU_GetDefaults ($hash, 0); } else { my $retmsg = "HMCCUCHN: Unknown argument $opt, choose one of devstate:noArg defaults:noArg datapoint"; my ($a, $c) = split(":", $hash->{ccuaddr}); my @valuelist; my $valuecount = HMCCU_GetValidDatapoints ($hash, $hash->{ccutype}, $c, 1, \@valuelist); $retmsg .= ":".join(",",@valuelist) if ($valuecount > 0); $retmsg .= " update:noArg deviceinfo config configlist configdesc:noArg"; return $retmsg; } } 1; =pod =item device =item summary controls HMCCU client devices for Homematic CCU2 - FHEM integration =begin html

HMCCUCHN

=end html =cut