###################################################################### # # 88_HMCCUDEV.pm # # $Id$ # # Version 5.0 # # (c) 2022 zap (zap01 t-online de) # ###################################################################### # Client device for Homematic devices. # Requires module 88_HMCCU.pm ###################################################################### package main; use strict; use warnings; # use Data::Dumper; 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 ($@); my $HMCCUDEV_VERSION = '5.0 220431743'; ###################################################################### # Initialize module ###################################################################### sub HMCCUDEV_Initialize ($) { my ($hash) = @_; $hash->{version} = $HMCCUDEV_VERSION; $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,hideStdReadings,replaceStdReadings,noBoundsChecking,logCommand,noReadings,trace,showMasterReadings,showLinkReadings,showDeviceReadings,showServiceReadings '. 'ccureadingfilter:textField-long '. 'ccureadingformat:name,namelc,address,addresslc,datapoint,datapointlc '. 'ccureadingname:textField-long ccuSetOnChange ccuReadingPrefix devStateFlags '. 'ccuget:State,Value ccuscaleval ccuverify:0,1,2 disable:0,1 '. 'hmstatevals:textField-long statevals substexcl substitute:textField-long statechannel statedatapoint '. 'controlchannel 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 [control-channel] ". "['readonly'] ['noDefaults'|'defaults'] [forceDev] [iodev={iodev-name}] ". "[sd={state-datapoint}] [cd={control-datapoint}]"; 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", "Use HMCCUCHN instead" ); my @warnmsg = ( "OK", "Control channel ambiguous. You can change the default control channel in device definition or with attribute controldatapoint", "Device type not known by HMCCU. Please set control and/or state channel with attributes controldatapoint and statedatapoint" ); my ($devname, $devtype, $devspec) = splice (@$a, 0, 3); my $ioHash = undef; # Handle some legacy options return 'Virtual devices are no longer supported. Use FHEM built in features like readingsgroup or structure' if ($devspec eq 'virtual'); HMCCU_Log ($hash, 2, "Found old device definition syntax using group or groupexp. Group options will be ignored in future versions.") if (exists($h->{group}) || exists($h->{groupexp})); # 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}{forcedev} = 0; $hash->{hmccu}{detect} = 0; $hash->{hmccu}{defSDP} = $h->{sd} if (exists($h->{sd})); $hash->{hmccu}{defCDP} = $h->{cd} if (exists($h->{cd})); $hash->{hmccu}{setDefaults} = 0; # Parse optional command line parameters foreach my $arg (@$a) { if (lc($arg) eq 'readonly') { $hash->{readonly} = 'yes'; } elsif (lc($arg) eq 'nodefaults') { $hash->{hmccu}{nodefaults} = 1 if ($init_done); } elsif (lc($arg) eq 'defaults') { $hash->{hmccu}{nodefaults} = 0 if ($init_done); } elsif (lc($arg) eq 'forcedev') { $hash->{hmccu}{forcedev} = 1; } 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, 3, "Cannot detect IO device, maybe CCU not ready or device doesn't exist on CCU"); $hash->{ccudevstate} = 'pending'; return undef; } } # Initialize FHEM device, set IO device my $rc = HMCCUDEV_InitDevice ($ioHash, $hash); if (HMCCU_IsIntNum ($rc)) { return $errmsg[$rc] if ($rc > 0 && $rc < scalar(@errmsg)); HMCCU_LogDisplay ($hash, 2, $warnmsg[-$rc]) if ($rc < 0 && -$rc < scalar(@warnmsg)); return undef; } else { return $rc; } } ###################################################################### # 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 ambiguous # -2 = Device type not known by HMCCU ###################################################################### sub HMCCUDEV_InitDevice ($$) { my ($ioHash, $devHash) = @_; my $name = $devHash->{NAME}; my $devspec = $devHash->{hmccu}{devspec}; my $gdcount = 0; my $gdname = $devspec; # Check if device is valid 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; $devHash->{ccuif} = $di; $devHash->{ccuaddr} = $da; $devHash->{ccuname} = $dn; $devHash->{ccutype} = $dt; $devHash->{hmccu}{channels} = $dc; # Inform HMCCU device about client device return 2 if (!HMCCU_AssignIODevice ($devHash, $ioHash->{NAME})); $devHash->{ccudevstate} = 'active'; my $rc = 0; if ($init_done && !HMCCU_IsDelayedInit ($ioHash)) { my $detect = HMCCU_DetectDevice ($ioHash, $da, $di); return "Specify option 'forceDev' for HMCCUDEV or use HMCCUCHN instead (recommended). Command: define $name HMCCUCHN $detect->{defAdd}" if (defined($detect) && $detect->{defMod} eq 'HMCCUCHN' && $devHash->{hmccu}{forcedev} == 0); # Interactive device definition HMCCU_SetSCAttributes ($ioHash, $devHash, $detect); # Set selection lists for attributes statedatapoint and controldatapoint HMCCU_AddDevice ($ioHash, $di, $da, $devHash->{NAME}); # Add device to internal IO device hashes HMCCU_UpdateDevice ($ioHash, $devHash); # Set device information like firmware and links HMCCU_UpdateDeviceRoles ($ioHash, $devHash); # Set CCU type, CCU subtype and roles HMCCU_SetInitialAttributes ($ioHash, $name); # Set global attributes as defined in IO device attribute ccudef-attributes if (defined($detect) && $detect->{level} > 0) { $rc = -1 if ($detect->{level} != 5 && $detect->{controlRoleCount} > 1); if (defined($devHash->{hmccu}{defSDP})) { my ($chn, $dpt) = split /\./, $devHash->{hmccu}{defSDP}; if (defined($dpt)) { $detect->{defSCh} = $chn; $detect->{defSDP} = $devHash->{hmccu}{defSDP}; } } if (defined($devHash->{hmccu}{defCDP})) { my ($chn, $dpt) = split /\./, $devHash->{hmccu}{defCDP}; if (defined($dpt)) { $detect->{defCCh} = $chn; $detect->{defCDP} = $devHash->{hmccu}{defCDP}; } } my ($sc, $sd, $cc, $cd, $rsd, $rcd) = HMCCU_SetDefaultSCDatapoints ($ioHash, $devHash, $detect, 1); HMCCU_Log ($devHash, 2, "Cannot set default state- and/or control datapoints. Maybe device type not known by HMCCU") if ($rsd == 0 && $rcd == 0); if (!exists($devHash->{hmccu}{nodefaults}) || $devHash->{hmccu}{nodefaults} == 0) { my $chn = $detect->{defCCh} != -1 ? $detect->{defCCh} : $detect->{defSCh}; # Don't let device definition fail if default attributes cannot be set my ($rc, $retMsg) = HMCCU_SetDefaultAttributes ($devHash, { mode => 'update', role => undef, roleChn => $chn, }); if (!$rc) { HMCCU_Log ($devHash, 2, $retMsg); HMCCU_Log ($devHash, 2, 'No HMCCU 4.3 default attributes found during device definition') if (!HMCCU_SetDefaults ($devHash)); } } } else { $rc = -2; # Device type not known by HMCCU } # Update readings HMCCU_GetUpdate ($devHash, $da); } # Parse group options if ($devHash->{ccuif} eq 'VirtualDevices') { 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); } $devHash->{ccugroup} = join (',', @devlist) if (scalar(@devlist) > 0); } return $rc; } ###################################################################### # Delete device ###################################################################### sub HMCCUDEV_Undef ($$) { my ($hash, $arg) = @_; if ($hash->{IODev}) { HMCCU_RemoveDevice ($hash->{IODev}, $hash->{ccuif}, $hash->{ccuaddr}, $hash->{NAME}); } 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 $clHash = $defs{$name}; my $ioHash = HMCCU_GetHash ($clHash); my $clType = $clHash->{TYPE}; if ($cmd eq 'set') { return "$clType [$name] Missing value of attribute $attrname" if (!defined($attrval)); if ($attrname eq 'IODev') { $clHash->{IODev} = $defs{$attrval}; } elsif ($attrname eq 'statevals') { return "$clType [$name] Attribute statevals ignored. Device is read only" if ($clHash->{readonly} eq 'yes'); return "$clType [$name] Attribute statevals ignored. Device type is known by HMCCU" if ($clHash->{hmccu}{detect} > 0); if ($init_done && !HMCCU_IsValidControlDatapoint ($clHash)) { HMCCU_LogDisplay ($clHash, 2, 'Warning: Attribute controldatapoint not set or set to invalid datapoint'); } } elsif ($attrname =~ /^(state|control)(channel|datapoint)$/) { my $chn = $attrval; if ($attrname eq 'statedatapoint' || $attrname eq 'controldatapoint') { if ($attrval =~ /^([0-9]{1,2})\.(.+)$/) { $chn = $1; } else { return "$clType [$name] Value of attribute $attrname must be in format channel.datapoint"; } } else { return "$clType [$name] Value of attribute $attrname must be a valid channel number" if (!HMCCU_IsIntNum ($attrval)); $chn = $attrval; } my $role = HMCCU_GetChannelRole ($clHash, $chn); return "$clType [$name] Invalid value $attrval for attribute $attrname" if (!HMCCU_SetSCDatapoints ($clHash, $attrname, $attrval, $role, 1)); } elsif ($attrname eq 'devStateFlags') { my @t = split(':', $attrval); return "$clType [$name] Missing flag and/or value expression in attribute $attrname" if (scalar(@t) != 3); } } elsif ($cmd eq 'del') { if ($attrname =~ /^(state|control)(channel|datapoint)$/) { # Reset value HMCCU_SetSCDatapoints ($clHash, $attrname); delete $clHash->{hmccu}{roleCmds} if (exists($clHash->{hmccu}{roleCmds}) && (!exists($clHash->{hmccu}{control}{chn}) || $clHash->{hmccu}{control}{chn} eq '')); if ($init_done && $clHash->{hmccu}{setDefaults} == 0) { # Try to set default state and control datapoint and update command list my ($sc, $sd, $cc, $cd, $rsd, $rcd) = HMCCU_SetDefaultSCDatapoints ($ioHash, $clHash, undef, 1); HMCCU_Log ($clHash, 2, "Deleted attribute $attrname but cannot set default state- and/or control datapoints") if ($rsd == 0 && $rcd == 0); } } } return; } ###################################################################### # Set commands ###################################################################### sub HMCCUDEV_Set ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; my $opt = shift @$a // return 'No set command specified'; my $lcopt = 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)); # Build set command syntax my $syntax = 'clear defaults:reset,update,old,forceReset'; # Command readingFilter depends on readable datapoints my @dpRList = (); my $dpRCount = HMCCU_GetValidDatapoints ($hash, $hash->{ccutype}, -1, 5, \@dpRList); $syntax .= ' readingFilter:multiple-strict,'.join(',', @dpRList) if ($dpRCount > 0); # Commands only available in read/write mode if ($hash->{readonly} ne 'yes') { $syntax .= ' config'; my $dpWCount = HMCCU_GetValidDatapoints ($hash, $hash->{ccutype}, -1, 2); $syntax .= ' datapoint' if ($dpWCount > 0); my $addCmds = $hash->{hmccu}{cmdlist}{set} // ''; $syntax .= " $addCmds" if ($addCmds ne ''); } # Log commands HMCCU_Log ($hash, 3, "set $name $opt ".join (' ', @$a)) if ($opt ne '?' && (HMCCU_IsFlag ($name, 'logCommand') || HMCCU_IsFlag ($ioName, 'logCommand'))); if ($lcopt eq 'control') { return HMCCU_ExecuteSetControlCommand ($hash, $a, $h); } elsif ($lcopt eq 'datapoint') { return HMCCU_ExecuteSetDatapointCommand ($hash, $a, $h); } elsif ($lcopt eq 'toggle') { return HMCCU_ExecuteToggleCommand ($hash); } elsif (exists($hash->{hmccu}{roleCmds}{set}{$opt})) { return HMCCU_ExecuteRoleCommand ($ioHash, $hash, 'set', $opt, $a, $h); } elsif ($lcopt eq 'clear') { return HMCCU_ExecuteSetClearCommand ($hash, $a); } elsif ($lcopt =~ /^(config|values)$/) { return HMCCU_ExecuteSetParameterCommand ($ioHash, $hash, $opt, $a, $h); } elsif ($lcopt eq 'readingfilter') { my $filter = shift @$a // return HMCCU_SetError ($hash, "Usage: set $name readingFilter {datapointList}"); $filter = join(';', map { (my $f = $_) =~ s/\.(.+)/\.\^$1\$/; $f } split(',', $filter)); return CommandAttr (undef, "$name ccureadingfilter $filter"); } elsif ($lcopt eq 'defaults') { my $mode = shift @$a // 'update'; return HMCCU_SetError ($hash, "Usage: get $name defaults [forceReset|old|reset|update]") if ($mode !~ /^(forceReset|reset|old|update)$/); my $rc = 0; my $retMsg = ''; $hash->{hmccu}{setDefaults} = 1; # Make sure that readings are not refreshed after each set attribute command ($rc, $retMsg) = HMCCU_SetDefaultAttributes ($hash, { mode => $mode, role => undef, roleChn => undef }) if ($mode ne 'old'); if (!$rc) { $rc = HMCCU_SetDefaults ($hash); $retMsg .= $rc ? "\nSet version 4.3 attributes" : "\nNo version 4.3 default attributes found"; } $retMsg = 'OK' if ($retMsg eq ''); $hash->{hmccu}{setDefaults} = 0; HMCCU_RefreshReadings ($hash) if ($rc); return HMCCU_SetError ($hash, $retMsg); } else { return "Unknown argument $opt choose one of $syntax"; } } ###################################################################### # Get commands ###################################################################### sub HMCCUDEV_Get ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; my $opt = shift @$a // return 'No get command specified'; my $lcopt = 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'); # Build set command syntax my $syntax = 'update config paramsetDesc:noArg deviceInfo:noArg values extValues'; # Command datapoint depends on readable datapoints my @dpRList; my $dpRCount = HMCCU_GetValidDatapoints ($hash, $ccutype, -1, 1, \@dpRList); $syntax .= ' datapoint:'.join(",", @dpRList) if ($dpRCount > 0); # Additional device specific commands my $addCmds = $hash->{hmccu}{cmdlist}{get} // ''; $syntax .= " $addCmds" if ($addCmds ne ''); # Log commands HMCCU_Log ($hash, 3, "get $name $opt ".join (' ', @$a)) if ($opt ne '?' && $ccuflags =~ /logCommand/ || HMCCU_IsFlag ($ioName, 'logCommand')); if ($lcopt eq 'datapoint') { my $objname = shift @$a // return HMCCU_SetError ($hash, "Usage: get $name datapoint [{channel-number}.]{datapoint}"); my $chn; my $dpt; if ($objname =~ /^([0-9]+)\.(.+)$/) { ($chn, $dpt) = ($1, $2); return HMCCU_SetError ($hash, -7) if ($chn >= $hash->{hmccu}{channels}); } else { my ($sc, $sd, $cc, $cd) = HMCCU_GetSCDatapoints ($hash); return HMCCU_SetError ($hash, -11) if ($sc eq ''); ($chn, $dpt) = ($sc, $objname); } return HMCCU_SetError ($hash, -8, $objname) if (!HMCCU_IsValidParameter ($hash, HMCCU_GetChannelAddr ($hash, $chn), 'VALUES', $dpt, 1)); $objname = "$ccuif.$ccuaddr:$chn.$dpt"; my ($rc, $result) = HMCCU_GetDatapoint ($hash, $objname, 0); return HMCCU_SetError ($hash, $rc, $result) if ($rc < 0); return $result; } elsif ($lcopt eq 'deviceinfo') { my $extended = shift @$a; return HMCCU_ExecuteGetDeviceInfoCommand ($ioHash, $hash, $ccuaddr, defined($extended) ? 1 : 0); } elsif ($lcopt =~ /^(config|values|update)$/) { my $filter = shift @$a; 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}); my $result = HMCCU_ExecuteGetParameterCommand ($ioHash, $hash, $lcopt, \@addList, $filter); return HMCCU_SetError ($hash, "Can't get device description") if (!defined($result)); return HMCCU_DisplayGetParameterResult ($ioHash, $hash, $result); } elsif ($lcopt eq 'extvalues') { my $filter = shift @$a; my $rc = HMCCU_GetUpdate ($hash, $ccuaddr, $filter); return $rc < 0 ? HMCCU_SetError ($hash, $rc) : 'OK'; } elsif ($lcopt eq 'paramsetdesc') { my $result = HMCCU_ParamsetDescToStr ($ioHash, $hash); return defined($result) ? $result : HMCCU_SetError ($hash, "Can't get device model"); } elsif (exists($hash->{hmccu}{roleCmds}{get}{$opt})) { return HMCCU_ExecuteRoleCommand ($ioHash, $hash, 'get', $opt, $a, $h); } else { return "Unknown argument $opt choose one of $syntax"; } } 1; =pod =item device =item summary controls HMCCU client devices for Homematic CCU - FHEM integration =begin html

HMCCUDEV

=end html =cut