###################################################################### # # 88_HMCCUDEV.pm # # $Id: 88_HMCCUDEV.pm 18552 2019-02-10 11:52:28Z zap $ # # Version 4.4.033 # # (c) 2020 zap (zap01 t-online de) # ###################################################################### # Client device for Homematic devices. # Requires module 88_HMCCU.pm ###################################################################### package main; use strict; use warnings; use SetExtensions; require "$attr{global}{modpath}/FHEM/88_HMCCU.pm"; sub HMCCUDEV_Initialize ($); sub HMCCUDEV_Define ($@); sub HMCCUDEV_InitDevice ($$); sub HMCCUDEV_Undef ($$); sub HMCCUDEV_Rename ($$); sub HMCCUDEV_Set ($@); sub HMCCUDEV_Get ($@); sub HMCCUDEV_Attr ($@); ###################################################################### # Initialize module ###################################################################### sub HMCCUDEV_Initialize ($) { my ($hash) = @_; $hash->{DefFn} = 'HMCCUDEV_Define'; $hash->{UndefFn} = 'HMCCUDEV_Undef'; $hash->{RenameFn} = 'HMCCUDEV_Rename'; $hash->{SetFn} = 'HMCCUDEV_Set'; $hash->{GetFn} = 'HMCCUDEV_Get'; $hash->{AttrFn} = 'HMCCUDEV_Attr'; $hash->{parseParams} = 1; $hash->{AttrList} = 'IODev ccuaggregate:textField-long ccucalculate:textField-long '. 'ccuflags:multiple-strict,ackState,logCommand,noReadings,trace,showMasterReadings,showLinkReadings,showDeviceReadings '. 'ccureadingfilter:textField-long '. 'ccureadingformat:name,namelc,address,addresslc,datapoint,datapointlc '. 'ccureadingname:textField-long ccuSetOnChange ccuReadingPrefix '. 'ccuget:State,Value ccuscaleval ccuverify:0,1,2 disable:0,1 '. 'hmstatevals:textField-long statevals substexcl substitute:textField-long statechannel '. 'controlchannel statedatapoint controldatapoint stripnumber peer:textField-long traceFilter '. $readingFnAttributes; } ###################################################################### # Define device ###################################################################### sub HMCCUDEV_Define ($@) { my ($hash, $a, $h) = @_; my $name = $hash->{NAME}; my $usage = "Usage: define $name HMCCUDEV {device|'virtual'} [control-channel] ". "['readonly'] ['noDefaults'|'defaults'] [iodev={iodev-name}] [address={virtual-device-no}]". "[{groupexp=regexp|group={device|channel}[,...]]"; return $usage if (scalar(@$a) < 3); my @errmsg = ( "OK", "Invalid or unknown CCU device name or address", "Can't assign I/O device", "No devices in group", "No matching CCU devices found", "Type of virtual device not defined", "Device type not found", "Too many virtual devices", "Control channel ambiguous. Please specify control channel in device definition" ); my @warnmsg = ( "OK", "Control channel ambiguous. Please specify control channel in device definition or attribute controldatapoint" ); my ($devname, $devtype, $devspec) = splice (@$a, 0, 3); my $ioHash = undef; # Store some definitions for delayed initialization $hash->{readonly} = 'no'; $hash->{hmccu}{devspec} = $devspec; $hash->{hmccu}{groupexp} = $h->{groupexp} if (exists ($h->{groupexp})); $hash->{hmccu}{group} = $h->{group} if (exists ($h->{group})); $hash->{hmccu}{nodefaults} = $init_done ? 0 : 1; $hash->{hmccu}{semDefaults} = 0; if (exists($h->{address})) { return 'Option address not allowed' if ($init_done || $devspec ne 'virtual'); $hash->{hmccu}{address} = $h->{address}; } else { return 'Option address not specified' if (!$init_done && $devspec eq 'virtual'); } # Parse optional command line parameters foreach my $arg (@$a) { if ($arg eq 'readonly') { $hash->{readonly} = 'yes'; } elsif (lc($arg) eq 'nodefaults' && $init_done) { $hash->{hmccu}{nodefaults} = 1; } elsif ($arg eq 'defaults' && $init_done) { $hash->{hmccu}{nodefaults} = 0; } elsif ($arg =~ /^[0-9]+$/) { $attr{$name}{controlchannel} = $arg; } else { return $usage; } } # IO device can be set by command line parameter iodev, otherwise try to detect IO device if (exists($h->{iodev})) { return "IO device $h->{iodev} does not exist" if (!exists($defs{$h->{iodev}})); return "Type of device $h->{iodev} is not HMCCU" if ($defs{$h->{iodev}}->{TYPE} ne 'HMCCU'); $ioHash = $defs{$h->{iodev}}; } else { # The following call will fail for non virtual devices during FHEM start if CCU is not ready $ioHash = $devspec eq 'virtual' ? HMCCU_GetHash (0) : HMCCU_FindIODevice ($devspec); } if ($init_done) { # Interactive define command while CCU not ready if (!defined($ioHash)) { my ($ccuactive, $ccuinactive) = HMCCU_IODeviceStates (); return $ccuinactive > 0 ? 'CCU and/or IO device not ready. Please try again later' : 'Cannot detect IO device'; } } else { # CCU not ready during FHEM start if (!defined($ioHash) || $ioHash->{ccustate} ne 'active') { HMCCU_Log ($hash, 2, 'Cannot detect IO device, maybe CCU not ready. Trying later ...'); $hash->{ccudevstate} = 'pending'; return undef; } } # Initialize FHEM device, set IO device my $rc = HMCCUDEV_InitDevice ($ioHash, $hash); return $errmsg[$rc] if ($rc > 0 && $rc < scalar(@errmsg)); HMCCU_LogDisplay ($hash, 2, $warnmsg[-$rc]) if ($init_done && $rc < 0 && -$rc < scalar(@warnmsg)); 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 # 3 = No devices in group # 4 = No matching CCU devices found # 5 = Type of virtual device not defined # 6 = Device type not found # 7 = Too many virtual devices # -1 = Control channel must be specified ###################################################################### sub HMCCUDEV_InitDevice ($$) { my ($ioHash, $devHash) = @_; my $name = $devHash->{NAME}; my $devspec = $devHash->{hmccu}{devspec}; my $gdcount = 0; my $gdname = $devspec; if ($devspec eq 'virtual') { my $no = 0; if (exists($devHash->{hmccu}{address})) { # Only true during FHEM start $no = $devHash->{hmccu}{address}; } else { # Search for free address. Maximum of 10000 virtual devices allowed. for (my $i=1; $i<=10000; $i++) { my $va = sprintf ("VIR%07d", $i); if (!HMCCU_IsValidDevice ($ioHash, $va, 1)) { $no = $i; last; } } return 7 if ($no == 0); $devHash->{DEF} .= " address=$no"; } # Inform HMCCU device about client device return 2 if (!HMCCU_AssignIODevice ($devHash, $ioHash->{NAME})); $devHash->{ccuif} = 'fhem'; $devHash->{ccuaddr} = sprintf ("VIR%07d", $no); $devHash->{ccuname} = $name; $devHash->{ccudevstate} = 'active'; } else { return 1 if (!HMCCU_IsValidDevice ($ioHash, $devspec, 7)); my ($di, $da, $dn, $dt, $dc) = HMCCU_GetCCUDeviceParam ($ioHash, $devspec); return 1 if (!defined($da)); $gdname = $dn; # Inform HMCCU device about client device return 2 if (!HMCCU_AssignIODevice ($devHash, $ioHash->{NAME})); $devHash->{ccuif} = $di; $devHash->{ccuaddr} = $da; $devHash->{ccuname} = $dn; $devHash->{ccutype} = $dt; $devHash->{ccudevstate} = 'active'; $devHash->{hmccu}{channels} = $dc; if ($init_done) { # Interactive device definition HMCCU_AddDevice ($ioHash, $di, $da, $devHash->{NAME}); HMCCU_UpdateDevice ($ioHash, $devHash); HMCCU_UpdateDeviceRoles ($ioHash, $devHash); my ($sc, $sd, $cc, $cd, $sdCnt, $cdCnt) = HMCCU_GetSpecialDatapoints ($devHash); return -1 if ($cdCnt > 2); HMCCU_UpdateRoleCommands ($ioHash, $devHash, $attr{$devHash->{NAME}}{controlchannel}); HMCCU_UpdateAdditionalCommands ($ioHash, $devHash, $attr{$devHash->{NAME}}{controlchannel}); if (!exists($devHash->{hmccu}{nodefaults}) || $devHash->{hmccu}{nodefaults} == 0) { if (!HMCCU_SetDefaultAttributes ($devHash, { mode => 'update', role => undef, ctrlChn => $cc eq '' ? undef : $cc })) { HMCCU_Log ($devHash, 2, "No role attributes found"); HMCCU_SetDefaults ($devHash); } } HMCCU_GetUpdate ($devHash, $da, 'Value'); } } # Parse group options if ($devHash->{ccuif} eq 'VirtualDevices' || $devHash->{ccuif} eq 'fhem') { my @devlist = (); if (exists ($devHash->{hmccu}{groupexp})) { # Group devices specified by name expression $gdcount = HMCCU_GetMatchingDevices ($ioHash, $devHash->{hmccu}{groupexp}, 'dev', \@devlist); return 4 if ($gdcount == 0); } elsif (exists ($devHash->{hmccu}{group})) { # Group devices specified by comma separated name list my @gdevlist = split (',', $devHash->{hmccu}{group}); $devHash->{ccugroup} = '' if (scalar(@gdevlist) > 0); foreach my $gd (@gdevlist) { return 1 if (!HMCCU_IsValidDevice ($ioHash, $gd, 7)); my ($gda, $gdc) = HMCCU_GetAddress ($ioHash, $gd); push @devlist, $gdc eq '' ? "$gda:$gdc" : $gda; $gdcount++; } } else { # Group specified by CCU virtual group name @devlist = HMCCU_GetGroupMembers ($ioHash, $gdname); $gdcount = scalar (@devlist); } return 3 if ($gdcount == 0); $devHash->{ccugroup} = join (',', @devlist); if ($devspec eq 'virtual') { my $dev = shift @devlist; my $devtype = HMCCU_GetDeviceType ($ioHash, $dev, 'n/a'); my $devna = $devtype eq 'n/a' ? 1 : 0; for my $d (@devlist) { if (HMCCU_GetDeviceType ($ioHash, $d, 'n/a') ne $devtype) { $devna = 1; last; } } my $rc = 0; if ($devna) { $devHash->{ccutype} = 'n/a'; $devHash->{readonly} = 'yes'; $rc = HMCCU_CreateDevice ($ioHash, $devHash->{ccuaddr}, $name, undef, $dev); } else { $devHash->{ccutype} = $devtype; $rc = HMCCU_CreateDevice ($ioHash, $devHash->{ccuaddr}, $name, $devtype, $dev); } return $rc+4 if ($rc > 0); # Set default attributes $attr{$name}{ccureadingformat} = 'name'; } } return 0; } ###################################################################### # Delete device ###################################################################### sub HMCCUDEV_Undef ($$) { my ($hash, $arg) = @_; if ($hash->{IODev}) { HMCCU_RemoveDevice ($hash->{IODev}, $hash->{ccuif}, $hash->{ccuaddr}, $hash->{NAME}); HMCCU_DeleteDevice ($hash->{IODev}) if ($hash->{ccuif} eq 'fhem'); } return undef; } ###################################################################### # Rename device ###################################################################### sub HMCCUDEV_Rename ($$) { my ($newName, $oldName) = @_; my $clHash = $defs{$newName}; my $ioHash = defined($clHash) ? $clHash->{IODev} : undef; HMCCU_RenameDevice ($ioHash, $clHash, $oldName); } ###################################################################### # Set attribute ###################################################################### sub HMCCUDEV_Attr ($@) { my ($cmd, $name, $attrname, $attrval) = @_; my $hash = $defs{$name}; if ($cmd eq 'set') { return "Missing value of attribute $attrname" if (!defined($attrval)); if ($attrname eq 'IODev') { $hash->{IODev} = $defs{$attrval}; } elsif ($attrname eq 'statevals') { return "Device is read only" if ($hash->{readonly} eq 'yes'); } elsif ($attrname eq 'statechannel') { $hash->{hmccu}{state}{chn} = $attrval; } elsif ($attrname eq 'statedatapoint') { if ($attrval =~ /^([0-9]{1,2})\.(.+)/) { $hash->{hmccu}{state}{chn} = $1; $hash->{hmccu}{state}{dpt} = $2; } else { $hash->{hmccu}{state}{dpt} = $attrval; } } elsif ($attrname eq 'controlchannel') { $hash->{hmccu}{control}{chn} = $attrval; } elsif ($attrname eq 'controldatapoint') { if ($attrval =~ /^([0-9]{1,2})\.(.+)/) { $hash->{hmccu}{control}{chn} = $1; $hash->{hmccu}{control}{dpt} = $2; } else { $hash->{hmccu}{control}{dpt} = $attrval; } } } HMCCU_RefreshReadings ($hash) if ($init_done); return; } ###################################################################### # Set commands ###################################################################### sub HMCCUDEV_Set ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; my $opt = shift @$a // return 'No set command specified'; $opt = lc($opt); # Check device state return "Device state doesn't allow set commands" if (!defined($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending' || !defined($hash->{IODev}) || ($hash->{readonly} eq 'yes' && $opt !~ /^(\?|clear|config|defaults)$/) || AttrVal ($name, 'disable', 0) == 1); my $ioHash = $hash->{IODev}; my $ioName = $ioHash->{NAME}; return ($opt eq '?' ? undef : 'Cannot perform set commands. CCU busy') if (HMCCU_IsRPCStateBlocking ($ioHash)); # Get state and control datapoints my ($sc, $sd, $cc, $cd) = HMCCU_GetSpecialDatapoints ($hash); # Get additional commands my $cmdList = $hash->{hmccu}{cmdlist} // ''; # Some commands require a control channel and datapoint if ($opt =~ /^(control|toggle)$/) { return HMCCU_SetError ($hash, -14) if ($cd eq ''); return HMCCU_SetError ($hash, -12) if ($cc eq ''); return HMCCU_SetError ($hash, -8) if (!HMCCU_IsValidDatapoint ($hash, $hash->{ccutype}, $cc, $cd, 2)); return HMCCU_SetError ($hash, -7) if ($cc >= $hash->{hmccu}{channels}); } my $result = ''; my $rc; # Log commands HMCCU_Log ($hash, 3, "set $name $opt ".join (' ', @$a)) if ($opt ne '?' && (HMCCU_IsFlag ($name, 'logCommand') || HMCCU_IsFlag ($ioName, 'logCommand'))); if ($opt eq 'control') { my $value = shift @$a // return HMCCU_SetError ($hash, "Usage: set $name control {value}"); my $stateVals = HMCCU_GetStateValues ($hash, $cd, $cc); $rc = HMCCU_SetMultipleDatapoints ($hash, { "001.$hash->{ccuif}.$hash->{ccuaddr}:$cc.$cd" => HMCCU_Substitute ($value, $stateVals, 1, undef, '') } ); return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); } elsif ($opt eq 'datapoint') { return HMCCU_ExecuteSetDatapointCommand ($hash, $a, $h, $cc, $cd); } elsif ($opt eq 'toggle') { return HMCCU_ExecuteToggleCommand ($hash, $cc, $cd); } elsif (exists($hash->{hmccu}{roleCmds}{$opt})) { return HMCCU_ExecuteRoleCommand ($ioHash, $hash, $opt, $cc, $a, $h); } elsif ($opt eq 'clear') { return HMCCU_ExecuteSetClearCommand ($hash, $a); } elsif ($opt =~ /^(config|values)$/) { return HMCCU_ExecuteSetParameterCommand ($ioHash, $hash, $opt, $a, $h); } elsif ($opt eq 'defaults') { my $mode = shift @$a // 'update'; $rc = HMCCU_SetDefaultAttributes ($hash, { mode => $mode, role => undef, ctrlChn => $cc }); $rc = HMCCU_SetDefaults ($hash) if (!$rc); HMCCU_RefreshReadings ($hash) if ($rc); return HMCCU_SetError ($hash, $rc == 0 ? 'No default attributes found' : 'OK'); } else { my $retmsg = 'clear defaults:reset,update'; if ($hash->{readonly} ne 'yes') { $retmsg .= ' config datapoint'; $retmsg .= " $cmdList" if ($cmdList ne ''); } return AttrTemplate_Set ($hash, $retmsg, $name, $opt, @$a); } } ###################################################################### # Get commands ###################################################################### sub HMCCUDEV_Get ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; my $opt = shift @$a // return 'No get command specified'; $opt = lc($opt); # Get I/O device return "Device state doesn't allow set commands" if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending' || !defined ($hash->{IODev}) || AttrVal ($name, "disable", 0) == 1); my $ioHash = $hash->{IODev}; my $ioName = $ioHash->{NAME}; # Check if CCU is busy return $opt eq '?' ? undef : 'Cannot perform get commands. CCU busy' if (HMCCU_IsRPCStateBlocking ($ioHash)); # Get parameters of current device my $ccutype = $hash->{ccutype}; my $ccuaddr = $hash->{ccuaddr}; my $ccuif = $hash->{ccuif}; my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my ($sc, $sd, $cc, $cd) = HMCCU_GetSpecialDatapoints ($hash); # Virtual devices only support command get update return "HMCCUDEV: Unknown argument $opt, choose one of update:noArg" if ($ccuif eq 'fhem' && $opt ne 'update'); my $result = ''; my $rc; # Log commands HMCCU_Log ($hash, 3, "get $name $opt ".join (' ', @$a)) if ($opt ne '?' && $ccuflags =~ /logCommand/ || HMCCU_IsFlag ($ioName, 'logCommand')); if ($opt eq 'datapoint') { my $objname = shift @$a // return HMCCU_SetError ($hash, "Usage: get $name datapoint [{channel-number}.]{datapoint}"); if ($objname =~ /^([0-9]+)\..+$/) { my $chn = $1; return HMCCU_SetError ($hash, -7) if ($chn >= $hash->{hmccu}{channels}); } else { return HMCCU_SetError ($hash, -11) if ($sc eq ''); $objname = $sc.'.'.$objname; } return HMCCU_SetError ($hash, -8) if (!HMCCU_IsValidDatapoint ($hash, $ccutype, undef, $objname, 1)); $objname = $ccuif.'.'.$ccuaddr.':'.$objname; ($rc, $result) = HMCCU_GetDatapoint ($hash, $objname, 0); return HMCCU_SetError ($hash, $rc, $result) if ($rc < 0); HMCCU_SetState ($hash, "OK") if (exists ($hash->{STATE}) && $hash->{STATE} eq "Error"); return $result; } elsif ($opt eq 'deviceinfo') { return HMCCU_ExecuteGetDeviceInfoCommand ($ioHash, $hash, $ccuaddr, $sc, $sd, $cc, $cd); } elsif ($opt =~ /^(config|values|update)$/) { my @addList = ($ccuaddr); my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $ccuaddr, $ccuif); return HMCCU_SetError ($hash, "Can't get device description") if (!defined($devDesc)); push @addList, split (',', $devDesc->{CHILDREN}); return HMCCU_ExecuteGetParameterCommand ($ioHash, $hash, $opt, \@addList); } elsif ($opt eq 'paramsetdesc') { $result = HMCCU_ParamsetDescToStr ($ioHash, $hash); return defined($result) ? $result : HMCCU_SetError ($hash, "Can't get device model"); } elsif ($opt eq 'defaults') { $result = HMCCU_GetDefaults ($hash, 0); return $result; } elsif ($opt eq 'weekprogram') { my $program = shift @$a; return HMCCU_DisplayWeekProgram ($hash, $program); } else { my $retmsg = "HMCCUDEV: Unknown argument $opt, choose one of datapoint"; my @valuelist; my $valuecount = HMCCU_GetValidDatapoints ($hash, $ccutype, -1, 1, \@valuelist); $retmsg .= ':'.join(",", @valuelist) if ($valuecount > 0); $retmsg .= ' defaults:noArg update:noArg config:noArg'. ' paramsetDesc:noArg deviceInfo:noArg values:noArg'; $retmsg .= ' weekProgram:all,'.join(',', sort keys %{$hash->{hmccu}{tt}}) if (exists($hash->{hmccu}{tt})); return $retmsg; } } 1; =pod =item device =item summary controls HMCCU client devices for Homematic CCU - FHEM integration =begin html

HMCCUDEV

=end html =cut