###################################################################### # # 88_HMCCUCHN.pm # # $Id$ # # Version 5.0 # # (c) 2022 zap (zap01 t-online de) # ###################################################################### # Client device for Homematic channels. # Requires module 88_HMCCU.pm ###################################################################### package main; use strict; use warnings; use SetExtensions; # require "$attr{global}{modpath}/FHEM/88_HMCCU.pm"; sub HMCCUCHN_Initialize ($); sub HMCCUCHN_Define ($@); sub HMCCUCHN_InitDevice ($$); sub HMCCUCHN_Undef ($$); sub HMCCUCHN_Rename ($$); sub HMCCUCHN_Set ($@); sub HMCCUCHN_Get ($@); sub HMCCUCHN_Attr ($@); my $HMCCUCHN_VERSION = '5.0 220431743'; ###################################################################### # Initialize module ###################################################################### sub HMCCUCHN_Initialize ($) { my ($hash) = @_; $hash->{version} = $HMCCUCHN_VERSION; $hash->{DefFn} = 'HMCCUCHN_Define'; $hash->{UndefFn} = 'HMCCUCHN_Undef'; $hash->{RenameFn} = 'HMCCUCHN_Rename'; $hash->{SetFn} = 'HMCCUCHN_Set'; $hash->{GetFn} = 'HMCCUCHN_Get'; $hash->{AttrFn} = 'HMCCUCHN_Attr'; $hash->{parseParams} = 1; $hash->{AttrList} = 'IODev ccucalculate '. 'ccuflags:multiple-strict,hideStdReadings,replaceStdReadings,noBoundsChecking,ackState,logCommand,noReadings,trace,showMasterReadings,showLinkReadings,showDeviceReadings,showServiceReadings '. 'ccureadingfilter:textField-long statedatapoint controldatapoint '. 'ccureadingformat:name,namelc,address,addresslc,datapoint,datapointlc '. 'ccureadingname:textField-long ccuSetOnChange ccuReadingPrefix '. 'ccuscaleval ccuverify:0,1,2 ccuget:State,Value devStateFlags '. 'disable:0,1 hmstatevals:textField-long statevals substitute:textField-long '. 'substexcl stripnumber peer:textField-long traceFilter '. $readingFnAttributes; } ###################################################################### # Define device ###################################################################### sub HMCCUCHN_Define ($@) { my ($hash, $a, $h) = @_; my $name = $hash->{NAME}; my $usage = "Usage: define $name HMCCUCHN {device} ['readonly'] ['noDefaults'|'defaults'] [iodev={iodevname}]"; return $usage if (@$a < 3); my ($devname, $devtype, $devspec) = splice (@$a, 0, 3); my $ioHash; my @errmsg = ( "OK", "Invalid or unknown CCU device name or address", "Can't assign I/O device" ); my @warnmsg = ( "OK", "Unknown warning message", "Device type not known by HMCCU. Please set control and/or state channel with attributes controldatapoint and statedatapoint" ); my $existDev = HMCCU_ExistsClientDevice ($devspec, $devtype); return "FHEM device $existDev for CCU device $devspec already exists" if ($existDev ne ''); # Store some definitions for delayed initialization $hash->{hmccu}{devspec} = $devspec; # Defaults $hash->{readonly} = 'no'; $hash->{hmccu}{channels} = 1; $hash->{hmccu}{nodefaults} = $init_done ? 0 : 1; $hash->{hmccu}{detect} = 0; $hash->{hmccu}{setDefaults} = 0; # Parse optional command line parameters my $n = 0; while (my $arg = shift @$a) { return $usage if ($n == 3); if ($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); } else { return $usage; } $n++; } # IO device can be set by command line parameter iodev, otherwise try to detect IO device if (exists($h->{iodev})) { return "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 during FHEM start if CCU is not ready $ioHash = HMCCU_FindIODevice ($devspec); } if ($init_done) { # Interactive define command while CCU not ready or no IO device defined 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 or CCU device not found'; } } 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 = HMCCUCHN_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 # -2 = Device type not known by HMCCU ###################################################################### sub HMCCUCHN_InitDevice ($$) { my ($ioHash, $devHash) = @_; my $name = $devHash->{NAME}; my $devspec = $devHash->{hmccu}{devspec}; return 1 if (!HMCCU_IsValidChannel ($ioHash, $devspec, 7)); my ($di, $da, $dn, $dt, $dc) = HMCCU_GetCCUDeviceParam ($ioHash, $devspec); return 1 if (!defined($da)); # 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'; my $rc = 0; if ($init_done && !HMCCU_IsDelayedInit ($ioHash)) { my $detect = HMCCU_DetectDevice ($ioHash, $da, $di); # 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) { 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) { # Don't let device definition fail if default attributes cannot be set my ($rc, $retMsg) = HMCCU_SetDefaultAttributes ($devHash); 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; } HMCCU_GetUpdate ($devHash, $da); } return $rc; } ###################################################################### # Delete device ###################################################################### sub HMCCUCHN_Undef ($$) { my ($hash, $arg) = @_; if (defined($hash->{IODev})) { HMCCU_RemoveDevice ($hash->{IODev}, $hash->{ccuif}, $hash->{ccuaddr}, $hash->{NAME}); } return undef; } ###################################################################### # Rename device ###################################################################### sub HMCCUCHN_Rename ($$) { my ($newName, $oldName) = @_; my $clHash = $defs{$newName}; my $ioHash = defined($clHash) ? $clHash->{IODev} : undef; HMCCU_RenameDevice ($ioHash, $clHash, $oldName); } ###################################################################### # Set attribute ###################################################################### sub HMCCUCHN_Attr ($@) { my ($cmd, $name, $attrname, $attrval) = @_; my $clHash = $defs{$name}; my $ioHash = HMCCU_GetHash ($clHash); my $clType = $clHash->{TYPE}; if ($cmd eq 'set') { return 'Missing attribute value' 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)datapoint$/) { my $role = HMCCU_GetChannelRole ($clHash); 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)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) { my ($sc, $sd, $cc, $cd, $rsd, $rcd) = HMCCU_SetDefaultSCDatapoints ($ioHash, $clHash, undef, 1); HMCCU_Log ($clHash, 2, "Cannot set default state- and/or control datapoints") if ($rsd == 0 && $rcd == 0); } } } return undef; } ###################################################################### # Set commands ###################################################################### sub HMCCUCHN_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' && $lcopt !~ /^(\?|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 ($add, $chn) = split(":", $hash->{ccuaddr}); my @dpRList = (); my $dpRCount = HMCCU_GetValidDatapoints ($hash, $hash->{ccutype}, $chn, 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}, $chn, 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 ($opt eq 'clear') { return HMCCU_ExecuteSetClearCommand ($hash, $a); } elsif ($lcopt =~ /^(config|values)$/) { return HMCCU_ExecuteSetParameterCommand ($ioHash, $hash, $lcopt, $a, $h); } elsif ($lcopt eq 'readingfilter') { my $filter = shift @$a // return HMCCU_SetError ($hash, "Usage: set $name readingFilter {datapointList}"); $filter =~ s/,/\|/g; $filter = '^('.$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 HMCCUCHN_Get ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; my $opt = shift @$a // return 'No get command specified'; my $lcopt = lc($opt); return undef if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending' || !defined ($hash->{IODev}) || AttrVal ($name, "disable", 0) == 1); my $ioHash = $hash->{IODev}; my $ioName = $ioHash->{NAME}; return $opt eq '?' ? undef : 'Cannot perform get command. CCU busy' if (HMCCU_IsRPCStateBlocking ($ioHash)); 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 ($add, $chn) = split(":", $hash->{ccuaddr}); my @dpRList; my $dpRCount = HMCCU_GetValidDatapoints ($hash, $ccutype, $chn, 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 {datapoint}"); return HMCCU_SetError ($hash, -8, $objname) if (!HMCCU_IsValidParameter ($hash, $ccuaddr, 'VALUES', $objname, 1)); $objname = "$ccuif.$ccuaddr.$objname"; my ($rc, $result) = HMCCU_GetDatapoint ($hash, $objname, 0); return $rc < 0 ? HMCCU_SetError ($hash, $rc, $result) : $result; } elsif ($lcopt eq 'deviceinfo') { my $extended = shift @$a; my ($devAddr, undef) = HMCCU_SplitChnAddr ($ccuaddr); return HMCCU_ExecuteGetDeviceInfoCommand ($ioHash, $hash, $devAddr, defined($extended) ? 1 : 0); } elsif ($lcopt =~ /^(config|values|update)$/) { my $filter = shift @$a; my ($devAddr, undef) = HMCCU_SplitChnAddr ($ccuaddr); my @addList = ($devAddr, "$devAddr:0", $ccuaddr); 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 CCU2/3 - FHEM integration =begin html

HMCCUCHN

=end html =cut