diff --git a/fhem/CHANGED b/fhem/CHANGED index de2a1b32a..0c54f8e57 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - change: 88_HMCCU: Update to version 5.0 - bugfix: 73_AutoShuttersControl: fix regex to detect position event with dot's - change: 10_EIB: Move to deprecated diff --git a/fhem/FHEM/88_HMCCU.pm b/fhem/FHEM/88_HMCCU.pm index 4a1b94833..fd6d9f7bf 100755 --- a/fhem/FHEM/88_HMCCU.pm +++ b/fhem/FHEM/88_HMCCU.pm @@ -4,7 +4,7 @@ # # $Id$ # -# Version 4.3.025 +# Version 5.0 # # Module for communication between FHEM and Homematic CCU2/3. # @@ -12,7 +12,7 @@ # CCU group devices, HomeGear, CUxD, Osram Lightify, Homematic Virtual Layer # and Philips Hue (not tested) # -# (c) 2019 by zap (zap01 t-online de) +# (c) 2021 by zap (zap01 t-online de) # ############################################################################## # @@ -32,42 +32,34 @@ no if $] >= 5.017011, warnings => 'experimental::smartmatch'; use strict; use warnings; # use Data::Dumper; -# use Time::HiRes qw(usleep); -use IO::File; -use Fcntl 'SEEK_END', 'SEEK_SET', 'O_CREAT', 'O_RDWR'; +use Encode qw(decode encode); use RPC::XML::Client; use RPC::XML::Server; use HttpUtils; use SetExtensions; -use SubProcess; use HMCCUConf; # Import configuration data -my $HMCCU_CHN_DEFAULTS = \%HMCCUConf::HMCCU_CHN_DEFAULTS; -my $HMCCU_DEV_DEFAULTS = \%HMCCUConf::HMCCU_DEV_DEFAULTS; -my $HMCCU_SCRIPTS = \%HMCCUConf::HMCCU_SCRIPTS; +my $HMCCU_CONFIG_VERSION = $HMCCUConf::HMCCU_CONFIG_VERSION; +my $HMCCU_DEF_ROLE = \%HMCCUConf::HMCCU_DEF_ROLE; +my $HMCCU_STATECONTROL = \%HMCCUConf::HMCCU_STATECONTROL; +my $HMCCU_READINGS = \%HMCCUConf::HMCCU_READINGS; +my $HMCCU_ROLECMDS = \%HMCCUConf::HMCCU_ROLECMDS; +my $HMCCU_GETROLECMDS = \%HMCCUConf::HMCCU_GETROLECMDS; +my $HMCCU_ATTR = \%HMCCUConf::HMCCU_ATTR; +my $HMCCU_CONVERSIONS = \%HMCCUConf::HMCCU_CONVERSIONS; +my $HMCCU_CHN_DEFAULTS = \%HMCCUConf::HMCCU_CHN_DEFAULTS; +my $HMCCU_DEV_DEFAULTS = \%HMCCUConf::HMCCU_DEV_DEFAULTS; +my $HMCCU_SCRIPTS = \%HMCCUConf::HMCCU_SCRIPTS; # Custom configuration data my %HMCCU_CUST_CHN_DEFAULTS; my %HMCCU_CUST_DEV_DEFAULTS; # HMCCU version -my $HMCCU_VERSION = '4.3.025'; +my $HMCCU_VERSION = '5.0 212981850'; -# Constants and default values -my $HMCCU_MAX_IOERRORS = 100; -my $HMCCU_MAX_QUEUESIZE = 500; -my $HMCCU_TIME_WAIT = 100000; -my $HMCCU_TIME_TRIGGER = 10; - -# RPC ping interval for default interface, should be smaller than HMCCU_TIMEOUT_EVENT -my $HMCCU_TIME_PING = 300; - -my $HMCCU_TIMEOUT_CONNECTION = 10; -my $HMCCU_TIMEOUT_WRITE = 0.001; -my $HMCCU_TIMEOUT_ACCEPT = 1; -my $HMCCU_TIMEOUT_EVENT = 600; -my $HMCCU_STATISTICS = 500; +# Timeout for CCU requests (seconds) my $HMCCU_TIMEOUT_REQUEST = 4; # ReGa Ports @@ -101,35 +93,13 @@ my %HMCCU_RPC_SSL = ( 'BidCos-Wired' => 1, 'BidCos-RF' => 1, 'HmIP-RF' => 1, 'VirtualDevices' => 1 ); -# Initial intervals for registration of RPC callbacks and reading RPC queue -# -# X = Start RPC server -# X+HMCCU_INIT_INTERVAL1 = Register RPC callback -# X+HMCCU_INIT_INTERVAL2 = Read RPC Queue -# -my $HMCCU_INIT_INTERVAL0 = 12; -my $HMCCU_INIT_INTERVAL1 = 7; -my $HMCCU_INIT_INTERVAL2 = 5; - # Default values for delayed initialization during FHEM startup +my $HMCCU_INIT_INTERVAL0 = 12; my $HMCCU_CCU_PING_TIMEOUT = 1; -my $HMCCU_CCU_BOOT_DELAY = 180; +my $HMCCU_CCU_PING_SLEEP = 1; +my $HMCCU_CCU_BOOT_DELAY = 180; my $HMCCU_CCU_DELAYED_INIT = 59; -my $HMCCU_CCU_RPC_OFFSET = 20; - -# Number of arguments in RPC events -my %rpceventargs = ( - "EV", 3, # Datapoint value updated - "ND", 6, # New device created - "DD", 1, # Device deleted - "RD", 2, # Device renamed - "RA", 1, # Device readded - "UD", 2, # Device updated - "IN", 3, # RPC init - "EX", 3, # Exit RPC server - "SL", 2, # RPC server loop - "ST", 10 # Status -); +my $HMCCU_CCU_RPC_OFFSET = 20; # Datapoint operations my $HMCCU_OPER_READ = 1; @@ -173,206 +143,221 @@ my $HMCCU_DEF_HMSTATE = '^0\.UNREACH!(1|true):unreachable;^[0-9]\.LOW_?BAT!(1|tr # Placeholder for external addresses (i.e. HVL) my $HMCCU_EXT_ADDR = 'ZZZ0000000'; -# Binary RPC data types -my $BINRPC_INTEGER = 1; -my $BINRPC_BOOL = 2; -my $BINRPC_STRING = 3; -my $BINRPC_DOUBLE = 4; -my $BINRPC_BASE64 = 17; -my $BINRPC_ARRAY = 256; -my $BINRPC_STRUCT = 257; # Declare functions # FHEM standard functions sub HMCCU_Initialize ($); -sub HMCCU_Define ($$); +sub HMCCU_Define ($$$); sub HMCCU_InitDevice ($); sub HMCCU_Undef ($$); +sub HMCCU_Renane ($$); sub HMCCU_DelayedShutdown ($); sub HMCCU_Shutdown ($); sub HMCCU_Set ($@); sub HMCCU_Get ($@); sub HMCCU_Attr ($@); -sub HMCCU_AttrInterfacesPorts ($$$); sub HMCCU_Notify ($$); sub HMCCU_Detail ($$$$); +sub HMCCU_PostInit ($); # Aggregation sub HMCCU_AggregateReadings ($$); sub HMCCU_AggregationRules ($$); # Handling of default attributes -sub HMCCU_DetectDefaults ($$); sub HMCCU_ExportDefaults ($$); -sub HMCCU_ExportDefaultsCSV ($$); sub HMCCU_ImportDefaults ($); sub HMCCU_FindDefaults ($$); -sub HMCCU_GetDefaults ($$); +sub HMCCU_GetDefaults ($;$); sub HMCCU_SetDefaults ($); # Status and logging functions -sub HMCCU_Trace ($$$$); +sub HMCCU_Trace ($$$); sub HMCCU_Log ($$$;$); +sub HMCCU_LogDisplay ($$$;$); sub HMCCU_LogError ($$$); sub HMCCU_SetError ($@); sub HMCCU_SetState ($@); sub HMCCU_SetRPCState ($@); # Filter and modify readings -sub HMCCU_FilterReading ($$$); +sub HMCCU_FilterReading ($$$;$); sub HMCCU_FormatReadingValue ($$$); -sub HMCCU_GetReadingName ($$$$$$$); -sub HMCCU_ScaleValue ($$$$$); -sub HMCCU_Substitute ($$$$$); +sub HMCCU_GetReadingName ($$$$$$$;$); +sub HMCCU_ScaleValue ($$$$$;$); +sub HMCCU_StripNumber ($$;$); +sub HMCCU_Substitute ($$$$$;$$); sub HMCCU_SubstRule ($$$); sub HMCCU_SubstVariables ($$$); # Update client device readings -sub HMCCU_BulkUpdate ($$$$); -sub HMCCU_GetUpdate ($$$); +sub HMCCU_BulkUpdate ($$$;$); +sub HMCCU_GetUpdate ($$;$$); +sub HMCCU_RefreshReadings ($); sub HMCCU_UpdateCB ($$$); -sub HMCCU_UpdateClients ($$$$$$); -sub HMCCU_UpdateInternalValues ($$$$); +sub HMCCU_UpdateClients ($$$$;$$); +sub HMCCU_UpdateInternalValues ($$$$$); sub HMCCU_UpdateMultipleDevices ($$); sub HMCCU_UpdatePeers ($$$$); +sub HMCCU_UpdateParamsetReadings ($$$;$); sub HMCCU_UpdateSingleDatapoint ($$$$); -sub HMCCU_UpdateSingleDevice ($$$$); # RPC functions +sub HMCCU_CreateRPCDevice ($$$$); sub HMCCU_EventsTimedOut ($); sub HMCCU_GetRPCCallbackURL ($$$$$); sub HMCCU_GetRPCDevice ($$$); -sub HMCCU_GetRPCInterfaceList ($); -sub HMCCU_GetRPCPortList ($); +sub HMCCU_GetRPCInterfaceList ($;$); sub HMCCU_GetRPCServerInfo ($$$); -sub HMCCU_IsRPCServerRunning ($$$); +sub HMCCU_IsRPCServerRunning ($;$); sub HMCCU_IsRPCType ($$$); sub HMCCU_IsRPCStateBlocking ($); -sub HMCCU_ResetCounters ($); -sub HMCCU_RPCDeRegisterCallback ($); -sub HMCCU_RPCRegisterCallback ($); -sub HMCCU_RPCGetConfig ($$$$); -sub HMCCU_RPCSetConfig ($$$); -sub HMCCU_RPCRequest ($$$$$;$); +sub HMCCU_RPCRequest ($$$$;$$); sub HMCCU_StartExtRPCServer ($); -sub HMCCU_StartIntRPCServer ($); sub HMCCU_StopExtRPCServer ($;$); -sub HMCCU_StopRPCServer ($); # Parse and validate names and addresses sub HMCCU_ParseObject ($$$); sub HMCCU_IsDevAddr ($$); sub HMCCU_IsChnAddr ($$); -sub HMCCU_SplitChnAddr ($); +sub HMCCU_SplitChnAddr ($;$); sub HMCCU_SplitDatapoint ($;$); # FHEM device handling functions -sub HMCCU_AssignIODevice ($$$); -sub HMCCU_FindClientDevices ($$$$); +sub HMCCU_AssignIODevice ($$;$); +sub HMCCU_ExistsClientDevice ($$); +sub HMCCU_FindClientDevices ($$;$$); sub HMCCU_FindIODevice ($); sub HMCCU_GetHash ($@); sub HMCCU_GetAttribute ($$$$); sub HMCCU_GetFlags ($); sub HMCCU_GetAttrReadingFormat ($$); sub HMCCU_GetAttrStripNumber ($); -sub HMCCU_GetAttrSubstitute ($$); +sub HMCCU_GetAttrSubstitute ($;$); sub HMCCU_IODeviceStates (); sub HMCCU_IsFlag ($$); # Handle interfaces, devices and channels -sub HMCCU_CreateDevice ($$$$$); -sub HMCCU_DeleteDevice ($); +sub HMCCU_AddDevice ($$$;$); +sub HMCCU_AddDeviceDesc ($$$$); +sub HMCCU_AddDeviceModel ($$$$$$); +sub HMCCU_AddPeers ($$$); +sub HMCCU_CheckParameter ($$;$$$); +sub HMCCU_DetectDevice ($$$); +sub HMCCU_CreateFHEMDevices ($@); +sub HMCCU_CreateDevice ($@); +sub HMCCU_IdentifyRole ($$$$$); +sub HMCCU_DetectRolePattern ($;$$$$); +sub HMCCU_DeviceDescToStr ($$); +sub HMCCU_ExecuteRoleCommand ($@); +sub HMCCU_ExecuteGetDeviceInfoCommand ($@); +sub HMCCU_ExecuteGetParameterCommand ($@); +sub HMCCU_ExecuteSetClearCommand ($@); +sub HMCCU_ExecuteSetControlCommand ($@); +sub HMCCU_ExecuteSetDatapointCommand ($@); +sub HMCCU_ExecuteSetParameterCommand ($@); +sub HMCCU_DisplayGetParameterResult ($$$); +sub HMCCU_DisplayWeekProgram ($$$;$$); +sub HMCCU_ExistsDeviceModel ($$$;$); +sub HMCCU_FindParamDef ($$$); sub HMCCU_FormatDeviceInfo ($); -sub HMCCU_GetAddress ($$$$); +sub HMCCU_FormatHashTable ($); +sub HMCCU_GetAddress ($$;$$); sub HMCCU_GetAffectedAddresses ($); sub HMCCU_GetCCUDeviceParam ($$); -sub HMCCU_GetChannelName ($$$); +sub HMCCU_GetChannelName ($$;$); +sub HMCCU_GetChannelRole ($;$); +sub HMCCU_GetDeviceRoles ($$$;$); +sub HMCCU_GetClientDeviceModel ($;$); sub HMCCU_GetDefaultInterface ($); -sub HMCCU_GetDeviceChannels ($$$); -sub HMCCU_GetDeviceInfo ($$$); -sub HMCCU_GetDeviceInterface ($$$); +sub HMCCU_GetDeviceAddresses ($;$$); +sub HMCCU_GetDeviceConfig ($); +sub HMCCU_GetDeviceDesc ($$;$); +sub HMCCU_GetDeviceIdentifier ($$;$$); +sub HMCCU_GetDeviceInfo ($$;$); +sub HMCCU_GetDeviceInterface ($$;$); +sub HMCCU_GetInterfaceList ($); sub HMCCU_GetDeviceList ($); -sub HMCCU_GetDeviceName ($$$); +sub HMCCU_GetDeviceModel ($$$;$); +sub HMCCU_GetDeviceName ($$;$); sub HMCCU_GetDeviceType ($$$); sub HMCCU_GetFirmwareVersions ($$); sub HMCCU_GetGroupMembers ($$); sub HMCCU_GetMatchingDevices ($$$$); +sub HMCCU_GetParamDef ($$$;$); +sub HMCCU_GetParamValueConversion ($$$$$); +sub HMCCU_GetReceivers ($$$); sub HMCCU_IsValidChannel ($$$); sub HMCCU_IsValidDevice ($$$); sub HMCCU_IsValidDeviceOrChannel ($$$); +sub HMCCU_IsValidParameter ($$$$;$); +sub HMCCU_IsValidReceiver ($$$$); +sub HMCCU_ParamsetDescToStr ($$); +sub HMCCU_RemoveDevice ($$$;$); +sub HMCCU_RenameDevice ($$$); +sub HMCCU_ResetDeviceTables ($;$$); +sub HMCCU_SetSCAttributes ($$;$); +sub HMCCU_UpdateDevice ($$); +sub HMCCU_UpdateDeviceRoles ($$;$$); sub HMCCU_UpdateDeviceTable ($$); +sub HMCCU_UpdateRoleCommands ($$;$); +sub HMCCU_UpdateAdditionalCommands ($$;$$); # Handle datapoints sub HMCCU_FindDatapoint ($$$$$); -sub HMCCU_GetChannel ($$); sub HMCCU_GetDatapoint ($@); sub HMCCU_GetDatapointAttr ($$$$$); -sub HMCCU_GetDatapointCount ($$$); -sub HMCCU_GetDatapointList ($$$); -sub HMCCU_GetSpecialDatapoints ($$$$$); -sub HMCCU_GetSwitchDatapoint ($$$); -sub HMCCU_GetValidDatapoints ($$$$$); +sub HMCCU_GetDatapointList ($;$$); +sub HMCCU_GetSCDatapoints ($); +sub HMCCU_SetSCDatapoints ($$;$$); +sub HMCCU_GetStateValues ($;$$); +sub HMCCU_GetValidDatapoints ($$$$;$); sub HMCCU_IsValidDatapoint ($$$$$); -# sub HMCCU_SetDatapoint ($$$); +sub HMCCU_SetInitialAttributes ($$); +sub HMCCU_SetDefaultAttributes ($;$); sub HMCCU_SetMultipleDatapoints ($$); -sub HMCCU_SetMultipleParameters ($$$); - -# Internal RPC server functions -sub HMCCU_ResetRPCQueue ($$); -sub HMCCU_ReadRPCQueue ($); -sub HMCCU_ProcessEvent ($$); +sub HMCCU_SetMultipleParameters ($$$;$); # Homematic script and variable functions sub HMCCU_GetVariables ($$); sub HMCCU_HMCommand ($$$); sub HMCCU_HMCommandCB ($$$); sub HMCCU_HMCommandNB ($$$); -sub HMCCU_HMScriptExt ($$$$$); +sub HMCCU_HMScriptExt ($$;$$$); sub HMCCU_SetVariable ($$$$$); sub HMCCU_UpdateVariables ($); -# File queue functions -sub HMCCU_QueueOpen ($$); -sub HMCCU_QueueClose ($); -sub HMCCU_QueueReset ($); -sub HMCCU_QueueEnq ($$); -sub HMCCU_QueueDeq ($); - # Helper functions +sub HMCCU_BitsToStr ($$); sub HMCCU_BuildURL ($$); sub HMCCU_CalculateReading ($$); sub HMCCU_CorrectName ($); sub HMCCU_Encrypt ($); sub HMCCU_Decrypt ($); +sub HMCCU_DefStr ($;$$); sub HMCCU_DeleteReadings ($$); sub HMCCU_EncodeEPDisplay ($); sub HMCCU_ExprMatch ($$$); sub HMCCU_ExprNotMatch ($$$); +sub HMCCU_FlagsToStr ($$$;$$); +sub HMCCU_UpdateDeviceStates ($); sub HMCCU_GetDutyCycle ($); -sub HMCCU_GetHMState ($$$); +sub HMCCU_GetHMState ($$;$); sub HMCCU_GetIdFromIP ($$); sub HMCCU_GetTimeSpec ($); +sub HMCCU_IsFltNum ($;$); +sub HMCCU_IsIntNum ($); +sub HMCCU_ISO2UTF ($); sub HMCCU_Max ($$); -sub HMCCU_Min ($$); sub HMCCU_MaxHashEntries ($$); -sub HMCCU_RefToString ($); +sub HMCCU_Min ($$); +sub HMCCU_MinMax ($$$); +sub HMCCU_RefToString ($;$); sub HMCCU_ResolveName ($$); -sub HMCCU_TCPConnect ($$); +sub HMCCU_TCPConnect ($$;$); sub HMCCU_TCPPing ($$$); - -# Subprocess functions of internal RPC server -sub HMCCU_CCURPC_Write ($$); -sub HMCCU_CCURPC_OnRun ($); -sub HMCCU_CCURPC_OnExit (); -sub HMCCU_CCURPC_NewDevicesCB ($$$); -sub HMCCU_CCURPC_DeleteDevicesCB ($$$); -sub HMCCU_CCURPC_UpdateDeviceCB ($$$$); -sub HMCCU_CCURPC_ReplaceDeviceCB ($$$$); -sub HMCCU_CCURPC_ReaddDevicesCB ($$$); -sub HMCCU_CCURPC_EventCB ($$$$$); -sub HMCCU_CCURPC_ListDevicesCB ($$); - +sub HMCCU_UpdateReadings ($$;$); ################################################## # Initialize module @@ -382,30 +367,31 @@ sub HMCCU_Initialize ($) { my ($hash) = @_; - $hash->{DefFn} = "HMCCU_Define"; - $hash->{UndefFn} = "HMCCU_Undef"; - $hash->{SetFn} = "HMCCU_Set"; - $hash->{GetFn} = "HMCCU_Get"; - $hash->{ReadFn} = "HMCCU_Read"; - $hash->{AttrFn} = "HMCCU_Attr"; - $hash->{NotifyFn} = "HMCCU_Notify"; - $hash->{ShutdownFn} = "HMCCU_Shutdown"; - $hash->{DelayedShutdownFn} = "HMCCU_DelayedShutdown"; - $hash->{FW_detailFn} = "HMCCU_Detail"; + $hash->{version} = $HMCCU_VERSION; + + $hash->{DefFn} = 'HMCCU_Define'; + $hash->{UndefFn} = 'HMCCU_Undef'; + $hash->{RenameFn} = 'HMCCU_Rename'; + $hash->{SetFn} = 'HMCCU_Set'; + $hash->{GetFn} = 'HMCCU_Get'; + $hash->{ReadFn} = 'HMCCU_Read'; + $hash->{AttrFn} = 'HMCCU_Attr'; + $hash->{NotifyFn} = 'HMCCU_Notify'; + $hash->{ShutdownFn} = 'HMCCU_Shutdown'; + $hash->{DelayedShutdownFn} = 'HMCCU_DelayedShutdown'; + $hash->{FW_detailFn} = 'HMCCU_Detail'; $hash->{parseParams} = 1; - $hash->{AttrList} = "stripchar stripnumber ccuaggregate:textField-long". - " ccudefaults rpcinterfaces:multiple-strict,".join(',',sort keys %HMCCU_RPC_PORT). - " ccudef-hmstatevals:textField-long ccudef-substitute:textField-long". - " ccudef-readingname:textField-long ccudef-readingfilter:textField-long". - " ccudef-readingformat:name,namelc,address,addresslc,datapoint,datapointlc". - " ccudef-stripnumber". - " ccuflags:multiple-strict,extrpc,intrpc,procrpc,dptnocheck,noagg,nohmstate,". - "logEvents,noEvents,noInitialUpdate,noReadings,nonBlocking,reconnect,logPong,trace". - " ccuReqTimeout ccuGetVars rpcinterval:2,3,5,7,10 rpcqueue rpcPingCCU". - " rpcport:multiple-strict,".join(',',sort keys %HMCCU_RPC_NUMPORT). - " rpcserver:on,off rpcserveraddr rpcserverport rpctimeout rpcevtimeout parfile substitute". - " ccuget:Value,State ". + $hash->{AttrList} = 'stripchar stripnumber ccuaggregate:textField-long'. + ' ccudefaults createDeviceGroup'. + ' ccudef-hmstatevals:textField-long ccudef-substitute:textField-long'. + ' ccudef-readingformat:name,namelc,address,addresslc,datapoint,datapointlc'. + ' ccudef-stripnumber ccudef-attributes ccuReadingPrefix'. + ' ccuflags:multiple-strict,procrpc,dptnocheck,logCommand,noagg,nohmstate,updGroupMembers,'. + 'logEvents,noEvents,noInitialUpdate,noReadings,nonBlocking,reconnect,logPong,trace,logEnhanced'. + ' ccuReqTimeout ccuGetVars rpcPingCCU'. + ' rpcserver:on,off rpcserveraddr rpcserverport rpctimeout rpcevtimeout substitute'. + ' ccuget:Value,State '. $readingFnAttributes; } @@ -413,12 +399,13 @@ sub HMCCU_Initialize ($) # Define device ###################################################################### -sub HMCCU_Define ($$) +sub HMCCU_Define ($$$) { my ($hash, $a, $h) = @_; my $name = $hash->{NAME}; + my $usage = "Usage: define $name HMCCU {NameOrIP} [{ccunum}] [nosync] ccudelay={time} waitforccu={time} delayedinit={time}"; - return "Specify CCU hostname or IP address as a parameter" if (scalar (@$a) < 3); + return $usage if (scalar(@$a) < 3); # Setup http or ssl connection if ($$a[2] =~ /^(https?):\/\/(.+)/) { @@ -430,54 +417,69 @@ sub HMCCU_Define ($$) $hash->{host} = $$a[2]; } - $hash->{Clients} = ':HMCCUDEV:HMCCUCHN:HMCCURPC:HMCCURPCPROC:'; - $hash->{hmccu}{ccu}{delay} = exists ($h->{ccudelay}) ? $h->{ccudelay} : $HMCCU_CCU_BOOT_DELAY; - $hash->{hmccu}{ccu}{timeout} = exists ($h->{waitforccu}) ? $h->{waitforccu} : $HMCCU_CCU_PING_TIMEOUT; + $hash->{Clients} = ':HMCCUDEV:HMCCUCHN:HMCCURPCPROC:'; + $hash->{hmccu}{ccu}{delay} = $h->{ccudelay} // $HMCCU_CCU_BOOT_DELAY; + $hash->{hmccu}{ccu}{timeout} = $h->{waitforccu} // $HMCCU_CCU_PING_TIMEOUT; $hash->{hmccu}{ccu}{delayed} = 0; + $hash->{hmccu}{ccu}{sync} = 1; - # Check if TCL-Rega process is running on CCU (CCU reachable) - if (exists ($h->{delayedinit}) && $h->{delayedinit} > 0) { - return "Value for delayed initialization must be greater than $HMCCU_CCU_DELAYED_INIT" - if ($h->{delayedinit} <= $HMCCU_CCU_DELAYED_INIT); - $hash->{hmccu}{ccu}{delay} = $h->{delayedinit}; - $hash->{ccustate} = 'unreachable'; - HMCCU_Log ($hash, 1, "Forced delayed initialization"); + if (exists($h->{delayedinit}) && $h->{delayedinit} > 0) { + if (!$init_done) { + # Forced delayed initialization + return "Value for delayed initialization must be greater than $HMCCU_CCU_DELAYED_INIT" + if ($h->{delayedinit} <= $HMCCU_CCU_DELAYED_INIT); + $hash->{hmccu}{ccu}{delay} = $h->{delayedinit}; + $hash->{ccustate} = 'unreachable'; + HMCCU_Log ($hash, 1, 'Forced delayed initialization'); + } + else { + HMCCU_LogDisplay ($hash, 2, 'Forced delayed initialization is done during FHEM start'); + } } else { + # Check if TCL-Rega process is running on CCU (CCU is reachable) if (HMCCU_TCPPing ($hash->{host}, $HMCCU_REGA_PORT{$hash->{prot}}, $hash->{hmccu}{ccu}{timeout})) { $hash->{ccustate} = 'active'; + HMCCU_Log ($hash, 1, 'CCU port '.$HMCCU_REGA_PORT{$hash->{prot}}.' is reachable'); } else { $hash->{ccustate} = 'unreachable'; - HMCCU_Log ($hash, 1, "CCU port ".$HMCCU_REGA_PORT{$hash->{prot}}." is not reachable"); + HMCCU_LogDisplay ($hash, 1, 'CCU port '.$HMCCU_REGA_PORT{$hash->{prot}}.' is not reachable'); } } # Get CCU IP address $hash->{ccuip} = HMCCU_ResolveName ($hash->{host}, 'N/A'); - # Get CCU number (if more than one) - if (scalar (@$a) >= 4) { - return "CCU number must be in range 1-9" if ($$a[3] < 1 || $$a[3] > 9); - $hash->{CCUNum} = $$a[3]; + # Parse optional command line parameters + for (my $i=3; $i 9); + $hash->{CCUNum} = $$a[$i]; + } + elsif (lc($$a[$i]) eq 'nosync') { + $hash->{hmccu}{ccu}{sync} = 0; + } + else { + return $usage; + } } - else { + + # Get CCU number (if there is more than one) + if (!exists($hash->{CCUNum})) { # Count CCU devices - my $ccucount = 0; + $hash->{CCUNum} = 1; foreach my $d (keys %defs) { my $ch = $defs{$d}; - next if (!exists ($ch->{TYPE})); - $ccucount++ if ($ch->{TYPE} eq 'HMCCU' && $ch != $hash); + $hash->{CCUNum}++ if (exists($ch->{TYPE}) && $ch->{TYPE} eq 'HMCCU' && $ch != $hash); } - $hash->{CCUNum} = $ccucount+1; } - $hash->{version} = $HMCCU_VERSION; - $hash->{ccutype} = 'CCU2/3'; - $hash->{RPCState} = "inactive"; - $hash->{NOTIFYDEV} = "global,TYPE=(HMCCU|HMCCUDEV|HMCCUCHN)"; - $hash->{hmccu}{defInterface} = $HMCCU_RPC_PRIORITY[0]; - $hash->{hmccu}{defPort} = $HMCCU_RPC_PORT{$hash->{hmccu}{defInterface}}; + $hash->{version} = $HMCCU_VERSION; + $hash->{config} = $HMCCU_CONFIG_VERSION; + $hash->{ccutype} = 'CCU2/3'; + $hash->{RPCState} = 'inactive'; + $hash->{NOTIFYDEV} = 'global,TYPE=(HMCCU|HMCCUDEV|HMCCUCHN)'; $hash->{hmccu}{rpcports} = undef; HMCCU_Log ($hash, 1, "Initialized version $HMCCU_VERSION"); @@ -485,30 +487,26 @@ sub HMCCU_Define ($$) my $rc = 0; if ($hash->{ccustate} eq 'active') { # If CCU is alive read devices, channels, interfaces and groups - HMCCU_Log ($hash, 1, "HMCCU: Initializing device"); + HMCCU_Log ($hash, 1, 'Initializing device'); $rc = HMCCU_InitDevice ($hash); } - if ($hash->{ccustate} ne 'active' || $rc > 0) { - # Schedule update of CCU assets if CCU is not active during FHEM startup - if (!$init_done) { - $hash->{hmccu}{ccu}{delayed} = 1; - HMCCU_Log ($hash, 1, "Scheduling delayed initialization in ".$hash->{hmccu}{ccu}{delay}." seconds"); - InternalTimer (gettimeofday()+$hash->{hmccu}{ccu}{delay}, "HMCCU_InitDevice", $hash); - } + if (($hash->{ccustate} ne 'active' || $rc > 0) && !$init_done) { + # Schedule later update of CCU assets if CCU is not active during FHEM startup + $hash->{hmccu}{ccu}{delayed} = 1; + HMCCU_Log ($hash, 1, 'Scheduling delayed initialization in '.$hash->{hmccu}{ccu}{delay}.' seconds'); + InternalTimer (gettimeofday()+$hash->{hmccu}{ccu}{delay}, "HMCCU_InitDevice", $hash); } - $hash->{hmccu}{evtime} = 0; - $hash->{hmccu}{evtimeout} = 0; - $hash->{hmccu}{updatetime} = 0; - $hash->{hmccu}{rpccount} = 0; + $hash->{hmccu}{$_} = 0 for ('evtime', 'evtimeout', 'updatetime', 'rpccount', 'defaults'); - readingsBeginUpdate ($hash); - readingsBulkUpdate ($hash, "state", "Initialized"); - readingsBulkUpdate ($hash, "rpcstate", "inactive"); - readingsEndUpdate ($hash, 1); + HMCCU_UpdateReadings ($hash, { 'state' => 'Initialized', 'rpcstate' => 'inactive' }); - $attr{$name}{stateFormat} = "rpcstate/state"; + if ($init_done) { + # Set default attributes + $attr{$name}{stateFormat} = 'rpcstate/state'; + $attr{$name}{room} = 'Homematic'; + } return undef; } @@ -525,27 +523,66 @@ sub HMCCU_InitDevice ($) { my ($hash) = @_; my $name = $hash->{NAME}; + my $host = $hash->{host}; if ($hash->{hmccu}{ccu}{delayed} == 1) { - HMCCU_Log ($hash, 1, "HMCCU: Initializing devices"); - if (!HMCCU_TCPPing ($hash->{host}, $HMCCU_REGA_PORT{$hash->{prot}}, $hash->{hmccu}{ccu}{timeout})) { + HMCCU_Log ($hash, 1, 'Initializing devices'); + if (!HMCCU_TCPPing ($host, $HMCCU_REGA_PORT{$hash->{prot}}, $hash->{hmccu}{ccu}{timeout})) { $hash->{ccustate} = 'unreachable'; - HMCCU_Log ($hash, 1, "HMCCU: CCU port ".$HMCCU_REGA_PORT{$hash->{prot}}." is not reachable"); - return 1; + return HMCCU_Log ($hash, 1, "CCU port ".$HMCCU_REGA_PORT{$hash->{prot}}." is not reachable", 1); } } - + my ($devcnt, $chncnt, $ifcount, $prgcount, $gcount) = HMCCU_GetDeviceList ($hash); - if ($devcnt >= 0) { - HMCCU_Log ($hash, 1, "HMCCU: Read $devcnt devices with $chncnt channels from CCU ".$hash->{host}); - HMCCU_Log ($hash, 1, "HMCCU: Read $ifcount interfaces from CCU ".$hash->{host}); - HMCCU_Log ($hash, 1, "HMCCU: Read $prgcount programs from CCU ".$hash->{host}); - HMCCU_Log ($hash, 1, "HMCCU: Read $gcount virtual groups from CCU ".$hash->{host}); - return 0; + if ($ifcount > 0) { + setDevAttrList ($name, $modules{HMCCU}{AttrList}.' rpcinterfaces:multiple-strict,'.$hash->{ccuinterfaces}); + + HMCCU_Log ($hash, 1, [ + "Read $devcnt devices with $chncnt channels from CCU $host", + "Read $prgcount programs from CCU $host", + "Read $gcount virtual groups from CCU $host" + ]); + + # Interactive device definition + if ($init_done && $hash->{hmccu}{ccu}{delayed} == 0) { + # Force sync with CCU during interactive device definition + if ($hash->{hmccu}{ccu}{sync} == 1) { + HMCCU_LogDisplay ($hash, 1, 'Reading device config from CCU. This may take a couple of seconds ...'); + my ($cDev, $cPar, $cLnk) = HMCCU_GetDeviceConfig ($hash); + HMCCU_Log ($hash, 2, "Read RPC device configuration: devices/channels=$cDev parametersets=$cPar links=$cLnk"); + } + } } else { - HMCCU_Log ($hash, 1, "HMCCU: Error while reading device list from CCU ".$hash->{host}); - return 2; + return HMCCU_Log ($hash, 1, "No RPC interfaces found on CCU $host", 2); + } +} + +###################################################################### +# Tasks to be executed after all devices have been defined. Executed +# as timer function after FHEM has been initialized and startup is +# complete. +# Read device configuration from CCU +# Start RPC servers +###################################################################### + +sub HMCCU_PostInit ($) +{ + my ($hash) = @_; + + my $host = $hash->{host}; + + if ($hash->{ccustate} eq 'active') { + my $rpcServer = AttrVal ($hash->{NAME}, 'rpcserver', 'off'); + + HMCCU_Log ($hash, 1, 'Reading device config from CCU. This may take a couple of seconds ...'); + my ($cDev, $cPar, $cLnk) = HMCCU_GetDeviceConfig ($hash); + HMCCU_Log ($hash, 2, "Read device configuration: devices/channels=$cDev parametersets=$cPar links=$cLnk"); + + HMCCU_StartExtRPCServer ($hash) if ($rpcServer eq 'on'); + } + else { + HMCCU_Log ($hash, 1, 'CCU not active. Post FHEM start initialization failed'); } } @@ -563,15 +600,12 @@ sub HMCCU_Attr ($@) if ($attrname eq 'ccudefaults') { $rc = HMCCU_ImportDefaults ($attrval); return HMCCU_SetError ($hash, -16) if ($rc == 0); - if ($rc < 0) { - $rc = -$rc; - return HMCCU_SetError ($hash, - "Syntax error in default attribute file $attrval line $rc"); - } + return HMCCU_SetError ($hash, 'Syntax error in default attribute file $attrval line '.(-$rc)) + if ($rc < 0); } elsif ($attrname eq 'ccuaggregate') { $rc = HMCCU_AggregationRules ($hash, $attrval); - return HMCCU_SetError ($hash, "Syntax error in attribute ccuaggregate") if ($rc == 0); + return HMCCU_SetError ($hash, 'Syntax error in attribute ccuaggregate') if ($rc == 0); } elsif ($attrname eq 'ccuackstate') { return "HMCCU: Attribute ccuackstate is depricated. Use ccuflags with 'ackState' instead"; @@ -583,18 +617,10 @@ sub HMCCU_Attr ($@) my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my @flags = ($attrval =~ /(intrpc|extrpc|procrpc)/g); return "Flags extrpc, procrpc and intrpc cannot be combined" if (scalar (@flags) > 1); - if ($attrval =~ /(extrpc|intrpc|procrpc)/) { - my $rpcmode = $1; - if ($ccuflags !~ /$rpcmode/) { - return "Stop RPC server before switching RPC server" - if (HMCCU_IsRPCServerRunning ($hash, undef, undef)); - } - } - if ($attrval =~ /extrpc/) { - HMCCU_Log ($hash, 1, "RPC server mode extrpc will be ignored. Using procrpc instead"); - } - elsif ($attrval =~ /intrpc/) { - HMCCU_Log ($hash, 1, "RPC server mode intrpc is deprecated. Please use procrpc instead"); + if ($attrval =~ /(intrpc|extrpc)/) { + HMCCU_Log ($hash, 1, "RPC server mode $1 no longer supported. Using procrpc instead"); + $attrval =~ s/(extrpc|intrpc)/procrpc/; + $_[3] = $attrval; } } elsif ($attrname eq 'ccuGetVars') { @@ -611,11 +637,8 @@ sub HMCCU_Attr ($@) elsif ($attrname eq 'rpcdevice') { return "HMCCU: Attribute rpcdevice is depricated. Please remove it"; } - elsif ($attrname eq 'rpcinterfaces' || $attrname eq 'rpcport') { - if ($hash->{hmccu}{ccu}{delayed} == 0) { - my $msg = HMCCU_AttrInterfacesPorts ($hash, $attrname, $attrval); - return $msg if ($msg ne ''); - } + elsif ($attrname eq 'rpcport') { + return 'HMCCU: Attribute rpcport is no longer supported. Use rpcinterfaces instead'; } } elsif ($cmd eq 'del') { @@ -625,60 +648,11 @@ sub HMCCU_Attr ($@) elsif ($attrname eq 'ccuGetVars') { RemoveInternalTimer ($hash, "HMCCU_UpdateVariables"); } - elsif ($attrname eq 'rpcdevice') { - delete $hash->{RPCDEV} if (exists ($hash->{RPCDEV})); - } - elsif ($attrname eq 'rpcport' || $attrname eq 'rpcinterfaces') { - my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($hash); - $hash->{hmccu}{rpcports} = undef; - delete $attr{$name}{'rpcinterfaces'} if ($attrname eq 'rpcport'); - delete $attr{$name}{'rpcport'} if ($attrname eq 'rpcinterfaces'); - } } return undef; } -###################################################################### -# Set attributes rpcinterfaces and rpcport. -# Return empty string on success or error message on error. -###################################################################### - -sub HMCCU_AttrInterfacesPorts ($$$) -{ - my ($hash, $attr, $attrval) = @_; - my $name = $hash->{NAME}; - - if ($attr eq 'rpcinterfaces') { - my @ilist = split (',', $attrval); - my @plist = (); - foreach my $p (@ilist) { - my ($pn, $dc) = HMCCU_GetRPCServerInfo ($hash, $p, 'port,devcount'); - return "HMCCU: Illegal RPC interface $p" if (!defined ($pn)); - return "HMCCU: No devices assigned to interface $p" if ($dc == 0); - push (@plist, $pn); - } - return "No RPC interface specified" if (scalar (@plist) == 0); - $hash->{hmccu}{rpcports} = join (',', @plist); - $attr{$name}{"rpcport"} = $hash->{hmccu}{rpcports}; - } - elsif ($attr eq 'rpcport') { - my @plist = split (',', $attrval); - my @ilist = (); - foreach my $p (@plist) { - my ($in, $dc) = HMCCU_GetRPCServerInfo ($hash, $p, 'name,devcount'); - return "HMCCU: Illegal RPC port $p" if (!defined ($in)); - return "HMCCU: No devices assigned to interface $in" if ($dc == 0); - push (@ilist, $in); - } - return "No RPC port specified" if (scalar (@ilist) == 0); - $hash->{hmccu}{rpcports} = $attrval; - $attr{$name}{"rpcinterfaces"} = join (',', @ilist); - } - - return ''; -} - ###################################################################### # Parse aggregation rules for readings. # Syntax of aggregation rule is: @@ -699,46 +673,40 @@ sub HMCCU_AggregationRules ($$) my ($hash, $rulestr) = @_; my $name = $hash->{NAME}; + return 0 if ($rulestr eq ''); + # Delete existing aggregation rules - if (exists ($hash->{hmccu}{agg})) { - delete $hash->{hmccu}{agg}; - } - return if ($rulestr eq ''); + if (exists($hash->{hmccu}{agg})) { delete $hash->{hmccu}{agg}; } my @pars = ('name', 'filter', 'if', 'else'); # Extract aggregation rules my $cnt = 0; - my @rules = split (/[;\n]+/, $rulestr); - foreach my $r (@rules) { + foreach my $r (split(/[;\n]+/, $rulestr)) { $cnt++; # Set default rule parameters. Can be modified later - my %opt = ( 'read' => 'state', 'prefix' => 'RULE', 'coll' => 'NAME' ); + my %opt = ('read' => 'state', 'prefix' => 'RULE', 'coll' => 'NAME'); # Parse aggregation rule - my @specs = split (',', $r); - foreach my $spec (@specs) { - if ($spec =~ /^(name|filter|read|if|else|prefix|coll|html):(.+)$/) { - $opt{$1} = $2; - } + foreach my $spec (split(',', $r)) { + if ($spec =~ /^(name|filter|read|if|else|prefix|coll|html):(.+)$/) { $opt{$1} = $2; } } # Check if mandatory parameters are specified foreach my $p (@pars) { - return HMCCU_Log ($hash, 1, "Parameter $p is missing in aggregation rule $cnt.") - if (!exists ($opt{$p})); + return HMCCU_Log ($hash, 1, "Parameter $p is missing in aggregation rule $cnt.", 0) + if (!exists($opt{$p})); } my $fname = $opt{name}; my ($fincl, $fexcl) = split ('!', $opt{filter}); my ($ftype, $fexpr) = split ('=', $fincl); - return 0 if (!defined ($fexpr)); - my ($fcond, $fval) = split ('=', $opt{if}); - return 0 if (!defined ($fval)); + my ($fcond, $fval) = split ('=', $opt{if}); my ($fcoll, $fdflt) = split ('!', $opt{coll}); - $fdflt = 'no match' if (!defined ($fdflt)); - my $fhtml = exists ($opt{'html'}) ? $opt{'html'} : ''; + return 0 if (!defined($fexpr) || !defined($fval)); + $fdflt //= 'no match'; + my $fhtml = exists($opt{'html'}) ? $opt{'html'} : ''; # Read HTML template (optional) if ($fhtml ne '') { @@ -751,28 +719,27 @@ sub HMCCU_AggregationRules ($$) close (TEMPLATE); } else { - return HMCCU_Log ($hash, 1, "Can't open file $fhtml."); + return HMCCU_Log ($hash, 1, "Can't open file $fhtml.", 0); } # Parse template foreach my $line (@html) { chomp $line; my ($key, $h) = split /:/, $line, 2; - next if (!defined ($h) || $key =~ /^#/); - $tdef{$key} = $h; + $tdef{$key} = $h if (defined($h) && $key !~ /^#/); } # Some syntax checks - return HMCCU_Log ($hash, 1, "Missing definition row-odd in template file.") - if (!exists ($tdef{'row-odd'})); + return HMCCU_Log ($hash, 1, 'Missing definition row-odd in template file.', 0) + if (!exists($tdef{'row-odd'})); # Set default values - $tdef{'begin-html'} = '' if (!exists ($tdef{'begin-html'})); - $tdef{'end-html'} = '' if (!exists ($tdef{'end-html'})); - $tdef{'begin-table'} = "" if (!exists ($tdef{'begin-table'})); - $tdef{'end-table'} = "
" if (!exists ($tdef{'end-table'})); - $tdef{'default'} = 'no data' if (!exists ($tdef{'default'}));; - $tdef{'row-even'} = $tdef{'row-odd'} if (!exists ($tdef{'row-even'})); + $tdef{'begin-html'} = '' if (!exists($tdef{'begin-html'})); + $tdef{'end-html'} = '' if (!exists($tdef{'end-html'})); + $tdef{'begin-table'} = "" if (!exists($tdef{'begin-table'})); + $tdef{'end-table'} = "
" if (!exists($tdef{'end-table'})); + $tdef{'default'} = 'no data' if (!exists($tdef{'default'}));; + $tdef{'row-even'} = $tdef{'row-odd'} if (!exists($tdef{'row-even'})); foreach my $t (keys %tdef) { $hash->{hmccu}{agg}{$fname}{fhtml}{$t} = $tdef{$t}; @@ -781,7 +748,7 @@ sub HMCCU_AggregationRules ($$) $hash->{hmccu}{agg}{$fname}{ftype} = $ftype; $hash->{hmccu}{agg}{$fname}{fexpr} = $fexpr; - $hash->{hmccu}{agg}{$fname}{fexcl} = (defined ($fexcl) ? $fexcl : ''); + $hash->{hmccu}{agg}{$fname}{fexcl} = (defined($fexcl) ? $fexcl : ''); $hash->{hmccu}{agg}{$fname}{fread} = $opt{'read'}; $hash->{hmccu}{agg}{$fname}{fcond} = $fcond; $hash->{hmccu}{agg}{$fname}{ftrue} = $fval; @@ -790,7 +757,7 @@ sub HMCCU_AggregationRules ($$) $hash->{hmccu}{agg}{$fname}{fcoll} = $fcoll; $hash->{hmccu}{agg}{$fname}{fdflt} = $fdflt; } - + return 1; } @@ -843,80 +810,6 @@ sub HMCCU_ExportDefaults ($$) return 1; } -###################################################################### -# Export default attributes as CSV file. -###################################################################### - -sub HMCCU_ExportDefaultsCSV ($$) -{ - my ($filename, $all) = @_; - - my %attrlist = ( - '_type' => '', '_description' => '', '_channels' => '', - 'ccureadingfilter' => '', 'ccureadingname' => '', 'ccuscaleval' => '', 'cmdIcon' => '', 'controldatapoint' => '', - 'eventMap' => '', 'event-on-change-reading' => '', 'event-on-update-reading' => '', - 'genericDeviceType' => '', - 'hmstatevals' => '', - 'statedatapoint' => '', 'statevals' => '', 'stripnumber' => '', 'substexcl' => '', 'substitute' => '', - 'webCmd' => '', 'widgetOverride' => '' - ); - - return 0 if (!open (DEFFILE, ">$filename")); - - # Write header - print DEFFILE "_flag,".join (',', sort keys %attrlist)."\n"; - - # Write channel configurations - foreach my $t (keys %{$HMCCU_CHN_DEFAULTS}) { - print DEFFILE "C"; - $attrlist{'_type'} = $t; - foreach $a (sort keys %attrlist) { - my $v = exists ($HMCCU_CHN_DEFAULTS->{$t}{$a}) ? $HMCCU_CHN_DEFAULTS->{$t}{$a} : $attrlist{$a}; - print DEFFILE ",\"$v\""; - } - print DEFFILE "\n"; - } - - # Write device configurations - foreach my $t (keys %{$HMCCU_DEV_DEFAULTS}) { - print DEFFILE "D"; - $attrlist{'_type'} = $t; - foreach $a (sort keys %attrlist) { - my $v = exists ($HMCCU_DEV_DEFAULTS->{$t}{$a}) ? $HMCCU_DEV_DEFAULTS->{$t}{$a} : $attrlist{$a}; - print DEFFILE ",\"$v\""; - } - print DEFFILE "\n"; - } - - if ($all) { - # Write channel configurations - foreach my $t (keys %HMCCU_CUST_CHN_DEFAULTS) { - print DEFFILE "C"; - $attrlist{'_type'} = $t; - foreach $a (sort keys %attrlist) { - my $v = exists ($HMCCU_CUST_CHN_DEFAULTS{$t}{$a}) ? $HMCCU_CUST_CHN_DEFAULTS{$t}{$a} : $attrlist{$a}; - print DEFFILE ",\"$v\""; - } - print DEFFILE "\n"; - } - - # Write device configurations - foreach my $t (keys %HMCCU_CUST_DEV_DEFAULTS) { - print DEFFILE "D"; - $attrlist{'_type'} = $t; - foreach $a (sort keys %attrlist) { - my $v = exists ($HMCCU_CUST_DEV_DEFAULTS{$t}{$a}) ? $HMCCU_CUST_DEV_DEFAULTS{$t}{$a} : $attrlist{$a}; - print DEFFILE ",\"$v\""; - } - print DEFFILE "\n"; - } - } - - close (DEFFILE); - - return 1; -} - ###################################################################### # Import customer default attributes # Returns 1 on success. Returns negative line number on syntax errors. @@ -1023,11 +916,13 @@ sub HMCCU_SetDefaultsTemplate ($$) my ($hash, $template) = @_; my $name = $hash->{NAME}; + $hash->{hmccu}{semDefaults} = 1; foreach my $a (keys %{$template}) { next if ($a =~ /^_/); my $v = $template->{$a}; CommandAttr (undef, "$name $a $v"); } + $hash->{hmccu}{semDefaults} = 0; } ###################################################################### @@ -1037,11 +932,9 @@ sub HMCCU_SetDefaultsTemplate ($$) sub HMCCU_SetDefaults ($) { my ($hash) = @_; - my $name = $hash->{NAME}; # Set type specific attributes - my $template = HMCCU_FindDefaults ($hash, 0); - return 0 if (!defined ($template)); + my $template = HMCCU_FindDefaults ($hash, 0) // return 0; HMCCU_SetDefaultsTemplate ($hash, $template); return 1; @@ -1052,9 +945,10 @@ sub HMCCU_SetDefaults ($) # device types (mode = 1) with default attributes available. ###################################################################### -sub HMCCU_GetDefaults ($$) +sub HMCCU_GetDefaults ($;$) { my ($hash, $mode) = @_; + $mode //= 0; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my $ccutype = $hash->{ccutype}; @@ -1062,37 +956,54 @@ sub HMCCU_GetDefaults ($$) my $deffile = ''; if ($mode == 0) { - my $template = HMCCU_FindDefaults ($hash, 0); - return ($result eq '' ? "No default attributes defined" : $result) if (!defined ($template)); + if (exists($hash->{ccurolectrl}) && exists($HMCCU_STATECONTROL->{$hash->{ccurolectrl}})) { + $result .= "Support for role $hash->{ccurolectrl} of device type $ccutype is built in."; + } + elsif (exists($hash->{ccurolestate}) && exists($HMCCU_STATECONTROL->{$hash->{ccurolestate}})) { + $result .= "Support for role $hash->{ccurolestate} of device type $ccutype is built in."; + } + elsif (exists($hash->{hmccu}{role})) { + my @roleList = (); + foreach my $role (split(',', $hash->{hmccu}{role})) { + my ($rChn, $rNam) = split(':', $role); + push @roleList, $rNam if (exists($HMCCU_STATECONTROL->{$rNam})); + } + $result .= 'Support for role(s) '.join(',', @roleList)." of device type $ccutype is built in." + if (scalar(@roleList) > 0); + } + else { + my $template = HMCCU_FindDefaults ($hash, 0); + return ($result eq '' ? 'No default attributes defined' : $result) if (!defined($template)); - foreach my $a (keys %{$template}) { - next if ($a =~ /^_/); - my $v = $template->{$a}; - $result .= $a." = ".$v."\n"; + foreach my $a (keys %{$template}) { + next if ($a =~ /^_/); + my $v = $template->{$a}; + $result .= $a." = ".$v."\n"; + } } } else { - $result = "HMCCU Channels:\n------------------------------\n"; + $result = "HMCCU Channels:\n".('-' x 30)."\n"; foreach my $deftype (sort keys %{$HMCCU_CHN_DEFAULTS}) { my $tlist = $deftype; $tlist =~ s/\|/,/g; $result .= $HMCCU_CHN_DEFAULTS->{$deftype}{_description}." ($tlist), channels ". $HMCCU_CHN_DEFAULTS->{$deftype}{_channels}."\n"; } - $result .= "\nHMCCU Devices:\n------------------------------\n"; + $result .= "\nHMCCU Devices:\n".('-' x 30)."\n"; foreach my $deftype (sort keys %{$HMCCU_DEV_DEFAULTS}) { my $tlist = $deftype; $tlist =~ s/\|/,/g; $result .= $HMCCU_DEV_DEFAULTS->{$deftype}{_description}." ($tlist)\n"; } - $result .= "\nCustom Channels:\n-----------------------------\n"; + $result .= "\nCustom Channels:\n".('-' x 30)."\n"; foreach my $deftype (sort keys %HMCCU_CUST_CHN_DEFAULTS) { my $tlist = $deftype; $tlist =~ s/\|/,/g; $result .= $HMCCU_CUST_CHN_DEFAULTS{$deftype}{_description}." ($tlist), channels ". $HMCCU_CUST_CHN_DEFAULTS{$deftype}{_channels}."\n"; } - $result .= "\nCustom Devices:\n-----------------------------\n"; + $result .= "\nCustom Devices:\n".('-' x 30)."\n"; foreach my $deftype (sort keys %HMCCU_CUST_DEV_DEFAULTS) { my $tlist = $deftype; $tlist =~ s/\|/,/g; @@ -1103,17 +1014,6 @@ sub HMCCU_GetDefaults ($$) return $result; } -###################################################################### -# Try to detect default attributes -###################################################################### - -sub HMCCU_DetectDefaults ($$) -{ - my ($hash, $object) = @_; - - my $response = HMCCU_GetDeviceInfo ($hash, $object, 'Value'); -} - ###################################################################### # Handle FHEM events ###################################################################### @@ -1125,47 +1025,69 @@ sub HMCCU_Notify ($$) my $devname = $devhash->{NAME}; my $devtype = $devhash->{TYPE}; - my $disable = AttrVal ($name, 'disable', 0); - my $rpcserver = AttrVal ($name, 'rpcserver', 'off'); - my $ccuflags = HMCCU_GetFlags ($name); - - return if ($disable); + return if (AttrVal ($name, 'disable', 0) == 1); my $events = deviceEvents ($devhash, 1); - return if (! $events); - + return if (!$events); + # Process events foreach my $event (@{$events}) { if ($devname eq 'global') { + # Global event if ($event eq 'INITIALIZED') { - return if ($rpcserver eq 'off'); + # FHEM initialized. Schedule post initialization tasks my $delay = $hash->{ccustate} eq 'active' && $hash->{hmccu}{ccu}{delayed} == 0 ? $HMCCU_INIT_INTERVAL0 : $hash->{hmccu}{ccu}{delay}+$HMCCU_CCU_RPC_OFFSET; - Log3 $name, 0, "HMCCU: Start of RPC server after FHEM initialization in $delay seconds"; - if ($ccuflags =~ /(extrpc|procrpc)/) { - InternalTimer (gettimeofday()+$delay, "HMCCU_StartExtRPCServer", $hash, 0); - } - else { - InternalTimer (gettimeofday()+$delay, "HMCCU_StartIntRPCServer", $hash, 0); + HMCCU_Log ($hash, 0, "Scheduling post FHEM initialization tasks in $delay seconds"); + InternalTimer (gettimeofday()+$delay, "HMCCU_PostInit", $hash, 0); + } + elsif ($event =~ /^(ATTR|DELETEATTR)/ && $init_done) { + # Attribute of client device set or deleted + my $refreshAttrList = 'ccucalculate|ccuflags|ccureadingfilter|ccureadingformat|'. + 'ccureadingname|ccuReadingPrefix|ccuscaleval|controldatapoint|hmstatevals|'. + 'statedatapoint|statevals|substitute|substexcl|stripnumber'; + my $cmdAttrList = 'statechannel|statedatapoint|controlchannel|controldatapoint|statevals'; + + my ($aCmd, $aDev, $aAtt, $aVal) = split (/\s+/, $event); + $aAtt = $aVal if ($aCmd eq 'DELETEATTR'); + if (defined($aAtt)) { + my $clHash = $defs{$aDev}; + # Consider attr event only for HMCCUCHN or HMCCUDEV devices assigned to current IO device + if (defined($clHash->{TYPE}) && ($clHash->{TYPE} eq 'HMCCUCHN' || $clHash->{TYPE} eq 'HMCCUDEV') && + defined($clHash->{IODev}) && $clHash->{IODev} == $hash && + (!defined($clHash->{hmccu}{semDefaults}) || $clHash->{hmccu}{semDefaults} == 0)) { + if ($aAtt =~ /^($cmdAttrList)$/) { + my ($cc, $cd) = HMCCU_ControlDatapoint ($clHash); + # if ($cc ne '' && $cd ne '') { + HMCCU_UpdateRoleCommands ($hash, $clHash, $cc); + HMCCU_UpdateAdditionalCommands ($hash, $clHash, $cc, $cd); + # } + } + if ($aAtt =~ /^($refreshAttrList)$/i) { + HMCCU_RefreshReadings ($clHash); + } + } } } } else { - return if ($devtype ne 'HMCCUDEV' && $devtype ne 'HMCCUCHN'); + # Device must be assigned to IO device + return if (($devtype ne 'HMCCUDEV' && $devtype ne 'HMCCUCHN') || $devhash->{IODev} != $hash); + + # Update aggregated readings my ($r, $v) = split (": ", $event); - return if (!defined ($v)); - return if ($ccuflags =~ /noagg/); + return if (!defined($v) || HMCCU_IsFlag ($name, 'noagg')); foreach my $rule (keys %{$hash->{hmccu}{agg}}) { my $ftype = $hash->{hmccu}{agg}{$rule}{ftype}; my $fexpr = $hash->{hmccu}{agg}{$rule}{fexpr}; my $fread = $hash->{hmccu}{agg}{$rule}{fread}; - next if ($r !~ $fread); - next if ($ftype eq 'name' && $devname !~ /$fexpr/); - next if ($ftype eq 'type' && $devhash->{ccutype} !~ /$fexpr/); - next if ($ftype eq 'group' && AttrVal ($devname, 'group', 'null') !~ /$fexpr/); - next if ($ftype eq 'room' && AttrVal ($devname, 'room', 'null') !~ /$fexpr/); - next if ($ftype eq 'alias' && AttrVal ($devname, 'alias', 'null') !~ /$fexpr/); + next if ($r !~ $fread || + ($ftype eq 'name' && $devname !~ /$fexpr/) || + ($ftype eq 'type' && $devhash->{ccutype} !~ /$fexpr/) || + ($ftype eq 'group' && AttrVal ($devname, 'group', 'null') !~ /$fexpr/) || + ($ftype eq 'room' && AttrVal ($devname, 'room', 'null') !~ /$fexpr/) || + ($ftype eq 'alias' && AttrVal ($devname, 'alias', 'null') !~ /$fexpr/)); HMCCU_AggregateReadings ($hash, $rule); } @@ -1176,7 +1098,7 @@ sub HMCCU_Notify ($$) } ###################################################################### -# Enhance device details in FHEM WEB +# Enhance device details in FHEM web view ###################################################################### sub HMCCU_Detail ($$$$) @@ -1184,21 +1106,31 @@ sub HMCCU_Detail ($$$$) my ($FW_Name, $Device, $Room, $pageHash) = @_; my $hash = $defs{$Device}; - return defined ($hash->{host}) ? qq( - CCU Administration - - - - - - - -
- > CCU WebUI -
- > CUxD Config -
- ) : ''; + my $links = ''; + + if (defined($hash->{host})) { + $links = qq( +CCU Administration + + + + + ); + if (exists($hash->{hmccu}{interfaces}{CUxD})) { + $links .= qq( + + + + ); + } + $links .= '
+> CCU WebUI +
'; + } + + return $links; } ###################################################################### @@ -1217,67 +1149,46 @@ sub HMCCU_AggregateReadings ($$) my $table = ''; # Get rule parameters - my $ftype = $hash->{hmccu}{agg}{$rule}{ftype}; - my $fexpr = $hash->{hmccu}{agg}{$rule}{fexpr}; - my $fexcl = $hash->{hmccu}{agg}{$rule}{fexcl}; - my $fread = $hash->{hmccu}{agg}{$rule}{fread}; - my $fcond = $hash->{hmccu}{agg}{$rule}{fcond}; - my $ftrue = $hash->{hmccu}{agg}{$rule}{ftrue}; - my $felse = $hash->{hmccu}{agg}{$rule}{felse}; - my $fpref = $hash->{hmccu}{agg}{$rule}{fpref}; - my $fhtml = exists ($hash->{hmccu}{agg}{$rule}{fhtml}) ? 1 : 0; + my $r = $hash->{hmccu}{agg}{$rule}; + my $cnd = $r->{fcond}; + my $tr = $r->{ftrue}; my $resval; - $resval = $ftrue if ($fcond =~ /^(max|min|sum|avg)$/); + $resval = $r->{ftrue} if ($r->{fcond} =~ /^(max|min|sum|avg)$/); - my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)", undef, undef); + my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)"); foreach my $d (@devlist) { my $ch = $defs{$d}; my $cn = $ch->{NAME}; my $ct = $ch->{TYPE}; my $fmatch = ''; - $fmatch = $cn if ($ftype eq 'name'); - $fmatch = $ch->{ccutype} if ($ftype eq 'type'); - $fmatch = AttrVal ($cn, 'group', '') if ($ftype eq 'group'); - $fmatch = AttrVal ($cn, 'room', '') if ($ftype eq 'room'); - $fmatch = AttrVal ($cn, 'alias', '') if ($ftype eq 'alias'); - next if (!defined ($fmatch) || $fmatch eq '' || $fmatch !~ /$fexpr/ || ($fexcl ne '' && $fmatch =~ /$fexcl/)); + if ($r->{ftype} eq 'name') { $fmatch = $cn // ''; } + elsif ($r->{ftype} eq 'type') { $fmatch = $ch->{ccutype} // ''; } + elsif ($r->{ftype} eq 'group') { $fmatch = AttrVal ($cn, 'group', ''); } + elsif ($r->{ftype} eq 'room') { $fmatch = AttrVal ($cn, 'room', ''); } + elsif ($r->{ftype} eq 'alias') { $fmatch = AttrVal ($cn, 'alias', ''); } - my $fcoll = $hash->{hmccu}{agg}{$rule}{fcoll} eq 'NAME' ? - $cn : AttrVal ($cn, $hash->{hmccu}{agg}{$rule}{fcoll}, $cn); + next if ($fmatch eq '' || $fmatch !~ /$r->{fexpr}/ || ($r->{fexcl} ne '' && $fmatch =~ /$r->{fexcl}/)); + + my $fcoll = $r->{fcoll} eq 'NAME' ? $cn : AttrVal ($cn, $r->{fcoll}, $cn); # Compare readings - foreach my $r (keys %{$ch->{READINGS}}) { - next if ($r !~ /$fread/); - my $rv = $ch->{READINGS}{$r}{VAL}; + foreach my $rd (keys %{$ch->{READINGS}}) { + next if ($rd =~ /^\./ || $rd !~ /$r->{fread}/); + my $rv = $ch->{READINGS}{$rd}{VAL}; my $f = 0; - if (($fcond eq 'any' || $fcond eq 'all') && $rv =~ /$ftrue/) { - $mc++; - $f = 1; + if (($cnd eq 'any' || $cnd eq 'all') && $rv =~ /$tr/) { $mc++; $f = 1; } + if ($cnd eq 'max' && $rv > $resval) { $resval = $rv; $mc = 1; $f = 1; } + if ($cnd eq 'min' && $rv < $resval) { $resval = $rv; $mc = 1; $f = 1; } + if ($cnd eq 'sum' || $cnd eq 'avg') { $resval += $rv; $mc++; $f = 1; } + if ($cnd =~ /^(gt|lt|ge|le)$/ && (!HMCCU_IsFltNum ($rv) || !HMCCU_IsFltNum($tr))) { + HMCCU_Log ($hash, 4, "Aggregation value $rv of reading $cn.$r or $tr is not numeric"); + next; } - if ($fcond eq 'max' && $rv > $resval) { - $resval = $rv; - $mc = 1; - $f = 1; - } - if ($fcond eq 'min' && $rv < $resval) { - $resval = $rv; - $mc = 1; - $f = 1; - } - if ($fcond eq 'sum' || $fcond eq 'avg') { - $resval += $rv; - $mc++; - $f = 1; - } - if (($fcond eq 'gt' && $rv > $ftrue) || - ($fcond eq 'lt' && $rv < $ftrue) || - ($fcond eq 'ge' && $rv >= $ftrue) || - ($fcond eq 'le' && $rv <= $ftrue)) { - $mc++; - $f = 1; + if (($cnd eq 'gt' && $rv > $tr) || ($cnd eq 'lt' && $rv < $tr) || ($cnd eq 'ge' && $rv >= $tr) || ($cnd eq 'le' && $rv <= $tr)) { + $mc++; $f = 1; } if ($f) { $rl .= ($mc > 1 ? ",$fcoll" : $fcoll); @@ -1287,59 +1198,38 @@ sub HMCCU_AggregateReadings ($$) $dc++; } - $rl = $hash->{hmccu}{agg}{$rule}{fdflt} if ($rl eq ''); + $rl = $r->{fdflt} if ($rl eq ''); # HTML code generation - if ($fhtml) { + if ($r->{fhtml}) { if ($rl ne '') { - $table = $hash->{hmccu}{agg}{$rule}{fhtml}{'begin-html'}. - $hash->{hmccu}{agg}{$rule}{fhtml}{'begin-table'}; - $table .= $hash->{hmccu}{agg}{$rule}{fhtml}{'header'} - if (exists ($hash->{hmccu}{agg}{$rule}{fhtml}{'header'})); + $table = $r->{fhtml}{'begin-html'}.$r->{fhtml}{'begin-table'}; + $table .= $r->{fhtml}{'header'} if (exists($r->{fhtml}{'header'})); my $row = 1; foreach my $v (split (",", $rl)) { - my $t_row = ($row % 2) ? $hash->{hmccu}{agg}{$rule}{fhtml}{'row-odd'} : - $hash->{hmccu}{agg}{$rule}{fhtml}{'row-even'}; + my $t_row = ($row % 2) ? $r->{fhtml}{'row-odd'} : $r->{fhtml}{'row-even'}; $t_row =~ s/\/$v/; $table .= $t_row; $row++; } - $table .= $hash->{hmccu}{agg}{$rule}{fhtml}{'end-table'}. - $hash->{hmccu}{agg}{$rule}{fhtml}{'end-html'}; + $table .= $r->{fhtml}{'end-table'}.$r->{fhtml}{'end-html'}; } else { - $table = $hash->{hmccu}{agg}{$rule}{fhtml}{'begin-html'}. - $hash->{hmccu}{agg}{$rule}{fhtml}{'default'}. - $hash->{hmccu}{agg}{$rule}{fhtml}{'end-html'}; + $table = $r->{fhtml}{'begin-html'}.$r->{fhtml}{'default'}.$r->{fhtml}{'end-html'}; } } - if ($fcond eq 'any') { - $result = $mc > 0 ? $ftrue : $felse; - } - elsif ($fcond eq 'all') { - $result = $mc == $dc ? $ftrue : $felse; - } - elsif ($fcond eq 'min' || $fcond eq 'max' || $fcond eq 'sum') { - $result = $mc > 0 ? $resval : $felse; - } - elsif ($fcond eq 'avg') { - $result = $mc > 0 ? $resval/$mc : $felse; - } - elsif ($fcond =~ /^(gt|lt|ge|le)$/) { - $result = $mc; - } + if ($cnd eq 'any') { $result = $mc > 0 ? $tr : $r->{felse}; } + elsif ($cnd eq 'all') { $result = $mc == $dc ? $tr : $r->{felse}; } + elsif ($cnd =~ /^(min|max|sum)$/) { $result = $mc > 0 ? $resval : $r->{felse}; } + elsif ($cnd eq 'avg') { $result = $mc > 0 ? $resval/$mc : $r->{felse}; } + elsif ($cnd =~ /^(gt|lt|ge|le)$/) { $result = $mc; } - # Set readings - readingsBeginUpdate ($hash); - readingsBulkUpdate ($hash, $fpref.'state', $result); - readingsBulkUpdate ($hash, $fpref.'match', $mc); - readingsBulkUpdate ($hash, $fpref.'count', $dc); - readingsBulkUpdate ($hash, $fpref.'list', $rl); - readingsBulkUpdate ($hash, $fpref.'table', $table) if ($fhtml); - readingsEndUpdate ($hash, 1); + HMCCU_UpdateReadings ($hash, { $r->{fpref}.'state' => $result, $r->{fpref}.'match' => $mc, + $r->{fpref}.'count' => $dc, $r->{fpref}.'list' => $rl }); + readingsSingleUpdate ($hash, $r->{fpref}.'table', $table, 1) if ($r->{fhtml}); return $result; } @@ -1351,24 +1241,55 @@ sub HMCCU_AggregateReadings ($$) sub HMCCU_Undef ($$) { my ($hash, $arg) = @_; - -# HMCCU_Log ($hash, 3, "Undef()"); + my $name = $hash->{NAME}; # Shutdown RPC server HMCCU_Shutdown ($hash); - # Delete reference to IO module in client devices my @keylist = keys %defs; foreach my $d (@keylist) { - if (exists ($defs{$d}) && exists($defs{$d}{IODev}) && - $defs{$d}{IODev} == $hash) { - delete $defs{$d}{IODev}; + my $ch = $defs{$d} // next; + next if (!exists($ch->{TYPE}) || !exists($ch->{IODev}) || $ch->{IODev} != $hash); + if ($ch->{TYPE} eq 'HMCCUDEV' || $ch->{TYPE} eq 'HMCCUCHN') { + # Delete reference to IO module in client devices + delete $defs{$d}{IODev}; + } + elsif ($ch->{TYPE} eq 'HMCCURPCPROC') { + # Delete RPC server devices associated with deleted I/O device + HMCCU_Log ($hash, 1, "Deleting RPC server device $ch->{NAME}"); + CommandDelete (undef, $ch->{NAME}); } } + # Delete CCU credentials + my ($erruser, $encuser) = getKeyValue ($name.'_username'); + my ($errpass, $encpass) = getKeyValue ($name.'_password'); + setKeyValue ($name."_username", undef) if (!defined($erruser) && defined($encuser)); + setKeyValue ($name."_password", undef) if (!defined($errpass) && defined($encpass)); + return undef; } +###################################################################### +# Rename device +###################################################################### + +sub HMCCU_Rename ($$) +{ + my ($oldName, $newName); + + my ($erruser, $encuser) = getKeyValue ($oldName.'_username'); + my ($errpass, $encpass) = getKeyValue ($oldName.'_password'); + if (!defined($erruser) && defined($encuser)) { + setKeyValue ($newName."_username", $encuser); + setKeyValue ($oldName."_username", undef); + } + if (!defined($errpass) && defined($encpass)) { + setKeyValue ($newName."_password", $encpass); + setKeyValue ($oldName."_password", undef); + } +} + ###################################################################### # Delayed shutdown FHEM ###################################################################### @@ -1378,23 +1299,16 @@ sub HMCCU_DelayedShutdown ($) my ($hash) = @_; my $name = $hash->{NAME}; -# HMCCU_Log ($hash, 3, "DelayedShutdown()"); - - my $delay = HMCCU_Max (AttrVal ("global", "maxShutdownDelay", 10)-2, 0); + my $delay = HMCCU_Max (AttrVal ('global', 'maxShutdownDelay', 10)-2, 0); # Shutdown RPC server - if (!exists ($hash->{hmccu}{delayedShutdown})) { + if (!exists($hash->{hmccu}{delayedShutdown})) { $hash->{hmccu}{delayedShutdown} = $delay; HMCCU_Log ($hash, 1, "Graceful shutdown in $delay seconds"); - if (HMCCU_IsFlag ($name, "(extrpc|procrpc)")) { - HMCCU_StopExtRPCServer ($hash, 0); - } - else { - HMCCU_StopRPCServer ($hash); - } + HMCCU_StopExtRPCServer ($hash, 0); } else { - HMCCU_Log ($hash, 1, "Graceful shutdown already in progress"); + HMCCU_Log ($hash, 1, 'Graceful shutdown already in progress'); } return 1; @@ -1407,22 +1321,14 @@ sub HMCCU_DelayedShutdown ($) sub HMCCU_Shutdown ($) { my ($hash) = @_; - my $name = $hash->{NAME}; - -# HMCCU_Log ($hash, 3, "Shutdown()"); # Shutdown RPC server - if (!exists ($hash->{hmccu}{delayedShutdown})) { - HMCCU_Log ($hash, 1, "Immediate shutdown"); - if (HMCCU_IsFlag ($name, "(extrpc|procrpc)")) { - HMCCU_StopExtRPCServer ($hash, 0); - } - else { - HMCCU_StopRPCServer ($hash); - } + if (!exists($hash->{hmccu}{delayedShutdown})) { + HMCCU_Log ($hash, 1, 'Immediate shutdown'); + HMCCU_StopExtRPCServer ($hash, 0); } else { - HMCCU_Log ($hash, 1, "Graceful shutdown"); + HMCCU_Log ($hash, 1, 'Graceful shutdown'); } # Remove existing timer functions @@ -1439,46 +1345,51 @@ sub HMCCU_Set ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; - my $opt = shift @$a; - my $options = "var clear delete execute hmscript cleardefaults:noArg datapoint defaults:noArg ". - "importdefaults rpcregister:all rpcserver:on,off,restart ackmessages:noArg authentication ". - "prgActivate prgDeactivate"; - - return "No set command specified" if (!defined ($opt)); + my $opt = shift @$a // return 'No set command specified'; + my $options = "var clear delete execute hmscript cleardefaults:noArg datapoint ". + "importdefaults rpcregister:all rpcserver:on,off ackmessages:noArg authentication ". + "prgActivate prgDeactivate on:noArg off:noArg"; + $opt = lc($opt); - my @ifList = HMCCU_GetRPCInterfaceList ($hash); - if (scalar (@ifList) > 0) { - my $ifStr = join (',', @ifList); + my $interfaces = HMCCU_GetRPCInterfaceList ($hash, 0); + my @ifList = keys %$interfaces; + if (scalar(@ifList) > 0) { + my $ifStr = join(',', @ifList); $options =~ s/rpcregister:all/rpcregister:all,$ifStr/; } my $host = $hash->{host}; - $options = "initialize:noArg" if (exists ($hash->{hmccu}{ccu}{delayed}) && + $options = 'initialize:noArg' if (exists($hash->{hmccu}{ccu}{delayed}) && $hash->{hmccu}{ccu}{delayed} == 1 && $hash->{ccustate} eq 'unreachable'); -# return undef if ($hash->{ccustate} ne 'active'); - return "HMCCU: CCU busy, choose one of rpcserver:off" + return 'HMCCU: CCU busy, choose one of rpcserver:off' if ($opt ne 'rpcserver' && HMCCU_IsRPCStateBlocking ($hash)); my $usage = "HMCCU: Unknown argument $opt, choose one of $options"; - my $ccuflags = HMCCU_GetFlags ($name); - my $stripchar = AttrVal ($name, "stripchar", ''); - my $ccureadings = AttrVal ($name, "ccureadings", $ccuflags =~ /noReadings/ ? 0 : 1); + my $ccuflags = HMCCU_GetFlags ($name); + my $stripchar = AttrVal ($name, "stripchar", ''); + my $ccureadings = AttrVal ($name, "ccureadings", $ccuflags =~ /noReadings/ ? 0 : 1); my $ccureqtimeout = AttrVal ($name, "ccuReqTimeout", $HMCCU_TIMEOUT_REQUEST); my $readingformat = HMCCU_GetAttrReadingFormat ($hash, $hash); - my $substitute = HMCCU_GetAttrSubstitute ($hash, $hash); + my $substitute = HMCCU_GetAttrSubstitute ($hash); my $result; # Add program names to command execute - if (exists ($hash->{hmccu}{prg})) { + if (exists($hash->{hmccu}{prg})) { my @progs = (); my @aprogs = (); my @iprogs = (); foreach my $p (keys %{$hash->{hmccu}{prg}}) { + if (!exists($hash->{hmccu}{prg}{$p}{internal}) || !exists($hash->{hmccu}{prg}{$p}{active})) { + HMCCU_Log ($hash, 2, "Information for CCU program $p incomplete"); + next; + } if ($hash->{hmccu}{prg}{$p}{internal} eq 'false' && $p !~ /^\$/) { - push (@progs, $p); - push (@aprogs, $p) if ($hash->{hmccu}{prg}{$p}{active} eq 'true'); - push (@iprogs, $p) if ($hash->{hmccu}{prg}{$p}{active} eq 'false'); + my $pn = $p; + $pn =~ s/ /#/g; + push (@progs, $pn); + push (@aprogs, $pn) if ($hash->{hmccu}{prg}{$p}{active} eq 'true'); + push (@iprogs, $pn) if ($hash->{hmccu}{prg}{$p}{active} eq 'false'); } } if (scalar (@progs) > 0) { @@ -1495,65 +1406,61 @@ sub HMCCU_Set ($@) } if ($opt eq 'var') { - my $vartype; - $vartype = shift @$a if (scalar (@$a) == 3); - my $objname = shift @$a; - my $objvalue = shift @$a; $usage = "set $name $opt [{'bool'|'list'|'number'|'text'}] variable value [param=value [...]]"; - - return HMCCU_SetError ($hash, $usage) if (!defined ($objvalue)); + my $vartype; + $vartype = shift @$a if (scalar(@$a) == 3); + my $objname = shift @$a; + my $objvalue = shift @$a // return HMCCU_SetError ($hash, $usage); $objname =~ s/$stripchar$// if ($stripchar ne ''); - $objvalue =~ s/\\_/%20/g; - $h->{name} = $objname if (!defined ($h) && defined ($vartype)); + $h->{name} = $objname if (!defined($h) && defined($vartype)); $result = HMCCU_SetVariable ($hash, $objname, $objvalue, $vartype, $h); return HMCCU_SetError ($hash, $result) if ($result < 0); - return HMCCU_SetState ($hash, "OK"); + return HMCCU_SetState ($hash, 'OK'); } elsif ($opt eq 'initialize') { - return HMCCU_SetError ($hash, "State of CCU must be unreachable") + return HMCCU_SetError ($hash, 'State of CCU must be unreachable') if ($hash->{ccustate} ne 'unreachable'); my $err = HMCCU_InitDevice ($hash); - return HMCCU_SetError ($hash, "CCU not reachable") if ($err == 1); + return HMCCU_SetError ($hash, 'CCU not reachable') if ($err == 1); return HMCCU_SetError ($hash, "Can't read device list from CCU") if ($err == 2); - return HMCCU_SetState ($hash, "OK"); + return HMCCU_SetState ($hash, 'OK'); } elsif ($opt eq 'authentication') { my $username = shift @$a; my $password = shift @$a; $usage = "set $name $opt username password"; - if (!defined ($username)) { + if (!defined($username)) { setKeyValue ($name."_username", undef); setKeyValue ($name."_password", undef); - return "Credentials for CCU authentication deleted"; - } - - return HMCCU_SetError ($hash, $usage) if (!defined ($password)); + return 'Credentials for CCU authentication deleted'; + } + return HMCCU_SetError ($hash, $usage) if (!defined($password)); my $encuser = HMCCU_Encrypt ($username); my $encpass = HMCCU_Encrypt ($password); - return HMCCU_SetError ($hash, "Encryption of credentials failed") if ($encuser eq '' || $encpass eq ''); + return HMCCU_SetError ($hash, 'Encryption of credentials failed') if ($encuser eq '' || $encpass eq ''); my $err = setKeyValue ($name."_username", $encuser); return HMCCU_SetError ($hash, "Can't store credentials. $err") if (defined ($err)); $err = setKeyValue ($name."_password", $encpass); return HMCCU_SetError ($hash, "Can't store credentials. $err") if (defined ($err)); - return "Credentials for CCU authentication stored"; + return 'Credentials for CCU authentication stored'; } elsif ($opt eq 'clear') { my $rnexp = shift @$a; HMCCU_DeleteReadings ($hash, $rnexp); - return HMCCU_SetState ($hash, "OK"); + return HMCCU_SetState ($hash, 'OK'); } elsif ($opt eq 'datapoint') { $usage = "set $name $opt [DevSpec] [Device[,...]].[Channel].Datapoint=Value [...]\n"; my $devSpec = shift @$a; - return HMCCU_SetError ($hash, $usage) if (scalar (keys %$h) < 1); + return HMCCU_SetError ($hash, $usage) if (scalar(keys %$h) < 1); my $cmd = 1; my %dpValues; @@ -1562,13 +1469,14 @@ sub HMCCU_Set ($@) if (defined($devSpec)) { @devSpecList = devspec2array ($devSpec); return HMCCU_SetError ($hash, "No FHEM device matching $devSpec in command set datapoint") - if (scalar (@devSpecList) == 0); + if (scalar(@devSpecList) == 0); } foreach my $dptSpec (keys %$h) { my $adr; my $chn; my $dpt; + my $value = $h->{$dptSpec}; my @t = split (/\./, $dptSpec); my @devList = (); @@ -1585,6 +1493,8 @@ sub HMCCU_Set ($@) foreach my $devName (@devList) { my $dh = $defs{$devName}; my $ccuif = $dh->{ccuif}; + my ($sc, $sd, $cc, $cd, $sdCnt, $cdCnt) = HMCCU_GetSCDatapoints ($dh); + my $stateVals = HMCCU_GetStateValues ($dh, $cd, $cc); if ($dh->{TYPE} eq 'HMCCUCHN') { if (defined ($t2)) { @@ -1612,60 +1522,54 @@ sub HMCCU_Set ($@) return HMCCU_SetError ($hash, "Invalid datapoint $dpt specified for device $devName") if (!HMCCU_IsValidDatapoint ($dh, $dh->{ccutype}, $chn, $dpt, 2)); - my $statevals = AttrVal ($dh->{NAME}, 'statevals', ''); - + $value = HMCCU_Substitute ($value, $stateVals, 1, undef, '') + if ($stateVals ne '' && "$chn" eq "$cc" && $dpt eq $cd); my $no = sprintf ("%03d", $cmd); - HMCCU_Log ($hash, 2, "$no.$ccuif.$devName:$chn.$dpt"); - $dpValues{"$no.$ccuif.$devName:$chn.$dpt"} = HMCCU_Substitute ($h->{$dptSpec}, $statevals, 1, undef, ''); + $dpValues{"$no.$ccuif.$devName:$chn.$dpt"} = $value; $cmd++; } } my $rc = HMCCU_SetMultipleDatapoints ($hash, \%dpValues); return HMCCU_SetError ($hash, $rc) if ($rc < 0); - return HMCCU_SetState ($hash, "OK"); - } + return HMCCU_SetState ($hash, 'OK'); + } elsif ($opt eq 'delete') { my $objname = shift @$a; - my $objtype = shift @$a; - $objtype = "OT_VARDP" if (!defined ($objtype)); + my $objtype = shift @$a // 'OT_VARDP'; $usage = "Usage: set $name $opt ccuobject ['OT_VARDP'|'OT_DEVICE']"; return HMCCU_SetError ($hash, $usage) - if (!defined ($objname) || $objtype !~ /^(OT_VARDP|OT_DEVICE)$/); + if (!defined($objname) || $objtype !~ /^(OT_VARDP|OT_DEVICE)$/); - $result = HMCCU_HMScriptExt ($hash, "!DeleteObject", { name => $objname, type => $objtype }, - undef, undef); + $result = HMCCU_HMScriptExt ($hash, "!DeleteObject", { name => $objname, type => $objtype }); return HMCCU_SetError ($hash, -2) if ($result =~ /^ERROR:.*/); - return HMCCU_SetState ($hash, "OK"); + return HMCCU_SetState ($hash, 'OK'); } elsif ($opt eq 'execute') { my $program = shift @$a; - $program .= ' '.join(' ', @$a) if (scalar (@$a) > 0); + $program .= ' '.join(' ', @$a) if (scalar(@$a) > 0); my $response; $usage = "Usage: set $name $opt program-name"; - return HMCCU_SetError ($hash, $usage) if (!defined ($program)); + return HMCCU_SetError ($hash, $usage) if (!defined($program)); my $cmd = qq(dom.GetObject("$program").ProgramExecute()); my $value = HMCCU_HMCommand ($hash, $cmd, 1); - return HMCCU_SetState ($hash, "OK") if (defined ($value)); - return HMCCU_SetError ($hash, "Program execution error"); + return HMCCU_SetState ($hash, 'OK') if (defined($value)); + return HMCCU_SetError ($hash, 'Program execution error'); } elsif ($opt eq 'prgActivate' || $opt eq 'prgDeactivate') { - my $program = shift @$a; - my $mode = $opt eq 'prgActivate' ? 'true' : 'false'; $usage = "Usage: set $name $opt program-name"; + my $program = shift @$a // return HMCCU_SetError ($hash, $usage); + my $mode = $opt eq 'prgActivate' ? 'true' : 'false'; - return HMCCU_SetError ($hash, $usage) if (!defined ($program)); - - $result = HMCCU_HMScriptExt ($hash, "!ActivateProgram", { name => $program, mode => $mode }, - undef, undef); + $result = HMCCU_HMScriptExt ($hash, '!ActivateProgram', { name => $program, mode => $mode }); return HMCCU_SetError ($hash, -2) if ($result =~ /^ERROR:.*/); - return HMCCU_SetState ($hash, "OK"); + return HMCCU_SetState ($hash, 'OK'); } elsif ($opt eq 'hmscript') { my $script = shift @$a; @@ -1676,26 +1580,23 @@ sub HMCCU_Set ($@) $usage = "Usage: set $name $opt {file|!function|'['code']'} ['dump'] [parname=value [...]]"; # If no parameter is specified list available script functions - if (!defined ($script)) { - $response = "Available HomeMatic script functions:\n". - "-------------------------------------\n"; + if (!defined($script)) { + $response = "Available HomeMatic script functions:\n".('-' x 37)."\n"; foreach my $scr (keys %{$HMCCU_SCRIPTS}) { $response .= "$scr ".$HMCCU_SCRIPTS->{$scr}{syntax}."\n". $HMCCU_SCRIPTS->{$scr}{description}."\n\n"; - } - - $response .= $usage; - return $response; + } + return $response.$usage; } - return HMCCU_SetError ($hash, $usage) if (defined ($dump) && $dump ne 'dump'); + return HMCCU_SetError ($hash, $usage) if (defined($dump) && $dump ne 'dump'); # Execute script - $response = HMCCU_HMScriptExt ($hash, $script, $h, undef, undef); + $response = HMCCU_HMScriptExt ($hash, $script, $h); return HMCCU_SetError ($hash, -2, $response) if ($response =~ /^ERROR:/); - HMCCU_SetState ($hash, "OK"); - return $response if (! $ccureadings || defined ($dump)); + HMCCU_SetState ($hash, 'OK'); + return $response if (! $ccureadings || defined($dump)); foreach my $line (split /[\n\r]+/, $response) { my @tokens = split /=/, $line; @@ -1703,10 +1604,10 @@ sub HMCCU_Set ($@) my $reading; my ($int, $add, $chn, $dpt, $nam, $flags) = HMCCU_ParseObject ($hash, $tokens[0], $HMCCU_FLAG_INTERFACE); - ($add, $chn) = HMCCU_GetAddress ($hash, $nam, '', '') if ($flags == $HMCCU_FLAGS_NCD); + ($add, $chn) = HMCCU_GetAddress ($hash, $nam) if ($flags == $HMCCU_FLAGS_NCD); if ($flags == $HMCCU_FLAGS_IACD || $flags == $HMCCU_FLAGS_NCD) { - $objects{$add}{$chn}{$dpt} = $tokens[1]; + $objects{$add}{$chn}{VALUES}{$dpt} = $tokens[1]; $objcount++; } else { @@ -1722,82 +1623,48 @@ sub HMCCU_Set ($@) return defined ($dump) ? $response : undef; } elsif ($opt eq 'rpcregister') { - return HMCCU_SetError ($hash, "HMCCU: Command not supported by internal RPC server") - if ($ccuflags !~ /procrpc/); - - my $ifName = shift @$a; + my $ifName = shift @$a // 'all'; $result = ''; - @ifList = (defined ($ifName) && $ifName ne 'all') ? ($ifName) : HMCCU_GetRPCInterfaceList ($hash); + @ifList = ($ifName) if ($ifName ne 'all'); foreach my $i (@ifList) { my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, $i); if ($rpcdev eq '') { - Log3 $name, 2, "HMCCU: Can't find HMCCURPCPROC device for interface $i"; + HMCCU_Log ($hash, 2, "Can't find HMCCURPCPROC device for interface $i"); next; } my $res = AnalyzeCommandChain (undef, "set $rpcdev register"); - $result .= $res if (defined ($res)); + $result .= $res if (defined($res)); } - return HMCCU_SetState ($hash, "OK", $result); + return HMCCU_SetState ($hash, 'OK', $result); } - elsif ($opt eq 'rpcserver') { - my $action = shift @$a; - $action = shift @$a if ($action eq $opt); - $usage = "Usage: set $name $opt {'on'|'off'|'restart'}"; + elsif ($opt =~ /^(rpcserver|on|off)$/) { + my $action = $opt eq 'rpcserver' ? shift @$a : $opt; + $usage = "Usage: set $name $opt {'on'|'off'}"; - return HMCCU_SetError ($hash, $usage) - if (!defined ($action) || $action !~ /^(on|off|restart)$/); + return HMCCU_SetError ($hash, "Usage: set $name [rpcserver] {'on'|'off'}") + if (!defined($action) || $action !~ /^(on|off)$/); if ($action eq 'on') { - if ($ccuflags =~ /(extrpc|procrpc)/) { - return HMCCU_SetError ($hash, "Start of RPC server failed") - if (!HMCCU_StartExtRPCServer ($hash)); - } - else { - return HMCCU_SetError ($hash, "Start of RPC server failed") - if (!HMCCU_StartIntRPCServer ($hash)); - } + return HMCCU_SetError ($hash, 'Start of RPC server failed') + if (!HMCCU_StartExtRPCServer ($hash)); } - elsif ($action eq 'off') { - if ($ccuflags =~ /(extrpc|procrpc)/) { - return HMCCU_SetError ($hash, "Stop of RPC server failed") - if (!HMCCU_StopExtRPCServer ($hash)); - } - else { - return HMCCU_SetError ($hash, "Stop of RPC server failed") - if (!HMCCU_StopRPCServer ($hash)); - } - } - elsif ($action eq 'restart') { - return "HMCCU: No RPC server running" if (!HMCCU_IsRPCServerRunning ($hash, undef, undef)); - - if ($ccuflags !~ /(extrpc|procrpc)/) { - return "HMCCU: Can't stop RPC server" if (!HMCCURPC_StopRPCServer ($hash)); - HMCCU_SetRPCState ($hash, 'restarting'); - DoTrigger ($name, "RPC server restarting"); - } - else { - return HMCCU_SetError ($hash, "HMCCU: restart not supported by external RPC server"); - } + else { + return HMCCU_SetError ($hash, 'Stop of RPC server failed') + if (!HMCCU_StopExtRPCServer ($hash)); } - return HMCCU_SetState ($hash, "OK"); + return HMCCU_SetState ($hash, 'OK'); } elsif ($opt eq 'ackmessages') { - my $response = HMCCU_HMScriptExt ($hash, "!ClearUnreachable", undef, undef, undef); + my $response = HMCCU_HMScriptExt ($hash, "!ClearUnreachable"); return HMCCU_SetError ($hash, -2, $response) if ($response =~ /^ERROR:/); return HMCCU_SetState ($hash, "OK", "Unreach errors in CCU cleared"); } - elsif ($opt eq 'defaults') { - my $rc = HMCCU_SetDefaults ($hash); - return HMCCU_SetError ($hash, "HMCCU: No default attributes found") if ($rc == 0); - return HMCCU_SetState ($hash, "OK"); - } elsif ($opt eq 'cleardefaults') { %HMCCU_CUST_CHN_DEFAULTS = (); - %HMCCU_CUST_DEV_DEFAULTS = (); - - return HMCCU_SetState ($hash, "OK", "Default attributes deleted"); + %HMCCU_CUST_DEV_DEFAULTS = (); + return HMCCU_SetState ($hash, 'OK', 'Default attributes deleted'); } elsif ($opt eq 'importdefaults') { my $filename = shift @$a; @@ -1827,305 +1694,208 @@ sub HMCCU_Get ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; - my $opt = shift @$a; + my $opt = shift @$a // return 'No get command specified'; + $opt = lc($opt); - return "No get command specified" if (!defined ($opt)); - - my $options = "defaults:noArg exportdefaults devicelist dump dutycycle:noArg vars update". - " updateccu parfile configdesc firmware rpcevents:noArg rpcstate:noArg deviceinfo". - " ccumsg:alarm,service"; + my $options = "create createDev detectDev defaults:noArg exportDefaults dutycycle:noArg vars update". + " updateCCU paramsetDesc firmware rpcEvents:noArg rpcState:noArg deviceInfo". + " ccuMsg:alarm,service ccuConfig:noArg ccuDevices:noArg". + " internal:groups,interfaces,versions"; + if (defined($hash->{hmccu}{ccuSuppDevList}) && $hash->{hmccu}{ccuSuppDevList} ne '') { + $options =~ s/createDev/createDev:$hash->{hmccu}{ccuSuppDevList}/; + } + if (defined($hash->{hmccu}{ccuDevList}) && $hash->{hmccu}{ccuDevList} ne '') { + $options =~ s/detectDev/detectDev:$hash->{hmccu}{ccuDevList}/; + $options =~ s/deviceInfo/deviceInfo:$hash->{hmccu}{ccuDevList}/; + $options =~ s/paramsetDesc/paramsetDesc:$hash->{hmccu}{ccuDevList}/; + } my $usage = "HMCCU: Unknown argument $opt, choose one of $options"; - my $host = $hash->{host}; return undef if ($hash->{hmccu}{ccu}{delayed} || $hash->{ccustate} ne 'active'); - return "HMCCU: CCU busy, choose one of rpcstate:noArg" + return 'HMCCU: CCU busy, choose one of rpcstate:noArg' if ($opt ne 'rpcstate' && HMCCU_IsRPCStateBlocking ($hash)); my $ccuflags = HMCCU_GetFlags ($name); my $ccureadings = AttrVal ($name, "ccureadings", $ccuflags =~ /noReadings/ ? 0 : 1); - my $parfile = AttrVal ($name, "parfile", ''); + + my $interfaces = HMCCU_GetRPCInterfaceList ($hash, 0); + my @ifList = keys %$interfaces; my $readname; my $readaddr; my $result = ''; my $rc; - - if ($opt eq 'dump') { - my $content = shift @$a; - my $filter = shift @$a; - $filter = '.*' if (!defined ($filter)); - $usage = "Usage: get $name dump {'datapoints'|'devtypes'} [filter]"; - - my %foper = (1, "R", 2, "W", 4, "E", 3, "RW", 5, "RE", 6, "WE", 7, "RWE"); - my %ftype = (2, "B", 4, "F", 16, "I", 20, "S"); - - return HMCCU_SetError ($hash, $usage) if (!defined ($content)); - - if ($content eq 'devtypes') { - foreach my $devtype (sort keys %{$hash->{hmccu}{dp}}) { - $result .= $devtype."\n" if ($devtype =~ /$filter/); - } - } - elsif ($content eq 'datapoints') { - foreach my $devtype (sort keys %{$hash->{hmccu}{dp}}) { - next if ($devtype !~ /$filter/); - foreach my $chn (sort keys %{$hash->{hmccu}{dp}{$devtype}{ch}}) { - foreach my $dpt (sort keys %{$hash->{hmccu}{dp}{$devtype}{ch}{$chn}}) { - my $t = $hash->{hmccu}{dp}{$devtype}{ch}{$chn}{$dpt}{type}; - my $o = $hash->{hmccu}{dp}{$devtype}{ch}{$chn}{$dpt}{oper}; - $result .= $devtype.".".$chn.".".$dpt." [". - (exists($ftype{$t}) ? $ftype{$t} : $t)."] [". - (exists($foper{$o}) ? $foper{$o} : $o)."]\n"; - } - } - } - } - else { - return HMCCU_SetError ($hash, $usage); - } - - return HMCCU_SetState ($hash, "OK", ($result eq '') ? "No data found" : $result); - } - elsif ($opt eq 'vars') { - my $varname = shift @$a; - $usage = "Usage: get $name vars {regexp}[,...]"; - return HMCCU_SetError ($hash, $usage) if (!defined ($varname)); - + + if ($opt eq 'vars') { + my $varname = shift @$a // return HMCCU_SetError ($hash, "Usage: get $name vars {regexp}"); ($rc, $result) = HMCCU_GetVariables ($hash, $varname); return HMCCU_SetError ($hash, $rc, $result) if ($rc < 0); - - return HMCCU_SetState ($hash, "OK", $ccureadings ? undef : $result); + return HMCCU_SetState ($hash, 'OK', $result); } elsif ($opt eq 'update' || $opt eq 'updateccu') { - my $devexp = shift @$a; - $devexp = '.*' if (!defined ($devexp)); - $usage = "Usage: get $name $opt [device-expr [{'State'|'Value'}]]"; - my $ccuget = shift @$a; - $ccuget = 'Attr' if (!defined ($ccuget)); - return HMCCU_SetError ($hash, $usage) if ($ccuget !~ /^(Attr|State|Value)$/); - my $nonBlocking = HMCCU_IsFlag ($name, 'nonBlocking') ? 1 : 0; - - HMCCU_UpdateClients ($hash, $devexp, $ccuget, ($opt eq 'updateccu') ? 1 : 0, undef, 0); - return HMCCU_SetState ($hash, "OK"); + my $devexp = shift @$a // '.*'; + my $ccuget = shift @$a // 'Attr'; + return HMCCU_SetError ($hash, "Usage: get $name $opt [device-expr [{'State'|'Value'}]]") + if ($ccuget !~ /^(Attr|State|Value)$/); + HMCCU_UpdateClients ($hash, $devexp, $ccuget, ($opt eq 'updateccu') ? 1 : 0); + return HMCCU_SetState ($hash, 'OK'); } - elsif ($opt eq 'parfile') { - my $par_parfile = shift @$a; - my @parameters; - my $parcount; - - if (defined ($par_parfile)) { - $parfile = $par_parfile; + elsif ($opt eq 'ccudevices') { + my $devTable = ''. + ''; + foreach my $di (sort keys %{$hash->{hmccu}{device}}) { + foreach my $da (sort keys %{$hash->{hmccu}{device}{$di}}) { + next if ($hash->{hmccu}{device}{$di}{$da}{_addtype} ne 'dev'); + my $chn = exists($hash->{hmccu}{dev}{$da}) ? $hash->{hmccu}{dev}{$da}{channels} : '?'; + my @roles = HMCCU_GetDeviceRoles ($hash, $di, $da, 1); + my %suppRoles; + $suppRoles{$_}++ for @roles; + $devTable .= "". + "". + "". + "". + "". + "\n"; + } } - else { - return HMCCU_SetError ($hash, "No parameter file specified") if ($parfile eq ''); - } - - # Read parameter file - if (open (PARFILE, "<$parfile")) { - @parameters = ; - $parcount = scalar @parameters; - close (PARFILE); - } - else { - return HMCCU_SetError ($hash, -16); - } - - return HMCCU_SetError ($hash, "Empty parameter file") if ($parcount < 1); - - ($rc, $result) = HMCCU_GetChannel ($hash, \@parameters); - return HMCCU_SetError ($hash, $rc) if ($rc < 0); - - return HMCCU_SetState ($hash, "OK", $ccureadings ? undef : $result); + $devTable .= '
NameModelInterfaceAddressChannelsSupported roles
$hash->{hmccu}{device}{$di}{$da}{_name}$hash->{hmccu}{device}{$di}{$da}{_model}$di$da$chn".join('
', map { "$_ [$suppRoles{$_}x]" } sort keys %suppRoles)."
'; + return $devTable; } elsif ($opt eq 'deviceinfo') { - my $device = shift @$a; - $usage = "Usage: get $name $opt device [{'State'|'Value'}]"; - - return HMCCU_SetError ($hash, $usage) if (!defined ($device)); - - my $ccuget = shift @$a; - $ccuget = 'Attr' if (!defined ($ccuget)); - return HMCCU_SetError ($hash, $usage) if ($ccuget !~ /^(Attr|State|Value)$/); - - return HMCCU_SetError ($hash, -1) if (!HMCCU_IsValidDeviceOrChannel ($hash, $device, $HMCCU_FL_ALL)); - $result = HMCCU_GetDeviceInfo ($hash, $device, $ccuget); - return HMCCU_SetError ($hash, -2) if ($result eq '' || $result =~ /^ERROR:.*/); - HMCCU_SetState ($hash, "OK"); - return HMCCU_FormatDeviceInfo ($result); + my $device = shift @$a // return HMCCU_SetError ($hash, "Usage: get $name $opt {device} [extended]"); + my $extended = shift @$a; + my ($int, $add, $chn, $dpt, $nam, $flags) = HMCCU_ParseObject ($hash, $device, + $HMCCU_FLAG_FULLADDR); + return HMCCU_SetError ($hash, -1, $device) if (!($flags & $HMCCU_FLAG_ADDRESS)); + return HMCCU_ExecuteGetDeviceInfoCommand ($hash, $hash, $add, defined($extended) ? 1 : 0); } elsif ($opt eq 'rpcevents') { - if ($ccuflags =~ /(extrpc|procrpc)/) { -# my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, undef); -# return HMCCU_SetError ($hash, "HMCCU: External RPC server not found") if ($rpcdev eq ''); -# $result = AnalyzeCommandChain (undef, "get $rpcdev rpcevents"); -# return HMCCU_SetState ($hash, "OK", $result) if (defined ($result)); -# return HMCCU_SetError ($hash, "No event statistics available"); -# } -# elsif ($ccuflags =~ /procrpc/) { - $result = ''; - my @iflist = HMCCU_GetRPCInterfaceList ($hash); - foreach my $ifname (@iflist) { - my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, $ifname); - if ($rpcdev eq '') { - Log3 $name, 2, "HMCCU: Can't find HMCCURPCPROC device for interface $ifname"; - next; - } - my $res = AnalyzeCommandChain (undef, "get $rpcdev rpcevents"); - $result .= $res if (defined ($result)); + $result = ''; + foreach my $ifname (@ifList) { + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, $ifname); + if ($rpcdev eq '') { + HMCCU_Log ($hash, 2, "Can't find HMCCURPCPROC device for interface $ifname"); + next; } - return HMCCU_SetState ($hash, "OK", $result) if ($result ne ''); - return HMCCU_SetError ($hash, "No event statistics available"); - } - else { - return HMCCU_SetError ($hash, "No event statistics available") - if (!exists ($hash->{hmccu}{evs}) || !exists ($hash->{hmccu}{evr})); - foreach my $stkey (sort keys %{$hash->{hmccu}{evr}}) { - $result .= "S: ".$stkey." = ".$hash->{hmccu}{evs}{$stkey}."\n"; - $result .= "R: ".$stkey." = ".$hash->{hmccu}{evr}{$stkey}."\n"; - } - return HMCCU_SetState ($hash, "OK", $result); + my $res = AnalyzeCommandChain (undef, "get $rpcdev rpcevents"); + $result .= $res if (defined($res)); } + return HMCCU_SetState ($hash, 'OK', $result) if ($result ne ''); + return HMCCU_SetError ($hash, 'No event statistics available'); } elsif ($opt eq 'rpcstate') { my @hm_pids = (); - my @hm_tids = (); - $result = "No RPC processes or threads are running"; - - if (HMCCU_IsRPCServerRunning ($hash, \@hm_pids, \@hm_tids)) { - $result = "RPC process(es) running with pid(s) ". - join (',', @hm_pids) if (scalar (@hm_pids) > 0); - $result = "RPC thread(s) running with tid(s) ". - join (',', @hm_tids) if (scalar (@hm_tids) > 0); + if (HMCCU_IsRPCServerRunning ($hash, \@hm_pids)) { + $result = 'RPC process(es) running with pid(s) '. + join (',', @hm_pids) if (scalar(@hm_pids) > 0); } - - return HMCCU_SetState ($hash, "OK", $result); + else { + $result = 'No RPC processes or threads are running'; + } + return HMCCU_SetState ($hash, 'OK', $result); } - elsif ($opt eq 'devicelist') { + elsif ($opt eq 'ccuconfig') { my ($devcount, $chncount, $ifcount, $prgcount, $gcount) = HMCCU_GetDeviceList ($hash); return HMCCU_SetError ($hash, -2) if ($devcount < 0); - return HMCCU_SetError ($hash, "No devices received from CCU") if ($devcount == 0); - $result = "Read $devcount devices with $chncount channels from CCU"; + return HMCCU_SetError ($hash, 'No devices received from CCU') if ($devcount == 0); + HMCCU_ResetDeviceTables ($hash); + my ($cDev, $cPar, $cLnk) = HMCCU_GetDeviceConfig ($hash); + return "Devices: $devcount, Channels: $chncount\nDevice descriptions: $cDev\n". + "Paramset descriptions: $cPar\nLinks/Peerings: $cLnk\n". + "Interfaces: $ifcount\nPrograms: $prgcount\nVirtual groups: $gcount"; + } + elsif ($opt eq 'detectdev') { + $usage = "Usage: get $name detectDev device-name"; + my $devSpec = shift @$a // return HMCCU_SetError ($hash, $usage); - my $optcmd = shift @$a; - if (defined ($optcmd)) { - if ($optcmd eq 'dump') { - $result .= "\n-----------------------------------------\n"; - my $n = 0; - foreach my $add (sort keys %{$hash->{hmccu}{dev}}) { - if ($hash->{hmccu}{dev}{$add}{addtype} eq 'dev') { - $result .= "Device ".'"'.$hash->{hmccu}{dev}{$add}{name}.'"'." [".$add."] ". - "Type=".$hash->{hmccu}{dev}{$add}{type}."\n"; - $n = 0; + foreach my $iface (keys %{$hash->{hmccu}{device}}) { + foreach my $address (keys %{$hash->{hmccu}{device}{$iface}}) { + if ($hash->{hmccu}{device}{$iface}{$address}{_name} eq $devSpec) { + my $detect = HMCCU_DetectDevice ($hash, $address, $iface); + if (defined($detect)) { + return HMCCU_RefToString ($detect); } else { - $result .= " Channel $n ".'"'.$hash->{hmccu}{dev}{$add}{name}.'"'. - " [".$add."]\n"; - $n++; + return "Automatic detection of $devSpec not possible"; } } - return $result; - } - elsif ($optcmd eq 'create') { - $usage = "Usage: get $name create {devexp|chnexp} [t={'chn'|'dev'|'all'}] [s=suffix] ". - "[p=prefix] [f=format] ['defattr'] ['duplicates'] [save] [attr=val [...]]"; - my $devdefaults = 0; - my $duplicates = 0; - my $savedef = 0; - my $newcount = 0; - - # Process command line parameters - my $devspec = shift @$a; - my $devprefix = exists ($h->{p}) ? $h->{p} : ''; - my $devsuffix = exists ($h->{'s'}) ? $h->{'s'} : ''; - my $devtype = exists ($h->{t}) ? $h->{t} : 'dev'; - my $devformat = exists ($h->{f}) ? $h->{f} : '%n'; - return HMCCU_SetError ($hash, $usage) - if ($devtype !~ /^(dev|chn|all)$/ || !defined ($devspec)); - foreach my $defopt (@$a) { - if ($defopt eq 'defattr') { $devdefaults = 1; } - elsif ($defopt eq 'duplicates') { $duplicates = 1; } - elsif ($defopt eq 'save') { $savedef = 1; } - else { return HMCCU_SetError ($hash, $usage); } - } - - # Get list of existing client devices - my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)", undef, undef); - - foreach my $add (sort keys %{$hash->{hmccu}{dev}}) { - my $defmod = $hash->{hmccu}{dev}{$add}{addtype} eq 'dev' ? 'HMCCUDEV' : 'HMCCUCHN'; - my $ccuname = $hash->{hmccu}{dev}{$add}{name}; - my $ccudevname = HMCCU_GetDeviceName ($hash, $add, $ccuname); - next if ($devtype ne 'all' && $devtype ne $hash->{hmccu}{dev}{$add}{addtype}); - next if (HMCCU_ExprNotMatch ($ccuname, $devspec, 1)); - - # Build FHEM device name - my $devname = $devformat; - $devname = $devprefix.$devname.$devsuffix; - $devname =~ s/%n/$ccuname/g; - $devname =~ s/%d/$ccudevname/g; - $devname =~ s/%a/$add/g; - $devname =~ s/[^A-Za-z\d_\.]+/_/g; - - # Check for duplicate device definitions - if (!$duplicates) { - next if (exists ($defs{$devname})); - my $devexists = 0; - foreach my $exdev (@devlist) { - if ($defs{$exdev}->{ccuaddr} eq $add) { - $devexists = 1; - last; - } - } - next if ($devexists); - } - - # Define new client device - my $ret = CommandDefine (undef, $devname." $defmod ".$add); - if ($ret) { - Log3 $name, 2, "HMCCU: [$name] Define command failed $devname $defmod $ccuname"; - Log3 $name, 2, "$defmod: $ret"; - $result .= "\nCan't create device $devname. $ret"; - next; - } - - # Set device attributes - HMCCU_SetDefaults ($defs{$devname}) if ($devdefaults); - foreach my $da (keys %$h) { - next if ($da =~ /^[pstf]$/); - $ret = CommandAttr (undef, "$devname $da ".$h->{$da}); - if ($ret) { - Log3 $name, 2, "HMCCU: Attr command failed $devname $da ".$h->{$da}; - Log3 $name, 2, "$defmod: $ret"; - } - } - Log3 $name, 2, "$defmod: Created device $devname"; - $result .= "\nCreated device $devname"; - $newcount++; - } - - CommandSave (undef, undef) if ($newcount > 0 && $savedef); - $result .= "\nCreated $newcount client devices"; } } + + return "Device $devSpec not found"; + } + elsif ($opt eq 'create' || $opt eq 'createdev') { + $usage = $opt eq 'createdev' ? + "Usage: get $name createDev device-name" : + "Usage: get $name create device-expr [s=suffix] [p=prefix] [f=format] ". + "['noDefaults'] ['save'] ['forceDev'] [attr=val [...]]"; - return HMCCU_SetState ($hash, "OK", $result); + # Process command line parameters + my $devSpec = shift @$a // return HMCCU_SetError ($hash, $usage); + my $devPrefix = $h->{'p'} // ''; # Prefix of FHEM device name + my $devSuffix = $h->{'s'} // ''; # Suffix of FHEM device name + my $devFormat = $h->{'f'} // '%n'; # Format string for FHEM device name + my @options = (); + my $saveDef = 0; + foreach my $defOpt (@$a) { + if (lc($defOpt) eq 'nodefaults') { push @options, 'noDefaults'; } + elsif (lc($defOpt) eq 'save') { $saveDef = 1; } + elsif (lc($defOpt) eq 'forcedev') { push @options, 'forceDev'; } + else { return HMCCU_SetError ($hash, $usage); } + } + $devSpec = '^'.$devSpec.'$' if ($opt eq 'createdev'); + my $defOpts = join(' ', @options); + + # Setup attributes for new devices + my %ah = (); + foreach my $da (keys %$h) { $ah{$da} = $h->{$da} if ($da !~ /^[psf]$/); } + + my $cs = HMCCU_CreateFHEMDevices ($hash, $devSpec, $devPrefix, $devSuffix, $devFormat, $defOpts, \%ah); + + # Statistics + # + # {statsValue}{devName} = addr.ccuName + # + # {notDetected}{$ccuDevName}: CCU device with $devName not detected + # {notSupported}{$ccuDevName}: CCU device type is not supported by create commands + # {fhemExists}{$fhemDevName}: FHEM device with $devName already exists + # {devDefined}{$fhemDevName}: FHEM device for $devAdd already exists + # {defFailed}{$fhemDevName}: Device definition failed + # {defSuccess}{$fhemDevName}: Device defined + # {attrFailed}{$fhemDevName.$attr}: Attribute setting failed + + # Save FHEM config + CommandSave (undef, undef) if (scalar(keys %{$cs->{defSuccess}}) > 0 && $saveDef); + + # Prepare summary + my %csText = ( + notDetected => 'Not detected CCU devices: ', + notSupported => 'Not supported by create command: ', + fhemExists => 'FHEM device already exists: ', + devDefined => 'HMCCUCHN devices already defined for: ', + defFailed => 'Failed to define devices: ', + defSuccess => 'New devices successfuly defined: ', + attrFailed => 'Failed to assign attributes: ' + ); + $result = "Results of create command:"; + foreach my $sk (keys %csText) { + $result .= "\n$csText{$sk}\n".join('', map { " $_ = $cs->{$sk}{$_}\n" } keys %{$cs->{$sk}}) if (scalar(keys %{$cs->{$sk}}) > 0); + } + + return HMCCU_SetState ($hash, 'OK', $result); } elsif ($opt eq 'dutycycle') { - my $dc = HMCCU_GetDutyCycle ($hash); - return HMCCU_SetState ($hash, "OK"); + HMCCU_GetDutyCycle ($hash); + return HMCCU_SetState ($hash, 'OK'); } elsif ($opt eq 'firmware') { - my $devtype = shift @$a; - $devtype = '.*' if (!defined ($devtype)); - my $dtexp = $devtype; - $dtexp = '.*' if ($devtype eq 'full'); + my $devtype = shift @$a // '.*'; + my $dtexp = $devtype eq 'full' ? '.*' : $devtype; my $dc = HMCCU_GetFirmwareVersions ($hash, $dtexp); - return "Found no firmware downloads" if ($dc == 0); + return 'Found no firmware downloads' if ($dc == 0); $result = "Found $dc firmware downloads. Click on the new version number for download\n\n"; if ($devtype eq 'full') { - $result .= - "Type Available Date\n". - "-----------------------------------------\n"; + $result .= "Type Available Date\n".('-' x 41)."\n"; foreach my $ct (keys %{$hash->{hmccu}{type}}) { $result .= sprintf "%-20s %-9s %-10s\n", $ct, $hash->{hmccu}{type}{$ct}{download}, @@ -2133,15 +1903,14 @@ sub HMCCU_Get ($@) } } else { - my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)", undef, undef); + my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)"); return $result if (scalar (@devlist) == 0); $result .= - "Device Type Current Available Date\n". - "---------------------------------------------------------------------------\n"; + "Device Type Current Available Date\n".('-' x 76)."\n"; foreach my $dev (@devlist) { my $ch = $defs{$dev}; my $ct = uc($ch->{ccutype}); - my $fw = defined ($ch->{firmware}) ? $ch->{firmware} : 'N/A'; + my $fw = $ch->{firmware} // 'N/A'; next if (!exists ($hash->{hmccu}{type}{$ct}) || $ct !~ /$dtexp/); $result .= sprintf "%-25s %-20s %-7s %-9s %-10s\n", $ch->{NAME}, $ct, $fw, $hash->{hmccu}{type}{$ct}{download}, @@ -2149,34 +1918,26 @@ sub HMCCU_Get ($@) } } - return HMCCU_SetState ($hash, "OK", $result); + return HMCCU_SetState ($hash, 'OK', $result); } elsif ($opt eq 'defaults') { $result = HMCCU_GetDefaults ($hash, 1); - return HMCCU_SetState ($hash, "OK", $result); + return HMCCU_SetState ($hash, 'OK', $result); } elsif ($opt eq 'exportdefaults') { - my $filename = shift @$a; - $usage = "Usage: get $name $opt filename ['all'] ['csv']"; - my $csv = 0; + $usage = "Usage: get $name $opt filename ['all']"; + my $filename = shift @$a // return HMCCU_SetError ($hash, $usage); my $all = 0; + my $defopt = shift @$a; + $all = 1 if (defined($defopt) && $defopt eq 'all'); - foreach my $defopt (@$a) { - if ($defopt eq 'csv') { $csv = 1; } - elsif ($defopt eq 'all') { $all = 1; } - else { return HMCCU_SetError ($hash, $usage); } - } - - return HMCCU_SetError ($hash, $usage) if (!defined ($filename)); - - my $rc = $csv ? HMCCU_ExportDefaultsCSV ($filename, $all) : HMCCU_ExportDefaults ($filename, $all); + my $rc = HMCCU_ExportDefaults ($filename, $all); return HMCCU_SetError ($hash, -16) if ($rc == 0); - return HMCCU_SetState ($hash, "OK", "Default attributes written to $filename"); + return HMCCU_SetState ($hash, 'OK', "Default attributes written to $filename"); } elsif ($opt eq 'aggregation') { - my $rule = shift @$a; - $usage = "Usage: get $name $opt {'all'|'rule'}"; - return HMCCU_SetError ($hash, $usage) if (!defined ($rule)); + $usage = "Usage: get $name $opt {'all'|rule}"; + my $rule = shift @$a // return HMCCU_SetError ($hash, $usage); if ($rule eq 'all') { foreach my $r (keys %{$hash->{hmccu}{agg}}) { @@ -2185,45 +1946,65 @@ sub HMCCU_Get ($@) } } else { - return HMCCU_SetError ($hash, "HMCCU: Aggregation rule does not exist") - if (!exists ($hash->{hmccu}{agg}{$rule})); + return HMCCU_SetError ($hash, "HMCCU: Aggregation rule $rule does not exist") + if (!exists($hash->{hmccu}{agg}{$rule})); $result = HMCCU_AggregateReadings ($hash, $rule); $result = "$rule = $result"; } - return HMCCU_SetState ($hash, "OK", $ccureadings ? undef : $result); + return HMCCU_SetState ($hash, 'OK', $ccureadings ? undef : $result); } - elsif ($opt eq 'configdesc') { - my $ccuobj = shift @$a; + elsif ($opt eq 'paramsetdesc') { $usage = "Usage: get $name $opt {device|channel}"; - return HMCCU_SetError ($hash, $usage) if (!defined ($ccuobj)); - - my ($rc, $res) = HMCCU_RPCGetConfig ($hash, $ccuobj, "getParamsetDescription", undef); - return HMCCU_SetError ($hash, $rc) if ($rc < 0); - return HMCCU_SetState ($hash, "OK", $res); + my $ccuobj = shift @$a // return HMCCU_SetError ($hash, $usage); + my ($int, $add, $chn, $dpt, $nam, $flags) = HMCCU_ParseObject ($hash, $ccuobj, + $HMCCU_FLAG_FULLADDR); + return HMCCU_SetError ($hash, 'Invalid device or address') + if (!($flags & $HMCCU_FLAG_ADDRESS)); + $result = HMCCU_ParamsetDescToStr ($hash, $add); + return defined($result) ? $result : HMCCU_SetError ($hash, "Can't get device description"); } elsif ($opt eq 'ccumsg') { - my $msgtype = shift @$a; - $usage = "Usage: get $name $opt {service|alarm}"; - return HMCCU_SetError ($hash, $usage) if (!defined ($msgtype)); - + my $msgtype = shift @$a // return HMCCU_SetError ($hash, "Usage: get $name $opt {service|alarm}"); my $script = ($msgtype eq 'service') ? "!GetServiceMessages" : "!GetAlarms"; - my $res = HMCCU_HMScriptExt ($hash, $script, undef, undef, undef); - + + my $res = HMCCU_HMScriptExt ($hash, $script); return HMCCU_SetError ($hash, "Error") if ($res eq '' || $res =~ /^ERROR:.*/); # Generate event for each message foreach my $msg (split /[\n\r]+/, $res) { - next if ($msg =~ /^[0-9]+$/); - DoTrigger ($name, $msg); + DoTrigger ($name, $msg) if ($msg !~ /^[0-9]+$/); } - return HMCCU_SetState ($hash, "OK", $res); + return HMCCU_SetState ($hash, 'OK', $res); + } + elsif ($opt eq 'internal') { + my $parameter = shift @$a // return HMCCU_SetError ($hash, "Usage: get $name $opt {internalParameter}"); + if ($parameter eq 'groups') { + return exists($hash->{hmccu}{grp}) && scalar(keys %{$hash->{hmccu}{grp}}) > 0 ? + ''.HMCCU_FormatHashTable ($hash->{hmccu}{grp}).'' : + 'No virtual groups found'; + } + elsif ($parameter eq 'interfaces') { + return exists($hash->{hmccu}{interfaces}) && scalar(keys %{$hash->{hmccu}{interfaces}}) > 0 ? + ''.HMCCU_FormatHashTable ($hash->{hmccu}{interfaces}).'' : + 'No interfaces found'; + } + elsif ($parameter eq 'versions') { + my $v = ''; + foreach my $m ('HMCCU', 'HMCCURPCPROC', 'HMCCUDEV', 'HMCCUCHN') { + $v .= "$m: ".($modules{$m}{version} // '???').'
' if (exists($modules{$m})); + } + return $v ne '' ? "$v" : 'No module versions found'; + } + else { + return HMCCU_SetError ($hash, "Invalid internal parameter $parameter"); + } } else { if (exists ($hash->{hmccu}{agg})) { my @rules = keys %{$hash->{hmccu}{agg}}; - $usage .= " aggregation:all,".join (',', @rules) if (scalar (@rules) > 0); + $usage .= " aggregation:all,".join (',', @rules) if (scalar(@rules) > 0); } return $usage; } @@ -2265,8 +2046,6 @@ sub HMCCU_ParseObject ($$$) my ($i, $a, $c, $d, $n, $f) = ('', '', '', '', '', '', 0); my $extaddr; - my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($hash); - # "ccu:" is default. Remove it. $object =~ s/^ccu://g; @@ -2368,21 +2147,21 @@ sub HMCCU_ParseObject ($$$) # Check if name is a valid channel name if ($f & $HMCCU_FLAG_NAME) { - my ($add, $chn) = HMCCU_GetAddress ($hash, $n, '', ''); + my ($add, $chn) = HMCCU_GetAddress ($hash, $n); if ($chn ne '') { $f = $f | $HMCCU_FLAG_CHANNEL; } if ($flags & $HMCCU_FLAG_FULLADDR) { - ($i, $a, $c) = (HMCCU_GetDeviceInterface ($hash, $add, $defInterface), $add, $chn); - $f |= $HMCCU_FLAG_INTERFACE; + ($i, $a, $c) = (HMCCU_GetDeviceInterface ($hash, $add), $add, $chn); + $f |= $HMCCU_FLAG_INTERFACE if ($i ne ''); $f |= $HMCCU_FLAG_ADDRESS if ($add ne ''); $f |= $HMCCU_FLAG_CHANNEL if ($chn ne ''); } } elsif ($f & $HMCCU_FLAG_ADDRESS && $i eq '' && ($flags & $HMCCU_FLAG_FULLADDR || $flags & $HMCCU_FLAG_INTERFACE)) { - $i = HMCCU_GetDeviceInterface ($hash, $a, $defInterface); - $f |= $HMCCU_FLAG_INTERFACE; + $i = HMCCU_GetDeviceInterface ($hash, $a); + $f |= $HMCCU_FLAG_INTERFACE if ($i ne ''); } return ($i, $a, $c, $d, $n, $f); @@ -2394,44 +2173,56 @@ sub HMCCU_ParseObject ($$$) # Parameter channel can be a channel name or a channel address without # interface specification. # Filter rule syntax is either: -# [N:]{Channel-Number|Channel-Name-Expr}!Datapoint-Expr +# [N:]{Channel-No[,Channel-No]|Channel-Name-Expr}!Datapoint-Expr # or -# [N:][Channel-Number.]Datapoint-Expr +# [N:][Channel-No[,Channel-No].]Datapoint-Expr # Multiple filter rules must be separated by ; ###################################################################### -sub HMCCU_FilterReading ($$$) +sub HMCCU_FilterReading ($$$;$) { - my ($hash, $chn, $dpt) = @_; + my ($hash, $chn, $dpt, $ps) = @_; my $name = $hash->{NAME}; - my $fnc = "FilterReading"; - - my $hmccu_hash = HMCCU_GetHash ($hash); - return 1 if (!defined ($hmccu_hash)); - - my $grf = AttrVal ($hmccu_hash->{NAME}, 'ccudef-readingfilter', '.*'); - my $rf = AttrVal ($name, 'ccureadingfilter', $grf); - $rf = $grf.";".$rf if ($rf ne $grf && $grf ne '.*'); - - my $chnnam = ''; - my $chnnum = ''; - my $devadd = ''; - - # Get channel name and channel number - if (HMCCU_IsValidChannel ($hmccu_hash, $chn, $HMCCU_FL_ADDRESS)) { - $chnnam = HMCCU_GetChannelName ($hmccu_hash, $chn, ''); - ($devadd, $chnnum) = HMCCU_SplitChnAddr ($chn); + my $ioHash = HMCCU_GetHash ($hash) // return 1; + + if (defined($ps)) { + $ps = 'LINK' if ($ps =~ /^LINK\..+$/); } else { - ($devadd, $chnnum) = HMCCU_GetAddress ($hash, $chn, '', ''); - $chnnam = $chn; + $ps = 'VALUES'; } - - HMCCU_Trace ($hash, 2, $fnc, "chn=$chn, chnnam=$chnnam chnnum=$chnnum dpt=$dpt, rules=$rf"); - + + my $flags = HMCCU_GetFlags ($name); + my @flagList = $flags =~ /show(Master|Link|Device|Service)Readings/g; + push (@flagList, 'VALUES'); + my $dispFlags = uc(join(',', @flagList)); + my ($sc, $sd) = HMCCU_StateDatapoint ($hash); + my ($cc, $cd) = HMCCU_ControlDatapoint ($hash); + my $rfAtt = AttrVal ($name, 'ccureadingfilter', ''); + my $rf = $rfAtt eq '' ? '.*' : $rfAtt; + + my $chnnam = ''; + my ($devadd, $chnnum) = HMCCU_SplitChnAddr ($chn); + if ($chnnum ne 'd') { + # Get channel name and channel number + $chnnam = HMCCU_GetChannelName ($ioHash, $chn); + if ($chnnam eq '') { + ($devadd, $chnnum) = HMCCU_GetAddress ($hash, $chn); + $chnnam = $chn; + } + } + + HMCCU_Trace ($hash, 2, "chn=$chn, cName=$chnnam cNum=$chnnum dpt=$dpt, rules=$rf dispFlags=$dispFlags ps=$ps"); + return 0 if (($dispFlags !~ /DEVICE/ && ($chnnum eq 'd' || $chnnum eq '0')) || $dispFlags !~ /$ps/); + + # By default show only VALUE readings of control and state channels + # HMCCU_Trace ($hash, 2, "rfAtt=$rfAtt, sc=$sc, cc=$cc, ps=$ps"); + # return 0 if ($rfAtt eq '' && $sc ne '' && "$sc" ne "$chnnum" && $cc ne '' && "$cc" ne "$chnnum" && $ps eq 'VALUES'); + foreach my $r (split (';', $rf)) { my $rm = 1; my $cn = ''; + my $cnl = ''; # Negative filter if ($r =~ /^N:/) { @@ -2441,9 +2232,10 @@ sub HMCCU_FilterReading ($$$) # Get filter criteria my ($c, $f) = split ("!", $r); - if (defined ($f)) { + if (defined($f)) { next if ($c eq '' || $chnnam eq '' || $chnnum eq ''); $cn = $c if ($c =~ /^([0-9]{1,2})$/); + $cnl = $c if ($c =~ /^[0-9]{1,2}(,[0-9]{1,2})+$/); } else { $c = ''; @@ -2451,24 +2243,31 @@ sub HMCCU_FilterReading ($$$) $cn = $1; $f = $2; } + elsif ($r =~ /^[0-9]{1,2}(,[0-9]{1,2})+\.(.+)$/) { + ($cnl, $f) = split /\./, $r, 2; + } else { $cn = ''; $f = $r; } } - HMCCU_Trace ($hash, 2, undef, " check rm=$rm f=$f cn=$cn c=$c"); + $cnl = ",$cnl," if ($cnl ne ''); + + HMCCU_Trace ($hash, 2, " check rm=$rm f=$f cn=$cn cnl=$cnl c=$c"); # Positive filter return 1 if ( $rm && ( ( ($cn ne '' && "$chnnum" eq "$cn") || + ($cnl ne '' && $cnl =~ /,$chnnum,/) || ($c ne '' && $chnnam =~ /$c/) || - ($cn eq '' && $c eq '') + ($cn eq '' && $c eq '' && $cnl eq '') || + ($chnnum eq 'd') ) && $dpt =~ /$f/ ) ); - # Negate filter + # Negative filter return 1 if ( !$rm && ( ($cn ne '' && "$chnnum" ne "$cn") || @@ -2476,7 +2275,7 @@ sub HMCCU_FilterReading ($$$) $dpt !~ /$f/ ) ); - HMCCU_Trace ($hash, 2, undef, " check result false"); + HMCCU_Trace ($hash, 2, " check result false"); } return 0; @@ -2487,8 +2286,10 @@ sub HMCCU_FilterReading ($$$) # # Parameters: # -# Interface,Address,ChannelNo,Datapoint,ChannelNam,ReadingFormat -# ReadingFormat := { name[lc] | datapoint[lc] | address[lc] } +# Interface,Address,ChannelNo,Datapoint,ChannelNam,Format,Paramset +# Format := { name[lc] | datapoint[lc] | address[lc] | formatStr } +# formatStr := Any text containing at least one format pattern +# pattern := { %a, %c, %n, %d, %A, %C, %N, %D } # # Valid combinations: # @@ -2498,89 +2299,131 @@ sub HMCCU_FilterReading ($$$) # # Reading names can be modified or new readings can be added by # setting attribut ccureadingname. -# Returns list of readings names. +# Returns list of readings names. Return empty list on error. ###################################################################### -sub HMCCU_GetReadingName ($$$$$$$) +sub HMCCU_GetReadingName ($$$$$$$;$) { - my ($hash, $i, $a, $c, $d, $n, $rf) = @_; + my ($hash, $i, $a, $c, $d, $n, $rf, $ps) = @_; + $c //= ''; + $i //= ''; + $ps //= 'VALUES'; my $name = $hash->{NAME}; - - my $hmccu_hash = HMCCU_GetHash ($hash); - return '' if (!defined ($hmccu_hash)); + my $type = $hash->{TYPE}; + my %prefix = ( 'MASTER' => 'R-', 'LINK' => 'L-', 'VALUES' => '', 'SERVICE' => 'S-', + 'PEER' => 'P-', 'DEVICE' => 'R-' ); + + my $ioHash = HMCCU_GetHash ($hash); + return () if (!defined($ioHash) || !defined($d) || $d eq ''); + + my @rcv = (); + if ($ps =~ /^LINK\.(.+)$/) { + @rcv = HMCCU_GetDeviceIdentifier ($ioHash, $1); + $ps = 'LINK'; + } + my $rn = ''; my @rnlist; - Log3 $name, 1, "HMCCU: ChannelNo undefined: Addr=".$a if (!defined ($c)); + $rf //= HMCCU_GetAttrReadingFormat ($hash, $ioHash); - $rf = HMCCU_GetAttrReadingFormat ($hash, $hmccu_hash) if (!defined ($rf)); - my $gsr = AttrVal ($hmccu_hash->{NAME}, 'ccudef-readingname', ''); - my $sr = AttrVal ($name, 'ccureadingname', $gsr); - $sr .= ";".$gsr if ($sr ne $gsr && $gsr ne ''); - - # Datapoint is mandatory - return '' if ($d eq ''); - - if ($rf eq 'datapoint' || $rf eq 'datapointlc') { - $rn = (defined ($c) && $c ne '') ? $c.'.'.$d : $d; - } - elsif ($rf eq 'name' || $rf eq 'namelc') { - if ($n eq '') { - if ($a ne '' && $c ne '') { $n = HMCCU_GetChannelName ($hmccu_hash, $a.':'.$c, ''); } - elsif ($a ne '' && $c eq '') { $n = HMCCU_GetDeviceName ($hmccu_hash, $a, ''); } - else { return ''; } + my @srl = (); + my $crn = AttrVal ($name, 'ccureadingname', ''); + push @srl, $crn if ($crn ne ''); + if ((exists($hash->{hmccu}{control}{chn}) && "$c" eq $hash->{hmccu}{control}{chn}) || + (exists($hash->{hmccu}{state}{chn}) && "$c" eq $hash->{hmccu}{state}{chn})) { + my $role = HMCCU_GetChannelRole ($hash, $c); + if ($role ne '' && exists($HMCCU_READINGS->{$role})) { + $crn = $HMCCU_READINGS->{$role}; + $crn =~ s/C#\./$c\./g; + push @srl, $crn; } + } + my $sr = join (';', @srl); + + HMCCU_Trace ($hash, 2, "sr=$sr"); + + # Complete missing values + if ($n eq '' && $a ne '') { + $n = ($c ne '') ? + HMCCU_GetChannelName ($ioHash, $a.':'.$c) : + HMCCU_GetDeviceName ($ioHash, $a); + } + elsif ($n ne '' && $a eq '') { + ($a, $c) = HMCCU_GetAddress ($ioHash, $n); + } + if ($i eq '' && $a ne '') { + $i = HMCCU_GetDeviceInterface ($ioHash, $a); + } - return '' if ($n eq ''); + # Get reading prefix definitions + $ps = 'DEVICE' if (($c eq '0' && $ps eq 'MASTER') || $c eq 'd'); + my $readingPrefix = HMCCU_GetAttribute ($ioHash, $hash, 'ccuReadingPrefix', ''); + foreach my $pd (split (',', $readingPrefix)) { + my ($rSet, $rPre) = split (':', $pd); + $prefix{$rSet} = $rPre if (defined($rPre) && exists($prefix{$rSet})); + } + my $rpf = exists($prefix{$ps}) ? $prefix{$ps} : ''; + + if ($rf =~ /^datapoint(lc|uc)?$/) { + $rn = $c ne '' && $c ne 'd' && $type ne 'HMCCUCHN' ? $c.'.'.$d : $d; + } + elsif ($rf =~ /^name(lc|uc)?$/) { + return () if ($n eq ''); $rn = $n.'.'.$d; } - elsif ($rf eq 'address' || $rf eq 'addresslc') { - if ($a eq '' && $n ne '') { - ($a, $c) = HMCCU_GetAddress ($hmccu_hash, $n, '', ''); - } - - if ($a ne '') { - my $t = $a; - $i = HMCCU_GetDeviceInterface ($hmccu_hash, $a, '') if ($i eq ''); - $t = $i.'.'.$t if ($i ne ''); - $t = $t.'.'.$c if ($c ne ''); - - $rn = $t.'.'.$d; - } + elsif ($rf =~ /^address(lc|uc)?$/) { + return () if ($a eq ''); + my $t = $a; + $t = $i.'.'.$t if ($i ne ''); + $t = $t.'.'.$c if ($c ne ''); + $rn = $t.'.'.$d; + } + elsif ($rf =~ /\%/) { + $rn = $rf; + if ($a ne '') { $rn =~ s/\%a/lc($a)/ge; $rn =~ s/\%A/uc($a)/ge; } + if ($n ne '') { $rn =~ s/\%n/lc($n)/ge; $rn =~ s/\%N/uc($n)/ge; } + if ($c ne '') { $rn =~ s/\%c/lc($c)/ge; $rn =~ s/\%C/uc($c)/ge; } + $rn =~ s/\%d/lc($d)/ge; + $rn =~ s/\%D/uc($d)/ge; } - push (@rnlist, $rn); + if (scalar (@rcv) > 0) { + push (@rnlist, map { $rpf.$_.'-'.$rn } @rcv); + } + else { + push (@rnlist, $rpf.$rn); + } # Rename and/or add reading names - if ($sr ne '') { - my @rules = split (';', $sr); - foreach my $rr (@rules) { - my ($rold, $rnew) = split (':', $rr); - next if (!defined ($rnew)); - my @rnewList = split (',', $rnew); - next if (scalar (@rnewList) < 1); - if ($rnlist[0] =~ /$rold/) { - foreach my $rnew (@rnewList) { - if ($rnew =~ /^\+(.+)$/) { - my $radd = $1; - $radd =~ s/$rold/$radd/; - push (@rnlist, $radd); - } - else { - $rnlist[0] =~ s/$rold/$rnew/; - last; - } + my @rules = split (';', $sr); + foreach my $rr (@rules) { + my ($rold, $rnew) = split (':', $rr); + next if (!defined ($rnew)); + my @rnewList = split (',', $rnew); + next if (scalar (@rnewList) < 1); + if ($rnlist[0] =~ /$rold/) { + foreach my $rnew (@rnewList) { + if ($rnew =~ /^\+(.+)$/) { + my $radd = $1; + $radd =~ s/$rold/$radd/; + push (@rnlist, $radd); + } + else { + $rnlist[0] =~ s/$rold/$rnew/; + last; } } } } - # Convert to lowercase - $rnlist[0] = lc($rnlist[0]) if ($rf =~ /lc$/); + # Convert to lower or upper case + $rnlist[0] = lc($rnlist[0]) if ($rf =~ /^(datapoint|name|address)lc$/); + $rnlist[0] = uc($rnlist[0]) if ($rf =~ /^(datapoint|name|address)uc$/); # Return array of corrected reading names - return map { HMCCU_CorrectName ($_) } @rnlist; + return HMCCU_Unique (map { HMCCU_CorrectName ($_) } @rnlist); } ###################################################################### @@ -2599,12 +2442,16 @@ sub HMCCU_FormatReadingValue ($$$) { my ($hash, $value, $dpt) = @_; my $name = $hash->{NAME}; - my $fnc = "FormatReadingValue"; + if (!defined($value)) { + HMCCU_Trace ($hash, 2, "Value undefined for datapoint $dpt"); + return $value; + } + my $stripnumber = HMCCU_GetAttrStripNumber ($hash); - if ($stripnumber ne 'null' && $value =~ /^[+-]?\d*\.?\d+(?:(?:e|E)\d+)?$/) { - my $isint = $value =~ /^[+-]?[0-9]+$/ ? 1 : 0; + if ($stripnumber ne 'null' && HMCCU_IsFltNum ($value)) { + my $isint = HMCCU_IsIntNum ($value) ? 2 : 0; foreach my $sr (split (';', $stripnumber)) { my ($d, $s) = split ('!', $sr); @@ -2614,76 +2461,139 @@ sub HMCCU_FormatReadingValue ($$$) else { $s = $sr; } - - if ($s eq '0' && !$isint) { return sprintf ("%d", $value); } - elsif ($s eq '1' && !$isint) { return sprintf ("%.1f", $value); } - elsif ($s eq '2' && !$isint) { return sprintf ("%g", $value); } - elsif ($s =~ /^-([0-9])$/ && !$isint) { my $f = '%.'.$1.'f'; return sprintf ($f, $value); } - elsif ($s =~ /^%.+$/) { return sprintf ($s, $value); } + + return HMCCU_StripNumber ($value, $s, $isint | 1); } - HMCCU_Trace ($hash, 2, $fnc, "sn = $stripnumber, dpt=$dpt, isint=$isint, value $value not changed"); + HMCCU_Trace ($hash, 2, "sn = $stripnumber, dpt=$dpt, isint=$isint, value $value not changed"); } else { - my $h = unpack "H*", $value; - HMCCU_Trace ($hash, 2, $fnc, "sn = $stripnumber, Value $value $h not changed"); + my $h = uc(unpack "H*", $value); + HMCCU_Trace ($hash, 2, "sn = $stripnumber, Value $value 0x$h not changed"); } return $value; } +###################################################################### +# Format number +# Parameter: +# $value - Any value (non numeric values will be ignored) +# $strip - +# $number - 0=detect, 1=number, 2=integer +###################################################################### + +sub HMCCU_StripNumber ($$;$) +{ + my ($value, $strip, $number) = @_; + $number //= 0; + + if ($number & 1 || $value =~ /^[+-]?\d*\.?\d+(?:(?:e|E)\d+)?$/) { + my $isint = ($number & 2 || $value =~ /^[+-]?[0-9]+$/) ? 1 : 0; + + if ($strip eq '0' && !$isint) { return sprintf ("%d", $value); } + elsif ($strip eq '1' && !$isint) { return sprintf ("%.1f", $value); } + elsif ($strip eq '2' && !$isint) { return sprintf ("%g", $value); } + elsif ($strip =~ /^-([0-9])$/ && !$isint) { my $f = '%.'.$1.'f'; return sprintf ($f, $value); } + elsif ($strip =~ /^%.+$/) { return sprintf ($strip, $value); } + } + + return $value; +} + +###################################################################### +# Convert float to int, if float == int +###################################################################### + +sub HMCCU_StripZero ($) +{ + my ($value) = @_; + + return HMCCU_IsFltNum ($value) && int($value) == $value ? int($value) : $value; +} + ###################################################################### # Log message if trace flag is set. # Will output multiple log file entries if parameter msg is separated # by
###################################################################### -sub HMCCU_Trace ($$$$) +sub HMCCU_Trace ($$$) { - my ($hash, $level, $fnc, $msg) = @_; + my ($hash, $level, $msg) = @_; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; - return if (!HMCCU_IsFlag ($name, "trace")); + return if (!HMCCU_IsFlag ($name, 'trace')); + + my $pid = $$; + my $fnc = (caller(1))[3] // 'unknown'; + + my $traceFilter = AttrVal ($name, 'traceFilter', '.*'); + return if ($fnc !~ /$traceFilter/); foreach my $m (split ("
", $msg)) { - $m = "[$name] $fnc: $m" if (defined ($fnc) && $fnc ne ''); - Log3 $name, $level, "$type: $m"; + Log3 $name, $level, "$type: [$name : $pid] [$fnc] $m"; } } +###################################################################### +# Log message. Optionally show message on the screen, if async output +# is possible. +###################################################################### + +sub HMCCU_LogDisplay ($$$;$) +{ + my ($hash, $level, $msg, $rc) = @_; + + if ($init_done && exists($hash->{CL})) { + my $devType = $hash->{TYPE} // ''; + my $devName = defined($hash->{NAME}) ? " [$hash->{NAME}]" : ''; + my $cl = $hash->{CL}; + InternalTimer (gettimeofday()+1, sub { asyncOutput ($cl, "$devType $devName $msg") }, undef, 1); + } + + return HMCCU_Log ($hash, $level, $msg, $rc); +} + ###################################################################### # Log message with module type, device name and process id. # Return parameter rc or 0. # Parameter source can be a device hash reference, a string reference # or a string. +# Parameter msg can be an array reference or a string. ###################################################################### sub HMCCU_Log ($$$;$) { my ($source, $level, $msg, $rc) = @_; + $rc //= 0; - my $r = ref($source); + my ($cf, $cp, $cl) = caller; + my $r = defined($source) ? ref($source) : 'N/A'; my $pid = $$; - my $name = "n/a"; - my $type = "n/a"; + my $name = 'N/A'; + + if ($r eq 'HASH') { $name = $source->{NAME} // 'N/A'; } + elsif ($r eq 'SCALAR') { $name = $$source; } + else { $name = $source // 'N/A'; } - $rc = 0 if (!defined($rc)); - if ($r eq 'HASH') { - $name = $source->{NAME} if (exists ($source->{NAME})); - $type = $source->{TYPE} if (exists ($source->{TYPE})); + my $hash = $defs{$name}; + my $type = defined($hash) ? $hash->{TYPE} : 'N/A'; + if (defined($hash) && HMCCU_IsFlag ($hash, 'logEnhanced')) { + $type .= ":$cl"; + $name .= " : $pid"; } - elsif ($r eq 'SCALAR') { - $name = $$source; - $type = $defs{$name}->{TYPE} if (exists ($defs{$name})); + my $logname = exists($defs{$name}) ? $name : undef; + + if (ref($msg) eq 'ARRAY') { + foreach my $m (@$msg) { + Log3 $logname, $level, "$type [$name] $m"; + } } else { - $name = $source; - $type = $defs{$name}->{TYPE} if (exists ($defs{$name})); + Log3 $logname, $level, "$type [$name] $msg"; } - - Log3 $name, $level, "$type: [$name : $pid] $msg"; - return $rc; } @@ -2695,9 +2605,7 @@ sub HMCCU_LogError ($$$) { my ($hash, $level, $msg) = @_; - HMCCU_Log ($hash, $level, $msg, undef); - - return "ERROR: $msg"; + return HMCCU_Log ($hash, $level, $msg, "ERROR: $msg"); } ###################################################################### @@ -2711,6 +2619,7 @@ sub HMCCU_LogError ($$$) sub HMCCU_SetError ($@) { my ($hash, $text, $addinfo) = @_; + $addinfo //= ''; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my $msg; @@ -2720,7 +2629,7 @@ sub HMCCU_SetError ($@) -3 => 'Cannot detect IO device', -4 => 'Device deleted in CCU', -5 => 'No response from CCU', - -6 => 'Update of readings disabled. Set attribute ccureadings first', + -6 => 'Update of readings disabled. Remove ccuflag noReadings', -7 => 'Invalid channel number', -8 => 'Invalid datapoint', -9 => 'Interface does not support RPC calls', @@ -2739,17 +2648,14 @@ sub HMCCU_SetError ($@) ); if ($text ne 'OK' && $text ne '0') { - $msg = exists ($errlist{$text}) ? $errlist{$text} : $text; - $msg = $type.": ".$name." ". $msg; - if (defined ($addinfo) && $addinfo ne '') { - $msg .= ". $addinfo"; - } + $msg = exists($errlist{$text}) ? $errlist{$text} : $text; + $msg = "$type: $name $msg"; + $msg .= ". $addinfo" if ($addinfo ne ''); HMCCU_Log ($hash, 1, $msg); - return HMCCU_SetState ($hash, "Error", $msg); - } - else { - return HMCCU_SetState ($hash, "OK"); + return HMCCU_SetState ($hash, 'Error', $msg); } + + return HMCCU_SetState ($hash, 'OK'); } ###################################################################### @@ -2765,7 +2671,7 @@ sub HMCCU_SetState ($@) my $ccuflags = HMCCU_GetFlags ($name); $ccuflags .= ',ackState' if ($hash->{TYPE} eq 'HMCCU' && $ccuflags !~ /ackState/); - if (defined ($hash) && defined ($text) && $ccuflags =~ /ackState/) { + if (defined($hash) && defined($text) && $ccuflags =~ /ackState/) { readingsSingleUpdate ($hash, 'state', $text, 1) if (ReadingsVal ($name, 'state', '') ne $text); } @@ -2789,28 +2695,17 @@ sub HMCCU_SetRPCState ($@) my $filter; my $rpcstate = $state; - if ($ccuflags =~ /intrpc/ || $ccuflags !~ /(intrpc|extrpc|procrpc)/) { - if ($state ne $hash->{RPCState}) { - $hash->{RPCState} = $state; - readingsSingleUpdate ($hash, "rpcstate", $state, 1); - HMCCU_Log ($hash, 4, "Set rpcstate to $state"); - HMCCU_Log ($hash, 1, $msg) if (defined ($msg)); - HMCCU_Log ($hash, 1, "All RPC servers $state"); - DoTrigger ($name, "RPC server $state"); - if ($state eq 'running') { - HMCCU_UpdateClients ($hash, '.*', 'Value', 0, $filter, 1); - } - } - } - elsif (defined ($iface) && $ccuflags =~ /(procrpc|extrpc)/) { + if (defined($iface)) { # Set interface state my ($ifname) = HMCCU_GetRPCServerInfo ($hash, $iface, 'name'); $hash->{hmccu}{interfaces}{$ifname}{state} = $state if (defined ($ifname)); # Count number of processes in state running, error or inactive # Prepare filter for updating client devices - my %stc = ("running" => 0, "error" => 0, "inactive" => 0); - my @iflist = HMCCU_GetRPCInterfaceList ($hash); + my %stc = ('running' => 0, 'error' => 0, 'inactive' => 0); + my $interfaces = HMCCU_GetRPCInterfaceList ($hash, 0); + my @iflist = keys %$interfaces; + my $ifCount = scalar(@iflist); foreach my $i (@iflist) { my $st = $hash->{hmccu}{interfaces}{$i}{state}; $stc{$st}++ if (exists ($stc{$st})); @@ -2823,35 +2718,35 @@ sub HMCCU_SetRPCState ($@) } # Determine overall process state - my $rpcstate = 'null'; - $rpcstate = "running" if ($stc{"running"} == scalar (@iflist)); - $rpcstate = "inactive" if ($stc{"inactive"} == scalar (@iflist)); - $rpcstate = "error" if ($stc{"error"} == scalar (@iflist)); - - if ($rpcstate =~ /^(running|inactive|error)$/) { - if ($rpcstate ne $hash->{RPCState}) { - $hash->{RPCState} = $rpcstate; - readingsSingleUpdate ($hash, "rpcstate", $rpcstate, 1); - HMCCU_Log ($hash, 4, "Set rpcstate to $rpcstate"); - HMCCU_Log ($hash, 1, $msg, undef) if (defined ($msg)); - HMCCU_Log ($hash, 1, "All RPC servers $rpcstate"); - DoTrigger ($name, "RPC server $rpcstate"); - if ($rpcstate eq 'running' && defined ($filter)) { - HMCCU_UpdateClients ($hash, '.*', 'Value', 0, $filter, 1); - } + $rpcstate = 'null'; + foreach my $rpcst (keys %stc) { + if ($stc{$rpcst} == $ifCount) { + $rpcstate = $rpcst; + last; } } + + if ($rpcstate ne 'null' && $rpcstate ne $hash->{RPCState}) { + $hash->{RPCState} = $rpcstate; + readingsSingleUpdate ($hash, "rpcstate", $rpcstate, 1); + HMCCU_Log ($hash, 4, "Set rpcstate to $rpcstate"); + HMCCU_Log ($hash, 1, $msg) if (defined($msg)); + HMCCU_Log ($hash, 1, "All RPC servers $rpcstate"); + DoTrigger ($name, "RPC server $rpcstate"); + HMCCU_UpdateClients ($hash, '.*', 'Value', 0, $filter, 1) + if ($rpcstate eq 'running' && defined($filter)); + } } # Set I/O device state if ($rpcstate eq 'running' || $rpcstate eq 'inactive') { - HMCCU_SetState ($hash, "OK"); + HMCCU_SetState ($hash, 'OK'); } elsif ($rpcstate eq 'error') { - HMCCU_SetState ($hash, "error"); + HMCCU_SetState ($hash, 'error'); } else { - HMCCU_SetState ($hash, "busy"); + HMCCU_SetState ($hash, 'busy'); } return undef; @@ -2861,45 +2756,102 @@ sub HMCCU_SetRPCState ($@) # Substitute first occurrence of regular expression or fixed string. # Floating point values are ignored without datapoint specification. # Integer values are compared with complete value. -# mode: 0=Substitute regular expression, 1=Substitute text +# $hashOrRule - +# $mode - 0=Substitute regular expression, 1=Substitute text +# $chn - A channel number. Ignored if $dpt contains a Channel +# number. +# $dpt - Datapoint name. If it contains a channel number, it +# overrides parameter $chn +# $type - Role of a channel, i.e. SHUTTER_CONTACT (optional). +# $devDesc - Device description reference (optional). ###################################################################### -sub HMCCU_Substitute ($$$$$) +sub HMCCU_Substitute ($$$$$;$$) { - my ($value, $substrule, $mode, $chn, $dpt) = @_; + my ($value, $hashOrRule, $mode, $chn, $dpt, $type, $devDesc) = @_; + + my $substrule = ''; + my $ioHash; my $rc = 0; my $newvalue; + + if (defined($hashOrRule)) { + if (ref($hashOrRule) eq 'HASH') { + $ioHash = HMCCU_GetHash ($hashOrRule); + $substrule = HMCCU_GetAttrSubstitute ($hashOrRule, $ioHash); + } + else { + $substrule = $hashOrRule; + } + } - return $value if (!defined ($substrule) || $substrule eq ''); - - # Remove channel number from datapoint if specified + # Separate channel number from datapoint if specified if ($dpt =~ /^([0-9]{1,2})\.(.+)$/) { ($chn, $dpt) = ($1, $2); } - + my @rulelist = split (';', $substrule); foreach my $rule (@rulelist) { my @ruletoks = split ('!', $rule); - if (@ruletoks == 2 && $dpt ne '' && $mode == 0) { - my @dptlist = split (',', $ruletoks[0]); - foreach my $d (@dptlist) { - my $c = -1; - if ($d =~ /^([0-9]{1,2})\.(.+)$/) { - ($c, $d) = ($1, $2); - } - if ($d eq $dpt && ($c == -1 || !defined($chn) || $c == $chn)) { - ($rc, $newvalue) = HMCCU_SubstRule ($value, $ruletoks[1], $mode); - return $newvalue; + if (scalar(@ruletoks) == 2 && $dpt ne '' && $mode == 0) { + my ($r, $f) = split (':', $ruletoks[0], 2); + if (!defined($f)) { + $f = $r; + $r = undef; + } + if (!defined($r) || (defined($type) && $r eq $type)) { + my @dptlist = split (',', $f); + foreach my $d (@dptlist) { + my $c = -1; + if ($d =~ /^([0-9]{1,2})\.(.+)$/) { + ($c, $d) = ($1, $2); + } + if ($d eq $dpt && ($c == -1 || !defined($chn) || $c == $chn)) { + ($rc, $newvalue) = HMCCU_SubstRule ($value, $ruletoks[1], $mode); + return $newvalue if ($rc == 1); + } } } } - elsif (@ruletoks == 1) { + elsif (scalar(@ruletoks) == 1) { return $value if ($value !~ /^[+-]?\d+$/ && $value =~ /^[+-]?\d*\.?\d+(?:(?:e|E)\d+)?$/); ($rc, $newvalue) = HMCCU_SubstRule ($value, $ruletoks[0], $mode); return $newvalue if ($rc == 1); } } + # Original value not modified by rules. Use default conversion depending on type/role + # Default conversion can be overriden by attribute ccudef-substitute in I/O device + + # Substitute by rules defined in CONVERSIONS table + if (!defined($type) || $type eq '') { + $type = defined($devDesc) && defined($devDesc->{INDEX}) ? $devDesc->{TYPE} : 'DEFAULT'; + } + if (exists($HMCCU_CONVERSIONS->{$type}{$dpt}{$value})) { + return $HMCCU_CONVERSIONS->{$type}{$dpt}{$value}; + } + elsif (exists($HMCCU_CONVERSIONS->{DEFAULT}{$dpt}{$value})) { + return $HMCCU_CONVERSIONS->{DEFAULT}{$dpt}{$value}; + } + + # Substitute enumerations + if (defined($devDesc) && defined($ioHash)) { + my $paramDef = HMCCU_GetParamDef ($ioHash, $devDesc, 'VALUES', $dpt); + if (!defined($paramDef) && defined($paramDef->{TYPE}) && + $paramDef->{TYPE} eq 'ENUM' && defined($paramDef->{VALUE_LIST})) { + my $i = defined($paramDef->{MIN}) ? $paramDef->{MIN} : 0; + if ($mode) { + my %enumVals = map { $_ => $i++ } split(',', $paramDef->{VALUE_LIST}); + return $enumVals{$value} if (exists($enumVals{$value})); + } + else { + my @enumList = split(',', $paramDef->{VALUE_LIST}); + my $idx = $value-$i; + return $enumList[$idx] if ($idx >= 0 && $idx < scalar(@enumList)); + } + } + } + return $value; } @@ -2917,15 +2869,15 @@ sub HMCCU_Substitute ($$$$$) sub HMCCU_SubstRule ($$$) { - my ($value, $substitutes, $mode ) = @_; + my ($value, $substitutes, $mode) = @_; my $rc = 0; $substitutes =~ s/\$\{value\}/$value/g; - my @sub_list = split /,/,$substitutes; + my @sub_list = split /[, ]/,$substitutes; foreach my $s (@sub_list) { my ($regexp, $text) = split /:/,$s,2; - next if (!defined ($regexp) || !defined($text)); + next if (!defined($regexp) || !defined($text)); if ($regexp =~ /^#([+-]?\d*\.?\d+?)\-([+-]?\d*\.?\d+?)$/) { my ($mi, $ma) = ($1, $2); if ($value =~ /^\d*\.?\d+?$/ && $value >= $mi && $value <= $ma) { @@ -2935,12 +2887,12 @@ sub HMCCU_SubstRule ($$$) } elsif ($mode == 0 && $value =~ /$regexp/ && $value !~ /^[+-]?\d+$/) { my $x = eval { $value =~ s/$regexp/$text/ }; - $rc = 1 if (defined ($x)); + $rc = 1 if (defined($x)); last; } elsif (($mode == 1 || $value =~ /^[+-]?\d+$/) && $value =~ /^$regexp$/) { my $x = eval { $value =~ s/^$regexp$/$text/ }; - $rc = 1 if (defined ($x)); + $rc = 1 if (defined($x)); last; } } @@ -2965,44 +2917,37 @@ sub HMCCU_SubstRule ($$$) sub HMCCU_SubstVariables ($$$) { my ($clhash, $text, $dplist) = @_; - my $fnc = "HMCCU_SubstVariables"; - my @varlist; - if (defined ($dplist)) { - @varlist = split (',', $dplist); - } - else { - @varlist = keys %{$clhash->{hmccu}{dp}}; - } + my @varlist = defined($dplist) ? split (',', $dplist) : keys %{$clhash->{hmccu}{dp}}; - HMCCU_Trace ($clhash, 2, $fnc, "text=$text"); + HMCCU_Trace ($clhash, 2, "text=$text"); # Substitute datapoint variables by value foreach my $dp (@varlist) { my ($chn, $dpt) = split (/\./, $dp); - HMCCU_Trace ($clhash, 2, $fnc, "var=$dp"); + HMCCU_Trace ($clhash, 2, "var=$dp"); - if (defined ($clhash->{hmccu}{dp}{$dp}{OSVAL})) { - $text =~ s/\$\$\{?$dp\}?/$clhash->{hmccu}{dp}{$dp}{OSVAL}/g; - $text =~ s/\$\$\{?$dpt\}?/$clhash->{hmccu}{dp}{$dp}{OSVAL}/g; + if (defined ($clhash->{hmccu}{dp}{$dp}{VALUES}{OSVAL})) { + $text =~ s/\$\$\{?$dp\}?/$clhash->{hmccu}{dp}{$dp}{VALUES}{OSVAL}/g; + $text =~ s/\$\$\{?$dpt\}?/$clhash->{hmccu}{dp}{$dp}{VALUES}{OSVAL}/g; } - if (defined ($clhash->{hmccu}{dp}{$dp}{SVAL})) { - $text =~ s/\$\{?$dp\}?/$clhash->{hmccu}{dp}{$dp}{SVAL}/g; - $text =~ s/\$\{?$dpt\}?/$clhash->{hmccu}{dp}{$dp}{SVAL}/g; + if (defined ($clhash->{hmccu}{dp}{$dp}{VALUES}{SVAL})) { + $text =~ s/\$\{?$dp\}?/$clhash->{hmccu}{dp}{$dp}{VALUES}{SVAL}/g; + $text =~ s/\$\{?$dpt\}?/$clhash->{hmccu}{dp}{$dp}{VALUES}{SVAL}/g; } - if (defined ($clhash->{hmccu}{dp}{$dp}{OVAL})) { - $text =~ s/\%\%\{?$dp\}?/$clhash->{hmccu}{dp}{$dp}{OVAL}/g; - $text =~ s/\%\%\{?$dpt\}?/$clhash->{hmccu}{dp}{$dp}{OVAL}/g; + if (defined ($clhash->{hmccu}{dp}{$dp}{VALUES}{OVAL})) { + $text =~ s/\%\%\{?$dp\}?/$clhash->{hmccu}{dp}{$dp}{VALUES}{OVAL}/g; + $text =~ s/\%\%\{?$dpt\}?/$clhash->{hmccu}{dp}{$dp}{VALUES}{OVAL}/g; } - if (defined ($clhash->{hmccu}{dp}{$dp}{VAL})) { - $text =~ s/\%\{?$dp\}?/$clhash->{hmccu}{dp}{$dp}{VAL}/g; - $text =~ s/\%\{?$dpt\}?/$clhash->{hmccu}{dp}{$dp}{VAL}/g; - $text =~ s/$dp/$clhash->{hmccu}{dp}{$dp}{VAL}/g; + if (defined ($clhash->{hmccu}{dp}{$dp}{VALUES}{VAL})) { + $text =~ s/\%\{?$dp\}?/$clhash->{hmccu}{dp}{$dp}{VALUES}{VAL}/g; + $text =~ s/\%\{?$dpt\}?/$clhash->{hmccu}{dp}{$dp}{VALUES}{VAL}/g; + $text =~ s/$dp/$clhash->{hmccu}{dp}{$dp}{VALUES}{VAL}/g; } } - HMCCU_Trace ($clhash, 2, $fnc, "text=$text"); + HMCCU_Trace ($clhash, 2, "text=$text"); return $text; } @@ -3010,65 +2955,77 @@ sub HMCCU_SubstVariables ($$$) ###################################################################### # Update all datapoint/readings of all client devices matching # specified regular expression. Update will fail if device is deleted -# or disabled or if attribute ccureadings of a device is set to 0. +# or disabled or if ccuflag noReadings is set. # If fromccu is 1 regular expression is compared to CCU device name. # Otherwise it's compared to FHEM device name. If ifname is specified # only devices belonging to interface ifname are updated. ###################################################################### -sub HMCCU_UpdateClients ($$$$$$) +sub HMCCU_UpdateClients ($$$$;$$) { my ($hash, $devexp, $ccuget, $fromccu, $ifname, $nonBlock) = @_; my $fhname = $hash->{NAME}; - my $c = 0; + $nonBlock //= HMCCU_IsFlag ($fhname, 'nonBlocking'); my $dc = 0; - my $filter = "ccudevstate=active"; - $filter .= ",ccuif=$ifname" if (defined ($ifname)); + my $filter = 'ccudevstate=active'; + $filter .= ",ccuif=$ifname" if (defined($ifname)); $ccuget = AttrVal ($fhname, 'ccuget', 'Value') if ($ccuget eq 'Attr'); - my $list = ''; + my @list = (); if ($fromccu) { foreach my $name (sort keys %{$hash->{hmccu}{adr}}) { next if ($name !~ /$devexp/ || !($hash->{hmccu}{adr}{$name}{valid})); - my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)", undef, $filter); + my @devlist = HMCCU_FindClientDevices ($hash, '(HMCCUDEV|HMCCUCHN)', undef, $filter); $dc += scalar(@devlist); foreach my $d (@devlist) { my $ch = $defs{$d}; - next if (!defined ($ch->{IODev}) || !defined ($ch->{ccuaddr})); - next if ($ch->{ccuaddr} ne $hash->{hmccu}{adr}{$name}{address}); - next if ($ch->{ccuif} eq 'fhem'); - next if (!HMCCU_IsValidDeviceOrChannel ($hash, $ch->{ccuaddr}, $HMCCU_FL_ADDRESS)); - $list .= ($list eq '') ? $name : ",$name"; - $c++; + next if (!defined($ch->{IODev}) || !defined($ch->{ccuaddr}) || + $ch->{ccuaddr} ne $hash->{hmccu}{adr}{$name}{address} || + !HMCCU_IsValidDeviceOrChannel ($hash, $ch->{ccuaddr}, $HMCCU_FL_ADDRESS)); + push @list, $name; } } } else { - my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)", $devexp, $filter); + my @devlist = HMCCU_FindClientDevices ($hash, '(HMCCUDEV|HMCCUCHN)', $devexp, $filter); $dc = scalar(@devlist); foreach my $d (@devlist) { my $ch = $defs{$d}; - next if (!defined ($ch->{IODev}) || !defined ($ch->{ccuaddr}) || $ch->{ccuif} eq 'fhem'); - next if (!HMCCU_IsValidDeviceOrChannel ($hash, $ch->{ccuaddr}, $HMCCU_FL_ADDRESS)); - my $name = HMCCU_GetDeviceName ($hash, $ch->{ccuaddr}, ''); - next if ($name eq ''); - $list .= ($list eq '') ? $name : ",$name"; - $c++; + my $cn = $ch->{NAME}; + if (!defined($ch->{IODev})) { + HMCCU_Log ($hash, 2, "Device $cn not updated. I/O device not specified"); + next; + } + if (!defined($ch->{ccuaddr})) { + HMCCU_Log ($hash, 2, "Device $cn not updated. CCU address not specified"); + next; + } + if (!HMCCU_IsValidDeviceOrChannel ($hash, $ch->{ccuaddr}, $HMCCU_FL_ADDRESS)) { + HMCCU_Log ($hash, 2, "Device $cn not updated. Address $ch->{ccuaddr} is not valid"); + next; + } + my $name = HMCCU_GetDeviceName ($hash, $ch->{ccuaddr}); + if ($name eq '') { + HMCCU_Log ($hash, 2, "Device $cn not updated. Can't get device name for address $ch->{ccuaddr}"); + next; + } + push @list, $name; } } - return HMCCU_Log ($hash, 2, "HMCCU: Found no devices to update") if ($c == 0); + my $c = scalar(@list); + return HMCCU_Log ($hash, 2, 'Found no devices to update') if ($c == 0); HMCCU_Log ($hash, 2, "Updating $c of $dc client devices matching devexp=$devexp filter=$filter"); - if (HMCCU_IsFlag ($fhname, 'nonBlocking') || $nonBlock) { - HMCCU_HMScriptExt ($hash, '!GetDatapointsByDevice', { list => $list, ccuget => $ccuget }, + if ($nonBlock) { + HMCCU_HMScriptExt ($hash, '!GetDatapointsByDevice', { list => join(',', @list), ccuget => $ccuget }, \&HMCCU_UpdateCB, { logCount => 1, devCount => $c }); return 1; } else { my $response = HMCCU_HMScriptExt ($hash, '!GetDatapointsByDevice', - { list => $list, ccuget => $ccuget }, undef, undef); + { list => join(',', @list), ccuget => $ccuget }); return -2 if ($response eq '' || $response =~ /^ERROR:.*/); HMCCU_UpdateCB ({ ioHash => $hash, logCount => 1, devCount => $c }, undef, $response); @@ -3076,114 +3033,27 @@ sub HMCCU_UpdateClients ($$$$$$) } } -########################################################################## -# Create virtual device in internal device tables. -# If sourceAddr is specified, parameter newType will be ignored. -# Return 0 on success or error code: -# 1 = newType not defined -# 2 = Device with newType not found in internal tables -########################################################################## - -sub HMCCU_CreateDevice ($$$$$) -{ - my ($hash, $newAddr, $newName, $newType, $sourceAddr) = @_; - - my %object; - - if (!defined ($sourceAddr)) { - # Search for type in device table - return 1 if (!defined ($newType)); - foreach my $da (keys %{$hash->{hmccu}{dev}}) { - if ($hash->{hmccu}{dev}{$da}{type} eq $newType) { - $sourceAddr = $da; - last; - } - } - return 2 if (!defined ($sourceAddr)); - } - else { - $newType = $hash->{hmccu}{dev}{$sourceAddr}{type}; - } - - # Device attributes - $object{$newAddr}{flag} = 'N'; - $object{$newAddr}{addtype} = 'dev'; - $object{$newAddr}{channels} = $hash->{hmccu}{dev}{$sourceAddr}{channels}; - $object{$newAddr}{name} = $newName; - $object{$newAddr}{type} = $newType; - $object{$newAddr}{interface} = 'fhem'; - $object{$newAddr}{chndir} = 0; - - # Channel attributes - for (my $chn=0; $chn<$object{$newAddr}{channels}; $chn++) { - my $ca = "$newAddr:$chn"; - $object{$ca}{flag} = 'N'; - $object{$ca}{addtype} = 'chn'; - $object{$ca}{channels} = 1; - $object{$ca}{name} = "$newName:$chn"; - $object{$ca}{chndir} = $hash->{hmccu}{dev}{"$sourceAddr:$chn"}{chndir}; - $object{$ca}{rxmode} = $hash->{hmccu}{dev}{"$sourceAddr:$chn"}{rxmode}; - $object{$ca}{usetype} = $hash->{hmccu}{dev}{"$sourceAddr:$chn"}{usetype}; - } - - HMCCU_UpdateDeviceTable ($hash, \%object); - - return 0; -} - -########################################################################## -# Remove virtual device from internal device tables. -########################################################################## - -sub HMCCU_DeleteDevice ($) -{ - my ($clthash) = @_; - my $name = $clthash->{NAME}; - - return if (!exists ($clthash->{IODev}) || !exists ($clthash->{ccuif}) || - !exists ($clthash->{ccuaddr})); - return if ($clthash->{ccuif} ne 'fhem'); - - my $hmccu_hash = $clthash->{IODev}; - my $devaddr = $clthash->{ccuaddr}; - my $channels = exists ($clthash->{channels}) ? $clthash->{channels} : 0; - - # Delete device address entries - if (exists ($hmccu_hash->{hmccu}{dev}{$devaddr})) { - delete $hmccu_hash->{hmccu}{dev}{$devaddr}; - } - - # Delete channel address and name entries - for (my $chn=0; $chn<=$channels; $chn++) { - if (exists ($hmccu_hash->{hmccu}{dev}{"$devaddr:$chn"})) { - delete $hmccu_hash->{hmccu}{dev}{"$devaddr:$chn"}; - } - if (exists ($hmccu_hash->{hmccu}{adr}{"$name:$chn"})) { - delete $hmccu_hash->{hmccu}{adr}{"$name:$chn"}; - } - } - - # Delete device name entries - if (exists ($hmccu_hash->{hmccu}{adr}{$name})) { - delete $hmccu_hash->{hmccu}{adr}{$name}; - } -} - ########################################################################## # Update parameters in internal device tables and client devices. # Parameter devices is a hash reference with following keys: # {address} -# {address}{flag} := [N, D, R] (N=New, D=Deleted, R=Renamed) -# {address}{addtype} := [chn, dev] -# {address}{channels} := Number of channels -# {address}{name} := Device or channel name -# {address}{type} := Homematic device type -# {address}{usetype} := Usage type -# {address}{interface} := Device interface ID -# {address}{firmware} := Firmware version of device -# {address}{version} := Version of RPC device description -# {address}{rxmode} := Transmit mode -# {address}{chndir} := Channel direction: 0=none, 1=sensor, 2=actor +# {address}{flag} := [N, D, R] (N=New, D=Deleted, R=Renamed) +# {address}{addtype} := [chn, dev] for channel or device +# {address}{channels} := Number of channels +# {address}{name} := Device or channel name +# {address}{type} := Homematic device type +# {address}{usetype} := Usage type +# {address}{interface} := Device interface ID +# {address}{firmware} := Firmware version of device +# {address}{version} := Version of RPC device description +# {address}{rxmode} := Transmit mode +# {address}{direction} := Channel direction: 0=none, 1=sensor, 2=actor +# {address}{paramsets} := Comma separated list of supported paramsets +# {address}{sourceroles} := Link sender roles +# {address}{targetroles} := Link receiver roles +# {address}{children} := Comma separated list of channels +# {address}{parent} := Parent device +# {address}{aes} := AES flag # If flag is 'D' the hash must contain an entry for the device address # and for each channel address. ########################################################################## @@ -3195,14 +3065,16 @@ sub HMCCU_UpdateDeviceTable ($$) my $devcount = 0; my $chncount = 0; + HMCCU_Log ($hash, 2, "Updating device table"); + # Update internal device table foreach my $da (keys %{$devices}) { my $nm = $hash->{hmccu}{dev}{$da}{name} if (defined ($hash->{hmccu}{dev}{$da}{name})); $nm = $devices->{$da}{name} if (defined ($devices->{$da}{name})); - if ($devices->{$da}{flag} eq 'N' && defined ($nm)) { + if ($devices->{$da}{flag} eq 'N' && defined($nm)) { my $at = ''; - if (defined ($devices->{$da}{addtype})) { + if (defined($devices->{$da}{addtype})) { $at = $devices->{$da}{addtype}; } else { @@ -3210,44 +3082,39 @@ sub HMCCU_UpdateDeviceTable ($$) $at = 'dev' if (HMCCU_IsDevAddr ($da, 0)); } if ($at eq '') { - Log3 $name, 2, "HMCCU: Cannot detect type of address $da"; + HMCCU_Log ($hash, 2, "Cannot detect type of address $da. Ignored."); next; } - Log3 $name, 2, "HMCCU: Duplicate name for device/channel $nm address=$da in CCU." - if (exists ($hash->{hmccu}{adr}{$nm}) && $at ne $hash->{hmccu}{adr}{$nm}{addtype}); + HMCCU_Log ($hash, 2, "Duplicate name for device/channel $nm address=$da in CCU.") + if (exists($hash->{hmccu}{adr}{$nm}) && $at ne $hash->{hmccu}{adr}{$nm}{addtype}); # Updated or new device/channel - $hash->{hmccu}{dev}{$da}{addtype} = $at; - $hash->{hmccu}{dev}{$da}{name} = $nm if (defined ($nm)); - $hash->{hmccu}{dev}{$da}{valid} = 1; - $hash->{hmccu}{dev}{$da}{channels} = $devices->{$da}{channels} - if (defined ($devices->{$da}{channels})); - $hash->{hmccu}{dev}{$da}{type} = $devices->{$da}{type} - if (defined ($devices->{$da}{type})); - $hash->{hmccu}{dev}{$da}{usetype} = $devices->{$da}{usetype} - if (defined ($devices->{$da}{usetype})); - $hash->{hmccu}{dev}{$da}{interface} = $devices->{$da}{interface} - if (defined ($devices->{$da}{interface})); - $hash->{hmccu}{dev}{$da}{version} = $devices->{$da}{version} - if (defined ($devices->{$da}{version})); - $hash->{hmccu}{dev}{$da}{firmware} = $devices->{$da}{firmware} - if (defined ($devices->{$da}{firmware})); - $hash->{hmccu}{dev}{$da}{rxmode} = $devices->{$da}{rxmode} - if (defined ($devices->{$da}{rxmode})); - $hash->{hmccu}{dev}{$da}{chndir} = $devices->{$da}{chndir} - if (defined ($devices->{$da}{chndir})); - if (defined ($nm)) { - $hash->{hmccu}{adr}{$nm}{address} = $da; - $hash->{hmccu}{adr}{$nm}{addtype} = $hash->{hmccu}{dev}{$da}{addtype}; - $hash->{hmccu}{adr}{$nm}{valid} = 1; + $hash->{hmccu}{dev}{$da}{addtype} = $at; + $hash->{hmccu}{dev}{$da}{valid} = 1; + + foreach my $k ('channels', 'type', 'usetype', 'interface', 'version', + 'firmware', 'rxmode', 'direction', 'paramsets', 'sourceroles', 'targetroles', + 'children', 'parent', 'aes') { + $hash->{hmccu}{dev}{$da}{$k} = $devices->{$da}{$k} + if (defined($devices->{$da}{$k})); + } + + if (defined($nm)) { + $hash->{hmccu}{dev}{$da}{name} = $nm; + $hash->{hmccu}{adr}{$nm}{address} = $da; + $hash->{hmccu}{adr}{$nm}{addtype} = $hash->{hmccu}{dev}{$da}{addtype}; + $hash->{hmccu}{adr}{$nm}{valid} = 1; } } - elsif ($devices->{$da}{flag} eq 'D' && exists ($hash->{hmccu}{dev}{$da})) { + elsif ($devices->{$da}{flag} eq 'D' && exists($hash->{hmccu}{dev}{$da})) { # Device deleted, mark as invalid $hash->{hmccu}{dev}{$da}{valid} = 0; $hash->{hmccu}{adr}{$nm}{valid} = 0 if (defined ($nm)); + my $iface = $hash->{hmccu}{dev}{$da}{interface}; + $hash->{hmccu}{device}{$iface}{$da}{_valid} = 0 + if (exists($hash->{hmccu}{device}{$iface}{$da})); } - elsif ($devices->{$da}{flag} eq 'R' && exists ($hash->{hmccu}{dev}{$da})) { + elsif ($devices->{$da}{flag} eq 'R' && exists($hash->{hmccu}{dev}{$da})) { # Device replaced, change address my $na = $devices->{hmccu}{newaddr}; # Copy device entries and delete old device entries @@ -3260,30 +3127,18 @@ sub HMCCU_UpdateDeviceTable ($$) } # Delayed initialization if CCU was not ready during FHEM start - if ($hash->{hmccu}{ccu}{delayed} == 1) { - # Initialize interface and port lists - HMCCU_AttrInterfacesPorts ($hash, 'rpcinterfaces', $attr{$name}{rpcinterfaces}) - if (exists ($attr{$name}{rpcinterfaces})); - HMCCU_AttrInterfacesPorts ($hash, 'rpcport', $attr{$name}{rpcport}) - if (exists ($attr{$name}{rpcport})); - + if ($hash->{hmccu}{ccu}{delayed} == 1) { # Initialize pending client devices my @cdev = HMCCU_FindClientDevices ($hash, '(HMCCUDEV|HMCCUCHN|HMCCURPCPROC)', undef, 'ccudevstate=pending'); - if (scalar (@cdev) > 0) { + if (scalar(@cdev) > 0) { HMCCU_Log ($hash, 2, "Initializing ".scalar(@cdev)." client devices in state 'pending'"); foreach my $cd (@cdev) { my $ch = $defs{$cd}; my $ct = $ch->{TYPE}; my $rc = 0; - if ($ct eq 'HMCCUDEV') { - $rc = HMCCUDEV_InitDevice ($hash, $ch); - } - elsif ($ct eq 'HMCCUCHN') { - $rc = HMCCUCHN_InitDevice ($hash, $ch); - } - elsif ($ct eq 'HMCCURPCPROC') { - $rc = HMCCURPCPROC_InitDevice ($hash, $ch); - } + if ($ct eq 'HMCCUDEV') { $rc = HMCCUDEV_InitDevice ($hash, $ch); } + elsif ($ct eq 'HMCCUCHN') { $rc = HMCCUCHN_InitDevice ($hash, $ch); } + elsif ($ct eq 'HMCCURPCPROC') { $rc = HMCCURPCPROC_InitDevice ($hash, $ch); } HMCCU_Log ($hash, 3, "Can't initialize client device ".$ch->{NAME}) if ($rc > 0); } } @@ -3292,37 +3147,30 @@ sub HMCCU_UpdateDeviceTable ($$) } # Update client devices - my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)", undef, undef); + my @devlist = HMCCU_FindClientDevices ($hash, '(HMCCUDEV|HMCCUCHN)'); foreach my $d (@devlist) { my $ch = $defs{$d}; my $ct = $ch->{TYPE}; - next if (!exists ($ch->{ccuaddr})); + next if (!exists($ch->{ccuaddr})); my $ca = $ch->{ccuaddr}; - next if (!exists ($devices->{$ca})); + next if (!exists($devices->{$ca})); if ($devices->{$ca}{flag} eq 'N') { # New device or new device information $ch->{ccudevstate} = 'active'; if ($ct eq 'HMCCUDEV') { - $ch->{ccutype} = $hash->{hmccu}{dev}{$ca}{type} - if (defined ($hash->{hmccu}{dev}{$ca}{type})); - $ch->{firmware} = $devices->{$ca}{firmware} - if (defined ($devices->{$ca}{firmware})); + $ch->{firmware} = $devices->{$ca}{firmware} // '?'; + $ch->{ccutype} = $devices->{$ca}{type} // '?'; } else { - $ch->{chntype} = $devices->{$ca}{usetype} - if (defined ($devices->{$ca}{usetype})); my ($add, $chn) = HMCCU_SplitChnAddr ($ca); - $ch->{ccutype} = $devices->{$add}{type} - if (defined ($devices->{$add}{type})); - $ch->{firmware} = $devices->{$add}{firmware} - if (defined ($devices->{$add}{firmware})); + $ch->{chntype} = $devices->{$ca}{usetype} // '?'; + $ch->{ccutype} = $devices->{$add}{type} // '?'; + $ch->{firmware} = $devices->{$add}{firmware} // '?'; } - $ch->{ccuname} = $hash->{hmccu}{dev}{$ca}{name} - if (defined ($hash->{hmccu}{dev}{$ca}{name})); - $ch->{ccuif} = $hash->{hmccu}{dev}{$ca}{interface} - if (defined ($devices->{$ca}{interface})); - $ch->{channels} = $hash->{hmccu}{dev}{$ca}{channels} - if (defined ($hash->{hmccu}{dev}{$ca}{channels})); + $ch->{ccuname} = $hash->{hmccu}{dev}{$ca}{name} if (defined($hash->{hmccu}{dev}{$ca}{name})); + $ch->{ccuif} = $hash->{hmccu}{dev}{$ca}{interface} if (defined($devices->{$ca}{interface})); + $ch->{hmccu}{channels} = $hash->{hmccu}{dev}{$ca}{channels} + if (defined($hash->{hmccu}{dev}{$ca}{channels})); } elsif ($devices->{$ca}{flag} eq 'D') { # Deleted device @@ -3347,6 +3195,1335 @@ sub HMCCU_UpdateDeviceTable ($$) return ($devcount, $chncount); } +###################################################################### +# Delete device table entries +# New version +###################################################################### + +sub HMCCU_ResetDeviceTables ($;$$) +{ + my ($hash, $iface, $address) = @_; + + if (defined($iface)) { + if (defined($address)) { + $hash->{hmccu}{device}{$iface}{$address} = (); + } + else { + $hash->{hmccu}{device}{$iface} = (); + } + } + else { + $hash->{hmccu}{device} = (); + $hash->{hmccu}{model} = (); + $hash->{hmccu}{snd} = (); + $hash->{hmccu}{rcv} = (); + } +} + +###################################################################### +# Create FHEM device(s) for CCU device(s) +###################################################################### + +sub HMCCU_CreateFHEMDevices ($@) +{ + my ($hash, $devSpec, $devPrefix, $devSuffix, $devFormat, $defOpts, $ah) = @_; + + # Statistics + # + # {statsValue}{devName} = addr.ccuName + # + # {notDetected}{$ccuDevName}: CCU device with $devName not detected + # {notSupported}{$ccuDevName}: CCU device type is not supported by create commands + # {fhemExists}{$fhemDevName}: FHEM device with $devName already exists + # {devDefined}{$fhemDevName}: FHEM device for $devAdd already exists + # {defFailed}{$fhemDevName}: Device definition failed + # {defSuccess}{$fhemDevName}: Device defined + # {attrFailed}{$fhemDevName.$attr}: Attribute setting failed + my %cs = (); + + foreach my $iface (keys %{$hash->{hmccu}{device}}) { + foreach my $address (keys %{$hash->{hmccu}{device}{$iface}}) { + my $ccuName = $hash->{hmccu}{device}{$iface}{$address}{_name}; + next if ($hash->{hmccu}{device}{$iface}{$address}{_addtype} ne 'dev' || + HMCCU_ExprNotMatch ($ccuName, $devSpec, 1)); + + my $ccuType = $hash->{hmccu}{device}{$iface}{$address}{_model}; + if ($ccuType =~ /^(HM-RCV-50|HmIP-RCV-50)$/) { + $cs{notSupported}{$ccuName} = "$address [$ccuName]"; + next; + } + + # Detect FHEM device type + my $detect = HMCCU_DetectDevice ($hash, $address, $iface); + if (!defined($detect) || $detect->{level} == 0) { + $cs{notDetected}{$ccuName} = "$address [$ccuName]"; + next; + } + + my $defMod = $detect->{defMod}; + my $defAdd = $detect->{defAdd}; + + # Build FHEM device name + my $devName = HMCCU_MakeDeviceName ($defAdd, $devPrefix, $devFormat, $devSuffix, $ccuName); + + if ($detect->{level} == 1) { + # Simple HMCCUCHN device + HMCCU_CreateDevice ($hash, $ccuName, $devName, $defMod, $defAdd, $defOpts, $ah, \%cs); + } + elsif ($detect->{level} == 2) { + # Multiple identical channels + if ($defOpts =~ /forcedev/i) { + # Force creation of HMCCUDEV + $ah->{statedatapoint} = $detect->{defSDP} if ($detect->{defSCh} != -1 && $detect->{stateRoleCount} > 1); + $ah->{controldatapoint} = $detect->{defCDP} if ($detect->{defCCh} != -1 && $detect->{controlRoleCount} > 1); + HMCCU_CreateDevice ($hash, $ccuName, $devName, 'HMCCUDEV', $defAdd, $defOpts, $ah, \%cs); + } + else { + HMCCU_BuildGroupAttr ($hash, $address, $ccuType, $ah) if ($detect->{controlRoleCount}+$detect->{stateRoleCount} > 1); + + # Create a HMCCUCHN for each channel + if ($detect->{controlRoleCount} > 0) { + # First create a HMCCUCHN for each control channel + foreach my $cc (keys %{$detect->{controlRole}}) { + my $ccuChnName = $hash->{hmccu}{device}{$iface}{"$address:$cc"}{_name}; + my $devChnName = HMCCU_MakeDeviceName ($defAdd, $devPrefix, $devFormat, $devSuffix, $ccuChnName); + HMCCU_CreateDevice ($hash, $ccuChnName, $devChnName, $defMod, "$defAdd:$cc", $defOpts, $ah, \%cs); + } + } + # Create a HMCCUCHN for each channel without control datapoint + foreach my $sc (keys %{$detect->{stateRole}}) { + if (!exists($detect->{controlRole}{$sc})) { + my $ccuChnName = $hash->{hmccu}{device}{$iface}{"$address:$sc"}{_name}; + my $devChnName = HMCCU_MakeDeviceName ($defAdd, $devPrefix, $devFormat, $devSuffix, $ccuChnName); + HMCCU_CreateDevice ($hash, $ccuChnName, $devChnName, $defMod, "$defAdd:$sc", $defOpts, $ah, \%cs); + } + } + } + } + elsif ($detect->{level} == 3 || $detect->{level} == 4) { + # Multiple roles + $ah->{statedatapoint} = $detect->{defSDP} if ($detect->{defSCh} != -1 && $detect->{stateRoleCount} > 1); + $ah->{controldatapoint} = $detect->{defCDP} if ($detect->{defCCh} != -1 && $detect->{controlRoleCount} > 1); + HMCCU_CreateDevice ($hash, $ccuName, $devName, $defMod, $defAdd, $defOpts, $ah, \%cs); + } + elsif ($detect->{level} == 5) { + # 4-channel role patterns, create a HMCCUDEV for each occurrence + my $rpCount = scalar(keys %{$detect->{rolePattern}}); + HMCCU_BuildGroupAttr ($hash, $address, $ccuType, $ah, '%n') if ($detect->{rolePatternCount} > 1); + HMCCU_Log ($hash, 3, "4-channel role patterns found $rpCount"); + my @rpChannels = map { ($_, $_+1, $_+2, $_+3) } keys %{$detect->{rolePattern}}; + foreach my $firstChannel (keys %{$detect->{rolePattern}}) { + my $ccuChnName = $hash->{hmccu}{device}{$iface}{"$address:$firstChannel"}{_name}; + my $devChnName = HMCCU_MakeDeviceName ($defAdd, $devPrefix, $devFormat, $devSuffix, $ccuChnName); + my $defPar = ''; + $defPar .= ' sd='.$detect->{rolePattern}{$firstChannel}{stateChannel}.'.'.$detect->{rolePattern}{$firstChannel}{stateDatapoint} + if (exists($detect->{rolePattern}{$firstChannel}{stateChannel})); + $defPar .= ' cd='.$detect->{rolePattern}{$firstChannel}{controlChannel}.'.'.$detect->{rolePattern}{$firstChannel}{controlDatapoint} + if (exists($detect->{rolePattern}{$firstChannel}{controlChannel})); + + # Reset attributes (currently statedatapoint and controldatapoint are not needed) + delete $ah->{statedatapoint} if (exists($ah->{statedatapoint})); + delete $ah->{controldatapoint} if (exists($ah->{controldatapoint})); + delete $ah->{ccureadingfilter} if (exists($ah->{ccureadingfilter})); + + # Build list of channel numbers for reading filter + my @rfChannels = map { HMCCU_IsArrayElement ($_, @rpChannels) ? () : $_ } keys %{$detect->{stateRole}}; + push @rfChannels, $detect->{rolePattern}{$firstChannel}{stateChannel} // (); + push @rfChannels, $detect->{rolePattern}{$firstChannel}{controlChannel} // (); + $ah->{ccureadingfilter} = join(',',sort @rfChannels).'..*'; + + HMCCU_CreateDevice ($hash, $ccuChnName, $devChnName, $defMod, $defAdd, $defOpts.$defPar, $ah, \%cs); + } + } + } + } + + return \%cs; +} + +###################################################################### +# Create a new FHEM HMCCUCHN or HMCCUDEV device +# Parameters: +# ccuName - Name of device in CCU +# devName - Name of new FHEM device +# defMod - Module for device definition +# defAdd - Address for device definition +# defOpts - String with additional command options +# ah - Hash reference of device attributes +# cs - Hash reference with device create results +# Return values: +# 0 - Error(s) +# 1 - Success (even if attributes cannot be set) +# Results are stored in hash $cs. Value is $defAdd.$ccuName: +# {fhemExists}{$devName}: FHEM device with $devName already exists +# {devDefined}{$devName}: FHEM device for $devAdd already exists +# {defFailed}{$devName}: Device definition failed +# {defSuccess}{$devName}: Device defined +# {attrFailed}{$devName.$attr}: Attribute setting failed +###################################################################### + +sub HMCCU_CreateDevice ($@) +{ + my ($hash, $ccuName, $devName, $defMod, $defAdd, $defOpts, $ah, $cs) = @_; + + # Check for existing FHEM devices with same name + if (exists($defs{$devName})) { + $cs->{fhemExists}{$devName} = "$defAdd [$ccuName]"; + return 0; + } + + # Check for existing FHEM devices for CCU address (HMCCUCHN only) + if ($defMod eq 'HMCCUCHN' && HMCCU_ExistsClientDevice ($defAdd, $defMod)) { + $cs->{devDefined}{$devName} = "$defAdd [$ccuName]"; + return 0; + } + + # Define new client device + $defOpts //= ''; + my $cmd = "$devName $defMod $defAdd"; + $cmd .= " $defOpts" if ($defOpts ne ''); + my $ret = CommandDefine (undef, $cmd); + if ($ret) { + HMCCU_Log ($hash, 2, "Define command failed $cmd. $ret"); + $cs->{defFailed}{$devName} = "$defAdd [$ccuName]"; + return 0; + } + else { + $cs->{defSuccess}{$devName} = "$defAdd [$ccuName]"; + + # Set device attributes +# HMCCU_SetInitialAttributes ($hash, $devName); + foreach my $da (keys %$ah) { + $ret = CommandAttr (undef, "$devName $da ".$ah->{$da}); + if ($ret) { + HMCCU_Log ($hash, 2, "Attr command failed $devName $da ".$ah->{$da}.". $ret"); + $cs->{attrFailed}{$devName.$da} = "$defAdd [$ccuName]"; + } + } + } + + return 1; +} + +###################################################################### +# Build group attribute +###################################################################### + +sub HMCCU_BuildGroupAttr ($$$$;$) +{ + my ($ioHash, $address, $ccuType, $ah, $groupName) = @_; + + $groupName //= AttrVal ($ioHash->{NAME}, 'createDeviceGroup', ''); + if ($groupName ne '') { + my $devName = HMCCU_GetDeviceName ($ioHash, $address); + $groupName =~ s/%n/$devName/g; + $groupName =~ s/%a/$address/g; + $groupName =~ s/%t/$ccuType/g; + $ah->{group} = $groupName; + return 1; + } + + return 0; +} + +###################################################################### +# Create device name +###################################################################### + +sub HMCCU_MakeDeviceName ($$$$$) +{ + my ($defAdd, $devPrefix, $devFormat, $devSuffix, $ccuName) = @_; + + my $devName = $devPrefix.$devFormat.$devSuffix; + $devName =~ s/%n/$ccuName/g; + $devName =~ s/%a/$defAdd/g; + $devName =~ s/[^A-Za-z\d_\.]+/_/g; + + return $devName; +} + +###################################################################### +# Add new CCU or FHEM device or channel +# If $devName is undef, a new device or channel will be added to IO +# device if it doesn't exist. +# This function is called during device definition in HMCCUDEV and +# HMCCUCHN. +###################################################################### + +sub HMCCU_AddDevice ($$$;$) +{ + my ($ioHash, $iface, $address, $devName) = @_; + + if (defined($devName)) { + # Device description must exist + return if (!exists($ioHash->{hmccu}{device}{$iface}{$address})); + my @devList = (); + if (defined($ioHash->{hmccu}{device}{$iface}{$address}{_fhem})) { + @devList = split (',', $ioHash->{hmccu}{device}{$iface}{$address}{_fhem}); + # Prevent duplicate device names + foreach my $d (@devList) { return if ($d eq $devName); } + } + + push @devList, $devName; + $ioHash->{hmccu}{device}{$iface}{$address}{_fhem} = join(',', @devList); + } + elsif (!exists($ioHash->{hmccu}{device}{$iface}{$address})) { + my ($rpcDevice, undef) = HMCCU_GetRPCDevice ($ioHash, 0, $iface); + if ($rpcDevice ne '') { + my $rpcHash = $defs{$rpcDevice}; + HMCCURPCPROC_GetDeviceDesc ($rpcHash, $address); + HMCCURPCPROC_GetParamsetDesc ($rpcHash, $address); + } + } +} + +###################################################################### +# Delete CCU or FHEM device or Channel +# If $devName is undef, a device or a channel will be removed from +# IO device. +###################################################################### + +sub HMCCU_RemoveDevice ($$$;$) +{ + my ($ioHash, $iface, $address, $devName) = @_; + + return if (!exists($ioHash->{hmccu}{device}{$iface}{$address})); + + if (defined($devName)) { + if (defined($ioHash->{hmccu}{device}{$iface}{$address}{_fhem})) { + my @devList = grep { $_ ne $devName } split(',', $ioHash->{hmccu}{device}{$iface}{$address}{_fhem}); + $ioHash->{hmccu}{device}{$iface}{$address}{_fhem} = scalar(@devList) > 0 ? + join(',', @devList) : undef; + } + } + elsif (exists($ioHash->{hmccu}{device}{$iface}{$address})) { + delete $ioHash->{hmccu}{device}{$iface}{$address}; + } +} + +###################################################################### +# Update client device or channel +# Store receiver, sender +###################################################################### + +sub HMCCU_UpdateDevice ($$) +{ + my ($ioHash, $clHash) = @_; + + return if (!exists($clHash->{ccuif}) || !exists($clHash->{ccuaddr})); + + my $clType = $clHash->{TYPE}; + my $address = $clHash->{ccuaddr}; + my $iface = $clHash->{ccuif}; + my ($da, $dc) = HMCCU_SplitChnAddr ($address); + + # Update device information + if (exists($ioHash->{hmccu}{device}{$iface}{$da})) { + $clHash->{firmware} = $ioHash->{hmccu}{device}{$iface}{$da}{FIRMWARE} // '?'; + } + + # Update link receivers + if (exists($ioHash->{hmccu}{snd}{$iface}{$da})) { + delete $clHash->{receiver} if (exists($clHash->{receiver})); + my @rcvList = (); + + foreach my $c (sort keys %{$ioHash->{hmccu}{snd}{$iface}{$da}}) { +# next if ($clType eq 'HMCCUCHN' && "$c" ne "$dc"); + foreach my $r (keys %{$ioHash->{hmccu}{snd}{$iface}{$da}{$c}}) { + my ($la, $lc) = HMCCU_SplitChnAddr ($r); + next if ($la eq $da); # Ignore link if receiver = current device + my @rcvNames = HMCCU_GetDeviceIdentifier ($ioHash, $r, $iface); + my $rcvFlags = HMCCU_FlagsToStr ('peer', 'FLAGS', + $ioHash->{hmccu}{snd}{$iface}{$da}{$c}{$r}{FLAGS}, ','); + push @rcvList, map { $_.($rcvFlags ne 'OK' ? " [".$rcvFlags."]" : '') } @rcvNames; + } + } + + $clHash->{receiver} = join (',', HMCCU_Unique (@rcvList)) if (scalar(@rcvList) > 0); + } + + # Update link senders + if (exists($ioHash->{hmccu}{rcv}{$iface}{$da})) { + delete $clHash->{sender} if (exists($clHash->{sender})); + my @sndList = (); + + foreach my $c (sort keys %{$ioHash->{hmccu}{rcv}{$iface}{$da}}) { +# next if ($clType eq 'HMCCUCHN' && "$c" ne "$dc"); + foreach my $s (keys %{$ioHash->{hmccu}{rcv}{$iface}{$da}{$c}}) { + my ($la, $lc) = HMCCU_SplitChnAddr ($s); + next if ($la eq $da); # Ignore link if sender = current device + my @sndNames = HMCCU_GetDeviceIdentifier ($ioHash, $s, $iface); + my $sndFlags = HMCCU_FlagsToStr ('peer', 'FLAGS', + $ioHash->{hmccu}{snd}{$iface}{$da}{$c}{$s}{FLAGS}, ','); + push @sndList, map { $_.($sndFlags ne 'OK' ? " [".$sndFlags."]" : '') } @sndNames; + } + } + + $clHash->{sender} = join (',', HMCCU_Unique (@sndList)) if (scalar(@sndList) > 0); + } +} + +###################################################################### +# Update device roles +###################################################################### + +sub HMCCU_UpdateDeviceRoles ($$;$$) +{ + my ($ioHash, $clHash, $iface, $address) = @_; + + my $clType = $clHash->{TYPE}; + + $iface //= $clHash->{ccuif}; + $address //= $clHash->{ccuaddr}; + return if (!defined($address)); + + my $dd = HMCCU_GetDeviceDesc ($ioHash, $address, $iface); + if (!defined($dd)) { + HMCCU_Log ($clHash, 2, "Can't get device description for $address ".stacktraceAsString(undef)); + return; + } + + if ($clType eq 'HMCCUCHN' && defined($dd->{TYPE})) { +# $clHash->{ccurole} = $dd->{TYPE}; + $clHash->{hmccu}{role} = $dd->{INDEX}.':'.$dd->{TYPE}; + my $pdd = HMCCU_GetDeviceDesc ($ioHash, $dd->{PARENT}, $iface); + if (defined($pdd)) { + $clHash->{ccutype} = defined($pdd->{TYPE}) && $pdd->{TYPE} ne '' ? $pdd->{TYPE} : '?'; + $clHash->{ccusubtype} = defined($pdd->{SUBTYPE}) && $pdd->{SUBTYPE} ne '' ? $pdd->{SUBTYPE} : $clHash->{ccutype}; + } + } + elsif ($clType eq 'HMCCUDEV' && defined($dd->{CHILDREN})) { + my @roles = (); + foreach my $c (split(',', $dd->{CHILDREN})) { + my $cdd = HMCCU_GetDeviceDesc ($ioHash, $c, $iface); + if (defined($cdd) && defined($cdd->{TYPE}) && $cdd->{TYPE} ne '') { + push @roles, $cdd->{INDEX}.':'.$cdd->{TYPE}; + } + } + $clHash->{hmccu}{role} = join(',', @roles) if (scalar(@roles) > 0); + $clHash->{ccutype} = defined($dd->{TYPE}) && $dd->{TYPE} ne '' ? $dd->{TYPE} : '?'; + $clHash->{ccusubtype} = defined($dd->{SUBTYPE}) && $dd->{SUBTYPE} ne '' ? $dd->{SUBTYPE} : $clHash->{ccutype}; + } +} + +###################################################################### +# Rename a client device +###################################################################### + +sub HMCCU_RenameDevice ($$$) +{ + my ($ioHash, $clHash, $oldName) = @_; + + return 0 if (!defined($ioHash) || !defined($clHash) || !exists($clHash->{ccuif}) || + !exists($clHash->{ccuaddr})); + + my $name = $clHash->{NAME}; + my $iface = $clHash->{ccuif}; + my $address = $clHash->{ccuaddr}; + + return 0 if (!exists($ioHash->{hmccu}{device}{$iface}{$address})); + + if (exists($ioHash->{hmccu}{device}{$iface}{$address}{_fhem})) { + my @devList = map { $_ eq $oldName ? $name : $_ } split(',', $ioHash->{hmccu}{device}{$iface}{$address}{_fhem}); + $ioHash->{hmccu}{device}{$iface}{$address}{_fhem} = join(',', @devList); + } + else { + $ioHash->{hmccu}{device}{$iface}{$address}{_fhem} = $name; + } + + # Update links, but not the roles + HMCCU_UpdateDevice ($ioHash, $clHash); + + return 1; +} + +###################################################################### +# Initialize user attributes statedatapoint and controldatapoint +###################################################################### + +sub HMCCU_SetSCAttributes ($$;$) +{ + my ($ioHash, $clHash, $detect) = @_; + + my $name = $clHash->{NAME} // return; + my $type = $clHash->{TYPE} // return; + my $ccuType = $clHash->{ccutype} // return; + my $ccuAddr = $clHash->{ccuaddr} // return; + my $ccuIf = $clHash->{ccuif} // return; + $detect //= HMCCU_DetectDevice ($ioHash, $ccuAddr, $ccuIf); + + # Get readable and writeable datapoints + my @dpWrite = (); + my @dpRead = (); + my ($da, $dc) = HMCCU_SplitChnAddr ($ccuAddr, -2); + my $dpWriteCnt = HMCCU_GetValidDatapoints ($clHash, $ccuType, $dc, 2, \@dpWrite); + my $dpReadCnt = HMCCU_GetValidDatapoints ($clHash, $ccuType, $dc, 5, \@dpRead); + + # Detect device and initialize attribute lists for statedatapoint and controldatapoint + my @userattr = grep (!/statedatapoint|controldatapoint/, split(' ', $modules{$clHash->{TYPE}}{AttrList})); + if (defined($detect) && $detect->{level} > 0) { + $clHash->{hmccu}{detect} = $detect->{level}; + if ($type eq 'HMCCUDEV') { + push @userattr, 'statedatapoint:select,'. + join(',', sort map { $_.'.'.$detect->{stateRole}{$_}{datapoint} } keys %{$detect->{stateRole}}) + if ($detect->{stateRoleCount} > 0); + push @userattr, 'controldatapoint:select,'. + join(',', sort map { $_.'.'.$detect->{controlRole}{$_}{datapoint} } keys %{$detect->{controlRole}}) + if ($detect->{controlRoleCount} > 0); + } + elsif ($type eq 'HMCCUCHN') { + push @userattr, 'statedatapoint:select,'. + join(',', sort map { $detect->{stateRole}{$_}{datapoint} } keys %{$detect->{stateRole}}) + if ($detect->{stateRoleCount} > 0); + push @userattr, 'controldatapoint:select,'. + join(',', sort map { $detect->{controlRole}{$_}{datapoint} } keys %{$detect->{controlRole}}) + if ($detect->{controlRoleCount} > 0); + } + } + else { + push @userattr, 'statedatapoint:select,'.join(',', sort @dpRead) if ($dpReadCnt > 0); + push @userattr, 'controldatapoint:select,'.join(',', sort @dpWrite) if ($dpWriteCnt > 0); + $clHash->{hmccu}{detect} = 0; + } + + # Make sure that generic attributes are available, if no role attributes found + push @userattr, 'statedatapoint' if (!grep(/statedatapoint/, @userattr)); + push @userattr, 'controldatapoint' if (!grep(/controldatapoint/, @userattr)); + + setDevAttrList ($name, join(' ', @userattr)) if (scalar(@userattr) > 0); +} + +###################################################################### +# Return role of a channel as stored in device hash +# Parameter chnNo is ignored for HMCCUCHN devices. If chnNo is not +# specified for a HMCCUDEV device, the control channel is used. +# Returns role name or empty string if role cannot be detected. +###################################################################### + +sub HMCCU_GetChannelRole ($;$) +{ + my ($clHash, $chnNo) = @_; + + return '' if (!defined($clHash->{hmccu}{role}) || $clHash->{hmccu}{role} eq ''); + + if (!defined($chnNo) || $chnNo eq '' || $chnNo == -1) { + if ($clHash->{TYPE} eq 'HMCCUCHN') { + my ($ad, $cc) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + $chnNo = $cc; + } + } + if (defined($chnNo) && $chnNo ne '' && $chnNo != -1) { + foreach my $role (split(',', $clHash->{hmccu}{role})) { + my ($c, $r) = split(':', $role); + return $r if (defined($r) && "$c" eq "$chnNo"); + } + } + + return ''; +} + +###################################################################### +# Return role(s) of a device or a channel identified by address +# Parameters: +# $mode: 0 = All roles, 1 = Only supported roles +###################################################################### + +sub HMCCU_GetDeviceRoles ($$$;$) +{ + my ($ioHash, $iface, $address, $mode) = @_; + $mode //= 0; # By default get all roles + my @roles = (); + + if (exists($ioHash->{hmccu}{device}{$iface}{$address})) { + if ($ioHash->{hmccu}{device}{$iface}{$address}{_addtype} eq 'dev') { + foreach my $chAddress (split(',', $ioHash->{hmccu}{device}{$iface}{$address}{CHILDREN})) { + my $r = $ioHash->{hmccu}{device}{$iface}{$chAddress}{TYPE}; + push @roles, $r if (exists($ioHash->{hmccu}{device}{$iface}{$chAddress}) && + $ioHash->{hmccu}{device}{$iface}{$chAddress}{_addtype} eq 'chn' && + ($mode == 0 || ($mode == 1 && exists($HMCCU_STATECONTROL->{$r})))); + } + } + elsif ($ioHash->{hmccu}{device}{$iface}{$address}{_addtype} eq 'chn') { + my $r = $ioHash->{hmccu}{device}{$iface}{$address}{TYPE}; + push @roles, $r if ($mode == 0 || ($mode == 1 && exists($HMCCU_STATECONTROL->{$r}))); + } + } + + return @roles; +} + +###################################################################### +# Get device configuration for all interfaces from CCU +###################################################################### + +sub HMCCU_GetDeviceConfig ($) +{ + my ($ioHash, $ifaceExpr) = @_; + + my ($cDev, $cPar, $cLnk) = (0, 0, 0); + my $c = 0; + + my $interfaces = HMCCU_GetRPCInterfaceList ($ioHash, 0); + foreach my $iface (keys %$interfaces) { + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($ioHash, 1, $iface); + if ($rpcdev ne '') { + my $rpcHash = $defs{$rpcdev}; + HMCCU_Log ($ioHash, 2, "Reading Device Descriptions for interface $iface"); + $c = HMCCURPCPROC_GetDeviceDesc ($rpcHash); + HMCCU_Log ($ioHash, 2, "Read $c Device Descriptions for interface $iface"); + $cDev += $c; + HMCCU_Log ($ioHash, 2, "Reading Paramset Descriptions for interface $iface"); + $c = HMCCURPCPROC_GetParamsetDesc ($rpcHash); + HMCCU_Log ($ioHash, 2, "Read $c Paramset Descriptions for interface $iface"); + $cPar += $c; + HMCCU_Log ($ioHash, 2, "Reading Peer Descriptions for interface $iface"); + $c = HMCCURPCPROC_GetPeers ($rpcHash); + HMCCU_Log ($ioHash, 2, "Read $c Peer Descriptions for interface $iface"); + $cLnk += $c; + } + else { + HMCCU_Log ($ioHash, 2, "No RPC device found for interface $iface. Can't read device config."); + } + } + + my @ccuDevList = (); + my @ccuSuppDevList = (); + my @ccuSuppTypes = (); + my @ccuNotSuppTypes = (); + foreach my $di (sort keys %{$ioHash->{hmccu}{device}}) { + foreach my $da (sort keys %{$ioHash->{hmccu}{device}{$di}}) { + next if ($ioHash->{hmccu}{device}{$di}{$da}{_addtype} ne 'dev'); + my $devName = $ioHash->{hmccu}{device}{$di}{$da}{_name}; + my $devModel = $ioHash->{hmccu}{device}{$di}{$da}{_model}; + if ($devName =~ / /) { + $devName = qq("$devName"); + $devName =~ s/ /#/g; + } + push @ccuDevList, $devName; + my $detect = HMCCU_DetectDevice ($ioHash, $da, $di); + if (defined($detect) && $da ne 'HmIP-RCV-1' && $da ne 'BidCoS-RF') { + push @ccuSuppDevList, $devName; + push @ccuSuppTypes, $devModel; + } + else { + push @ccuNotSuppTypes, $devModel; + } + } + } + $ioHash->{hmccu}{ccuDevList} = join(',', sort @ccuDevList); + $ioHash->{hmccu}{ccuSuppDevList} = join(',', sort @ccuSuppDevList); + $ioHash->{hmccu}{ccuTypes}{supported} = join(',', sort @ccuSuppTypes); + $ioHash->{hmccu}{ccuTypes}{unsupported} = join(',', sort @ccuNotSuppTypes); + + # Set CCU firmware version + if (exists($ioHash->{hmccu}{device}{'BidCos-RF'}) && exists($ioHash->{hmccu}{device}{'BidCos-RF'}{'BidCoS-RF'})) { + $ioHash->{firmware} = $ioHash->{hmccu}{device}{'BidCos-RF'}{'BidCoS-RF'}{FIRMWARE} // '?'; + } + + # Get defined FHEM devices + my @devList = HMCCU_FindClientDevices ($ioHash, '(HMCCUDEV|HMCCUCHN)'); + + # Add devices + foreach my $d (@devList) { + my $clHash = $defs{$d}; + if (exists($clHash->{ccuaddr}) && exists($clHash->{ccuif})) { + HMCCU_AddDevice ($ioHash, $clHash->{ccuif}, $clHash->{ccuaddr}, $d); + } + } + + # Update FHEM devices + foreach my $d (@devList) { + my $clHash = $defs{$d}; + my $name = $clHash->{NAME}; + + if (!exists($clHash->{ccuaddr})) { + HMCCU_Log ($ioHash, 2, "Disabling client device $name because CCU address is missing. Does the device exist on CCU?"); + CommandAttr (undef, "$name disable 1"); + next; + } + HMCCU_SetSCAttributes ($ioHash, $clHash); + HMCCU_UpdateDevice ($ioHash, $clHash); + HMCCU_UpdateDeviceRoles ($ioHash, $clHash); + + my ($sc, $sd, $cc, $cd) = HMCCU_GetSCDatapoints ($clHash); + + HMCCU_UpdateRoleCommands ($ioHash, $clHash, $cc); + HMCCU_UpdateAdditionalCommands ($ioHash, $clHash, $cc, $cd); + } + + return ($cDev, $cPar, $cLnk); +} + +###################################################################### +# Add new device. +# Arrays are converted to a comma separated string. Device description +# is stored in $hash->{hmccu}{device}. +# Address type and name of interface will be added to standard device +# description in hash elements "_addtype" and "_interface". +# Parameters: +# $desc - Hash reference with RPC device description. +# $key - Key of device description hash (i.e. "ADDRESS"). +# $iface - RPC interface name (i.e. "BidCos-RF"). +###################################################################### + +sub HMCCU_AddDeviceDesc ($$$$) +{ + my ($hash, $desc, $key, $iface) = @_; + + return 0 if (!exists($desc->{$key})); + + my $k = $desc->{$key}; + + foreach my $p (keys %$desc) { + if (ref($desc->{$p}) eq 'ARRAY') { + $hash->{hmccu}{device}{$iface}{$k}{$p} = join(',', @{$desc->{$p}}); + } + else { + my $d = $desc->{$p}; + $d =~ s/ /,/g; + $hash->{hmccu}{device}{$iface}{$k}{$p} = $d; + } + } + + $hash->{hmccu}{device}{$iface}{$k}{_interface} = $iface; + if (defined($desc->{PARENT}) && $desc->{PARENT} ne '') { + $hash->{hmccu}{device}{$iface}{$k}{_addtype} = 'chn'; + $hash->{hmccu}{device}{$iface}{$k}{_fw_ver} = $hash->{hmccu}{device}{$iface}{$desc->{PARENT}}{_fw_ver}; + $hash->{hmccu}{device}{$iface}{$k}{_model} = $hash->{hmccu}{device}{$iface}{$desc->{PARENT}}{_model}; + $hash->{hmccu}{device}{$iface}{$k}{_name} = HMCCU_GetChannelName ($hash, $k); + } + else { + $hash->{hmccu}{device}{$iface}{$k}{_addtype} = 'dev'; + my $fw_ver = $desc->{FIRMWARE}; + $fw_ver =~ s/[-\.]/_/g; + $hash->{hmccu}{device}{$iface}{$k}{_fw_ver} = $fw_ver."-".$desc->{VERSION}; + $hash->{hmccu}{device}{$iface}{$k}{_model} = $desc->{TYPE}; + $hash->{hmccu}{device}{$iface}{$k}{_name} = HMCCU_GetDeviceName ($hash, $k); + $hash->{hmccu}{device}{$iface}{$k}{_valid} = 1; + } + + return 1; +} + +###################################################################### +# Get device description. +# Parameters: +# $hash - Hash reference of IO device. +# $address - Address of device or channel. Accepts a channel address +# with channel number 'd' as an alias for device address. +# $iface - Interface name (optional). +# Return hash reference for device description or undef on error. +###################################################################### + +sub HMCCU_GetDeviceDesc ($$;$) +{ + my ($hash, $address, $iface) = @_; + + return undef if (!exists($hash->{hmccu}{device})); + + if (!defined($address)) { + HMCCU_Log ($hash, 2, "Address not defined for device\n".stacktraceAsString(undef)); + return undef; + } + + my @ifaceList = (); + if (defined($iface)) { + push (@ifaceList, $iface); + } + else { + push (@ifaceList, keys %{$hash->{hmccu}{device}}); + } + + $address =~ s/:d//; + foreach my $i (@ifaceList) { + return $hash->{hmccu}{device}{$i}{$address} + if (exists($hash->{hmccu}{device}{$i}{$address})); + } + + return undef; +} + +###################################################################### +# Get list of device identifiers +# CCU device names are preceeded by "ccu:". +# If no names were found, the address is returned. +###################################################################### + +sub HMCCU_GetDeviceIdentifier ($$;$$) +{ + my ($ioHash, $address, $iface, $chnNo) = @_; + + my @idList = (); + my ($da, $dc) = HMCCU_SplitChnAddr ($address); +# my $c = defined($chnNo) ? ' #'.$chnNo : ''; + my $c = ''; + + my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $address, $iface); + if (defined($devDesc)) { + if (defined($devDesc->{_fhem})) { + push @idList, map { $_.$c } split(',', $devDesc->{_fhem}); + } + elsif (defined($devDesc->{PARENT}) && $devDesc->{PARENT} ne '') { + push @idList, HMCCU_GetDeviceIdentifier ($ioHash, $devDesc->{PARENT}, $iface, $dc); + } + elsif (defined($devDesc->{_name})) { + push @idList, "ccu:".$devDesc->{_name}; + } + } + + return scalar(@idList) > 0 ? @idList : ($address); +} + +###################################################################### +# Convert device description to string +# Parameter $object can be a device or channel address or a client +# device hash reference. +###################################################################### + +sub HMCCU_DeviceDescToStr ($$) +{ + my ($ioHash, $object) = @_; + + my $result = ''; + my $address; + my $iface; + + if (ref($object) eq 'HASH') { + $address = $object->{ccuaddr}; + $iface = $object->{ccuif}; + } + else { + $address = $object; + } + + my ($devAddr, $chnNo) = HMCCU_SplitChnAddr ($address); + my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $devAddr, $iface); + return undef if (!defined($devDesc)); + my @addList = ($devAddr); + push (@addList, split (',', $devDesc->{CHILDREN})) + if (defined($devDesc->{CHILDREN}) && $devDesc->{CHILDREN} ne ''); + + foreach my $a (@addList) { + my ($d, $c) = HMCCU_SplitChnAddr ($a); + next if ($chnNo ne '' && "$c" ne '0' && "$c" ne "$chnNo" && $c ne ''); + + $devDesc = HMCCU_GetDeviceDesc ($ioHash, $a, $iface); + return undef if (!defined($devDesc)); + + my $channelType = $devDesc->{TYPE}; + my $status = exists($HMCCU_STATECONTROL->{$channelType}) ? ' known' : ''; + $result .= $a eq $devAddr ? "Device $a" : "Channel $a"; + $result .= " $devDesc->{_name} [$channelType]$status
"; + foreach my $n (sort keys %{$devDesc}) { + next if ($n =~ /^_/ || $n =~ /^(ADDRESS|TYPE|INDEX|VERSION)$/ || + !defined($devDesc->{$n}) || $devDesc->{$n} eq ''); + $result .= "  $n: ".HMCCU_FlagsToStr ('device', $n, $devDesc->{$n}, ',', '')."
"; + } + } + + return $result; +} + +###################################################################### +# Convert parameter set description to string +# Parameter $object can be an address or a reference to a client hash. +###################################################################### + +sub HMCCU_ParamsetDescToStr ($$) +{ + my ($ioHash, $object) = @_; + + my $result = ''; + my $address; + my $iface; + + if (ref($object) eq 'HASH') { + $address = $object->{ccuaddr}; + $iface = $object->{ccuif}; + } + else { + $address = $object; + } + + my ($devAddr, $chnNo) = HMCCU_SplitChnAddr ($address); + +# BUG? +# my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $address, $iface); + my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $devAddr, $iface); + return undef if (!defined($devDesc)); + my $model = HMCCU_GetDeviceModel ($ioHash, $devDesc->{_model}, $devDesc->{_fw_ver}); + return undef if (!defined($model)); + + my @chnList = (); + if ($chnNo eq '') { + push @chnList, sort keys %{$model}; + unshift (@chnList, pop(@chnList)) if (exists($model->{'d'})); + } + else { + push @chnList, 'd', 0, $chnNo; + } + + foreach my $c (@chnList) { + $result .= $c eq 'd' ? "Device
" : "Channel $c
"; + foreach my $ps (sort keys %{$model->{$c}}) { + $result .= "  Paramset $ps
"; + $result .= join ("
", map { + "    ".$_.": ". + $model->{$c}{$ps}{$_}{TYPE}. + " [".HMCCU_FlagsToStr ('model', 'OPERATIONS', $model->{$c}{$ps}{$_}{OPERATIONS}, ',', '')."]". + " [".HMCCU_FlagsToStr ('model', 'FLAGS', $model->{$c}{$ps}{$_}{FLAGS}, ',', '')."]". + " RANGE=".HMCCU_StripNumber ($model->{$c}{$ps}{$_}{MIN}, 2). + "...".HMCCU_StripNumber ($model->{$c}{$ps}{$_}{MAX}, 2). + " DFLT=".HMCCU_StripNumber ($model->{$c}{$ps}{$_}{DEFAULT}, 2). + HMCCU_ISO2UTF (HMCCU_DefStr ($model->{$c}{$ps}{$_}{UNIT}, " UNIT=")). + HMCCU_DefStr ($model->{$c}{$ps}{$_}{VALUE_LIST}, " VALUES=") + } sort keys %{$model->{$c}{$ps}})."
"; + } + } + + $result .= ''; + + return $result; +} + +###################################################################### +# Get device addresses. +# Parameters: +# $iface - Interface name. If set to undef, all devices are +# returned. +# $filter - Filter expression in format Attribute=RegExp[,...]. +# Attribute is a valid device description parameter name or +# "_addtype" or "_interface". +# Return array with addresses. +###################################################################### + +sub HMCCU_GetDeviceAddresses ($;$$) +{ + my ($hash, $iface, $filter) = @_; + + my @addList = (); + my @ifaceList = (); + + return @addList if (!exists($hash->{hmccu}{device})); + + if (defined($iface)) { + push (@ifaceList, $iface); + } + else { + push (@ifaceList, keys %{$hash->{hmccu}{device}}); + } + + if (defined($filter)) { + my %f = (); + foreach my $fd (split (',', $filter)) { + my ($fa, $fv) = split ('=', $fd); + $f{$fa} = $fv if (defined($fv)); + } + return undef if (scalar(keys(%f)) == 0); + + foreach my $i (@ifaceList) { + foreach my $a (keys %{$hash->{hmccu}{device}{$i}}) { + my $n = 0; + foreach my $fr (keys(%f)) { + if (HMCCU_ExprNotMatch ($hash->{hmccu}{device}{$i}{$a}{$fr}, $f{$fr}, 1)) { + $n = 1; + last; + } + } + push (@addList, $a) if ($n == 0); + } + } + } + else { + foreach my $i (@ifaceList) { + push (@addList, keys %{$hash->{hmccu}{device}{$i}}); + } + } + + return @addList; +} + +###################################################################### +# Check if device model is already known by HMCCU +# $type - The device model +# $fw_ver - combined key of firmware and description version +# $chnNo - Channel number or 'd' for device +###################################################################### + +sub HMCCU_ExistsDeviceModel ($$$;$) +{ + my ($hash, $type, $fw_ver, $chnNo) = @_; + + return 0 if (!exists($hash->{hmccu}{model})); + + if (defined($chnNo)) { + return (exists($hash->{hmccu}{model}{$type}) && exists($hash->{hmccu}{model}{$type}{$fw_ver}) && + exists($hash->{hmccu}{model}{$type}{$fw_ver}{$chnNo}) ? 1 : 0); + } + else { + return (exists($hash->{hmccu}{model}{$type}) && exists($hash->{hmccu}{model}{$type}{$fw_ver}) ? 1 : 0); + } +} + +###################################################################### +# Add new device model +# Parameters: +# $desc - Hash reference with paramset description +# $type - The device model +# $fw_ver - combined key of firmware and description version +# $paramset - Name of parameter set +# $chnNo - Channel number or 'd' for device +###################################################################### + +sub HMCCU_AddDeviceModel ($$$$$$) +{ + my ($hash, $desc, $type, $fw_ver, $paramset, $chnNo) = @_; + + # Process list of parameter names + foreach my $p (keys %$desc) { + # Process parameter attributes + foreach my $a (keys %{$desc->{$p}}) { + if (ref($desc->{$p}{$a}) eq 'HASH') { + # Process sub attributes + foreach my $s (keys %{$desc->{$p}{$a}}) { + if (ref($desc->{$p}{$a}{$s}) eq 'ARRAY') { + # Store array elements as list + $hash->{hmccu}{model}{$type}{$fw_ver}{$chnNo}{$paramset}{$p}{$a}{$s} = join(',', @{$desc->{$p}{$a}{$s}}); + } + elsif (ref($desc->{$p}{$a}{$s}) eq 'HASH') { + HMCCU_Log ($hash, 2, "HASH ref $type $chnNo $p $a $s"); + $hash->{hmccu}{model}{$type}{$fw_ver}{$chnNo}{$paramset}{$p}{$a}{$s} = $desc->{$p}{$a}{$s}; + } + else { + # Value + $hash->{hmccu}{model}{$type}{$fw_ver}{$chnNo}{$paramset}{$p}{$a}{$s} = $desc->{$p}{$a}{$s}; + } + } + } + elsif (ref($desc->{$p}{$a}) eq 'ARRAY') { + $hash->{hmccu}{model}{$type}{$fw_ver}{$chnNo}{$paramset}{$p}{$a} = join(',', @{$desc->{$p}{$a}}); + } + else { + $hash->{hmccu}{model}{$type}{$fw_ver}{$chnNo}{$paramset}{$p}{$a} = $desc->{$p}{$a}; + } + } + } +} + +###################################################################### +# Get device model +# Parameters: +# $chnNo - Channel number. Use 'd' for device entry. If not defined +# a reference to the master entry is returned. +###################################################################### + +sub HMCCU_GetDeviceModel ($$$;$) +{ + my ($hash, $type, $fw_ver, $chnNo) = @_; + + return undef if (!exists($hash->{hmccu}{model})); + + if (defined($chnNo)) { + return (exists($hash->{hmccu}{model}{$type}{$fw_ver}{$chnNo}) ? + $hash->{hmccu}{model}{$type}{$fw_ver}{$chnNo} : undef); + } + else { + return (exists($hash->{hmccu}{model}{$type}{$fw_ver}) ? + $hash->{hmccu}{model}{$type}{$fw_ver} : undef); + } +} + +###################################################################### +# Get device model for client device +# Parameters: +# $hash - Hash reference for device of type HMCCUCHN or HMCCUDEV. +# $chnNo - Channel number. Use 'd' for device entry. If not defined +# a reference to the master entry is returned. +###################################################################### + +sub HMCCU_GetClientDeviceModel ($;$) +{ + my ($clHash, $chnNo) = @_; + + return undef if ( + ($clHash->{TYPE} ne 'HMCCUCHN' && $clHash->{TYPE} ne 'HMCCUDEV') || + (!defined($clHash->{ccuaddr}))); + + my $ioHash = HMCCU_GetHash ($clHash); + my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $clHash->{ccuaddr}, $clHash->{ccuif}); + + return defined($devDesc) ? + HMCCU_GetDeviceModel ($ioHash, $devDesc->{_model}, $devDesc->{_fw_ver}, $chnNo) : undef; +} + +###################################################################### +# Get parameter defintion of device model +# Parameters: +# $hash - Hash reference of IO device. +# $object - Device or channel address or device description +# reference. +# $paramset - Valid paramset for device or channel. +# $parameter - Parameter name. +# Returns undef on error. On success return a reference to the +# parameter or parameter set definition, if $parameter is not +# specified. +###################################################################### + +sub HMCCU_GetParamDef ($$$;$) +{ + my ($hash, $object, $paramset, $parameter) = @_; + + return undef if (!defined($object)); + + my $devDesc = ref($object) eq 'HASH' ? $object : HMCCU_GetDeviceDesc ($hash, $object); + + if (defined($devDesc)) { + # Build device address and channel number + my $a = $devDesc->{ADDRESS}; + my ($devAddr, $chnNo) = ($a =~ /:[0-9]{1,2}$/) ? HMCCU_SplitChnAddr ($a) : ($a, 'd'); + + my $model = HMCCU_GetDeviceModel ($hash, $devDesc->{_model}, $devDesc->{_fw_ver}, $chnNo); + if (!defined($paramset)) { + HMCCU_Log ($hash, 2, "$a Paramset not defined ".stacktraceAsString(undef)); + return undef; + } + if (defined($model) && exists($model->{$paramset})) { + if (defined($parameter)) { + return exists($model->{$paramset}{$parameter}) ? $model->{$paramset}{$parameter} : undef; + } + else { + return $model->{$paramset} + } + } + } + + return undef; +} + +###################################################################### +# Find parameter defintion of device model +# Parameters: +# $hash - Hash reference of IO device. +# $object - Device or channel address or device description +# reference. +# $parameter - Parameter name. +# Returns (undef,undef) on error. Otherwise parameter set name and +# reference to the parameter definition. +###################################################################### + +sub HMCCU_FindParamDef ($$$) +{ + my ($hash, $object, $parameter) = @_; + + my $devDesc = ref($object) eq 'HASH' ? $object : HMCCU_GetDeviceDesc ($hash, $object); + + if (defined($devDesc)) { + # Build device address and channel number + my $a = $devDesc->{ADDRESS}; + my ($devAddr, $chnNo) = ($a =~ /:[0-9]{1,2}$/) ? HMCCU_SplitChnAddr ($a) : ($a, 'd'); + + my $model = HMCCU_GetDeviceModel ($hash, $devDesc->{_model}, $devDesc->{_fw_ver}, $chnNo); + if (defined($model)) { + foreach my $ps (keys %$model) { + return ($ps, $model->{$ps}{$parameter}) if (exists($model->{$ps}{$parameter})); + } + } + } + + return (undef, undef); +} + +###################################################################### +# Check if parameter exists +# Parameters: +# $clHash - Hash reference of client device. +# $object - Device or channel address or device description +# reference. +# $ps - Parameter set name. +# $parameter - Parameter name. +# $oper - Access mode: +# 1 = parameter readable +# 2 = parameter writeable +# 4 = parameter events +# Returns 0 or 1 +###################################################################### + +sub HMCCU_IsValidParameter ($$$$;$) +{ + my ($clHash, $object, $ps, $parameter, $oper) = @_; + + $oper //= 7; + my $ioHash = HMCCU_GetHash ($clHash) // return 0; + + my $devDesc = ref($object) eq 'HASH' ? $object : HMCCU_GetDeviceDesc ($ioHash, $object); + + if (defined($devDesc)) { + # Build device address and channel number + my $a = $devDesc->{ADDRESS}; + my ($devAddr, $chnNo) = ($a =~ /:[0-9]{1,2}$/) ? HMCCU_SplitChnAddr ($a) : ($a, 'd'); + + my $model = HMCCU_GetDeviceModel ($ioHash, $devDesc->{_model}, $devDesc->{_fw_ver}, $chnNo); + if (defined($model)) { + my @parList = ref($parameter) eq 'HASH' ? keys %$parameter : ($parameter); + foreach my $p (@parList) { + return 0 if (!exists($model->{$ps}) || !exists($model->{$ps}{$p}) || + !($model->{$ps}{$p}{OPERATIONS} & $oper)); + } + return 1; + } + } + + return 0; +} + +###################################################################### +# Convert parameter value +# Parameters: +# $hash - Hash reference of IO device. +# $object - Device/channel address or device description reference. +# $paramset - Parameter set. +# $parameter - Parameter name. +# $value - Parameter value. +# Return converted or original value. +###################################################################### + +sub HMCCU_GetParamValueConversion ($$$$$) +{ + my ($hash, $object, $paramset, $parameter, $value) = @_; + + # Conversion table + my %ct = ( + 'BOOL' => { 0 => 'false', 1 => 'true' } + ); + + return $value if (!defined($object)); + + $paramset = 'LINK' if ($paramset =~ /^LINK\..+$/); + my $paramDef = HMCCU_GetParamDef ($hash, $object, $paramset, $parameter) // return $value; + my $type = $paramDef->{TYPE} // return $value; + + return $ct{$type}{$value} if (exists($ct{$type}) && exists($ct{$type}{$value})); + + if ($type eq 'ENUM' && exists($paramDef->{VALUE_LIST})) { + my @vl = split(',', $paramDef->{VALUE_LIST}); + return $vl[$value] if ($value =~ /^[0-9]+$/ && $value < scalar(@vl)); + } + + return $value; +} + +###################################################################### +# Update client devices with peering information +# In addition peering information is stored in hash of IO device. +###################################################################### + +sub HMCCU_AddPeers ($$$) +{ + my ($ioHash, $peerList, $iface) = @_; + + foreach my $p (@$peerList) { + my ($sd, $sc) = HMCCU_SplitChnAddr ($p->{SENDER}); + my ($rd, $rc) = HMCCU_SplitChnAddr ($p->{RECEIVER}); + $ioHash->{hmccu}{snd}{$iface}{$sd}{$sc}{$p->{RECEIVER}}{NAME} = $p->{NAME}; + $ioHash->{hmccu}{snd}{$iface}{$sd}{$sc}{$p->{RECEIVER}}{DESCRIPTION} = $p->{DESCRIPTION}; + $ioHash->{hmccu}{snd}{$iface}{$sd}{$sc}{$p->{RECEIVER}}{FLAGS} = $p->{FLAGS}; + $ioHash->{hmccu}{rcv}{$iface}{$rd}{$rc}{$p->{SENDER}}{NAME} = $p->{NAME}; + $ioHash->{hmccu}{rcv}{$iface}{$rd}{$rc}{$p->{SENDER}}{DESCRIPTION} = $p->{DESCRIPTION}; + $ioHash->{hmccu}{rcv}{$iface}{$rd}{$rc}{$p->{SENDER}}{FLAGS} = $p->{FLAGS}; + } + + return scalar(@$peerList); +} + +###################################################################### +# Get list of receivers for a source address +###################################################################### + +sub HMCCU_GetReceivers ($$$) +{ + my ($ioHash, $address, $iface) = @_; + + my ($sd, $sc) = HMCCU_SplitChnAddr ($address); + if (exists($ioHash->{hmccu}{snd}{$iface}{$sd}{$sc})) { + return keys %{$ioHash->{hmccu}{snd}{$iface}{$sd}{$sc}}; + } + + return (); +} + +###################################################################### +# Check if receiver exists for a source address +###################################################################### + +sub HMCCU_IsValidReceiver ($$$$) +{ + my ($ioHash, $address, $iface, $receiver) = @_; + + my ($sd, $sc) = HMCCU_SplitChnAddr ($address); + return exists($ioHash->{hmccu}{snd}{$iface}{$sd}{$sc}{$receiver}) ? 1 : 0; +} + +####################################################################### +# Convert bitmask to text +# Parameters: +# $set - 'device', 'model' or 'peer'. +# $flag - Name of parameter. +# $value - Value of parameter. +# $sep - String separator. Default = ''. +# $default - Default value is returned if no bit is set. +# Return empty string on error. Return $default if no bit set. +# Return $value if $flag is not a bitmask. +###################################################################### + +sub HMCCU_FlagsToStr ($$$;$$) +{ + my ($set, $flag, $value, $sep, $default) = @_; + + $value //= 0; + $default //= ''; + $sep //= ''; + + my %bitmasks = ( + 'device' => { + 'DIRECTION' => { 1 => 'SENDER', 2 => 'RECEIVER' }, + 'FLAGS' => { 1 => 'Visible', 2 => 'Internal', 8 => 'DontDelete' }, + 'RX_MODE' => { 1 => 'ALWAYS', 2 => 'BURST', 4 => 'CONFIG', 8 => 'WAKEUP', 16 => 'LAZY_CONFIG' } + }, + 'model' => { + 'FLAGS' => { 1 => 'Visible', 2 => 'Internal', 4 => 'Transform', 8 => 'Service', 16 => 'Sticky' }, + 'OPERATIONS' => { 1 => 'R', 2 => 'W', 4 => 'E' } + }, + 'peer' => { + 'FLAGS' => { 1 => 'SENDER_BROKEN', 2 => 'RECEIVER_BROKEN' } + } + ); + + my %mappings = ( + 'device' => { + 'DIRECTION' => { 0 => 'NONE' } + }, + 'peer' => { + 'FLAGS' => { 0 => 'OK' } + } + ); + + return $value if (!exists($bitmasks{$set}{$flag}) && !exists($mappings{$set}{$flag})); + + return $mappings{$set}{$flag}{$value} + if (exists($mappings{$set}{$flag}) && exists($mappings{$set}{$flag}{$value})); + + if (exists($bitmasks{$set}{$flag})) { + my @list = (); + foreach my $b (sort keys %{$bitmasks{$set}{$flag}}) { + push (@list, $bitmasks{$set}{$flag}{$b}) if ($value & $b); + } + + return scalar(@list) == 0 ? $default : join($sep, @list); + } + + return $value; +} + ###################################################################### # Update a single client device datapoint considering scaling, reading # format and value substitution. @@ -3355,178 +4532,241 @@ sub HMCCU_UpdateDeviceTable ($$) sub HMCCU_UpdateSingleDatapoint ($$$$) { - my ($hash, $chn, $dpt, $value) = @_; + my ($clHash, $chn, $dpt, $value) = @_; - my $hmccu_hash = HMCCU_GetHash ($hash); - return $value if (!defined ($hmccu_hash)); - + my $ioHash = HMCCU_GetHash ($clHash) // $value; my %objects; - my $ccuaddr = $hash->{ccuaddr}; + my $ccuaddr = $clHash->{ccuaddr}; my ($devaddr, $chnnum) = HMCCU_SplitChnAddr ($ccuaddr); - $objects{$devaddr}{$chn}{$dpt} = $value; + $objects{$devaddr}{$chn}{VALUES}{$dpt} = $value; -# my $rc = HMCCU_UpdateMultipleDevices ($hmccu_hash, \%objects); - my $rc = HMCCU_UpdateSingleDevice ($hmccu_hash, $hash, \%objects, undef); - return (ref ($rc)) ? $rc->{$devaddr}{$chn}{$dpt} : $value; + my $rc = HMCCU_UpdateParamsetReadings ($ioHash, $clHash, \%objects); + return (ref($rc)) ? $rc->{$devaddr}{$chn}{VALUES}{$dpt} : $value; } ###################################################################### # Update readings of client device. # Parameter objects is a hash reference which contains updated data # for devices: -# {devaddr}{channelno}{datapoint} = value -# If client device is virtual device group: check if group members are -# affected by updates and update readings in virtual group device. -# Return a hash reference with datapoints and new values: -# {devaddr}{datapoint} = value +# {devaddr}{channelno}{paramset}{parameter} = value +# For links format of paramset is "LINK.receiver". +# channelno = 'd' for device parameters. +# Return hash reference for results or undef on error. ###################################################################### -sub HMCCU_UpdateSingleDevice ($$$$) +sub HMCCU_UpdateParamsetReadings ($$$;$) { - my ($ccuhash, $clthash, $objects, $alref) = @_; - my $ccuname = $ccuhash->{NAME}; - my $cltname = $clthash->{NAME}; - my $clttype = $clthash->{TYPE}; - my $fnc = "UpdateSingleDevice"; - - return 0 if (!defined ($clthash->{IODev}) || !defined ($clthash->{ccuaddr})); - return 0 if ($clthash->{IODev} != $ccuhash); - - # Build list of relevant addresses in object data hash - my ($devaddr, $cnum) = HMCCU_SplitChnAddr ($clthash->{ccuaddr}); - my @addlist = defined ($alref) ? @$alref : ($devaddr); - - # Check if update of device allowed - my $disable = AttrVal ($cltname, 'disable', 0); - my $update = AttrVal ($cltname, 'ccureadings', 1); - return 0 if ($update == 0 || $disable == 1 || $clthash->{ccudevstate} ne 'active'); - - # Get device parameters and attributes - my $ccuflags = HMCCU_GetFlags ($ccuname); - my $cf = HMCCU_GetFlags ($cltname); - my $peer = AttrVal ($cltname, 'peer', 'null'); - my $crf = HMCCU_GetAttrReadingFormat ($clthash, $ccuhash); - my $substitute = HMCCU_GetAttrSubstitute ($clthash, $ccuhash); - my ($sc, $st, $cc, $cd, $ss, $cs) = HMCCU_GetSpecialDatapoints ($clthash, '', 'STATE', '', ''); - - # Virtual device flag - my $vg = 0; - $vg = 1 if (($clthash->{ccuif} eq 'VirtualDevices' || $clthash->{ccuif} eq 'fhem') && - exists ($clthash->{ccugroup})); - - HMCCU_Trace ($clthash, 2, $fnc, "$cltname Objects = ".join(',', @addlist)); + my ($ioHash, $clHash, $objects, $addListRef) = @_; - # Store the resulting readings + my $ioName = $ioHash->{NAME}; + my $clName = $clHash->{NAME}; + my $clType = $clHash->{TYPE}; + + return undef if (!defined($clHash->{IODev}) || !defined($clHash->{ccuaddr}) || + $clHash->{IODev} != $ioHash); + + # Resulting readings my %results; # Updated internal values - my @chkeys = (); + my @chKeys = (); - # Update readings of client device with data from address list - readingsBeginUpdate ($clthash); + # Check if update of device allowed + my $ccuflags = HMCCU_GetFlags ($ioName); + my $disable = AttrVal ($clName, 'disable', 0); + my $update = AttrVal ($clName, 'ccureadings', HMCCU_IsFlag ($clName, 'noReadings') ? 0 : 1); + return undef if ($update == 0 || $disable == 1 || $clHash->{ccudevstate} ne 'active'); - foreach my $addr (@addlist) { - next if (!exists ($objects->{$addr})); + # Build list of affected addresses + my ($devAddr, $chnNo) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + my @addList = defined ($addListRef) ? @$addListRef : ($devAddr); + + # Determine virtual device flag + my $vg = ($clHash->{ccuif} eq 'VirtualDevices' && exists($clHash->{ccugroup}) && $clHash->{ccugroup} ne '') ? 1 : 0; + + # Get client device attributes + my $clFlags = HMCCU_GetFlags ($clName); + my $clRF = HMCCU_GetAttrReadingFormat ($clHash, $ioHash); + my $peer = AttrVal ($clName, 'peer', 'null'); + my $clInt = $clHash->{ccuif}; + my ($sc, $sd, $cc, $cd) = HMCCU_GetSCDatapoints ($clHash); + + readingsBeginUpdate ($clHash); + + # Loop over all addresses + foreach my $a (@addList) { + next if (!exists($objects->{$a})); - HMCCU_Trace ($clthash, 2, $fnc, "Processing object $addr"); - - # Update channels of device - foreach my $chnnum (keys (%{$objects->{$addr}})) { - next if ($clttype eq 'HMCCUCHN' && "$chnnum" ne "$cnum" && "$chnnum" ne "0"); - next if ("$chnnum" eq "0" && $cf =~ /nochn0/); - my $chnadd = "$addr:$chnnum"; - - # Update datapoints of channel - foreach my $dpt (keys (%{$objects->{$addr}{$chnnum}})) { - my $value = $objects->{$addr}{$chnnum}{$dpt}; - next if (!defined ($value)); + # Loop over all channels of device, including channel 'd' + foreach my $c (keys %{$objects->{$a}}) { + next if (($clType eq 'HMCCUCHN' && "$c" ne "$chnNo" && "$c" ne "0" && "$c" ne "d")); + + if (ref($objects->{$a}{$c}) ne 'HASH') { + HMCCU_Log ($ioHash, 2, "object $a $c is not a hash reference\n".stacktraceAsString(undef)); + next; + } + + my $chnAddr = "$a:$c"; + my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $chnAddr, $clHash->{ccuif}); + my $chnType = defined($devDesc) ? $devDesc->{TYPE} : HMCCU_GetChannelRole ($clHash, $c); + + # Loop over all parameter sets + foreach my $ps (keys %{$objects->{$a}{$c}}) { + if (ref($objects->{$a}{$c}{$ps}) ne 'HASH') { + HMCCU_Log ($ioHash, 2, "object $a $c $ps is not a hash reference\n".stacktraceAsString(undef)); + next; + } - # Key for storing values in client hash. Indirect updates of virtual devices - # are stored with device address in key. - my $chkey = $devaddr ne $addr ? "$chnadd.$dpt" : "$chnnum.$dpt"; + # Loop over all parameters + foreach my $p (keys %{$objects->{$a}{$c}{$ps}}) { + my $v = $objects->{$a}{$c}{$ps}{$p}; + next if (!defined($v)); + my $fv = $v; + my $cv = $v; + my $sv; + + HMCCU_Trace ($clHash, 2, "ParamsetReading $a.$c.$ps.$p=$v"); + + # Key for storing values in client device hash. Indirect updates of virtual + # devices are stored with device address in key. + my $chKey = $devAddr ne $a ? "$chnAddr.$p" : "$c.$p"; + + # Store raw value in client device hash + HMCCU_UpdateInternalValues ($clHash, $chKey, $ps, 'VAL', $v); + + # Modify value: scale, format, substitute + $sv = HMCCU_ScaleValue ($clHash, $c, $p, $v, 0); + HMCCU_UpdateInternalValues ($clHash, $chKey, $ps, 'NVAL', $sv); + HMCCU_Trace ($clHash, 2, "$p: sv = $sv"); + $fv = HMCCU_FormatReadingValue ($clHash, $sv, $p); + $cv = HMCCU_Substitute ($fv, $clHash, 0, $c, $p, $chnType, $devDesc); + $cv = HMCCU_GetParamValueConversion ($ioHash, $devDesc, $ps, $p, $fv) + if (defined($devDesc) && "$fv" eq "$cv"); + + HMCCU_UpdateInternalValues ($clHash, $chKey, $ps, 'SVAL', $cv); + push @chKeys, $chKey; + + # Update 'state' and 'control' + HMCCU_BulkUpdate ($clHash, 'control', $fv, $cv) + if ($cd ne '' && $p eq $cd && $c eq $cc); + HMCCU_BulkUpdate ($clHash, 'state', $fv, $cv) + if ($p eq $sd && ($sc eq '' || $sc eq $c)); - # Store datapoint raw value in device hash - HMCCU_UpdateInternalValues ($clthash, $chkey, 'VAL', $value); - - HMCCU_Trace ($clthash, 2, $fnc, "dev=$cltname, chnadd/object=$chnadd, dpt=$dpt, key=$chkey, value=$value"); - - if (HMCCU_FilterReading ($clthash, $chnadd, $dpt)) { - # Modify reading name and value - my @readings = HMCCU_GetReadingName ($clthash, '', $addr, $chnnum, $dpt, '', $crf); - my $svalue = HMCCU_ScaleValue ($clthash, $chnnum, $dpt, $value, 0); - my $fvalue = HMCCU_FormatReadingValue ($clthash, $svalue, $dpt); - my $cvalue = HMCCU_Substitute ($fvalue, $substitute, 0, $chnnum, $dpt); -# my %calcs = HMCCU_CalculateReading ($clthash, $chkey); - - # Store the resulting value after scaling, formatting and substitution - HMCCU_UpdateInternalValues ($clthash, $chkey, 'SVAL', $cvalue); - push @chkeys, $chkey; - - # Store result, but not for indirect updates of virtual devices - $results{$devaddr}{$chnnum}{$dpt} = $cvalue if ($devaddr eq $addr); - - HMCCU_Trace ($clthash, 2, $fnc, - "device=$cltname, readings=".join(',', @readings). - ", orgvalue=$value value=$cvalue peer=$peer"); - - # Update readings - foreach my $rn (@readings) { - HMCCU_BulkUpdate ($clthash, $rn, $fvalue, $cvalue) if ($rn ne ''); - } -# foreach my $clcr (keys %calcs) { -# HMCCU_BulkUpdate ($clthash, $clcr, $calcs{$clcr}, $calcs{$clcr}); -# } - HMCCU_BulkUpdate ($clthash, 'control', $fvalue, $cvalue) - if ($cd ne '' && $dpt eq $cd && $chnnum eq $cc); - HMCCU_BulkUpdate ($clthash, 'state', $fvalue, $cvalue) - if ($dpt eq $st && ($sc eq '' || $sc eq $chnnum)); - # Update peers - HMCCU_UpdatePeers ($clthash, "$chnnum.$dpt", $cvalue, $peer) if (!$vg && $peer ne 'null'); + HMCCU_UpdatePeers ($clHash, "$c.$p", $cv, $peer) if (!$vg && $peer ne 'null'); + + # Store result, but not for indirect updates of virtual devices + $results{$devAddr}{$c}{$ps}{$p} = $cv if ($devAddr eq $a); + + my @rnList = HMCCU_GetReadingName ($clHash, $clInt, $a, $c, $p, '', $clRF, $ps); + my $dispFlag = HMCCU_FilterReading ($clHash, $chnAddr, $p, $ps) ? '' : '.'; + foreach my $rn (@rnList) { + HMCCU_Trace ($clHash, 2, "rn=$rn, dispFlag=$dispFlag, fv=$fv, cv=$cv"); + HMCCU_BulkUpdate ($clHash, $dispFlag.$rn, $fv, $cv); + } } } - } + } } - - if (scalar (@chkeys) > 0) { - my %calcs = HMCCU_CalculateReading ($clthash, \@chkeys); - foreach my $clcr (keys %calcs) { - HMCCU_BulkUpdate ($clthash, $clcr, $calcs{$clcr}, $calcs{$clcr}); + + # Calculate additional readings + if (scalar (@chKeys) > 0) { + my %calc = HMCCU_CalculateReading ($clHash, \@chKeys); + foreach my $cr (keys %calc) { + HMCCU_BulkUpdate ($clHash, $cr, $calc{$cr}, $calc{$cr}); } } + # Update device states + HMCCU_UpdateDeviceStates ($clHash); + # Calculate and update HomeMatic state if ($ccuflags !~ /nohmstate/) { - my ($hms_read, $hms_chn, $hms_dpt, $hms_val) = HMCCU_GetHMState ($cltname, $ccuname, undef); - HMCCU_BulkUpdate ($clthash, $hms_read, $hms_val, $hms_val) if (defined ($hms_val)); + my ($hms_read, $hms_chn, $hms_dpt, $hms_val) = HMCCU_GetHMState ($clName, $ioName); + HMCCU_BulkUpdate ($clHash, $hms_read, $hms_val, $hms_val) if (defined($hms_val)); } - readingsEndUpdate ($clthash, 1); + readingsEndUpdate ($clHash, 1); return \%results; } ###################################################################### -# Store datapoint values in device hash. -# Parameter type is VAL or SVAL. +# Refresh readings of a client device ###################################################################### -sub HMCCU_UpdateInternalValues ($$$$) +sub HMCCU_RefreshReadings ($) { - my ($ch, $chkey, $type, $value) = @_; + my ($clHash) = @_; + + return if ($clHash->{hmccu}{semDefaults} == 1); + + my $ioHash = HMCCU_GetHash ($clHash) // return; + + HMCCU_DeleteReadings ($clHash, '.*'); + + my %objects; + my ($devAddr, undef) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + + for my $dp (keys %{$clHash->{hmccu}{dp}}) { + foreach my $ps (keys %{$clHash->{hmccu}{dp}{$dp}}) { + my ($chnNo, $par) = split (/\./, $dp); + if (defined($par) && defined($clHash->{hmccu}{dp}{$dp}{$ps}{VAL})) { + $objects{$devAddr}{$chnNo}{$ps}{$par} = $clHash->{hmccu}{dp}{$dp}{$ps}{VAL}; + } + } + } + + if (scalar(keys %objects) > 0) { + HMCCU_UpdateParamsetReadings ($ioHash, $clHash, \%objects); + } +} + +###################################################################### +# Store datapoint values in device hash. +# Parameter type is VAL or SVAL. +# Structure: +# {hmccu}{dp}{channel.datapoint}{paramset}{VAL|OVAL|SVAL|OSVAL} +# {hmccu}{tt}{program}{section}{daynum}{entrynum} +###################################################################### + +sub HMCCU_UpdateInternalValues ($$$$$) +{ + my ($ch, $chkey, $paramset, $type, $value) = @_; my $otype = "O".$type; + my %weekDay = ('SUNDAY', 0, 'MONDAY', 1, 'TUESDAY', 2, 'WEDNESDAY', 3, + 'THURSDAY', 4, 'FRIDAY', 5, 'SATURDAY', 6); + my $weekDayExp = join('|', keys %weekDay); + + # Store time/value tables + if ($type eq 'SVAL') { + if ($chkey =~ /^[0-9d]+\.P([0-9])_([A-Z]+)_($weekDayExp)_([0-9]+)$/) { + my ($prog, $valName, $day, $time) = ($1, $2, $3, $4); + $prog--; + if (exists($weekDay{$day})) { + $ch->{hmccu}{tt}{$prog}{$valName}{$weekDay{$day}}{$time} = $value; + } + } + elsif ($chkey =~ /^[0-9d]+\.([A-Z]+)_($weekDayExp)_([0-9]+)$/) { + my ($valName, $day, $time) = ($1, $2, $3); + if (exists($weekDay{$day})) { + $ch->{hmccu}{tt}{0}{$valName}{$weekDay{$day}}{$time} = $value; + } + } + } + # Save old value - if (exists ($ch->{hmccu}{dp}{$chkey}{$type})) { - $ch->{hmccu}{dp}{$chkey}{$otype} = $ch->{hmccu}{dp}{$chkey}{$type}; + if (exists ($ch->{hmccu}{dp}{$chkey}{$paramset}{$type})) { + $ch->{hmccu}{dp}{$chkey}{$paramset}{$otype} = $ch->{hmccu}{dp}{$chkey}{$paramset}{$type}; } else { - $ch->{hmccu}{dp}{$chkey}{$otype} = $value; + $ch->{hmccu}{dp}{$chkey}{$paramset}{$otype} = $value; } # Store new value - $ch->{hmccu}{dp}{$chkey}{$type} = $value; + $ch->{hmccu}{dp}{$chkey}{$paramset}{$type} = $value; } ###################################################################### @@ -3542,27 +4782,26 @@ sub HMCCU_UpdateMultipleDevices ($$) { my ($hash, $objects) = @_; my $name = $hash->{NAME}; - my $fnc = "UpdateMultipleDevices"; my $c = 0; # Check syntax return 0 if (!defined ($hash) || !defined ($objects)); # Update reading in matching client devices - my @devlist = HMCCU_FindClientDevices ($hash, "(HMCCUDEV|HMCCUCHN)", undef, - "ccudevstate=active"); + my @devlist = HMCCU_FindClientDevices ($hash, '(HMCCUDEV|HMCCUCHN)', undef, + 'ccudevstate=active'); foreach my $d (@devlist) { - my $ch = $defs{$d}; - if (!defined ($ch)) { + my $clHash = $defs{$d}; + if (!defined($clHash)) { HMCCU_Log ($name, 2, "Can't find hash for device $d"); next; } - my @addrlist = HMCCU_GetAffectedAddresses ($ch); + my @addrlist = HMCCU_GetAffectedAddresses ($clHash); next if (scalar (@addrlist) == 0); foreach my $addr (@addrlist) { if (exists ($objects->{$addr})) { - my $rc = HMCCU_UpdateSingleDevice ($hash, $ch, $objects, \@addrlist); - $c++ if (ref ($rc)); + my $rc = HMCCU_UpdateParamsetReadings ($hash, $clHash, $objects, \@addrlist); + $c++ if (ref($rc)); last; } } @@ -3573,21 +4812,24 @@ sub HMCCU_UpdateMultipleDevices ($$) ###################################################################### # Get list of device addresses including group device members. +# For virtual devices group members are only returned if ccuflag +# updGroupMembers is set. ###################################################################### sub HMCCU_GetAffectedAddresses ($) { - my ($clthash) = @_; + my ($clHash) = @_; my @addlist = (); - if ($clthash->{TYPE} eq 'HMCCUDEV' || $clthash->{TYPE} eq 'HMCCUCHN') { - if (exists ($clthash->{ccuaddr})) { - my ($devaddr, $cnum) = HMCCU_SplitChnAddr ($clthash->{ccuaddr}); + if ($clHash->{TYPE} eq 'HMCCUDEV' || $clHash->{TYPE} eq 'HMCCUCHN') { + my $ioHash = HMCCU_GetHash ($clHash); + my $ccuFlags = defined($ioHash) ? HMCCU_GetFlags ($ioHash) : 'null'; + if (exists($clHash->{ccuaddr})) { + my ($devaddr, $cnum) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); push @addlist, $devaddr; } - if (($clthash->{ccuif} eq 'VirtualDevices' || $clthash->{ccuif} eq 'fhem') && - exists ($clthash->{ccugroup})) { - push @addlist, split (',', $clthash->{ccugroup}); + if ($clHash->{ccuif} eq 'VirtualDevices' && $ccuFlags =~ /updGroupMembers/ && exists($clHash->{ccugroup}) && $clHash->{ccugroup} ne '') { + push @addlist, split (',', $clHash->{ccugroup}); } } @@ -3615,27 +4857,25 @@ sub HMCCU_GetAffectedAddresses ($) sub HMCCU_UpdatePeers ($$$$) { my ($clt_hash, $chndpt, $val, $peerattr) = @_; - my $fnc = "UpdatePeers"; my $io_hash = HMCCU_GetHash ($clt_hash); - HMCCU_Trace ($clt_hash, 2, $fnc, "chndpt=$chndpt val=$val peer=$peerattr"); + HMCCU_Trace ($clt_hash, 2, "chndpt=$chndpt val=$val peer=$peerattr"); - my @rules = split (/[;\n]+/, $peerattr); - foreach my $r (@rules) { - HMCCU_Trace ($clt_hash, 2, $fnc, "rule=$r"); + foreach my $r (split (/[;\n]+/, $peerattr)) { + HMCCU_Trace ($clt_hash, 2, "rule=$r"); my ($vars, $cond, $type, $act) = split (/:/, $r, 4); next if (!defined ($act)); - HMCCU_Trace ($clt_hash, 2, $fnc, "vars=$vars, cond=$cond, type=$type, act=$act"); + HMCCU_Trace ($clt_hash, 2, "vars=$vars, cond=$cond, type=$type, act=$act"); next if ($cond !~ /$chndpt/); # Check if rule is affected by datapoint update my $ex = 0; foreach my $dpt (split (",", $vars)) { - HMCCU_Trace ($clt_hash, 2, $fnc, "dpt=$dpt"); + HMCCU_Trace ($clt_hash, 2, "dpt=$dpt"); $ex = 1 if ($ex == 0 && $dpt eq $chndpt); if (!exists ($clt_hash->{hmccu}{dp}{$dpt})) { - HMCCU_Trace ($clt_hash, 2, $fnc, "Datapoint $dpt does not exist on hash"); + HMCCU_Trace ($clt_hash, 2, "Datapoint $dpt does not exist on hash"); } last if ($ex == 1); } @@ -3644,87 +4884,56 @@ sub HMCCU_UpdatePeers ($$$$) # Substitute variables and evaluate condition $cond = HMCCU_SubstVariables ($clt_hash, $cond, $vars); my $e = eval "$cond"; - HMCCU_Trace ($clt_hash, 2, $fnc, "eval $cond = $e") if (defined ($e)); - HMCCU_Trace ($clt_hash, 2, $fnc, "Error in eval $cond") if (!defined ($e)); - HMCCU_Trace ($clt_hash, 2, $fnc, "NoMatch in eval $cond") if (defined ($e) && $e eq ''); - next if (!defined ($e) || $e eq ''); + HMCCU_Trace ($clt_hash, 2, "eval $cond = $e") if (defined($e)); + HMCCU_Trace ($clt_hash, 2, "Error in eval $cond") if (!defined($e)); + HMCCU_Trace ($clt_hash, 2, "NoMatch in eval $cond") if (defined($e) && $e eq ''); + next if (!defined($e) || $e eq ''); # Substitute variables and execute action if ($type eq 'ccu' || $type eq 'hmccu') { my ($aobj, $aexp) = split (/=/, $act); $aexp =~ s/\$value/$val/g; $aexp = HMCCU_SubstVariables ($clt_hash, $aexp, $vars); - HMCCU_Trace ($clt_hash, 2, $fnc, "set $aobj to $aexp"); + HMCCU_Trace ($clt_hash, 2, "set $aobj to $aexp"); my ($int, $add, $chn, $dpt, $nam, $flags) = HMCCU_ParseObject ($io_hash, "$type:$aobj", $HMCCU_FLAG_INTERFACE); next if ($flags != $HMCCU_FLAGS_IACD && $flags != $HMCCU_FLAGS_NCD); HMCCU_SetMultipleDatapoints ($clt_hash, { "001.$int.$add:$chn.$dpt" => $aexp }); } - elsif ($type eq 'fhem') { - $act =~ s/\$value/$val/g; - $act = HMCCU_SubstVariables ($clt_hash, $act, $vars); - HMCCU_Trace ($clt_hash, 2, $fnc, "Execute command $act"); - AnalyzeCommandChain (undef, $act); - } } } ###################################################################### -# Get list of valid RPC interfaces. -# Binary interfaces are ignored if internal RPC server is used. +# Get hash with valid RPC interfaces and ports +# If $mode = 1, return only interfaces which have devices assigned. ###################################################################### -sub HMCCU_GetRPCInterfaceList ($) +sub HMCCU_GetRPCInterfaceList ($;$) { - my ($hash) = @_; + my ($hash, $mode) = @_; + $mode //= 0; my $name = $hash->{NAME}; - - my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($hash); - my @interfaces = (); - - my $ccuflags = HMCCU_GetFlags ($name); - - if (defined ($hash->{hmccu}{rpcports})) { - foreach my $p (split (',', $hash->{hmccu}{rpcports})) { - my ($ifname, $iftype) = HMCCU_GetRPCServerInfo ($hash, $p, 'name,type'); - next if (!defined ($ifname) || !defined ($iftype)); - push (@interfaces, $ifname) if ($iftype ne 'B' || $ccuflags =~ /(extrpc|procrpc)/); + my %interfaces = (); + + my $rpcInterfaces = AttrVal ($name, 'rpcinterfaces', ''); + if ($rpcInterfaces ne '') { + foreach my $in (split (',', $rpcInterfaces)) { + my ($pn, $dc) = HMCCU_GetRPCServerInfo ($hash, $in, 'port,devcount'); + if (defined($pn) && ($mode == 0 || ($mode == 1 && defined($dc) && $dc > 0))) { + $interfaces{$in} = $pn; + } } } - else { - @interfaces = ($defInterface); - } - - return @interfaces; -} - -###################################################################### -# Get list of valid RPC ports. -# Binary interfaces are ignored if internal RPC server is used. -###################################################################### - -sub HMCCU_GetRPCPortList ($) -{ - my ($hash) = @_; - my $name = $hash->{NAME}; - - my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($hash); - my @ports = (); - - my $ccuflags = HMCCU_GetFlags ($name); - - if (defined ($hash->{hmccu}{rpcports})) { - foreach my $p (split (',', $hash->{hmccu}{rpcports})) { - my ($ifname, $iftype) = HMCCU_GetRPCServerInfo ($hash, $p, 'name,type'); - next if (!defined ($ifname) || !defined ($iftype)); - push (@ports, $p) if ($iftype ne 'B' || $ccuflags =~ /(extrpc|procrpc)/); + elsif (defined($hash->{hmccu}{rpcports})) { + foreach my $pn (split (',', $hash->{hmccu}{rpcports})) { + my ($in, $it, $dc) = HMCCU_GetRPCServerInfo ($hash, $pn, 'name,type,devcount'); + if (defined($in) && defined($it) && ($mode == 0 || ($mode == 1 && defined($dc) && $dc > 0))) { + $interfaces{$in} = $pn; + } } } - else { - @ports = ($defPort); - } - return @ports; + return \%interfaces; } ###################################################################### @@ -3742,27 +4951,27 @@ sub HMCCU_EventsTimedOut ($) return 1 if (!HMCCU_IsFlag ($name, 'reconnect')); - HMCCU_Log ($hash, 2, "Reconnecting to CCU"); + HMCCU_Log ($hash, 2, 'Reconnecting to CCU'); # Register callback for each interface my $rc = 1; - my @iflist = HMCCU_GetRPCInterfaceList ($hash); - foreach my $ifname (@iflist) { + my $interfaces = HMCCU_GetRPCInterfaceList ($hash, 0); + foreach my $ifname (keys %$interfaces) { my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, $ifname); if ($rpcdev eq '') { HMCCU_Log ($hash, 0, "Can't find RPC device for interface $ifname"); $rc = 0; next; } - my $cl_hash = $defs{$rpcdev}; + my $clHash = $defs{$rpcdev}; # Check if CCU interface is reachable before registering callback - my ($nrc, $msg) = HMCCURPCPROC_RegisterCallback ($cl_hash, 2); + my ($nrc, $msg) = HMCCURPCPROC_RegisterCallback ($clHash, 2); $rc &= $nrc; if ($nrc) { - $cl_hash->{ccustate} = 'active'; + $clHash->{ccustate} = 'active'; } else { - HMCCU_Log ($cl_hash, 1, $msg); + HMCCU_Log ($clHash, 1, $msg); } } @@ -3771,25 +4980,25 @@ sub HMCCU_EventsTimedOut ($) ###################################################################### # Build RPC callback URL -# Parameter hash might be a HMCCU or a HMCCURPC hash. +# Parameter hash might be a HMCCU or a HMCCURPCPROC hash. ###################################################################### sub HMCCU_GetRPCCallbackURL ($$$$$) { my ($hash, $localaddr, $cbport, $clkey, $iface) = @_; - return undef if (!defined ($hash)); + return undef if (!defined($hash)); - my $hmccu_hash = $hash->{TYPE} eq 'HMCCURPCPROC' ? $hash->{IODev} : $hash; + my $ioHash = $hash->{TYPE} eq 'HMCCURPCPROC' ? $hash->{IODev} : $hash; - return undef if (!exists ($hmccu_hash->{hmccu}{interfaces}{$iface}) && - !exists ($hmccu_hash->{hmccu}{ifports}{$iface})); + return undef if (!exists($ioHash->{hmccu}{interfaces}{$iface}) && + !exists ($ioHash->{hmccu}{ifports}{$iface})); - my $ifname = $iface =~ /^[0-9]+$/ ? $hmccu_hash->{hmccu}{ifports}{$iface} : $iface; - return undef if (!exists ($hmccu_hash->{hmccu}{interfaces}{$ifname})); + my $ifname = $iface =~ /^[0-9]+$/ ? $ioHash->{hmccu}{ifports}{$iface} : $iface; + return undef if (!exists($ioHash->{hmccu}{interfaces}{$ifname})); - my $url = $hmccu_hash->{hmccu}{interfaces}{$ifname}{prot}."://$localaddr:$cbport/fh". - $hmccu_hash->{hmccu}{interfaces}{$ifname}{port}; + my $url = $ioHash->{hmccu}{interfaces}{$ifname}{prot}."://$localaddr:$cbport/fh". + $ioHash->{hmccu}{interfaces}{$ifname}{port}; $url =~ s/^https/http/; return $url; @@ -3809,26 +5018,19 @@ sub HMCCU_GetRPCServerInfo ($$$) my ($hash, $iface, $info) = @_; my @result = (); -# HMCCU_Log ($hash, 2, "Get RPC server info $info for port/interface $iface", 0); - - return @result if (!defined ($hash)); - return @result if (!exists ($hash->{hmccu}{interfaces}{$iface}) && - !exists ($hash->{hmccu}{ifports}{$iface})); + return @result if (!defined($hash) || + (!exists($hash->{hmccu}{interfaces}{$iface}) && !exists($hash->{hmccu}{ifports}{$iface}))); my $ifname = $iface =~ /^[0-9]+$/ ? $hash->{hmccu}{ifports}{$iface} : $iface; - return @result if (!exists ($hash->{hmccu}{interfaces}{$ifname})); - -# HMCCU_Log ($hash, 2, "Interface name = $ifname", 0); + return @result if (!exists($hash->{hmccu}{interfaces}{$ifname})); foreach my $i (split (',', $info)) { -# HMCCU_Log ($hash, 2, "Infotoken = $i", 0); if ($i eq 'name') { push (@result, $ifname); } else { my $v = exists ($hash->{hmccu}{interfaces}{$ifname}{$i}) ? $hash->{hmccu}{interfaces}{$ifname}{$i} : undef; -# HMCCU_Log ($hash, 2, "Tokenvalue = $v", 0); push @result, $v; } } @@ -3838,7 +5040,7 @@ sub HMCCU_GetRPCServerInfo ($$$) ###################################################################### # Check if RPC interface is of specified type. -# Parameter type is A for XML or B for binary. +# Parameter $type is A for XML or B for binary. ###################################################################### sub HMCCU_IsRPCType ($$$) @@ -3846,93 +5048,11 @@ sub HMCCU_IsRPCType ($$$) my ($hash, $iface, $type) = @_; my ($rpctype) = HMCCU_GetRPCServerInfo ($hash, $iface, 'type'); - return 0 if (!defined ($rpctype)); + return 0 if (!defined($rpctype)); return $rpctype eq $type ? 1 : 0; } -###################################################################### -# Register RPC callbacks at CCU if RPC-Server already in server loop -###################################################################### - -sub HMCCU_RPCRegisterCallback ($) -{ - my ($hash) = @_; - my $name = $hash->{NAME}; - my $localaddr = $hash->{hmccu}{localaddr}; - - my $rpcinterval = AttrVal ($name, 'rpcinterval', $HMCCU_INIT_INTERVAL2); - my @rpcports = HMCCU_GetRPCPortList ($hash); - - foreach my $port (@rpcports) { - my $clkey = 'CB'.$port; - my $cburl = HMCCU_GetRPCCallbackURL ($hash, $localaddr, $hash->{hmccu}{rpc}{$clkey}{cbport}, - $clkey, $port); - next if (!defined ($cburl)); - my ($url, $rpcflags) = HMCCU_GetRPCServerInfo ($hash, $port, 'url,flags'); - next if (!defined ($url) || !defined ($rpcflags)); - if ($hash->{hmccu}{rpc}{$clkey}{loop} == 1 || - $hash->{hmccu}{rpc}{$clkey}{state} eq "register") { - $hash->{hmccu}{rpc}{$clkey}{port} = $port; - $hash->{hmccu}{rpc}{$clkey}{clurl} = $url; - $hash->{hmccu}{rpc}{$clkey}{cburl} = $cburl; - $hash->{hmccu}{rpc}{$clkey}{loop} = 2; - $hash->{hmccu}{rpc}{$clkey}{state} = $rpcflags =~ /forceInit/ ? "running" : "registered"; - - Log3 $name, 1, "HMCCU: Registering callback $cburl with ID $clkey at $url"; - my $rpcclient = RPC::XML::Client->new ($url); - $rpcclient->send_request ("init", $cburl, $clkey); - Log3 $name, 1, "HMCCU: RPC callback with URL $cburl initialized"; - } - } - - # Schedule reading of RPC queue - InternalTimer (gettimeofday()+$rpcinterval, 'HMCCU_ReadRPCQueue', $hash, 0); -} - -###################################################################### -# Deregister RPC callbacks at CCU -###################################################################### - -sub HMCCU_RPCDeRegisterCallback ($) -{ - my ($hash) = @_; - my $name = $hash->{NAME}; - - foreach my $clkey (keys %{$hash->{hmccu}{rpc}}) { - my $rpchash = \%{$hash->{hmccu}{rpc}{$clkey}}; - if (exists ($rpchash->{cburl}) && $rpchash->{cburl} ne '') { - my $port = $rpchash->{port}; - my $rpcclient = RPC::XML::Client->new ($rpchash->{clurl}); - Log3 $name, 1, "HMCCU: Deregistering RPC server ".$rpchash->{cburl}. - " at ".$rpchash->{clurl}; - $rpcclient->send_request("init", $rpchash->{cburl}); - $rpchash->{cburl} = ''; - $rpchash->{clurl} = ''; - $rpchash->{cbport} = 0; - } - } -} - -###################################################################### -# Initialize statistic counters -###################################################################### - -sub HMCCU_ResetCounters ($) -{ - my ($hash) = @_; - my @counters = ('total', 'EV', 'ND', 'IN', 'DD', 'RA', 'RD', 'UD', 'EX', 'SL', 'ST'); - - foreach my $cnt (@counters) { - $hash->{hmccu}{ev}{$cnt} = 0; - } - delete $hash->{hmccu}{evs}; - delete $hash->{hmccu}{evr}; - - $hash->{hmccu}{evtimeout} = 0; - $hash->{hmccu}{evtime} = 0; -} - ###################################################################### # Start external RPC server via RPC device. # Return number of RPC servers or 0 on error. @@ -3947,16 +5067,15 @@ sub HMCCU_StartExtRPCServer ($) my $attrset = 0; # Change RPC type to procrpc - if ($ccuflags =~ /extrpc/) { - $ccuflags =~ s/extrpc/procrpc/g; + if ($ccuflags =~ /(extrpc|intrpc)/) { + $ccuflags =~ s/(extrpc|intrpc)/procrpc/g; CommandAttr (undef, "$name ccuflags $ccuflags"); $attrset = 1; # Disable existing devices of type HMCCURPC foreach my $d (keys %defs) { my $ch = $defs{$d}; - next if (!exists ($ch->{TYPE}) || !exists ($ch->{NAME})); - next if ($ch->{TYPE} ne 'HMCCURPC'); + next if (!exists ($ch->{TYPE}) || !exists ($ch->{NAME}) || $ch->{TYPE} ne 'HMCCURPC'); CommandAttr (undef, $ch->{NAME}." disable 1") if (IsDisabled ($ch->{NAME}) != 1); } } @@ -3964,10 +5083,11 @@ sub HMCCU_StartExtRPCServer ($) my $c = 0; my $d = 0; my $s = 0; - my @iflist = HMCCU_GetRPCInterfaceList ($hash); + my $interfaces = HMCCU_GetRPCInterfaceList ($hash, 0); + my @iflist = keys %$interfaces; foreach my $ifname1 (@iflist) { - HMCCU_Log ($hash, 2, "Get RPC device for interface $ifname1"); my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 1, $ifname1); + HMCCU_Log ($hash, 2, "RPC device for interface $ifname1: ".($rpcdev eq '' ? 'not found' : $rpcdev)); next if ($rpcdev eq '' || !defined ($hash->{hmccu}{interfaces}{$ifname1}{device})); $d++; $s++ if ($save); @@ -3975,11 +5095,11 @@ sub HMCCU_StartExtRPCServer ($) # Save FHEM config if new RPC devices were defined or attribute has changed if ($s > 0 || $attrset) { - HMCCU_Log ($hash, 1, "Saving FHEM config"); + HMCCU_Log ($hash, 1, 'Saving FHEM config'); CommandSave (undef, undef); } - if ($d == scalar (@iflist)) { + if ($d == scalar(@iflist)) { foreach my $ifname2 (@iflist) { my $dh = $defs{$hash->{hmccu}{interfaces}{$ifname2}{device}}; $hash->{hmccu}{interfaces}{$ifname2}{manager} = 'HMCCU'; @@ -3995,7 +5115,7 @@ sub HMCCU_StartExtRPCServer ($) return $c; } else { - HMCCU_Log ($hash, 0, "Definition of some RPC devices failed"); + HMCCU_Log ($hash, 0, 'Definition of some RPC devices failed'); } return 0; @@ -4010,195 +5130,24 @@ sub HMCCU_StopExtRPCServer ($;$) my ($hash, $wait) = @_; my $name = $hash->{NAME}; - if (HMCCU_IsFlag ($name, "(extrpc|procrpc)")) { - return HMCCU_Log ($hash, 0, "Module HMCCURPCPROC not loaded") if (!exists ($modules{'HMCCURPCPROC'})); - HMCCU_SetRPCState ($hash, 'stopping'); - - my $rc = 1; - my @iflist = HMCCU_GetRPCInterfaceList ($hash); - foreach my $ifname (@iflist) { - my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, $ifname); - if ($rpcdev eq '') { - HMCCU_Log ($hash, 0, "HMCCU: Can't find RPC device"); - next; - } - $hash->{hmccu}{interfaces}{$ifname}{manager} = 'HMCCU'; - $rc &= HMCCURPCPROC_StopRPCServer ($defs{$rpcdev}, $wait); - } + return HMCCU_Log ($hash, 0, 'Module HMCCURPCPROC not loaded', 0) + if (!exists($modules{'HMCCURPCPROC'})); - return $rc; - } -} + HMCCU_SetRPCState ($hash, 'stopping'); -###################################################################### -# Start internal file queue based RPC server. -# Return number of RPC server processes or 0 on error. -###################################################################### - -sub HMCCU_StartIntRPCServer ($) -{ - my ($hash) = @_; - my $name = $hash->{NAME}; - - # Timeouts - my $timeout = AttrVal ($name, 'rpctimeout', '0.01,0.25'); - my ($to_read, $to_write) = split (",", $timeout); - $to_write = $to_read if (!defined ($to_write)); - - # Address and ports - my $rpcqueue = AttrVal ($name, 'rpcqueue', '/tmp/ccuqueue'); - my $localaddr = AttrVal ($name, 'rpcserveraddr', ''); - my $rpcserverport = AttrVal ($name, 'rpcserverport', 5400); - my $rpcinterval = AttrVal ($name, 'rpcinterval', $HMCCU_INIT_INTERVAL1); - my @rpcportlist = HMCCU_GetRPCPortList ($hash); - my ($serveraddr) = HMCCU_GetRPCServerInfo ($hash, $rpcportlist[0], 'host'); - my $fork_cnt = 0; - - HMCCU_Log ($hash, 1, "Internal RPC server is depricated and will be removed soon. Set ccuflags to procrpc"); - - # Check for running RPC server processes - my @hm_pids; - my @hm_tids; - HMCCU_IsRPCServerRunning ($hash, \@hm_pids, \@hm_tids); - if (scalar (@hm_pids) > 0) { - return HMCCU_Log ($hash, 0, "RPC server(s) already running with PIDs ".join (',', @hm_pids), - scalar (@hm_pids)); - } - elsif (scalar (@hm_tids) > 0) { - return HMCCU_Log ($hash, 1, "RPC server(s) already running with TIDs ".join (',', @hm_tids), - 0); - } - - # Detect local IP address - if ($localaddr eq '') { - $localaddr = HMCCU_TCPConnect ($serveraddr, $rpcportlist[0]); - return HMCCU_Log ($hash, 1, "Can't connect to RPC host $serveraddr port".$rpcportlist[0], 0) - if ($localaddr eq ''); - } - $hash->{hmccu}{localaddr} = $localaddr; - - my $ccunum = $hash->{CCUNum}; - - # Fork child processes - foreach my $port (@rpcportlist) { - my $clkey = 'CB'.$port; - my $rpcqueueport = $rpcqueue."_".$port."_".$ccunum; - my $callbackport = $rpcserverport+$port+($ccunum*10); - - # Clear event queue - HMCCU_ResetRPCQueue ($hash, $port); - - # Create child process - Log3 $name, 2, "HMCCU: Create child process with timeouts $to_read and $to_write"; - my $child = SubProcess->new ({ onRun => \&HMCCU_CCURPC_OnRun, - onExit => \&HMCCU_CCURPC_OnExit, timeoutread => $to_read, timeoutwrite => $to_write }); - $child->{serveraddr} = $serveraddr; - $child->{serverport} = $port; - $child->{callbackport} = $callbackport; - $child->{devname} = $name; - $child->{queue} = $rpcqueueport; - - # Start child process - my $pid = $child->run (); - if (!defined ($pid)) { - Log3 $name, 1, "HMCCU: No RPC process for server $clkey started"; + my $rc = 1; + my $interfaces = HMCCU_GetRPCInterfaceList ($hash, 0); + foreach my $ifname (keys %$interfaces) { + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, $ifname); + if ($rpcdev eq '') { + HMCCU_Log ($hash, 0, "HMCCU: Can't find RPC device"); next; } - - Log3 $name, 0, "HMCCU: Child process for server $clkey started with PID $pid"; - $fork_cnt++; - - # Store child process parameters - $hash->{hmccu}{rpc}{$clkey}{child} = $child; - $hash->{hmccu}{rpc}{$clkey}{cbport} = $callbackport; - $hash->{hmccu}{rpc}{$clkey}{loop} = 0; - $hash->{hmccu}{rpc}{$clkey}{pid} = $pid; - $hash->{hmccu}{rpc}{$clkey}{queue} = $rpcqueueport; - $hash->{hmccu}{rpc}{$clkey}{state} = "starting"; - push (@hm_pids, $pid); - } - - $hash->{hmccu}{rpccount} = $fork_cnt; - - if ($fork_cnt > 0) { - # Set internals - $hash->{RPCPID} = join (',', @hm_pids); - $hash->{RPCPRC} = "internal"; - - HMCCU_SetRPCState ($hash, 'starting'); - - # Initialize statistic counters - HMCCU_ResetCounters ($hash); - - Log3 $name, 0, "HMCCU: [$name] RPC server(s) starting"; - DoTrigger ($name, "RPC server starting"); - - InternalTimer (gettimeofday()+$rpcinterval, 'HMCCU_ReadRPCQueue', $hash, 0); - } - - return $fork_cnt; -} - -###################################################################### -# Stop RPC server(s) by sending SIGINT to process(es) -###################################################################### - -sub HMCCU_StopRPCServer ($) -{ - my ($hash) = @_; - my $name = $hash->{NAME}; - my $pid = 0; - - # Deregister callback URLs in CCU - HMCCU_RPCDeRegisterCallback ($hash); - - # Send signal SIGINT to RPC server processes - foreach my $clkey (keys %{$hash->{hmccu}{rpc}}) { - my $rpchash = \%{$hash->{hmccu}{rpc}{$clkey}}; - if (exists ($rpchash->{pid}) && $rpchash->{pid} != 0) { - Log3 $name, 0, "HMCCU: Stopping RPC server $clkey with PID ".$rpchash->{pid}; - kill ('INT', $rpchash->{pid}); - $rpchash->{state} = "stopping"; - } - else { - $rpchash->{state} = "inactive"; - } + $hash->{hmccu}{interfaces}{$ifname}{manager} = 'HMCCU'; + $rc &= HMCCURPCPROC_StopRPCServer ($defs{$rpcdev}, $wait); } - # Update status - HMCCU_SetRPCState ($hash, 'stopping') if ($hash->{hmccu}{rpccount} > 0); - - # Wait - sleep (1); - - # Check if processes were terminated - my @hm_pids; - my @hm_tids; - HMCCU_IsRPCServerRunning ($hash, \@hm_pids, \@hm_tids); - if (scalar (@hm_pids) > 0) { - foreach my $pid (@hm_pids) { - Log3 $name, 0, "HMCCU: Stopping RPC server with PID $pid"; - kill ('INT', $pid); - } - } - Log3 $name, 0, "HMCCU: Externally launched RPC server detected." if (scalar (@hm_tids) > 0); - - # Wait - sleep (1); - - # Kill the rest - @hm_pids = (); - @hm_tids = (); - if (HMCCU_IsRPCServerRunning ($hash, \@hm_pids, \@hm_tids)) { - foreach my $pid (@hm_pids) { - kill ('KILL', $pid); - } - } - - # Store number of running RPC servers - $hash->{hmccu}{rpccount} = HMCCU_IsRPCServerRunning ($hash, undef, undef); - - return $hash->{hmccu}{rpccount} > 0 ? 0 : 1; + return $rc; } ###################################################################### @@ -4212,14 +5161,11 @@ sub HMCCU_IsRPCStateBlocking ($) { my ($hash) = @_; - if ($hash->{RPCState} eq "starting" || - $hash->{RPCState} eq "restarting" || - $hash->{RPCState} eq "stopping") { - return 1; - } - else { - return 0; - } + return (exists($hash->{RPCState}) && ( + $hash->{RPCState} eq 'starting' || + $hash->{RPCState} eq 'restarting' || + $hash->{RPCState} eq 'stopping' + )) ? 1 : 0; } ###################################################################### @@ -4228,45 +5174,23 @@ sub HMCCU_IsRPCStateBlocking ($) # defined also return process or thread IDs. ###################################################################### -sub HMCCU_IsRPCServerRunning ($$$) +sub HMCCU_IsRPCServerRunning ($;$) { - my ($hash, $pids, $tids) = @_; + my ($hash, $pids) = @_; my $name = $hash->{NAME}; my $c = 0; my $ccuflags = HMCCU_GetFlags ($name); - if (HMCCU_IsFlag ($name, "(extrpc|procrpc)")) { -# @$tids = () if (defined ($tids)); -# my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, undef); -# if ($rpcdev ne '') { -# my ($r, $a) = HMCCURPC_CheckThreadState ($defs{$rpcdev}, 6, 'running', $tids); -# $c = $r; -# } -# } -# elsif ($ccuflags =~ /procrpc/) { - @$pids = () if (defined ($pids)); - my @iflist = HMCCU_GetRPCInterfaceList ($hash); - foreach my $ifname (@iflist) { - my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, $ifname); - next if ($rpcdev eq ''); - my $rc = HMCCURPCPROC_CheckProcessState ($defs{$rpcdev}, 'running'); - if ($rc < 0 || $rc > 1) { - push (@$pids, $rc); - $c++; - } - } - } - else { - @$pids = () if (defined ($pids)); - foreach my $clkey (keys %{$hash->{hmccu}{rpc}}) { - if (defined ($hash->{hmccu}{rpc}{$clkey}{pid})) { - my $pid = $hash->{hmccu}{rpc}{$clkey}{pid}; - if ($pid != 0 && kill (0, $pid)) { - push (@$pids, $pid) if (defined ($pids)); - $c++; - } - } + @$pids = () if (defined($pids)); + my $interfaces = HMCCU_GetRPCInterfaceList ($hash, 0); + foreach my $ifname (keys %$interfaces) { + my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hash, 0, $ifname); + next if ($rpcdev eq ''); + my $rc = HMCCURPCPROC_CheckProcessState ($defs{$rpcdev}, 'running'); + if ($rc < 0 || $rc > 1) { + push (@$pids, $rc) if (defined($pids)); + $c++; } } @@ -4277,44 +5201,33 @@ sub HMCCU_IsRPCServerRunning ($$$) # Get channels and datapoints of CCU device ###################################################################### -sub HMCCU_GetDeviceInfo ($$$) +sub HMCCU_GetDeviceInfo ($$;$) { my ($hash, $device, $ccuget) = @_; + $ccuget //= 'Value'; my $name = $hash->{NAME}; my $devname = ''; my $response = ''; - my $hmccu_hash = HMCCU_GetHash ($hash); - return '' if (!defined ($hmccu_hash)); + my $ioHash = HMCCU_GetHash ($hash) // return ''; + + $ccuget = HMCCU_GetAttribute ($ioHash, $hash, 'ccuget', 'Value') if ($ccuget eq 'Attr'); - my @devlist; - if ($hash->{ccuif} eq 'fhem' && exists ($hash->{ccugroup})) { - push @devlist, split (",", $hash->{ccugroup}); + my ($int, $add, $chn, $dpt, $nam, $flags) = HMCCU_ParseObject ($ioHash, $device, 0); + if ($flags == $HMCCU_FLAG_ADDRESS) { + $devname = HMCCU_GetDeviceName ($ioHash, $add); + return '' if ($devname eq ''); } else { - push @devlist, $device; + $devname = $nam; } - return '' if (scalar (@devlist) == 0); - - $ccuget = HMCCU_GetAttribute ($hmccu_hash, $hash, 'ccuget', 'Value') if ($ccuget eq 'Attr'); - foreach my $dev (@devlist) { - my ($int, $add, $chn, $dpt, $nam, $flags) = HMCCU_ParseObject ($hmccu_hash, $dev, 0); - if ($flags == $HMCCU_FLAG_ADDRESS) { - $devname = HMCCU_GetDeviceName ($hmccu_hash, $add, ''); - return '' if ($devname eq ''); - } - else { - $devname = $nam; - } - - $response .= HMCCU_HMScriptExt ($hmccu_hash, "!GetDeviceInfo", - { devname => $devname, ccuget => $ccuget }, undef, undef); - HMCCU_Trace ($hash, 2, undef, - "Device=$devname Devname=$devname
". - "Script response = \n".$response."
". - "Script = GetDeviceInfo"); - } + $response .= HMCCU_HMScriptExt ($ioHash, "!GetDeviceInfo", + { devname => $devname, ccuget => $ccuget }); + HMCCU_Trace ($hash, 2, + "Device=$devname Devname=$devname
". + "Script response = \n".$response."
". + "Script = GetDeviceInfo"); return $response; } @@ -4329,23 +5242,66 @@ sub HMCCU_FormatDeviceInfo ($) { my ($devinfo) = @_; - my %vtypes = (0, "n", 2, "b", 4, "f", 6, "a", 8, "n", 11, "s", 16, "i", 20, "s", 23, "p", 29, "e"); + my %vtypes = (0, 'n', 2, 'b', 4, 'f', 6, 'a', 8, 'n', 11, 's', 16, 'i', 20, 's', 23, 'p', 29, 'e'); my $result = ''; my $c_oaddr = ''; foreach my $dpspec (split ("\n", $devinfo)) { - my ($c, $c_addr, $c_name, $d_name, $d_type, $d_value, $d_flags) = split (";", $dpspec); - if ($c_addr ne $c_oaddr) { - $result .= "CHN $c_addr $c_name\n"; - $c_oaddr = $c_addr; + if ($dpspec =~ /^D/) { + my ($t, $d_iface, $d_addr, $d_name, $d_type) = split (';', $dpspec); + $result .= "DEV $d_name $d_addr interface=$d_iface type=$d_type
"; + } + else { + my ($t, $c_addr, $c_name, $d_name, $d_type, $d_value, $d_flags) = split (';', $dpspec); + $d_name =~ s/^[^:]+:(.+)$/$1/; + if ($c_addr ne $c_oaddr) { + $result .= "CHN $c_addr $c_name
"; + $c_oaddr = $c_addr; + } + my $dt = exists($vtypes{$d_type}) ? $vtypes{$d_type} : $d_type; + $result .= "   $d_name = $d_value {$dt} [$d_flags]
"; } - my $t = exists ($vtypes{$d_type}) ? $vtypes{$d_type} : $d_type; - $result .= " DPT {$t} $d_name = $d_value [$d_flags]\n"; } return $result; } +###################################################################### +# Format a hash as HTML table +# {row1}{col1} = Value12 +# {row1}{col2} = Value12 +# {row2}{col1} = Value21 +# ... +###################################################################### + +sub HMCCU_FormatHashTable ($) +{ + my ($hash) = @_; + + my $t = 0; + my $result = ''; + foreach my $row (sort keys %$hash) { + if ($t == 0) { + # Begin of table with header + $result .= ''; + $result .= ''; + foreach my $col (sort keys %{$hash->{$row}}) { + $result .= ""; + } + $result .= "\n"; + $t = 1; + } + $result .= ""; + foreach my $col (sort keys %{$hash->{$row}}) { + $result .= ""; + } + $result .= "\n"; + } + $result .= '
Key$col
$row$hash->{$row}{$col}
'; + + return $result; +} + ###################################################################### # Get available firmware versions from EQ-3 server. # Firmware version, date and download link are stored in hash @@ -4359,9 +5315,9 @@ sub HMCCU_GetFirmwareVersions ($$) { my ($hash, $type) = @_; my $name = $hash->{NAME}; - my $ccureqtimeout = AttrVal ($name, "ccuReqTimeout", $HMCCU_TIMEOUT_REQUEST); + my $ccureqtimeout = AttrVal ($name, 'ccuReqTimeout', $HMCCU_TIMEOUT_REQUEST); - my $url = "http://www.eq-3.de/service/downloads.html"; + my $url = 'http://www.eq-3.de/service/downloads.html'; my $response = GetFileFromURL ($url, $ccureqtimeout, "suchtext=&suche_in=&downloadart=11"); my @download = $response =~ m/ $name }, undef, undef); + my $response = HMCCU_HMScriptExt ($hash, '!GetDevice', { name => $name }); return (-1, -1) if ($response eq '' || $response =~ /^ERROR:.*/); my @scrlines = split /[\n\r]+/,$response; foreach my $hmdef (@scrlines) { my @hmdata = split /;/,$hmdef; - next if (scalar (@hmdata) == 0); - my $typeprefix = ''; + next if (scalar(@hmdata) == 0); if ($hmdata[0] eq 'D') { next if (scalar (@hmdata) != 6); # 1=Interface 2=Device-Address 3=Device-Name 4=Device-Type 5=Channel-Count + # my $typeprefix = $hmdata[2] =~ /^CUX/ ? 'CUX-' : + # $hmdata[1] eq 'HVL' ? 'HVL-' : ''; $objects{$hmdata[2]}{addtype} = 'dev'; $objects{$hmdata[2]}{channels} = $hmdata[5]; $objects{$hmdata[2]}{flag} = 'N'; $objects{$hmdata[2]}{interface} = $hmdata[1]; $objects{$hmdata[2]}{name} = $hmdata[3]; - $typeprefix = "CUX-" if ($hmdata[2] =~ /^CUX/); - $typeprefix = "HVL-" if ($hmdata[1] eq 'HVL'); - $objects{$hmdata[2]}{type} = $typeprefix . $hmdata[4]; - $objects{$hmdata[2]}{chndir} = 0; + # $objects{$hmdata[2]}{type} = $typeprefix . $hmdata[4]; + $objects{$hmdata[2]}{type} = $hmdata[4]; + $objects{$hmdata[2]}{direction} = 0; $devname = $hmdata[3]; - $devtype = $typeprefix . $hmdata[4]; + # $devtype = $typeprefix . $hmdata[4]; + $devtype = $hmdata[4]; } elsif ($hmdata[0] eq 'C') { next if (scalar (@hmdata) != 4); @@ -4469,11 +5426,11 @@ sub HMCCU_GetDevice ($$) $objects{$hmdata[1]}{flag} = 'N'; $objects{$hmdata[1]}{name} = $hmdata[2]; $objects{$hmdata[1]}{valid} = 1; - $objects{$hmdata[1]}{chndir} = $hmdata[3]; + $objects{$hmdata[1]}{direction} = $hmdata[3]; } } - if (scalar (keys %objects) > 0) { + if (scalar(keys %objects) > 0) { # Update HMCCU device tables ($devcount, $chncount) = HMCCU_UpdateDeviceTable ($hash, \%objects); @@ -4484,6 +5441,43 @@ sub HMCCU_GetDevice ($$) return ($devcount, $chncount); } +###################################################################### +# Read list of CCU interfaces via Homematic Script. +# Return number of interfaces. +# Note: Doesn't update $hash->{hmccu}{interfaces} +###################################################################### + +sub HMCCU_GetInterfaceList ($) +{ + my ($hash) = @_; + + my $ifCount = 0; + my @ifNames = (); + my @ifPorts = (); + + my $response = HMCCU_HMScriptExt ($hash, "!GetInterfaceList"); + return '' if ($response eq '' || $response =~ /^ERROR:.*/); + + foreach my $hmdef (split /[\n\r]+/,$response) { + my @hmdata = split /;/,$hmdef; + if (scalar(@hmdata) == 3 && $hmdata[2] =~ /^([^:]+):\/\/([^:]+):([0-9]+)/) { + my ($prot, $ipaddr, $port) = ($1, $2, $3); + next if (!defined ($port) || $port eq ''); + $port -= 30000 if ($port >= 10000); + push @ifNames, $hmdata[0]; + push @ifPorts, $port; + $ifCount++; + } + } + + if ($ifCount > 0) { + $hash->{hmccu}{rpcports} = join(',', @ifPorts); + $hash->{ccuinterfaces} = join(',', @ifNames); + } + + return $ifCount; +} + ###################################################################### # Read list of CCU devices, channels, interfaces, programs and groups # via Homematic Script. @@ -4505,14 +5499,15 @@ sub HMCCU_GetDeviceList ($) my %objects = (); # Read devices, channels, interfaces and groups from CCU - my $response = HMCCU_HMScriptExt ($hash, "!GetDeviceList", undef, undef, undef); + my $response = HMCCU_HMScriptExt ($hash, "!GetDeviceList"); return (-1, -1, -1, -1, -1) if ($response eq '' || $response =~ /^ERROR:.*/); - my $groups = HMCCU_HMScriptExt ($hash, "!GetGroupDevices", undef, undef, undef); + my $groups = HMCCU_HMScriptExt ($hash, "!GetGroupDevices"); # CCU is reachable $hash->{ccustate} = 'active'; # Delete old entries + HMCCU_Log ($hash, 2, "Deleting old groups"); %{$hash->{hmccu}{dev}} = (); %{$hash->{hmccu}{adr}} = (); %{$hash->{hmccu}{interfaces}} = (); @@ -4532,7 +5527,7 @@ sub HMCCU_GetDeviceList ($) # {address}{firmware} := Firmware version of device # {address}{version} := Version of RPC device description # {address}{rxmode} := Transmit mode -# {address}{chndir} := Channel direction: 1=sensor 2=actor 0=none +# {address}{direction} := Channel direction: 1=sensor 2=actor 0=none my @scrlines = split /[\n\r]+/,$response; foreach my $hmdef (@scrlines) { @@ -4543,16 +5538,17 @@ sub HMCCU_GetDeviceList ($) if ($hmdata[0] eq 'D') { # Device next if (scalar (@hmdata) != 6); - # 1=Interface 2=Device-Address 3=Device-Name 4=Device-Type 5=Channel-Count + # @hmdata: 1=Interface 2=Device-Address 3=Device-Name 4=Device-Type 5=Channel-Count $objects{$hmdata[2]}{addtype} = 'dev'; $objects{$hmdata[2]}{channels} = $hmdata[5]; $objects{$hmdata[2]}{flag} = 'N'; $objects{$hmdata[2]}{interface} = $hmdata[1]; $objects{$hmdata[2]}{name} = $hmdata[3]; - $typeprefix = "CUX-" if ($hmdata[2] =~ /^CUX/); - $typeprefix = "HVL-" if ($hmdata[1] eq 'HVL'); - $objects{$hmdata[2]}{type} = $typeprefix . $hmdata[4]; - $objects{$hmdata[2]}{chndir} = 0; + # $typeprefix = "CUX-" if ($hmdata[2] =~ /^CUX/); + # $typeprefix = "HVL-" if ($hmdata[1] eq 'HVL'); + # $objects{$hmdata[2]}{type} = $typeprefix . $hmdata[4]; + $objects{$hmdata[2]}{type} = $hmdata[4]; + $objects{$hmdata[2]}{direction} = 0; # CCU information (address = BidCoS-RF) if ($hmdata[2] eq 'BidCoS-RF') { $hash->{ccuname} = $hmdata[3]; @@ -4571,13 +5567,13 @@ sub HMCCU_GetDeviceList ($) elsif ($hmdata[0] eq 'C') { # Channel next if (scalar (@hmdata) != 4); - # 1=Channel-Address 2=Channel-Name 3=Direction + # @hmdata: 1=Channel-Address 2=Channel-Name 3=Direction $objects{$hmdata[1]}{addtype} = 'chn'; $objects{$hmdata[1]}{channels} = 1; $objects{$hmdata[1]}{flag} = 'N'; $objects{$hmdata[1]}{name} = $hmdata[2]; $objects{$hmdata[1]}{valid} = 1; - $objects{$hmdata[1]}{chndir} = $hmdata[3]; + $objects{$hmdata[1]}{direction} = $hmdata[3]; } elsif ($hmdata[0] eq 'I') { # Interface @@ -4632,52 +5628,23 @@ sub HMCCU_GetDeviceList ($) } } + if ($ifcount > 0) { + $hash->{ccuinterfaces} = join (',', keys %{$hash->{hmccu}{interfaces}}); + $hash->{hmccu}{rpcports} = join (',', keys %{$hash->{hmccu}{ifports}}); + } + else { + HMCCU_Log ($hash, 1, "Found no interfaces on CCU"); + return (-1, -1, -1, -1, -1); + } + if (scalar (keys %objects) > 0) { - if ($ifcount > 0) { - # Configure interfaces and RPC ports - my $defInterface = $hash->{hmccu}{defInterface}; - my $f = 0; - $hash->{ccuinterfaces} = join (',', keys %{$hash->{hmccu}{interfaces}}); - if (!exists ($hash->{hmccu}{interfaces}{$defInterface}) || - $hash->{hmccu}{interfaces}{$defInterface}{devcount} == 0) { - HMCCU_Log ($hash, 1, "Default interface $defInterface does not exist or has no devices assigned. Changing default interface."); - foreach my $i (@HMCCU_RPC_PRIORITY) { - if ("$i" ne "$defInterface" && exists ($hash->{hmccu}{interfaces}{$i}) && - $hash->{hmccu}{interfaces}{$i}{devcount} > 0) { - $hash->{hmccu}{defInterface} = $i; - $hash->{hmccu}{defPort} = $HMCCU_RPC_PORT{$i}; - $f = 1; - HMCCU_Log ($hash, 1, "Changed default interface from $defInterface to $i"); - last; - } - } - if ($f == 0) { - HMCCU_Log ($hash, 1, "None of interfaces ".join(',', @HMCCU_RPC_PRIORITY)." exist on CCU"); - return (-1, -1, -1, -1, -1); - } - } - - # Remove invalid RPC ports - if (defined ($hash->{hmccu}{rpcports})) { - my @plist = (); - foreach my $p (split (',', $hash->{hmccu}{rpcports})) { - push (@plist, $p) if (exists ($hash->{hmccu}{interfaces}{$HMCCU_RPC_NUMPORT{$p}})); - } - $hash->{hmccu}{rpcports} = join (',', @plist); - } - } - else { - HMCCU_Log ($hash, 1, "Found no interfaces on CCU"); - return (-1, -1, -1, -1, -1); - } - # Update HMCCU device tables ($devcount, $chncount) = HMCCU_UpdateDeviceTable ($hash, \%objects); # Read available datapoints for each device type # This will lead to problems if some devices have different firmware versions # or links to system variables ! - HMCCU_GetDatapointList ($hash, undef, undef); + HMCCU_GetDatapointList ($hash); } # Store group configurations @@ -4685,7 +5652,7 @@ sub HMCCU_GetDeviceList ($) my @gnames = ($groups =~ m/"NAME":"([^"]+)"/g); my @gmembers = ($groups =~ m/"groupMembers":\[[^\]]+\]/g); my @gtypes = ($groups =~ m/"groupType":\{"id":"([^"]+)"/g); - + foreach my $gm (@gmembers) { my $gn = shift @gnames; my $gt = shift @gtypes; @@ -4695,20 +5662,20 @@ sub HMCCU_GetDeviceList ($) $gcount++; } } + else { + HMCCU_Log ($hash, 1, "Can't read virtual groups from CCU. Response: $groups"); + } # Store asset counters $hash->{hmccu}{ccu}{devcount} = $devcount; $hash->{hmccu}{ccu}{chncount} = $chncount; - $hash->{hmccu}{ccu}{ifcount} = $ifcount; + $hash->{hmccu}{ccu}{ifcount} = $ifcount; $hash->{hmccu}{ccu}{prgcount} = $prgcount; - $hash->{hmccu}{ccu}{gcount} = $gcount; - readingsBeginUpdate ($hash); - readingsBulkUpdate ($hash, "count_devices", $devcount); - readingsBulkUpdate ($hash, "count_channels", $chncount); - readingsBulkUpdate ($hash, "count_interfaces", $ifcount); - readingsBulkUpdate ($hash, "count_programs", $prgcount); - readingsBulkUpdate ($hash, "count_groups", $gcount); - readingsEndUpdate ($hash, 1); + $hash->{hmccu}{ccu}{gcount} = $gcount; + + HMCCU_UpdateReadings ($hash, { "count_devices" => $devcount, "count_channels" => $chncount, + "count_interfaces" => $ifcount, "count_programs" => $prgcount, "count_groups" => $gcount + }); return ($devcount, $chncount, $ifcount, $prgcount, $gcount); } @@ -4719,19 +5686,19 @@ sub HMCCU_GetDeviceList ($) # Return number of datapoints read. ###################################################################### -sub HMCCU_GetDatapointList ($$$) +sub HMCCU_GetDatapointList ($;$$) { my ($hash, $devname, $devtype) = @_; my $name = $hash->{NAME}; my @devunique; - if (defined ($devname) && defined ($devtype)) { - return 0 if (exists ($hash->{hmccu}{dp}{$devtype})); + if (defined($devname) && defined($devtype)) { + return 0 if (exists($hash->{hmccu}{dp}{$devtype})); push @devunique, $devname; } else { - if (exists ($hash->{hmccu}{dp})) { + if (exists($hash->{hmccu}{dp})) { delete $hash->{hmccu}{dp}; } @@ -4740,43 +5707,39 @@ sub HMCCU_GetDatapointList ($$$) foreach my $add (sort keys %{$hash->{hmccu}{dev}}) { next if ($hash->{hmccu}{dev}{$add}{addtype} ne 'dev'); my $dt = $hash->{hmccu}{dev}{$add}{type}; - if (defined ($dt)) { + if (defined($dt)) { if ($dt ne '' && !exists ($alltypes{$dt})) { $alltypes{$dt} = 1; push @devunique, $hash->{hmccu}{dev}{$add}{name}; } } else { - Log3 $name, 2, "HMCCU: Corrupt or invalid entry in device table for device $add"; + HMCCU_Log ($hash, 2, "Corrupt or invalid entry in device table for device $add"); } } } - - if (scalar (@devunique) == 0) { - Log3 $name, 2, "HMCCU: No device types found in device table. Cannot read datapoints."; - return 0; - } + + return HMCCU_Log ($hash, 2, "No device types found in device table. Cannot read datapoints.", 0) + if (scalar(@devunique) == 0); my $devlist = join (',', @devunique); - my $response = HMCCU_HMScriptExt ($hash, "!GetDatapointList", - { list => $devlist }, undef, undef); - if ($response eq '' || $response =~ /^ERROR:.*/) { - Log3 $name, 2, "HMCCU: Cannot get datapoint list"; - return 0; - } + my $response = HMCCU_HMScriptExt ($hash, '!GetDatapointList', { list => $devlist }); + return HMCCU_Log ($hash, 2, "Cannot get datapoint list", 0) + if ($response eq '' || $response =~ /^ERROR:.*/); my $c = 0; foreach my $dpspec (split /[\n\r]+/,$response) { my ($iface, $chna, $devt, $devc, $dptn, $dptt, $dpto) = split (";", $dpspec); - $devt = "CUX-".$devt if ($iface eq 'CUxD'); - $devt = "HVL-".$devt if ($iface eq 'HVL'); - $hash->{hmccu}{dp}{$devt}{spc}{ontime} = $devc.".".$dptn if ($dptn eq "ON_TIME"); - $hash->{hmccu}{dp}{$devt}{spc}{ramptime} = $devc.".".$dptn if ($dptn eq "RAMP_TIME"); - $hash->{hmccu}{dp}{$devt}{spc}{submit} = $devc.".".$dptn if ($dptn eq "SUBMIT"); - $hash->{hmccu}{dp}{$devt}{spc}{level} = $devc.".".$dptn if ($dptn eq "LEVEL"); + my $dcdp = "$devc.$dptn"; + # $devt = "CUX-".$devt if ($iface eq 'CUxD'); + # $devt = "HVL-".$devt if ($iface eq 'HVL'); +# $hash->{hmccu}{dp}{$devt}{spc}{ontime} = $dcdp if ($dptn eq 'ON_TIME'); +# $hash->{hmccu}{dp}{$devt}{spc}{ramptime} = $dcdp if ($dptn eq 'RAMP_TIME'); +# $hash->{hmccu}{dp}{$devt}{spc}{submit} = $dcdp if ($dptn eq 'SUBMIT'); +# $hash->{hmccu}{dp}{$devt}{spc}{level} = $dcdp if ($dptn eq 'LEVEL'); $hash->{hmccu}{dp}{$devt}{ch}{$devc}{$dptn}{type} = $dptt; $hash->{hmccu}{dp}{$devt}{ch}{$devc}{$dptn}{oper} = $dpto; - if (exists ($hash->{hmccu}{dp}{$devt}{cnt}{$dptn})) { + if (exists($hash->{hmccu}{dp}{$devt}{cnt}{$dptn})) { $hash->{hmccu}{dp}{$devt}{cnt}{$dptn}++; } else { @@ -4823,23 +5786,15 @@ sub HMCCU_IsValidDevice ($$$) elsif (HMCCU_IsDevAddr ($param, 0)) { $a = $param; } -# else { -# HMCCU_Log ($hash, 3, "$param is not a valid address", 0); -# } if (exists ($hash->{hmccu}{dev}{$a})) { return $hash->{hmccu}{dev}{$a}{valid}; } -# else { -# HMCCU_Log ($hash, 3, "Address $param not found", 0); -# } # Special address for Non-Homematic devices if (($mode & $HMCCU_FL_EXADDRESS) && exists ($hash->{hmccu}{dev}{$param})) { return $hash->{hmccu}{dev}{$param}{valid} && $hash->{hmccu}{dev}{$param}{addtype} eq 'dev' ? 1 : 0; } - -# HMCCU_Log ($hash, 3, "Invalid address $param", 0); } # Name @@ -4847,9 +5802,6 @@ sub HMCCU_IsValidDevice ($$$) if (exists ($hash->{hmccu}{adr}{$param})) { return $hash->{hmccu}{adr}{$param}{valid} && $hash->{hmccu}{adr}{$param}{addtype} eq 'dev' ? 1 : 0; } -# else { -# HMCCU_Log ($hash, 3, "Device $param not found", 0); -# } } return 0; @@ -4928,9 +5880,9 @@ sub HMCCU_GetCCUDeviceParam ($$) } } - return (undef, undef, undef, undef) if (!defined ($add)); + return (undef, undef, undef, undef) if (!defined($add)); ($devadd, $chn) = split (':', $add); - return (undef, undef, undef, undef) if (!defined ($devadd) || + return (undef, undef, undef, undef) if (!defined($devadd) || !exists ($hash->{hmccu}{dev}{$devadd}) || $hash->{hmccu}{dev}{$devadd}{valid} == 0); return ($hash->{hmccu}{dev}{$devadd}{interface}, $add, $hash->{hmccu}{dev}{$add}{name}, @@ -4942,59 +5894,65 @@ sub HMCCU_GetCCUDeviceParam ($$) # hash = hash of client or IO device # devtype = Homematic device type # chn = Channel number, -1=all channels -# oper = Valid operation: 1=Read, 2=Write, 4=Event -# dplistref = Reference for array with datapoints. +# oper = Valid operation, combination of 1=Read, 2=Write, 4=Event +# dplistref = Reference for array with datapoints (optional) # Return number of datapoints. ###################################################################### -sub HMCCU_GetValidDatapoints ($$$$$) +sub HMCCU_GetValidDatapoints ($$$$;$) { my ($hash, $devtype, $chn, $oper, $dplistref) = @_; + $chn //= -1; + + my $count = 0; + my $ioHash = HMCCU_GetHash ($hash); - my $hmccu_hash = HMCCU_GetHash ($hash); - - return 0 if (HMCCU_IsFlag ($hmccu_hash->{NAME}, "dptnocheck")); - return 0 if (!exists ($hmccu_hash->{hmccu}{dp})); - return HMCCU_Log ($hash, 2, "chn undefined") if (!defined ($chn)); +# return 0 if (HMCCU_IsFlag ($ioHash->{NAME}, 'dptnocheck') || !exists($ioHash->{hmccu}{dp})); + return 0 if (!exists($ioHash->{hmccu}{dp})); if ($chn >= 0) { - if (exists ($hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$chn})) { - foreach my $dp (sort keys %{$hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$chn}}) { - if ($hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$chn}{$dp}{oper} & $oper) { - push @$dplistref, $dp; + if (exists($ioHash->{hmccu}{dp}{$devtype}{ch}{$chn})) { + foreach my $dp (sort keys %{$ioHash->{hmccu}{dp}{$devtype}{ch}{$chn}}) { + if ($ioHash->{hmccu}{dp}{$devtype}{ch}{$chn}{$dp}{oper} & $oper) { + push @$dplistref, $dp if (defined($dplistref)); + $count++; } } } } else { - if (exists ($hmccu_hash->{hmccu}{dp}{$devtype})) { - foreach my $ch (sort keys %{$hmccu_hash->{hmccu}{dp}{$devtype}{ch}}) { - foreach my $dp (sort keys %{$hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$ch}}) { - if ($hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$ch}{$dp}{oper} & $oper) { - push @$dplistref, $ch.".".$dp; + if (exists ($ioHash->{hmccu}{dp}{$devtype})) { + foreach my $ch (sort keys %{$ioHash->{hmccu}{dp}{$devtype}{ch}}) { + next if ($ch == 0 && $chn == -2); + foreach my $dp (sort keys %{$ioHash->{hmccu}{dp}{$devtype}{ch}{$ch}}) { + if ($ioHash->{hmccu}{dp}{$devtype}{ch}{$ch}{$dp}{oper} & $oper) { + push @$dplistref, $ch.".".$dp if (defined($dplistref)); + $count++; } } } } } - return scalar (@$dplistref); + return $count; } ###################################################################### # Get datapoint attribute. # Valid attributes are 'oper' or 'type'. +# Return undef on error ###################################################################### sub HMCCU_GetDatapointAttr ($$$$$) { my ($hash, $devtype, $chnno, $dpt, $attr) = @_; - return undef if ($attr ne 'oper' && $attr ne 'type'); - return undef if (!exists ($hash->{hmccu}{dp}{$devtype})); - return undef if (!exists ($hash->{hmccu}{dp}{$devtype}{ch}{$chnno})); - return undef if (!exists ($hash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt})); - return $hash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt}{$attr}; + return ( + ($attr ne 'oper' && $attr ne 'type') || + (!exists($hash->{hmccu}{dp}{$devtype})) || + (!exists($hash->{hmccu}{dp}{$devtype}{ch}{$chnno})) || + (!exists($hash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt})) + ) ? undef : $hash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt}{$attr}; } ###################################################################### @@ -5010,24 +5968,23 @@ sub HMCCU_FindDatapoint ($$$$$) { my ($hash, $devtype, $chn, $dpt, $oper) = @_; - my $hmccu_hash = HMCCU_GetHash ($hash); - - return -1 if (!exists ($hmccu_hash->{hmccu}{dp})); + my $ioHash = HMCCU_GetHash ($hash); + return -1 if (!exists($ioHash->{hmccu}{dp})); if ($chn >= 0) { - if (exists ($hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$chn})) { - foreach my $dp (sort keys %{$hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$chn}}) { + if (exists($ioHash->{hmccu}{dp}{$devtype}{ch}{$chn})) { + foreach my $dp (sort keys %{$ioHash->{hmccu}{dp}{$devtype}{ch}{$chn}}) { return $chn if ($dp eq $dpt && - $hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$chn}{$dp}{oper} & $oper); + $ioHash->{hmccu}{dp}{$devtype}{ch}{$chn}{$dp}{oper} & $oper); } } } else { - if (exists ($hmccu_hash->{hmccu}{dp}{$devtype})) { - foreach my $ch (sort keys %{$hmccu_hash->{hmccu}{dp}{$devtype}{ch}}) { - foreach my $dp (sort keys %{$hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$ch}}) { + if (exists($ioHash->{hmccu}{dp}{$devtype})) { + foreach my $ch (sort keys %{$ioHash->{hmccu}{dp}{$devtype}{ch}}) { + foreach my $dp (sort keys %{$ioHash->{hmccu}{dp}{$devtype}{ch}{$ch}}) { return $ch if ($dp eq $dpt && - $hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$ch}{$dp}{oper} & $oper); + $ioHash->{hmccu}{dp}{$devtype}{ch}{$ch}{$dp}{oper} & $oper); } } } @@ -5036,25 +5993,6 @@ sub HMCCU_FindDatapoint ($$$$$) return -1; } -###################################################################### -# Get channel number and datapoint name for special datapoint. -# Valid modes are ontime, ramptime, submit, level -###################################################################### - -sub HMCCU_GetSwitchDatapoint ($$$) -{ - my ($hash, $devtype, $mode) = @_; - - my $hmccu_hash = HMCCU_GetHash ($hash); - - if (exists ($hmccu_hash->{hmccu}{dp}{$devtype}{spc}{$mode})) { - return $hmccu_hash->{hmccu}{dp}{$devtype}{spc}{$mode}; - } - else { - return ''; - } -} - ###################################################################### # Check if datapoint is valid. # Parameter chn can be a channel address or a channel number. If dpt @@ -5063,6 +6001,7 @@ sub HMCCU_GetSwitchDatapoint ($$$) # Parameter oper specifies access flag: # 1 = datapoint readable # 2 = datapoint writeable +# 4 = datapoint events # Return 1 if ccuflags is set to dptnocheck or datapoint is valid. # Otherwise 0. ###################################################################### @@ -5070,45 +6009,46 @@ sub HMCCU_GetSwitchDatapoint ($$$) sub HMCCU_IsValidDatapoint ($$$$$) { my ($hash, $devtype, $chn, $dpt, $oper) = @_; - my $fnc = "IsValidDatapoint"; - my $hmccu_hash = HMCCU_GetHash ($hash); - return 0 if (!defined ($hmccu_hash)); + my $ioHash = HMCCU_GetHash ($hash); + return 0 if (!defined($ioHash)); - if ($hash->{TYPE} eq 'HMCCU' && !defined ($devtype)) { - $devtype = HMCCU_GetDeviceType ($hmccu_hash, $chn, 'null'); + if ($hash->{TYPE} eq 'HMCCU' && !defined($devtype)) { + $devtype = HMCCU_GetDeviceType ($ioHash, $chn, 'null'); } - return 1 if (HMCCU_IsFlag ($hmccu_hash->{NAME}, "dptnocheck")); - return 1 if (!exists ($hmccu_hash->{hmccu}{dp})); + return 1 if (HMCCU_IsFlag ($ioHash->{NAME}, "dptnocheck") || !exists($ioHash->{hmccu}{dp})); my $chnno; - if (defined ($chn)) { + + if (defined($chn) && $chn ne '') { if ($chn =~ /^[0-9]{1,2}$/) { $chnno = $chn; } - elsif (HMCCU_IsValidChannel ($hmccu_hash, $chn, $HMCCU_FL_ADDRESS)) { + elsif (HMCCU_IsValidChannel ($ioHash, $chn, $HMCCU_FL_ADDRESS)) { my ($a, $c) = split(":",$chn); $chnno = $c; } else { - HMCCU_Trace ($hash, 2, $fnc, "$chn is not a valid channel address or number"); + HMCCU_Trace ($hash, 2, "$chn is not a valid channel address or number"); + HMCCU_Trace ($hash, 2, stacktraceAsString(undef)); return 0; } } - elsif ($dpt =~ /^([0-9]{1,2})\.(.+)$/) { + + if ($dpt =~ /^([0-9]{1,2})\.(.+)$/) { $chnno = $1; $dpt = $2; } - else { - HMCCU_Trace ($hash, 2, $fnc, "channel number missing in datapoint $dpt"); + + if (!defined($chnno) || $chnno eq '') { + HMCCU_Trace ($hash, 2, "channel number missing for datapoint $dpt"); return 0; } - - my $v = (exists ($hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt}) && - ($hmccu_hash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt}{oper} & $oper)) ? 1 : 0; - HMCCU_Trace ($hash, 2, $fnc, "devtype=$devtype, chnno=$chnno, dpt=$dpt, valid=$v"); + my $v = (exists($ioHash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt}) && + ($ioHash->{hmccu}{dp}{$devtype}{ch}{$chnno}{$dpt}{oper} & $oper)) ? 1 : 0; + HMCCU_Trace ($hash, 2, "devtype=$devtype, chnno=$chnno, dpt=$dpt, valid=$v"); return $v; } @@ -5126,7 +6066,9 @@ sub HMCCU_GetMatchingDevices ($$$$) my $c = 0; foreach my $name (sort keys %{$hash->{hmccu}{adr}}) { - next if ($name !~/$regexp/ || $hash->{hmccu}{adr}{$name}{addtype} ne $mode || + next if ( + $name !~/$regexp/ || + $hash->{hmccu}{adr}{$name}{addtype} ne $mode || $hash->{hmccu}{adr}{$name}{valid} == 0); push (@$listref, $hash->{hmccu}{adr}{$name}{address}); $c++; @@ -5140,10 +6082,11 @@ sub HMCCU_GetMatchingDevices ($$$$) # Channel number will be removed if specified. ###################################################################### -sub HMCCU_GetDeviceName ($$$) +sub HMCCU_GetDeviceName ($$;$) { my ($hash, $addr, $default) = @_; - + $default //= ''; + if (HMCCU_IsValidDeviceOrChannel ($hash, $addr, $HMCCU_FL_ADDRESS)) { $addr =~ s/:[0-9]+$//; return $hash->{hmccu}{dev}{$addr}{name}; @@ -5156,10 +6099,11 @@ sub HMCCU_GetDeviceName ($$$) # Get name of a CCU device channel by address. ###################################################################### -sub HMCCU_GetChannelName ($$$) +sub HMCCU_GetChannelName ($$;$) { my ($hash, $addr, $default) = @_; - + $default //= ''; + if (HMCCU_IsValidChannel ($hash, $addr, $HMCCU_FL_ADDRESS)) { return $hash->{hmccu}{dev}{$addr}{name}; } @@ -5178,30 +6122,12 @@ sub HMCCU_GetDeviceType ($$$) if (HMCCU_IsValidDeviceOrChannel ($hash, $addr, $HMCCU_FL_ADDRESS)) { $addr =~ s/:[0-9]+$//; - return $hash->{hmccu}{dev}{$addr}{type}; + return $hash->{hmccu}{dev}{$addr}{type} // $default; } return $default; } - -###################################################################### -# Get number of channels of a CCU device. -# Channel number will be removed if specified. -###################################################################### - -sub HMCCU_GetDeviceChannels ($$$) -{ - my ($hash, $addr, $default) = @_; - - if (HMCCU_IsValidDeviceOrChannel ($hash, $addr, $HMCCU_FL_ADDRESS)) { - $addr =~ s/:[0-9]+$//; - return $hash->{hmccu}{dev}{$addr}{channels}; - } - - return 0; -} - ###################################################################### # Get default RPC interface and port ###################################################################### @@ -5210,9 +6136,17 @@ sub HMCCU_GetDefaultInterface ($) { my ($hash) = @_; - my $ifname = exists ($hash->{hmccu}{defInterface}) ? $hash->{hmccu}{defInterface} : $HMCCU_RPC_PRIORITY[0]; + my $ifname = $HMCCU_RPC_PRIORITY[0]; my $ifport = $HMCCU_RPC_PORT{$ifname}; + foreach my $i (@HMCCU_RPC_PRIORITY) { + if (exists ($hash->{hmccu}{interfaces}{$i}) && $hash->{hmccu}{interfaces}{$i}{devcount} > 0) { + $ifname = $i; + $ifport = $HMCCU_RPC_PORT{$i}; + last; + } + } + return ($ifname, $ifport); } @@ -5221,9 +6155,10 @@ sub HMCCU_GetDefaultInterface ($) # Channel number will be removed if specified. ###################################################################### -sub HMCCU_GetDeviceInterface ($$$) +sub HMCCU_GetDeviceInterface ($$;$) { my ($hash, $addr, $default) = @_; + $default //= ''; if (HMCCU_IsValidDeviceOrChannel ($hash, $addr, $HMCCU_FL_ADDRESS)) { $addr =~ s/:[0-9]+$//; @@ -5242,9 +6177,11 @@ sub HMCCU_GetDeviceInterface ($$$) # returned. ###################################################################### -sub HMCCU_GetAddress ($$$$) +sub HMCCU_GetAddress ($$;$$) { my ($hash, $name, $defadd, $defchn) = @_; + $defadd //= ''; + $defchn //= ''; my $add = $defadd; my $chn = $defchn; my $chnno = $defchn; @@ -5258,7 +6195,7 @@ sub HMCCU_GetAddress ($$$$) $name = $1; $chnno = $2; } - return ($defadd, $defchn) if (!exists ($defs{$name})); + return ($defadd, $defchn) if (!exists($defs{$name})); my $dh = $defs{$name}; return ($defadd, $defchn) if ($dh->{TYPE} ne 'HMCCUCHN' && $dh->{TYPE} ne 'HMCCUDEV'); ($add, $chn) = HMCCU_SplitChnAddr ($dh->{ccuaddr}); @@ -5366,12 +6303,18 @@ sub HMCCU_IsDevAddr ($$) # Returns device address only if parameter is already a device address. ###################################################################### -sub HMCCU_SplitChnAddr ($) +sub HMCCU_SplitChnAddr ($;$) { - my ($addr) = @_; + my ($addr, $default) = @_; + $default //= ''; + if (!defined($addr)) { + HMCCU_Log ('HMCCU', 2, stacktraceAsString(undef)); + return ('', ''); + } + my ($dev, $chn) = split (':', $addr); - $chn = '' if (!defined ($chn)); + $chn = $default if (!defined ($chn)); return ($dev, $chn); } @@ -5379,10 +6322,13 @@ sub HMCCU_SplitChnAddr ($) sub HMCCU_SplitDatapoint ($;$) { my ($dpt, $defchn) = @_; + $defchn //= ''; + + return ('', '') if (!defined($dpt)); + + my @t = split (/\./, $dpt); - my @t = split ('.', $dpt); - - return (scalar (@t) > 1) ? @t : ($defchn, $t[0]); + return (scalar(@t) > 1) ? @t : ($defchn, $t[0]); } ###################################################################### @@ -5395,7 +6341,7 @@ sub HMCCU_SplitDatapoint ($;$) # returned. ###################################################################### -sub HMCCU_FindClientDevices ($$$$) +sub HMCCU_FindClientDevices ($$;$$) { my ($hash, $modexp, $namexp, $internal) = @_; my @devlist = (); @@ -5403,14 +6349,15 @@ sub HMCCU_FindClientDevices ($$$$) foreach my $d (keys %defs) { my $ch = $defs{$d}; my $m = 1; - next if (!defined ($ch->{TYPE}) || !defined ($ch->{NAME})); - next if (defined ($modexp) && $ch->{TYPE} !~ /$modexp/); - next if (defined ($namexp) && $ch->{NAME} !~ /$namexp/); - next if (defined ($hash) && exists ($ch->{IODev}) && $ch->{IODev} != $hash); - if (defined ($internal)) { + next if ( + (!defined($ch->{TYPE}) || !defined($ch->{NAME})) || + (defined ($modexp) && $ch->{TYPE} !~ /$modexp/) || + (defined ($namexp) && $ch->{NAME} !~ /$namexp/) || + (defined ($hash) && exists ($ch->{IODev}) && $ch->{IODev} != $hash)); + if (defined($internal)) { foreach my $intspec (split (',', $internal)) { my ($i, $v) = split ('=', $intspec); - if (defined ($v) && exists ($ch->{$i}) && $ch->{$i} !~ /$v/) { + if (!exists($ch->{$i}) || (defined ($v) && exists ($ch->{$i}) && $ch->{$i} !~ /$v/)) { $m = 0; last; } @@ -5422,6 +6369,27 @@ sub HMCCU_FindClientDevices ($$$$) return @devlist; } +###################################################################### +# Check if client device already exists +# Parameter $devSpec is the name or address of a CCU device or channel +# Return name of existing FHEM device or empty string. +###################################################################### + +sub HMCCU_ExistsClientDevice ($$) +{ + my ($devSpec, $type) = @_; + + foreach my $d (keys %defs) { + my $clHash = $defs{$d}; + return $clHash->{NAME} if (defined($clHash->{TYPE}) && $clHash->{TYPE} eq $type && + (defined($clHash->{ccuaddr}) && $clHash->{ccuaddr} eq $devSpec || + defined($clHash->{ccuname}) && $clHash->{ccuname} eq $devSpec) + ); + } + + return ''; +} + ###################################################################### # Get name of assigned client device of type HMCCURPCPROC. # Create a RPC device of type HMCCURPCPROC if none is found and @@ -5437,107 +6405,83 @@ sub HMCCU_GetRPCDevice ($$$) my ($hash, $create, $ifname) = @_; my $name = $hash->{NAME}; my $rpcdevname; - my $rpcdevtype = 'HMCCURPCPROC'; my $rpchost = $hash->{host}; my $rpcprot = $hash->{prot}; my $ccuflags = HMCCU_GetFlags ($name); - if ($ccuflags =~ /(procrpc|extrpc)/) { - return (HMCCU_Log ($hash, 1, "Interface not defined for RPC server of type HMCCURPCPROC", '')) - if (!defined ($ifname)); - ($rpcdevname, $rpchost) = HMCCU_GetRPCServerInfo ($hash, $ifname, 'device,host'); - return ($rpcdevname, 0) if (defined ($rpcdevname)); - return ('', 0) if (!defined ($rpchost)); -# $rpcdevtype = 'HMCCURPCPROC'; - } -# elsif ($ccuflags =~ /extrpc/) { -# if (defined ($hash->{RPCDEV})) { -# if (exists ($defs{$hash->{RPCDEV}})) { -# my $rpchash = $defs{$hash->{RPCDEV}}; -# return (HMCCU_Log ($hash, 1, "RPC device ".$hash->{RPCDEV}." is not assigned to $name", ''), 0) -# if (!defined ($rpchash->{IODev}) || $rpchash->{IODev} != $hash); -# } -# else { -# return (HMCCU_Log ($hash, 1, "RPC device ".$hash->{RPCDEV}." not found", ''), 0); -# } -# return $hash->{RPCDEV}; -# } -# } - else { - return (HMCCU_Log ($hash, 1, "No need for RPC device when using internal RPC server", '')); - } + return (HMCCU_Log ($hash, 1, 'Interface not defined for RPC server of type HMCCURPCPROC', '')) + if (!defined($ifname)); + ($rpcdevname, $rpchost) = HMCCU_GetRPCServerInfo ($hash, $ifname, 'device,host'); + return ($rpcdevname, 0) if (defined($rpcdevname)); + return ('', 0) if (!defined($rpchost)); # Search for RPC devices associated with I/O device my @devlist; foreach my $dev (keys %defs) { my $devhash = $defs{$dev}; - next if ($devhash->{TYPE} ne $rpcdevtype); - my $ip = 'null'; - if (!exists ($devhash->{rpcip})) { - $ip = HMCCU_Resolve ($devhash->{host}, 'null'); - } - else { - $ip = $devhash->{rpcip}; - } - next if ($devhash->{host} ne $rpchost && $ip ne $rpchost); -# next if ($rpcdevtype eq 'HMCCURPCPROC' && $devhash->{rpcinterface} ne $ifname); - next if ($devhash->{rpcinterface} ne $ifname); + next if ($devhash->{TYPE} ne 'HMCCURPCPROC'); + my $ip = !exists($devhash->{rpcip}) ? HMCCU_Resolve ($devhash->{host}, 'null') : $devhash->{rpcip}; + next if (($devhash->{host} ne $rpchost && $ip ne $rpchost) || $devhash->{rpcinterface} ne $ifname); push @devlist, $devhash->{NAME}; } - my $devcnt = scalar (@devlist); + my $devcnt = scalar(@devlist); if ($devcnt == 1) { -# if ($ccuflags =~ /extrpc/) { -# $hash->{RPCDEV} = $devlist[0]; -# } -# else { - $hash->{hmccu}{interfaces}{$ifname}{device} = $devlist[0]; -# } + $hash->{hmccu}{interfaces}{$ifname}{device} = $devlist[0]; return ($devlist[0], 0); } elsif ($devcnt > 1) { - return (HMCCU_Log ($hash, 2, "Found more than one RPC device for interface $ifname", '')); + return (HMCCU_Log ($hash, 2, "Found more than one RPC device for interface $ifname", ''), 0); } HMCCU_Log ($hash, 1, "No RPC device defined for interface $ifname"); # Create RPC device - if ($create) { - my $alias = "CCU RPC $ifname"; - my $rpccreate = ''; - $rpcdevname = "d_rpc"; + return $create ? HMCCU_CreateRPCDevice ($hash, $ifname, $rpcprot, $rpchost) : ('', 0); +} - # Ensure unique device name by appending last 2 digits of CCU IP address - $rpcdevname .= HMCCU_GetIdFromIP ($hash->{ccuip}, '') if (exists ($hash->{ccuip})); +###################################################################### +# Create a new device of type HMCCURPCPROC for a RPC interface +# Return (deviceName, 1) on success. +# Return (errorMessage, 0) on error. +###################################################################### - # Build device name and define command - $rpcdevname = makeDeviceName ($rpcdevname.$ifname); - $rpccreate = "$rpcdevname $rpcdevtype $rpcprot://$rpchost $ifname"; - return (HMCCU_Log ($hash, 2, "Device $rpcdevname already exists. Please delete or rename it.", '')) - if (exists ($defs{"$rpcdevname"})); - - # Create RPC device - HMCCU_Log ($hash, 1, "Creating new RPC device $rpcdevname"); - my $ret = CommandDefine (undef, $rpccreate); - if (!defined ($ret)) { - # RPC device created. Set/copy some attributes from HMCCU device - my %rpcdevattr = ('room' => 'copy', 'group' => 'copy', 'icon' => 'copy', - 'stateFormat' => 'rpcstate/state', 'eventMap' => '/rpcserver on:on/rpcserver off:off/', - 'verbose' => 2, 'alias' => $alias ); - foreach my $a (keys %rpcdevattr) { - my $v = $rpcdevattr{$a} eq 'copy' ? AttrVal ($name, $a, '') : $rpcdevattr{$a}; - CommandAttr (undef, "$rpcdevname $a $v") if ($v ne ''); - } -# $hash->{RPCDEV} = $rpcdevname if ($ccuflags =~ /extrpc/); - return ($rpcdevname, 1); - } - else { - Log3 $name, 1, "HMCCU: [$name] Definition of RPC device failed."; - Log3 $name, 1, "HMCCU: [$name] $ret"; - } - } +sub HMCCU_CreateRPCDevice ($$$$) +{ + my ($hash, $ifname, $rpcprot, $rpchost) = @_; - return ('', 0); + my $ccuNum = $hash->{CCUNum} // '1'; + my $rpcdevname = 'd_rpc'; + + # Ensure unique device name by appending last 2 digits of CCU IP address + my $uID = HMCCU_GetIdFromIP ($hash->{ccuip}, ''); + $rpcdevname .= $uID; + my $alias = "CCU ".($uID eq '' ? $ccuNum : $uID)." RPC $ifname"; + + # Build device name and define command + $rpcdevname = makeDeviceName ($rpcdevname.$ifname); + my $rpccreate = "$rpcdevname HMCCURPCPROC $rpcprot://$rpchost $ifname"; + return (HMCCU_Log ($hash, 2, "Device $rpcdevname already exists. Please delete or rename it.", ''), 0) + if (exists($defs{"$rpcdevname"})); + + # Create RPC device + HMCCU_Log ($hash, 1, "Creating new RPC device $rpcdevname for interface $ifname"); + my $ret = CommandDefine (undef, $rpccreate); + if (!defined($ret)) { + # RPC device created. Set/copy some attributes from I/O device + my %rpcdevattr = ('room' => 'copy', 'group' => 'copy', 'icon' => 'copy', + 'stateFormat' => 'rpcstate/state', 'eventMap' => '/rpcserver on:on/rpcserver off:off/', + 'verbose' => 2, 'alias' => $alias ); + foreach my $a (keys %rpcdevattr) { + my $v = $rpcdevattr{$a} eq 'copy' ? AttrVal ($hash->{NAME}, $a, '') : $rpcdevattr{$a}; + CommandAttr (undef, "$rpcdevname $a $v") if ($v ne ''); + } + return ($rpcdevname, 1); + } + else { + HMCCU_Log ($hash, 1, "Definition of RPC device for interface $ifname failed. $ret"); + return ($ret, 0); + } } ###################################################################### @@ -5549,24 +6493,22 @@ sub HMCCU_GetRPCDevice ($$$) # Return 1 on success or 0 on error. ###################################################################### -sub HMCCU_AssignIODevice ($$$) +sub HMCCU_AssignIODevice ($$;$) { - my ($hash, $ioname, $ifname) = @_; + my ($hash, $ioName, $ifName) = @_; my $type = $hash->{TYPE}; my $name = $hash->{NAME}; - my $hmccu_hash; + my $ioHash; - AssignIoPort ($hash, $ioname); + AssignIoPort ($hash, $ioName); - $hmccu_hash = $hash->{IODev} if (exists ($hash->{IODev})); - if (!defined ($hmccu_hash) || !exists ($hmccu_hash->{TYPE}) || $hmccu_hash->{TYPE} ne 'HMCCU') { - Log3 $name, 1, "$type: [$name] Can't assign I/O device"; - return 0; - } + $ioHash = $hash->{IODev} if (exists($hash->{IODev})); + return HMCCU_Log ($hash, 1, "Can't assign I/O device", 0) + if (!defined($ioHash) || !exists($ioHash->{TYPE}) || $ioHash->{TYPE} ne 'HMCCU'); - if ($type eq 'HMCCURPCPROC' && defined ($ifname) && exists ($hmccu_hash->{hmccu}{interfaces}{$ifname})) { + if ($type eq 'HMCCURPCPROC' && defined($ifName) && exists($ioHash->{hmccu}{interfaces}{$ifName})) { # Register RPC device - $hmccu_hash->{hmccu}{interfaces}{$ifname}{device} = $name; + $ioHash->{hmccu}{interfaces}{$ifName}{device} = $name; } return 1; @@ -5584,12 +6526,10 @@ sub HMCCU_FindIODevice ($) foreach my $dn (sort keys %defs) { my $ch = $defs{$dn}; - next if (!exists ($ch->{TYPE})); - next if ($ch->{TYPE} ne 'HMCCU'); my $disabled = AttrVal ($ch->{NAME}, 'disable', 0); - next if ($disabled); + next if (!exists($ch->{TYPE}) || $ch->{TYPE} ne 'HMCCU' || $disabled); - return $ch if (!defined ($param)); + return $ch if (!defined($param)); return $ch if (HMCCU_IsValidDeviceOrChannel ($ch, $param, $HMCCU_FL_ALL)); } @@ -5608,9 +6548,8 @@ sub HMCCU_IODeviceStates () # Search for first HMCCU device foreach my $dn (sort keys %defs) { my $ch = $defs{$dn}; - next if (!exists ($ch->{TYPE})); - next if ($ch->{TYPE} ne 'HMCCU'); - if (exists ($ch->{ccustate}) && $ch->{ccustate} eq 'active') { + next if (!exists($ch->{TYPE}) || $ch->{TYPE} ne 'HMCCU'); + if (exists($ch->{ccustate}) && $ch->{ccustate} eq 'active') { $active++; } else { @@ -5632,10 +6571,10 @@ sub HMCCU_GetHash ($@) { my ($hash) = @_; - if (defined ($hash) && $hash != 0) { + if (defined($hash) && $hash != 0) { if ($hash->{TYPE} eq 'HMCCUDEV' || $hash->{TYPE} eq 'HMCCUCHN') { - return $hash->{IODev} if (exists ($hash->{IODev})); - return HMCCU_FindIODevice ($hash->{ccuaddr}) if (exists ($hash->{ccuaddr})); + return $hash->{IODev} if (exists($hash->{IODev})); + return HMCCU_FindIODevice ($hash->{ccuaddr}) if (exists($hash->{ccuaddr})); } elsif ($hash->{TYPE} eq 'HMCCU') { return $hash; @@ -5645,7 +6584,7 @@ sub HMCCU_GetHash ($@) # Search for first HMCCU device foreach my $dn (sort keys %defs) { my $ch = $defs{$dn}; - next if (!exists ($ch->{TYPE})); + next if (!exists($ch->{TYPE})); return $ch if ($ch->{TYPE} eq 'HMCCU'); } @@ -5658,103 +6597,1923 @@ sub HMCCU_GetHash ($@) sub HMCCU_GetAttribute ($$$$) { - my ($hmccu_hash, $cl_hash, $attr_name, $attr_def) = @_; + my ($ioHash, $clHash, $attrName, $attrDefault) = @_; - my $value = AttrVal ($cl_hash->{NAME}, $attr_name, ''); - $value = AttrVal ($hmccu_hash->{NAME}, $attr_name, $attr_def) if ($value eq ''); + my $value = AttrVal ($clHash->{NAME}, $attrName, ''); + $value = AttrVal ($ioHash->{NAME}, $attrName, $attrDefault) if ($value eq ''); return $value; } ###################################################################### -# Get number of occurrences of datapoint. -# Return 0 if datapoint does not exist. +# Set initial attributes after device definition as defined in IO +# device attribute ccudef-attributes ###################################################################### -sub HMCCU_GetDatapointCount ($$$) +sub HMCCU_SetInitialAttributes ($$) { - my ($hash, $devtype, $dpt) = @_; + my ($ioHash, $clName) = @_; + + my $ccudefAttributes = AttrVal ($ioHash->{NAME}, 'ccudef-attributes', 'room=Homematic'); + foreach my $a (split(';', $ccudefAttributes)) { + my ($an, $av) = split('=', $a); + CommandAttr (undef, "$clName $an $av") if (defined($av)); + } +} + +###################################################################### +# Set default attributes for client device. +# Optionally delete obsolete attributes. +###################################################################### + +sub HMCCU_SetDefaultAttributes ($;$) +{ + my ($clHash, $parRef) = @_; + my $ioHash = HMCCU_GetHash ($clHash); + my $clType = $clHash->{TYPE}; + my $clName = $clHash->{NAME}; - if (exists ($hash->{hmccu}{dp}{$devtype}{cnt}{$dpt})) { - return $hash->{hmccu}{dp}{$devtype}{cnt}{$dpt}; + $parRef //= { mode => 'update', role => undef, roleChn => undef }; + my $role; + +# HMCCU_Log ($clHash, 2, HMCCU_RefToString($parRef)); + + if ($parRef->{mode} eq 'reset') { + # List of attributes to be removed + my @removeAttr = ('ccureadingname', 'ccuscaleval', 'eventMap', 'cmdIcon', + 'substitute', 'webCmd', 'widgetOverride' + ); + + my $detect = HMCCU_DetectDevice ($ioHash, $clHash->{ccuaddr}, $clHash->{ccuif}); + if (defined($detect) && $detect->{level} > 0) { + my ($sc, $sd, $cc, $cd, $rsd, $rcd) = HMCCU_SetDefaultSCDatapoints ($ioHash, $clHash, $detect, 1); + HMCCU_Log ($clHash, 2, "Cannot set default state- and/or control datapoints") + if ($rsd == 0 && $rcd == 0); + + $role = HMCCU_GetChannelRole ($clHash, $detect->{defCCh}); + + # Set attributes defined in IO device attribute ccudef-attributes + HMCCU_SetInitialAttributes ($ioHash, $clName); + + # Remove additional attributes if device type is supported by HMCCU + if (($detect->{level} == 5 && $detect->{rolePatternCount} > 1 && $clType eq 'HMCCUDEV' && + AttrVal ($clName, 'statedatapoint', '') eq '' && AttrVal ($clName, 'controldatapoint', '') eq '') || + ($detect->{defSCh} == -1 && $detect->{defCCh} == -1)) { + HMCCU_LogDisplay ($clHash, 2, "Please select a state and/or control datapoint by using attributes statedatapoint and controldatapoint"); + } + else { + push @removeAttr, 'statechannel', 'statedatapoint' if ($detect->{defSCh} != -1); + push @removeAttr, 'controlchannel', 'controldatapoint', 'statevals' if ($detect->{defCCh} != -1); + } + + # Remove attributes + HMCCU_DeleteAttributes ($clHash, \@removeAttr, 1); + + # Update command tables + HMCCU_UpdateRoleCommands ($ioHash, $clHash, $cc); + HMCCU_UpdateAdditionalCommands ($ioHash, $clHash, $cc, $cd); + } + else { + HMCCU_LogDisplay ($clHash, 2, "Device type $clHash->{ccutype} not known by HMCCU"); + # Remove attributes + HMCCU_DeleteAttributes ($clHash, \@removeAttr, 1); + } } else { + my ($sc, $sd, $cc, $cd) = HMCCU_GetSCDatapoints ($clHash); + $role = $parRef->{role} // HMCCU_GetChannelRole ($clHash, $parRef->{roleChn} // $cc); + } + + if (defined($role) && $role ne '') { + $clHash->{hmccu}{semDefaults} = 1; + + # Set additional attributes + if (exists($HMCCU_ATTR->{$role}) && !exists($HMCCU_ATTR->{$role}{_none_})) { + foreach my $a (keys %{$HMCCU_ATTR->{$role}}) { + CommandAttr (undef, "$clName $a ".$HMCCU_ATTR->{$role}{$a}); + } + } + + $clHash->{hmccu}{semDefaults} = 0; + return 1; + } + else { + HMCCU_Log ($clHash, 2, "Cannot detect role of $clName"); return 0; } } ###################################################################### -# Get channels and datapoints from attributes statechannel, -# statedatapoint and controldatapoint. -# Return attribute values. Attribute controldatapoint is splitted into -# controlchannel and datapoint name. If attribute statedatapoint -# contains channel number it is splitted into statechannel and -# datapoint name. -# If controldatapoint is not specified it will synchronized with -# statedatapoint. +# Delete list of attributes ###################################################################### -sub HMCCU_GetSpecialDatapoints ($$$$$) +sub HMCCU_DeleteAttributes ($$;$) { -# my ($hash, $defsc, $defsd, $defcc, $defcd) = @_; - my ($hash, $sc, $sd, $cc, $cd) = @_; - my $name = $hash->{NAME}; - my $type = $hash->{TYPE}; + my ($clHash, $attrList, $sem) = @_; + $sem //= 0; + my $clName = $clHash->{NAME}; + + $clHash->{hmccu}{semDefaults} = $sem; + foreach my $a (@$attrList) { + CommandDeleteAttr (undef, "$clName $a") if (exists($attr{$clName}{$a})); + } + $clHash->{hmccu}{semDefaults} = 0; +} + +###################################################################### +# Get state values of client device +# Return '' if no state values available +###################################################################### + +sub HMCCU_GetStateValues ($;$$) +{ + my ($clHash, $dpt, $ctrlChn) = @_; + $dpt //= ''; + $ctrlChn //= ''; + + my $sv = AttrVal ($clHash->{NAME}, 'statevals', ''); + if ($sv eq '' && $dpt ne '' && $ctrlChn ne '') { + my $role = HMCCU_GetChannelRole ($clHash, $ctrlChn); + HMCCU_Trace ($clHash, 2, "dpt=$dpt, ctrlChn=$ctrlChn, role=$role"); + if ($role ne '' && exists($HMCCU_STATECONTROL->{$role}) && + HMCCU_DetectSCDatapoint ($HMCCU_STATECONTROL->{$role}{C}, $clHash->{ccuif}) eq $dpt) + { + return $HMCCU_STATECONTROL->{$role}{V}; + } + } + + return $sv; +} + +###################################################################### +# Return additional commands depending on the channel role. +# +# Command-Defintion: +# 'Datapoint-Definition [...]' +# Datapoint-Definition: +# Paramset:Datapoints:[+|-]Value +# Paramset:Datapoints:?Parameter +# Paramset:Datapoints:?Parameter=Default-Value +# Paramset:Datapoints:#Parameter[=Value1[,...]] +# Paramset: +# V=VALUES, M=MASTER (channel), D=MASTER (device) +# Datapoints: +# List of parameter names separated by , +# If Parameter is preceded by ? any value is accepted. +# If Parameter is preceded by #, Datapoint must have type ENUM or +# a list of values must be specified. +# If Default-Value is preceeded by + or -, value is added to or +# subtracted from current datapoint value. +# +# Output format: +# {cmdType} - Command type 'set' or 'get' +# {'cmd'}{syntax} - Command syntax (input definition) +# {'cmd'}{channel} - Channel number +# {'cmd'}{role} - Channel role +# {'cmd'}{usage} - Usage string +# {'cmd'}{subcount} - Number of sub commands +# {'cmd'}{subcmd}{'nnn'}{ps} - Parameter set name +# {'cmd'}{subcmd}{'nnn'}{dpt} - Datapoint name +# {'cmd'}{subcmd}{'nnn'}{type} - Datapoint type +# {'cmd'}{subcmd}{'nnn'}{parname} - Parameter name (default=datapoint) +# {'cmd'}{subcmd}{'nnn'}{partype} - Parameter type (s. below) +# {'cmd'}{subcmd}{'nnn'}{args} - Comma separated list of valid values +# or default value or fix value or '' +# {'cmd'}{subcmd}{'nnn'}{min} - Minimum value +# {'cmd'}{subcmd}{'nnn'}{max} - Maximum value +# {'cmd'}{subcmd}{'nnn'}{unit} - Unit of parameter value +# {'cmd'}{subcmd}{'nnn'}{fnc} - Function name (called with parameter value) +# {cmdlist}{set} - Set command definition +# {cmdlist}{get} - Get command definition +# +# Datapoint types: BOOL, INTEGER, ENUM, ACTION, STRING +# +# Parameter types: +# 0 = no parameter +# 1 = argument required, list of valid parameters defined +# 2 = argument required, default value may be available +# 3 = fix value, no argument required +# 4 = fix internal value, no argument required, default possible +###################################################################### + +sub HMCCU_UpdateRoleCommands ($$;$) +{ + my ($ioHash, $clHash, $chnNo) = @_; + $chnNo //= ''; + + my %pset = ('V' => 'VALUES', 'M' => 'MASTER', 'D' => 'MASTER', 'I' => 'INTERNAL'); + my @cmdSetList = (); + my @cmdGetList = (); + return if (!defined($clHash->{hmccu}{role}) || $clHash->{hmccu}{role} eq ''); + + # Delete existing role commands + delete $clHash->{hmccu}{roleCmds} if (exists($clHash->{hmccu}{roleCmds})); + + URCROL: foreach my $chnRole (split(',', $clHash->{hmccu}{role})) { + my ($channel, $role) = split(':', $chnRole); + next URCROL if (!defined($role) || !exists($HMCCU_ROLECMDS->{$role})); + + URCCMD: foreach my $cmdKey (keys %{$HMCCU_ROLECMDS->{$role}}) { + next URCCMD if ($clHash->{TYPE} eq 'HMCCUCHN' && $chnNo ne '' && $chnNo != $channel && $chnNo ne 'd'); + my ($cmd, $cmdIf) = split (':', $cmdKey); + next URCCMD if (defined($cmdIf) && $clHash->{ccuif} !~ /$cmdIf/); + my $cmdSyntax = $HMCCU_ROLECMDS->{$role}{$cmdKey}; + my $cmdChn = $channel; + my $cmdType = 'set'; + if ($cmd =~ /^(set|get) (.+)$/) { + $cmdType = $1; + $cmd = $2; + } + my $parAccess = $cmdType eq 'set' ? 2 : 5; + + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{syntax} = $cmdSyntax; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{role} = $role; + + my $cnt = 0; + my $cmdDef = $cmd; + my $usage = $cmdDef; + my $cmdArgList = ''; + my @parTypes = (0, 0, 0, 0, 0); + + URCSUB: foreach my $subCmd (split(/\s+/, $cmdSyntax)) { + my $pt = 0; # Default = no parameter + my $scn = sprintf ("%03d", $cnt); # Subcommand number in command definition + my $subCmdNo = $cnt; # Subcommand number in command execution + my @subCmdList = split(/:/, $subCmd); + if ($subCmdList[0] =~ /^([0-9]+)$/) { + $subCmdNo = $1; + shift @subCmdList; + } + my ($ps, $dpt, $par, $fnc) = @subCmdList; + my $psName = $ps eq 'I' ? 'VALUES' : $pset{$ps}; + if (!defined($psName)) { + HMCCU_Log ($clHash, 2, "Invalid or undefined parameter set in $subCmd"); + next; + } + my ($addr, undef) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + $cmdChn = 'd' if ($ps eq 'D'); + + # Allow different parameter names for same command (depend on firmware revision of device) + my @dptList = split /,/, $dpt; + if (scalar(@dptList) > 1) { + foreach my $d (@dptList) { + if (HMCCU_IsValidParameter ($clHash, "$addr:$cmdChn", $psName, $d, $parAccess)) { + $dpt = $d; + last; + } + } + } + else { + if (!HMCCU_IsValidParameter ($clHash, "$addr:$cmdChn", $psName, $dpt, $parAccess)) { + HMCCU_Log ($clHash, 4, "Invalid parameter $addr:$cmdChn $psName $dpt $parAccess"); + next URCSUB; + } + } + + my $paramDef = HMCCU_GetParamDef ($ioHash, "$addr:$cmdChn", $psName, $dpt); + if (!defined($paramDef)) { + HMCCU_Log ($ioHash, 4, "INFO: Can't get definition of datapoint $addr:$cmdChn.$dpt. Ignoring command $cmd for device $clHash->{NAME}"); + next URCCMD; + } + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{scn} = sprintf("%03d", $subCmdNo); + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{min} = $paramDef->{MIN}; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{max} = $paramDef->{MAX}; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{unit} = $paramDef->{UNIT} // ''; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{ps} = $pset{$ps}; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{dpt} = $dpt; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{fnc} = $fnc // ''; + if ($paramDef->{TYPE} eq 'ENUM' && defined($paramDef->{VALUE_LIST})) { + # Build lookup table + my @el = split(',', $paramDef->{VALUE_LIST}); + my $i = 0; + foreach my $e (@el) { + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{look}{$e} = $i; + $i++; + } + + # Parameter definition contains names for min and max value + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{min} = + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{look}{$paramDef->{MIN}} + if (exists($clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{look}{$paramDef->{MIN}})); + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{max} = + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{look}{$paramDef->{MAX}} + if (exists($clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{look}{$paramDef->{MAX}})); + } + + if (defined($par) && $par ne '') { + if ($par =~ /^#(.+)$/) { + my ($pn, $pv) = split('=', $1); + + # Parameter list + my $argList = ''; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{parname} = $pn; + $pt = 1; # Enum / List of fixed values + + if ($paramDef->{TYPE} eq 'ENUM' && defined($paramDef->{VALUE_LIST})) { + # Parameter type ENUM + $argList = $paramDef->{VALUE_LIST}; + } + else { + # Parameter with list of values +# my ($pn, $pv) = split('=', $par); + $argList = $pv // ''; + my %valList; + if ($dpt eq $HMCCU_STATECONTROL->{$role}{C}) { + # If parameter is control datapoint, use values/conversions from HMCCU_STATECONTROL + foreach my $cv (split(',', $HMCCU_STATECONTROL->{$role}{V})) { + my ($vn, $vv) = split(':', $cv); + $valList{$vn} = $vv // $vn; + } + } + elsif (exists($HMCCU_CONVERSIONS->{$role}{$dpt})) { + # If a list of conversions exists, use values/conversions from HMCCU_CONVERSIONS + foreach my $cv (split(',', $argList)) { + if (exists($HMCCU_CONVERSIONS->{$role}{$dpt}{$cv})) { + $valList{$HMCCU_CONVERSIONS->{$role}{$dpt}{$cv}} = $cv; + } + else { + $valList{$cv} = $cv; + } + } + } + else { + # As fallback use values as specified + foreach my $cv (split(',', $argList)) { + $valList{$cv} = $cv; + } + } + + # Build the lookup table + my @el = split(',', $argList); + my $i = 0; + foreach my $e (@el) { + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{look}{$e} = $valList{$e} // $i; + $i++; + } + } + + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{args} = $argList; + $cmdArgList = $argList; + $usage .= " {$argList}"; + } + elsif ($par =~ /^\?(.+)$/) { + # User must specify a parameter (default value possible) + my ($pn, $pv) = split('=', $1); + $pt = 2; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{parname} = $pn; + if (defined($pv)) { + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{args} = "$pv"; + $usage .= " [$pn]"; + } + else { + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{args} = ''; + $usage .= " $pn"; + } + } + elsif ($par =~ /^\*(.+)$/) { + # Internal parameter taken from device hash (default value possible) + my ($pn, $pv) = split('=', $1); + $pt = 4; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{parname} = $pn; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{args} = $pv // ''; + } + else { + # Fix value. Command has no argument + my ($pn, $pv) = split('=', $par); + $pt = 3; + if (defined($pv)) { + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{parname} = $pn; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{args} = $pv; + } + else { + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{parname} = $dpt; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{args} = $par; + } + } + } + + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcmd}{$scn}{partype} = $pt; + $parTypes[$pt]++; + $cnt++; + } + + if ($cnt == 0) { + if (!exists($clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcount})) { + HMCCU_Log ($clHash, 4, "No datapoints found. Deleting command $cmd"); + delete $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}; + } + next URCCMD; + } + + if ($parTypes[1] == 1 && $parTypes[2] == 0 && $cmdArgList ne '') { + # Only one variable argument. Argument belongs to a predefined value list + # If values contain blanks, substitute blanks by # and enclose strings in quotes + $cmdDef .= ':'.join(',', map { $_ =~ / / ? '"'.(s/ /#/gr).'"' : $_ } split(',', $cmdArgList)); + } + elsif ($parTypes[1] == 0 && $parTypes[2] == 0) { + $cmdDef .= ':noArg'; + } + + if (exists($clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{channel})) { + # Same command in multiple channels. + # Channel number will be set to control channel during command execution + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{channel} = '?'; + } + else { + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{channel} = $cmdChn; + } + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{usage} = $usage; + $clHash->{hmccu}{roleCmds}{$cmdType}{$cmd}{subcount} = $cnt; + + if ($cmdType eq 'set') { + push @cmdSetList, $cmdDef; + } + else { + push @cmdGetList, $cmdDef; + } + } + } + + $clHash->{hmccu}{cmdlist}{set} = join(' ', @cmdSetList); + $clHash->{hmccu}{cmdlist}{get} = join(' ', @cmdGetList); + + return; +} + +###################################################################### +# Update additional commands which depend on device state +###################################################################### + +sub HMCCU_UpdateAdditionalCommands ($$;$$) +{ + my ($ioHash, $clHash, $cc, $cd) = @_; + $cc //= ''; + $cd //= ''; + + # No controldatapoint available (read only device) + if ($cd eq '' || $cc eq '') { + HMCCU_Log ($clHash, 4, "No control datapoint. Maybe device is read only or attribute will be set later"); + return; + } + + my $s = exists($clHash->{hmccu}{cmdlist}{set}) && $clHash->{hmccu}{cmdlist}{set} ne '' ? ' ' : ''; + my ($addr, $chn) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + + # Check if role of control channel is supported by HMCCU + my $role = HMCCU_GetChannelRole ($clHash, $cc); + if ($role ne '' && exists($HMCCU_STATECONTROL->{$role}) && + HMCCU_DetectSCDatapoint ($HMCCU_STATECONTROL->{$role}{C}, $clHash->{ccuif}) eq $cd) { + # Only add toggle command, ignore attribute statevals + my %stateCmds = split (/[:,]/, $HMCCU_STATECONTROL->{$role}{V}); + my @states = keys %stateCmds; + $clHash->{hmccu}{cmdlist}{set} .= $s.'toggle:noArg' if (scalar(@states) > 1); + return; + } + + my $sv = AttrVal ($clHash->{NAME}, 'statevals', ''); + if ($sv ne '') { + my %stateCmds = split (/[:,]/, $sv); + my @states = keys %stateCmds; + + my $paramDef = HMCCU_GetParamDef ($ioHash, "$addr:$cc", 'VALUES', $cd); + if (defined($paramDef)) { + foreach my $cmd (@states) { + $clHash->{hmccu}{roleCmds}{set}{$cmd}{channel} = $cc; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{role} = $role; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{subcount} = 1; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{syntax} = "V:$cd:".$stateCmds{$cmd}; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{usage} = $cmd; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{subcmd}{'000'}{partype} = 3; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{subcmd}{'000'}{args} = $stateCmds{$cmd}; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{subcmd}{'000'}{min} = $paramDef->{MIN}; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{subcmd}{'000'}{max} = $paramDef->{MAX}; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{subcmd}{'000'}{unit} = $paramDef->{UNIT} // ''; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{subcmd}{'000'}{ps} = 'VALUES'; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{subcmd}{'000'}{dpt} = $cd; + $clHash->{hmccu}{roleCmds}{set}{$cmd}{subcmd}{'000'}{fnc} = ''; + } + $clHash->{hmccu}{cmdlist}{set} .= $s.join(' ', map { $_ . ':noArg' } @states) + if (scalar(@states) > 0); + $clHash->{hmccu}{cmdlist}{set} .= ' toggle:noArg' + if (scalar(@states) > 1); + } + else { + HMCCU_Log ($clHash, 3, "Can't get definition of datapoint $addr:$cc.$cd. Ignoring commands ".join(',',@states)." for device $clHash->{NAME}"); + } + } +} + +###################################################################### +# Execute command related to role +# Parameters: +# $mode: 'set' or 'get' +# $command: The command +###################################################################### + +sub HMCCU_ExecuteRoleCommand ($@) +{ + my ($ioHash, $clHash, $mode, $command, $a, $h) = @_; + + my $name = $clHash->{NAME}; + my $rc; + my %dpval; + my %cfval; + my %inval; + my %cmdFnc; + my ($devAddr, undef) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + my $usage = $clHash->{hmccu}{roleCmds}{$mode}{$command}{usage}; + my $c = 0; + + my $channel = $clHash->{hmccu}{roleCmds}{$mode}{$command}{channel}; + if ("$channel" eq '?') { + my ($sc, $sd, $cc, $cd) = HMCCU_GetSCDatapoints ($clHash); + return HMCCU_SetError ($clHash, -12) if ($cc eq ''); + $channel = $cc; + } + my $chnAddr = "$devAddr:$channel"; + + foreach my $cmdNo (sort keys %{$clHash->{hmccu}{roleCmds}{$mode}{$command}{subcmd}}) { + my $cmd = $clHash->{hmccu}{roleCmds}{$mode}{$command}{subcmd}{$cmdNo}; + my $value; + my @par = (); + + if ($cmd->{ps} ne 'INTERNAL' && !HMCCU_IsValidParameter ($clHash, $chnAddr, $cmd->{ps}, $cmd->{dpt})) { + HMCCU_Trace ($clHash, 2, "Invalid parameter $cmd->{ps}.$cmd->{dpt} for command $command"); + return HMCCU_SetError ($clHash, -8, "$cmd->{ps}.$cmd->{dpt}"); + } + + if ($cmd->{partype} == 4) { + # Internal value + $value = $clHash->{hmccu}{intvalues}{$cmd->{parname}} // $cmd->{args}; + } + elsif ($cmd->{partype} == 3) { + # Fixed value + if ($cmd->{args} =~ /^[+-](.+)$/) { + # Delta value + return HMCCU_SetError ($clHash, "Current value of $channel.$cmd->{dpt} not available") + if (!defined($clHash->{hmccu}{dp}{"$channel.$cmd->{dpt}"}{$cmd->{ps}}{SVAL})); + $value = $clHash->{hmccu}{dp}{"$channel.$cmd->{dpt}"}{$cmd->{ps}}{SVAL}+int($cmd->{args}); + } + else { + $value = $cmd->{args}; + } + } + elsif ($cmd->{partype} == 2) { + # Normal value + $value = shift @$a // $cmd->{args}; + return HMCCU_SetError ($clHash, "Missing parameter $cmd->{parname}. Usage: $mode $name $usage") + if ($value eq ''); + if ($cmd->{args} =~ /^([+-])(.+)$/) { + # Delta value. Sign depends on sign of default value. Sign of specified value is ignored + my $sign = $1 eq '+' ? 1 : -1; + return HMCCU_SetError ($clHash, "Current value of $channel.$cmd->{dpt} not available") + if (!defined($clHash->{hmccu}{dp}{"$channel.$cmd->{dpt}"}{$cmd->{ps}}{NVAL})); + $value = $clHash->{hmccu}{dp}{"$channel.$cmd->{dpt}"}{$cmd->{ps}}{NVAL}+abs(int($value))*$sign; + } + if ($cmd->{unit} eq 's') { + $value = HMCCU_GetTimeSpec ($value); + return HMCCU_SetError ($clHash, 'Wrong time format. Use seconds or HH:MM[:SS]') + if ($value < 0); + } + } + else { + # Set of valid values + my $vl = shift @$a // return HMCCU_SetError ( + $clHash, "Missing parameter $cmd->{parname}. Usage: $mode $name $usage"); + $value = $cmd->{look}{$vl} // return HMCCU_SetError ( + $clHash, "Illegal value $vl. Use one of ". join(',', keys %{$cmd->{look}})); + push @par, $vl; + } + + # Align new value with min/max boundaries + if (exists($cmd->{min}) && exists($cmd->{max})) { + # Use mode = 0 in HMCCU_ScaleValue to get the min and max value allowed + HMCCU_Trace ($clHash, 2, "MinMax: value=$value, min=$cmd->{min}, max=$cmd->{max}"); + my $scMin = HMCCU_ScaleValue ($clHash, $channel, $cmd->{dpt}, $cmd->{min}, 0); + my $scMax = HMCCU_ScaleValue ($clHash, $channel, $cmd->{dpt}, $cmd->{max}, 0); + $value = HMCCU_MinMax ($value, $scMin, $scMax); + HMCCU_Trace ($clHash, 2, "scMin=$scMin, scMax=$scMax, scVal=$value"); + } + + if ($cmd->{ps} eq 'VALUES') { + # my $dno = sprintf ("%03d", $c); + # $dpval{"$dno.$clHash->{ccuif}.$chnAddr.$cmd->{dpt}"} = $value; + $dpval{"$cmd->{scn}.$clHash->{ccuif}.$chnAddr.$cmd->{dpt}"} = $value; + $c++; + } + elsif ($cmd->{ps} eq 'INTERNAL') { + $inval{$cmd->{parname}} = $value; + } + else { + $cfval{$cmd->{dpt}} = $value; + } + + push @par, $value if (defined($value)); + $cmdFnc{$cmdNo}{fnc} = $cmd->{fnc}; + $cmdFnc{$cmdNo}{par} = \@par; + } + + my $ndpval = scalar(keys %dpval); + my $ncfval = scalar(keys %cfval); + my $ninval = scalar(keys %inval); + + if ($mode eq 'set') { + # Set commands + if ($ninval > 0) { + # Internal commands + foreach my $iv (keys %inval) { + HMCCU_Trace ($clHash, 2, "Internal $iv=$inval{$iv}"); + $clHash->{hmccu}{intvalues}{$iv} = $inval{$iv}; + } + return HMCCU_SetError ($clHash, 0); + } + if ($ndpval > 0) { + # Datapoint commands + foreach my $dpv (keys %dpval) { HMCCU_Trace ($clHash, 2, "Datapoint $dpv=$dpval{$dpv}"); } + $rc = HMCCU_SetMultipleDatapoints ($clHash, \%dpval); + return HMCCU_SetError ($clHash, HMCCU_Min(0, $rc)); + } + if ($ncfval > 0) { + # Config commands + foreach my $pv (keys %cfval) { HMCCU_Trace ($clHash, 2, "Parameter $pv=$cfval{$pv}"); } + ($rc, undef) = HMCCU_SetMultipleParameters ($clHash, $chnAddr, \%cfval, 'MASTER'); + return HMCCU_SetError ($clHash, HMCCU_Min(0, $rc)); + } + } + else { + # Get commands + my $opt = ''; + if ($ndpval > 0 && $ncfval == 0) { $opt = 'values'; } + elsif ($ndpval == 0 && $ncfval > 0) { $opt = 'config'; } + elsif ($ndpval > 0 && $ncfval > 0) { $opt = 'update'; } + + if ($opt ne '') { + $chnAddr =~ s/:d$//; + my $resp = HMCCU_ExecuteGetParameterCommand ($ioHash, $clHash, $opt, [ $chnAddr ]); + return HMCCU_SetError ($clHash, "Cannot get values for command") if (!defined($resp)); + my $disp = ''; + foreach my $cmdNo (sort keys %cmdFnc) { + if ($cmdFnc{$cmdNo}{fnc} ne '') { + # :( + no strict "refs"; + $disp .= &{$cmdFnc{$cmdNo}{fnc}}($ioHash, $clHash, $resp, @{$cmdFnc{$cmdNo}{par}}); + use strict "refs"; + } + } + return $disp; + } + } + + return HMCCU_SetError ($clHash, "Command $command not executed"); +} + +###################################################################### +# Execute set clear command +###################################################################### + +sub HMCCU_ExecuteSetClearCommand ($@) +{ + my ($clHash, $a) = @_; + + my $delPar = shift @$a // '.*'; + my $rnexp = ''; + if ($delPar eq 'reset') { + $rnexp = '.*'; + delete $clHash->{hmccu}{dp}; + } + else { + $rnexp = $delPar; + } + HMCCU_DeleteReadings ($clHash, $rnexp); + return HMCCU_SetState ($clHash, "OK"); +} + +###################################################################### +# Execute set control command +###################################################################### + +sub HMCCU_ExecuteSetControlCommand ($@) +{ + my ($clHash, $a, $h) = @_; + + my $value = shift @$a // return HMCCU_SetError ($clHash, "Usage: set $clHash->{NAME} control {value}"); + my ($sc, $sd, $cc, $cd) = HMCCU_GetSCDatapoints ($clHash); + my $stateVals = HMCCU_GetStateValues ($clHash, $cd, $cc); + my $rc = HMCCU_SetMultipleDatapoints ($clHash, + { "001.$clHash->{ccuif}.$clHash->{ccuaddr}:$cc.$cd" => HMCCU_Substitute ($value, $stateVals, 1, undef, '') } + ); + return HMCCU_SetError ($clHash, HMCCU_Min(0, $rc)); +} + +###################################################################### +# Execute set datapoint command +###################################################################### + +sub HMCCU_ExecuteSetDatapointCommand ($@) +{ + my ($clHash, $a, $h) = @_; + + my $usage = "Usage: set $clHash->{NAME} datapoint [{channel-number}.]{datapoint} {value} [...]"; + my %dpval; + my $i = 0; + my ($devAddr, $chnNo) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + my ($sc, $sd, $cc, $cd) = HMCCU_GetSCDatapoints ($clHash); + my $stVals = HMCCU_GetStateValues ($clHash, $cd, $cc); + + push (@$a, %${h}) if (defined($h)); + while (my $dpt = shift @$a) { + my $value = shift @$a // return HMCCU_SetError ($clHash, $usage); + $i++; + + if ($clHash->{TYPE} eq 'HMCCUDEV') { + if ($dpt =~ /^([0-9]+)\..+$/) { + return HMCCU_SetError ($clHash, -7) if ($1 >= $clHash->{hmccu}{channels}); + } + else { + return HMCCU_SetError ($clHash, -12) if ($cc eq ''); + $dpt = "$cc.$dpt"; + } + } + else { + if ($dpt =~ /^([0-9]+)\..+$/) { + return HMCCU_SetError ($clHash, -7) if ($1 != $chnNo); + } + else { + $dpt = "$chnNo.$dpt"; + } + } + + $value = HMCCU_Substitute ($value, $stVals, 1, undef, '') if ($stVals ne '' && $dpt eq $cd); + + my $no = sprintf ("%03d", $i); + $dpval{"$no.$clHash->{ccuif}.$devAddr:$dpt"} = $value; + } + + return HMCCU_SetError ($clHash, $usage) if (scalar(keys %dpval) < 1); + + my $rc = HMCCU_SetMultipleDatapoints ($clHash, \%dpval); + return HMCCU_SetError ($clHash, HMCCU_Min(0, $rc)); +} + +###################################################################### +# Execute set config / values / link command +# Usage of command in FHEM for HMCCUCHN devices: +# MASTER: set config ['device'] parameter=value [...] +# LINKS: set config peer-address parameter=value [...] +# VALUES: set values parameter=value [...] +# Usage of command in FHEM for HMCCUDEV devices: +# MASTER: set config [channel] parameter=value [...] +# LINKS: set config channel peer-address parameter=value [...] +# VALUES: set values channel parameter=value [...] +###################################################################### + +sub HMCCU_ExecuteSetParameterCommand ($@) +{ + my ($ioHash, $clHash, $command, $a, $h) = @_; + + my $paramset = $command eq 'config' ? 'MASTER' : 'VALUES'; + my $rc; + my $result = ''; + my $receiver = ''; + my $ccuobj = $clHash->{ccuaddr}; + + return HMCCU_SetError ($clHash, 'No parameter specified') if ((scalar keys %{$h}) < 1); + + my $p = shift @$a; + if (defined($p)) { + if ($clHash->{TYPE} eq 'HMCCUDEV') { + if ($p =~ /^([0-9]{1,2})$/) { + return HMCCU_SetError ($clHash, -7) if ($p >= $clHash->{hmccu}{channels}); + $ccuobj .= ':'.$p; + $p = shift @$a; + if (defined($p)) { + $receiver = $p; + $paramset = 'LINK'; + } + } + } + else { + if (lc($p) eq 'device') { + ($ccuobj, undef) = HMCCU_SplitChnAddr ($ccuobj); + } + else { + $receiver = $p; + $paramset = 'LINK'; + } + } + } + + my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $ccuobj, $clHash->{ccuif}) // + return HMCCU_SetError ($clHash, "Can't get device description"); + return HMCCU_SetError ($clHash, "Paramset $paramset not supported by device or channel") + if ($devDesc->{PARAMSETS} !~ /$paramset/); + if (!HMCCU_IsValidParameter ($ioHash, $devDesc, $paramset, $h)) { + my @parList = HMCCU_GetParamDef ($ioHash, $devDesc, $paramset); + return HMCCU_SetError ($clHash, 'Invalid parameter specified. Valid parameters are '. + join(',', @parList)); + } + + if ($paramset eq 'VALUES' || $paramset eq 'MASTER') { + ($rc, $result) = HMCCU_SetMultipleParameters ($clHash, $ccuobj, $h, $paramset); + } + else { + if (exists($defs{$receiver}) && defined($defs{$receiver}->{TYPE})) { + my $clRecHash = $defs{$receiver}; + if ($clRecHash->{TYPE} eq 'HMCCUDEV') { + my $chnNo = shift @$a; + return HMCCU_SetError ($clHash, 'Channel number required for link receiver') + if (!defined($chnNo) || $chnNo !~ /^[0-9]{1,2}$/); + $receiver = $clRecHash->{ccuaddr}.":$chnNo"; + } + elsif ($clRecHash->{TYPE} eq 'HMCCUCHN') { + $receiver = $clRecHash->{ccuaddr}; + } + else { + return HMCCU_SetError ($clHash, "Receiver $receiver is not a HMCCUCHN or HMCCUDEV device"); + } + } + elsif (!HMCCU_IsChnAddr ($receiver, 0)) { + my ($rcvAdd, $rcvChn) = HMCCU_GetAddress ($ioHash, $receiver); + return HMCCU_SetError ($clHash, "$receiver is not a valid CCU channel name") + if ($rcvAdd eq '' || $rcvChn eq ''); + $receiver = "$rcvAdd:$rcvChn"; + } + + return HMCCU_SetError ($clHash, "$receiver is not a link receiver of $clHash->{NAME}") + if (!HMCCU_IsValidReceiver ($ioHash, $ccuobj, $clHash->{ccuif}, $receiver)); + ($rc, $result) = HMCCU_RPCRequest ($clHash, 'putParamset', $ccuobj, $receiver, $h); + } + + return HMCCU_SetError ($clHash, HMCCU_Min(0, $rc), $result); +} + +###################################################################### +# Execute toggle command +###################################################################### + +sub HMCCU_ExecuteToggleCommand ($@) +{ + my ($clHash) = @_; + + # Get state values related to control channel and datapoint + my ($sc, $sd, $cc, $cd) = HMCCU_GetSCDatapoints ($clHash); + my $stateVals = HMCCU_GetStateValues ($clHash, $cd, $cc); + my %stateCmds = split (/[:,]/, $stateVals); + my @states = keys %stateCmds; + + my $ccuif = $clHash->{ccuif}; + my ($devAddr, undef) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + my $stc = scalar(@states); + return HMCCU_SetError ($clHash, -15) if ($stc == 0); + + my $curState = defined($clHash->{hmccu}{dp}{"$cc.$cd"}{VALUES}{SVAL}) ? + $clHash->{hmccu}{dp}{"$cc.$cd"}{VALUES}{SVAL} : $states[0]; + + my $newState = ''; + my $st = 0; + while ($st < $stc) { + if ($states[$st] eq $curState ) { + $newState = ($st == $stc-1) ? $states[0] : $states[$st+1]; + last; + } + $st++; + } + + return HMCCU_SetError ($clHash, "Current device state doesn't match any state value") + if ($newState eq ''); + + my $rc = HMCCU_SetMultipleDatapoints ($clHash, + { "001.$ccuif.$devAddr:$cc.$cd" => $stateCmds{$newState} } + ); + return HMCCU_SetError ($clHash, HMCCU_Min(0, $rc)) +} + +###################################################################### +# Execute command to show device information +###################################################################### + +sub HMCCU_ExecuteGetDeviceInfoCommand ($@) +{ + my ($ioHash, $clHash, $address, $extended) = @_; + $extended //= 0; + + my $iface = HMCCU_GetDeviceInterface ($ioHash, $address); + my $result = HMCCU_GetDeviceInfo ($clHash, $address); + return HMCCU_SetError ($clHash, -2) if ($result eq ''); + + my ($sc, $sd, $cc, $cd) = HMCCU_GetSCDatapoints ($clHash); + my $devInfo = 'Device channels and datapoints

'; + $devInfo .= '
';
+	$devInfo .= HMCCU_FormatDeviceInfo ($result);
+	$devInfo .= '
'; + my $detect = HMCCU_DetectDevice ($ioHash, $address, $iface); + if (defined($detect)) { + $devInfo .= "
Device detection:
"; + if ($detect->{stateRoleCount} > 0) { + foreach my $c (sort keys %{$detect->{stateRole}}) { + my $stateChn = $detect->{stateRole}{$c}; + $devInfo .= "StateDatapoint = $c.$stateChn->{datapoint} [$stateChn->{role}]
"; + } + } + else { + $devInfo .= 'No state datapoint detected
'; + } + if ($detect->{controlRoleCount} > 0) { + foreach my $c (sort keys %{$detect->{controlRole}}) { + my $ctrlChn = $detect->{controlRole}{$c}; + $devInfo .= "ControlDatapoint = $c.$ctrlChn->{datapoint} [$ctrlChn->{role}]
"; + } + } + else { + $devInfo .= 'No control datapoint detected
'; + } + $devInfo .= $detect->{defMod} ne '' ? + "
Recommended module for device definition: $detect->{defMod}
" : + "
Failed to detect device settings. Device must be configured manually.
"; + if ($extended) { + $devInfo .= "
Detection level: $detect->{level}
". + "
Detected default state datapoint: $detect->{defSDP}
". + "
Detected default control datapoint: $detect->{defCDP}
". + "
Unique state roles: $detect->{uniqueStateRoleCount}
". + "
Unique control roles: $detect->{uniqueControlRoleCount}
"; + } + } + $devInfo .= "
Current state datapoint = $sc.$sd
"; + $devInfo .= "
Current control datapoint = $cc.$cd
"; + $devInfo .= '
Device description

';
+	$result = HMCCU_DeviceDescToStr ($ioHash, $clHash->{TYPE} eq 'HMCCU' ? $address : $clHash);
+	$devInfo .= '
'; + $devInfo .= defined($result) ? $result : "Can't get device description
"; + if ($clHash->{TYPE} ne 'HMCCU') { + $devInfo .= '
Defaults

'; + $devInfo .= HMCCU_GetDefaults ($clHash); + } + + return $devInfo; +} + +###################################################################### +# Execute commands to fetch device parameters +###################################################################### + +sub HMCCU_ExecuteGetParameterCommand ($@) +{ + my ($ioHash, $clHash, $command, $addList, $filter) = @_; + $filter //= '.*'; + + my %parSets = ('config' => 'MASTER,LINK,SERVICE', 'values' => 'VALUES', 'update' => 'VALUES,MASTER,LINK,SERVICE'); + my $defParamset = $parSets{$command}; + + my %objects; + foreach my $a (@$addList) { + my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $a, $clHash->{ccuif}); + return HMCCU_SetError ($clHash, "Can't get device description") if (!defined($devDesc)); + + my $paramset = $defParamset eq '' ? $devDesc->{PARAMSETS} : $defParamset; + my ($da, $dc) = HMCCU_SplitChnAddr ($a); + $dc = 'd' if ($dc eq ''); + + foreach my $ps (split (',', $paramset)) { + next if ($devDesc->{PARAMSETS} !~ /$ps/); + + if ($ps eq 'LINK') { + foreach my $rcv (HMCCU_GetReceivers ($ioHash, $a, $clHash->{ccuif})) { + my ($rc, $result) = HMCCU_RPCRequest ($clHash, 'getRawParamset', $a, $rcv); + next if ($rc < 0); + foreach my $p (keys %$result) { + $objects{$da}{$dc}{"LINK.$rcv"}{$p} = $result->{$p} if ($p =~ /$filter/); + } + } + } + else { + my ($rc, $result) = HMCCU_RPCRequest ($clHash, 'getRawParamset', $a, $ps); + if ($rc < 0) { + HMCCU_Log ($clHash, 2, "Can't get parameterset $ps for address $a"); + next; + } + foreach my $p (keys %$result) { + $objects{$da}{$dc}{$ps}{$p} = $result->{$p} if ($p =~ /$filter/); + } + } + } + } + + return \%objects; +} + +###################################################################### +# Convert results into a readable format +###################################################################### + +sub HMCCU_DisplayGetParameterResult ($$$) +{ + my ($ioHash, $clHash, $objects) = @_; + + my $res = ''; + my $flags = HMCCU_GetFlags ($clHash->{NAME}); + $res = "Readings for config parameters are not updated until you set showXXX flags in attribute ccuflags\n\n" + if ($flags !~ /show(Master|Device)/); + if (scalar(keys %$objects) > 0) { + my $convRes = HMCCU_UpdateParamsetReadings ($ioHash, $clHash, $objects); + if (defined($convRes)) { + foreach my $da (sort keys %$convRes) { + $res .= "Device $da\n"; + foreach my $dc (sort keys %{$convRes->{$da}}) { + foreach my $ps (sort keys %{$convRes->{$da}{$dc}}) { + $res .= " Channel $dc [$ps]\n". + join ("\n", map { + " ".$_.' = '.$convRes->{$da}{$dc}{$ps}{$_} + } sort keys %{$convRes->{$da}{$dc}{$ps}})."\n"; + } + } + } + } + } + + return $res; +} + +###################################################################### +# Get week program(s) as html table +###################################################################### + +sub HMCCU_DisplayWeekProgram ($$$;$$) +{ + my ($ioHash, $clHash, $resp, $programName, $program) = @_; + $programName //= 'all'; + $program //= 'all'; + + my @weekDay = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'); + + my $convRes = HMCCU_UpdateParamsetReadings ($ioHash, $clHash, $resp); + + return "No data available for week program(s) $program" + if (!exists($clHash->{hmccu}{tt}) || ($program ne 'all' && !exists($clHash->{hmccu}{tt}{$program}))); + + my $s = ''; + foreach my $w (sort keys %{$clHash->{hmccu}{tt}}) { + next if ("$w" ne "$program" && "$program" ne 'all'); + my $p = $clHash->{hmccu}{tt}{$w}; + my $pn = $programName ne 'all' ? $programName : $w+1; + $s .= '

Week Program '.$pn.'


'; + foreach my $d (sort keys %{$p->{ENDTIME}}) { + $s .= ''; + foreach my $h (sort { $a <=> $b } keys %{$p->{ENDTIME}{$d}}) { + $s .= ''; + last if ($p->{ENDTIME}{$d}{$h} eq '24:00'); + } + $s .= ''; + } + $s .= '
'.$weekDay[$d].''.$p->{ENDTIME}{$d}{$h}.' / '.$p->{TEMPERATURE}{$d}{$h}.'

'; + } + $s .= ''; + + return $s; +} + +###################################################################### +# Check if value matches parameter definition +# Parameter t can be a datapoint type or a hash reference to a role +# command parameter definition. +# Parameter list can be a comma separated list of valid values. +###################################################################### + +sub HMCCU_CheckParameter ($$;$$$) +{ + my ($v, $t, $min, $max, $list) = @_; + + return 0 if (!defined($v) || !defined($t)); + + my $type; + + if (ref($t) eq 'HASH') { + $type = $t->{type}; + if ($type eq 'ENUM') { + return exists($t->{look}) && exists($t->{look}{$v}) || + (HMCCU_IsIntNum ($v) && (!defined($min) || $v >= $min) && (!defined($max) || $v <= $max)) ? 1 : 0; + } + $min = $t->{min}; + $max = $t->{max}; + } + else { + $type = $t; + } + + if ($type eq 'BOOL') { + return $v =~ /^(true|false|1|0)$/ ? 1 : 0; + } + elsif ($type eq 'INTEGER') { + return HMCCU_IsIntNum ($v) && (!defined($min) || $v >= $min) && (!defined($max) || $v <= $max); + } + elsif ($type eq 'FLOAT' || $type eq 'DOUBLE') { + return HMCCU_IsFltNum ($v) && (!defined($min) || $v >= $min) && (!defined($max) || $v <= $max); + } + elsif ($type eq 'ENUM') { + if (HMCCU_IsIntNum ($v)) { + return (!defined($min) || $v >= $min) && (!defined($max) || $v <= $max) ? 1 : 0; + } + elsif (defined($list)) { + foreach my $le (split(',', $list)) { return 1 if ($v eq $le); } + } + } + elsif ($type eq 'ACTION') { + return $v =~ /^[01]$/ ? 1 : 0; + } + elsif ($type eq 'STRING') { + return 1; + } + + return 0; +} + +###################################################################### +# Set or delete state and control datapoints +# Parameter d specifies the value to be set: +# state, control, statechannel, statedatapoint, controlchannel, +# controldatapoint +# Parameter v is the statedatapoint or controldatapoint including +# the channel number. +# If parameter v is missing, the attribute is deleted +# Parameter r contains the role. +# Return: +# 0=Error, 1=Success +###################################################################### + +sub HMCCU_SetSCDatapoints ($$;$$) +{ + my ($clHash, $d, $v, $r) = @_; + + my $ioHash = HMCCU_GetHash ($clHash); + $r //= ''; + + # Flags: 1=statechannel, 2=statedatapoint, 4=controlchannel, 8=controldatapoint + my %flags = ( + 'state' => 3, 'control' => 12, + 'statechannel' => 1, 'statedatapoint' => 2, + 'controlchannel' => 4, 'controldatapoint' => 8 + ); + + my $chn; + my $dpt; + my $f = $flags{$d} // return 0; + + # $d becomes the hash key: state or control + $d =~ s/^(state|control)(channel|datapoint)$/$1/; + return 0 if ($d ne 'state' && $d ne 'control'); + + if (defined($v)) { + # Set value + return 0 if ($v eq ''); + + if ($f & 10) { + ($chn, $dpt) = $v =~ /^([0-9]{1,2})\.(.+)/ ? ($1, $2) : ($clHash->{hmccu}{$d}{chn}, $v); + } + elsif ($f & 5) { + return 0 if ($v !~ /^[0-9]{1,2}$/); + ($chn, $dpt) = ($v, $clHash->{hmccu}{$d}{dpt}); + } + + return 0 if ($init_done && defined($chn) && $chn ne '' && defined($dpt) && $dpt ne '' && + !HMCCU_IsValidDatapoint ($clHash, $clHash->{ccutype}, $chn, $dpt, $f & 3 ? 5 : 2)); + + $clHash->{ccurolestate} = $r if ($r ne '' && $f & 3); + $clHash->{ccurolectrl} = $r if ($r ne '' && $f & 12); + } + else { + # Delete value + $chn = '' if ($f & 5); + $dpt = '' if ($f & 10); + delete $clHash->{ccurolestate} if ($f & 3); + delete $clHash->{ccurolectrl} if ($f & 12); + } + + $clHash->{hmccu}{$d}{chn} = $chn if (defined($chn)); + $clHash->{hmccu}{$d}{dpt} = $dpt if (defined($dpt)); + + # Try to set missing state/control datapoint to the same datapoint + if (defined($chn) && defined($dpt) && defined($clHash->{ccuaddr})) { + my ($da, undef) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + if ($d eq 'control' && !HMCCU_IsValidStateDatapoint ($clHash, 1) && + HMCCU_IsValidParameter ($clHash, "$da:$chn", 'VALUES', $dpt, 5) + ) { + $clHash->{hmccu}{state}{chn} = $chn; + $clHash->{hmccu}{state}{dpt} = $dpt; + } + elsif ($d eq 'state' && !HMCCU_IsValidControlDatapoint ($clHash, 1) && + HMCCU_IsValidParameter ($clHash, "$da:$chn", 'VALUES', $dpt, 2) + ) { + $clHash->{hmccu}{control}{chn} = $chn; + $clHash->{hmccu}{control}{dpt} = $dpt; + } + } + + return 1; +} + +###################################################################### +# Set default state and control datapoint +# If $cmd == 1 update role commands +###################################################################### + +sub HMCCU_SetDefaultSCDatapoints ($$;$$) +{ + my ($ioHash, $clHash, $detect, $cmd) = @_; + + $detect //= HMCCU_DetectDevice ($ioHash, $clHash->{ccuaddr}, $clHash->{ccuif}); + $cmd //= 0; + + my ($sc, $sd, $cc, $cd) = ('', '', '', ''); + my $clName = $clHash->{NAME}; + my $clType = $clHash->{TYPE}; + + # Prio 4: Use information from device detection + if (defined($detect)) { + $sc = $detect->{defSCh} if ($detect->{defSCh} != -1); + $cc = $detect->{defCCh} if ($detect->{defCCh} != -1); + $sd = $detect->{stateRole}{$sc}{datapoint} if ($sc ne '' && exists($detect->{stateRole}{$sc})); + $cd = $detect->{controlRole}{$cc}{datapoint} if ($cc ne '' && exists($detect->{controlRole}{$cc})); + } + + # Prio 3: Use information stored in device hash (HMCCUDEV only) + if ($clType eq 'HMCCUDEV') { + # Support for level 5 devices + ($sc, $sd) = HMCCU_SplitDatapoint ($clHash->{hmccu}{defSDP}) if (defined($clHash->{hmccu}{defSDP})); + ($cc, $cd) = HMCCU_SplitDatapoint ($clHash->{hmccu}{defCDP}) if (defined($clHash->{hmccu}{defCDP})); + } + + # Prio 2: Use attribute statechannel and controlchannel for HMCCUDEV and channel address for HMCCUCHN + if ($clType eq 'HMCCUCHN') { + # State and control channel of HMCCUCHN devices is defined by channel address + my $da; + ($da, $sc) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + $cc = $sc; + } + else { + # Consider attributes statechannel and controlchannel for HMCCUDEV devices + $sc = AttrVal ($clName, 'statechannel', $sc); + $cc = AttrVal ($clName, 'controlchannel', $cc); + } + + # Prio 1: Use attributes statedatapoint and controldatapoint + # Attributes are overriding attributes statechannel and controlchannel for HMCCUDEV + my $asd = AttrVal ($clName, 'statedatapoint', ''); + my $acd = AttrVal ($clName, 'controldatapoint', ''); + if ($asd ne '') { + my @sa = split (/\./, $asd); + if (scalar(@sa) > 1) { + $sc = $sa[0] if ($clType eq 'HMCCUDEV'); + shift @sa; + } + $sd = $sa[0]; + } + if ($acd ne '') { + my @ca = split (/\./, $acd); + if (scalar(@ca) > 1) { + $cc = $ca[0] if ($clType eq 'HMCCUDEV'); + shift @ca; + } + $cd = $ca[0]; + } + + my $sr = $sc ne '' && defined($detect) && exists($detect->{stateRole}{$sc}) ? $detect->{stateRole}{$sc}{role} : ''; + my $cr = $cc ne '' && defined($detect) && exists($detect->{controlRole}{$cc}) ? $detect->{controlRole}{$cc}{role} : ''; + ($sc, $sd) = ('', '') if (!HMCCU_SetSCDatapoints ($clHash, 'statedatapoint', "$sc.$sd", $sr)); + ($cc, $cd) = ('', '') if (!HMCCU_SetSCDatapoints ($clHash, 'controldatapoint', "$cc.$cd", $cr)); + + if ($cmd) { + my $chn = $cc ne '' ? $cc : $sc; + my $dpt = $cd ne '' ? $cd : $sd; + + HMCCU_UpdateRoleCommands ($ioHash, $clHash, $chn); + HMCCU_UpdateAdditionalCommands ($ioHash, $clHash, $chn, $dpt); + } + + my $rsd = $sc ne '' && $sd ne '' ? 1 : 0; + my $rcd = $cc ne '' && $cd ne '' ? 1 : 0; + + return ($sc, $sd, $cc, $cd, $rsd, $rcd); +} + +###################################################################### +# Get state and control channel and datapoint of a device. +# Priority depends on FHEM device type: +# +# HMCCUCHN: +# 1. Datapoints from attributes statedatapoint, controldatapoint +# 2. Datapoints by role +# +# HMCCUDEV: +# 1. Attributes statechannel, controlchannel +# 2. Channel from attributes statedatapoint, controldatapoint +# 3. Datapoints from attributes statedatapoint, controldatapoint +# 4. Channel datapoint by role +# +# If controldatapoint is not specified it is synchronized with +# statedatapoint. +# +# Return (sc, sd, cc, cd, sdCnt, cdCnt) +# If sdCnt > 1 or cdCnt > 1 more than 1 matching rules were found +###################################################################### + +sub HMCCU_GetSCDatapoints ($) +{ + my ($clHash) = @_; + + my $ioHash = HMCCU_GetHash ($clHash); + my $type = $clHash->{TYPE}; + + my ($sc, $sd) = HMCCU_StateDatapoint ($clHash); + my ($cc, $cd) = HMCCU_ControlDatapoint ($clHash); + my $rsdCnt; + my $rcdCnt; + + # Detect by attributes + # ($sc, $sd, $cc, $cd, $rsdCnt, $rcdCnt) = HMCCU_DetectSCAttr ($clHash, $sc, $sd, $cc, $cd); + # return ($sc, $sd, $cc, $cd, $rsdCnt, $rcdCnt) if ($rsdCnt); + + # HMCCU_Log ($clHash, 2, "GetSCDatapoints 2: $sc, $sd, $cc, $cd"); + + return HMCCU_SetDefaultSCDatapoints ($ioHash, $clHash); +} + +sub HMCCU_ControlDatapoint ($) +{ + my ($clHash) = @_; + + return ($clHash->{hmccu}{control}{chn} // '', $clHash->{hmccu}{control}{dpt} // ''); +} + +sub HMCCU_IsValidControlDatapoint ($;$) +{ + my ($clHash, $checkHashOnly) = @_; + + return 0 if (!defined($clHash->{ccuaddr})); + + $checkHashOnly //= 0; + my ($cc, $cd) = HMCCU_ControlDatapoint ($clHash); + my ($da, $chnNo) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + + return $cc ne '' && $cd ne '' && ($checkHashOnly || HMCCU_IsValidParameter ($clHash, "$da:$cc", 'VALUES', $cd, 2)) ? 1 : 0; +} + +sub HMCCU_StateDatapoint ($) +{ + my ($clHash) = @_; + + return ($clHash->{hmccu}{state}{chn} // '', $clHash->{hmccu}{state}{dpt} // ''); +} + +sub HMCCU_IsValidStateDatapoint ($;$) +{ + my ($clHash, $checkHashOnly) = @_; + + return 0 if (!defined($clHash->{ccuaddr})); + + $checkHashOnly //= 0; + my ($sc, $sd) = HMCCU_StateDatapoint ($clHash); + my ($da, $chnNo) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + + return $sc ne '' && $sd ne '' && ($checkHashOnly || HMCCU_IsValidParameter ($clHash, "$da:$sc", 'VALUES', $sd, 5)) ? 1 : 0; +} + +###################################################################### +# Get state and control datapoints from attributes +# Return defaults passed as parameters if attribute(s) not defined +###################################################################### + +sub HMCCU_DetectSCAttr ($$$$$) +{ + my ($clHash, $sc, $sd, $cc, $cd) = @_; + my $name = $clHash->{NAME}; + my $type = $clHash->{TYPE}; + $sc //= ''; + $cc //= ''; + + my $da; + my $dc; + if (defined($clHash->{ccuaddr})) { + ($da, $dc) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); + } + + $sc = $dc if ($sc eq ''); + $cc = $dc if ($cc eq ''); my $statedatapoint = AttrVal ($name, 'statedatapoint', ''); - my $statechannel = AttrVal ($name, 'statechannel', ''); - my $controldatapoint = AttrVal ($name, 'controldatapoint', $statedatapoint); + my $controldatapoint = AttrVal ($name, 'controldatapoint', ''); + # Attributes controlchannel and statechannel are only valid for HMCCUDEV devices + if ($type eq 'HMCCUDEV') { + $sc = AttrVal ($name, 'statechannel', $sc); + $cc = AttrVal ($name, 'controlchannel', $cc); + } + + # If attribute statedatapoint is specified, use it. if ($statedatapoint ne '') { if ($statedatapoint =~ /^([0-9]+)\.(.+)$/) { - ($sc, $sd) = ($1, $2); + # Attribute statechannel overrides channel specification. + ($sc, $sd) = $sc eq '' ? ($1, $2) : ($sc, $2); } else { $sd = $statedatapoint; + if ($sc eq '') { + # Try to find state channel (datapoint must be readable or provide events) + my $c = HMCCU_FindDatapoint ($clHash, $type, -1, $sd, 5); + $sc = $c if ($c >= 0); + } } } - $sc = $statechannel if ($statechannel ne '' && $sc eq ''); + # If attribute controldatapoint is specified, use it. if ($controldatapoint ne '') { if ($controldatapoint =~ /^([0-9]+)\.(.+)$/) { - ($cc, $cd) = ($1, $2); + # Attribute controlchannel overrides channel specification in controldatapoint + ($cc, $cd) = $cc eq '' ? ($1, $2) : ($cc, $2); } else { $cd = $controldatapoint; + if ($cc eq '') { + # Try to find control channel (datapoint must be writeable) + my $c = HMCCU_FindDatapoint ($clHash, $type, -1, $cd, 4); + $cc = $c if ($c >= 0); + } + } + } + + my $rsdCnt = $sc ne '' && $sd ne '' ? 1 : 0; + my $rcdCnt = $cc ne '' && $cd ne '' ? 1 : 0; + + return ($sc, $sd, $cc, $cd, $rsdCnt, $rcdCnt); +} + +sub HMCCU_DetectSCChn ($;$$) +{ + my ($clHash, $sd, $cd) = @_; + $sd //= ''; + $cd //= ''; + + my $role = HMCCU_GetChannelRole ($clHash); + HMCCU_Trace ($clHash, 2, "role=$role"); + + if ($role ne '' && exists($HMCCU_STATECONTROL->{$role}) && $HMCCU_STATECONTROL->{$role}{F} & 1) { + my $nsd = HMCCU_DetectSCDatapoint ($HMCCU_STATECONTROL->{$role}{S}, $clHash->{ccuif}); + my $ncd = HMCCU_DetectSCDatapoint ($HMCCU_STATECONTROL->{$role}{C}, $clHash->{ccuif}); + HMCCU_Log ($clHash, 2, "statedatapoint of role and attribute do not match") + if ($nsd ne '' && $sd ne '' && $nsd ne $sd); + HMCCU_Log ($clHash, 2, "controldatapoint of role and attribute do not match") + if ($ncd ne '' && $cd ne '' && $ncd ne $cd); + + $sd = $nsd if ($nsd ne '' && $sd eq ''); + $cd = $ncd if ($ncd ne '' && $cd eq ''); + $clHash->{ccurolestate} = $role if ($nsd ne ''); + $clHash->{ccurolectrl} = $role if ($ncd ne ''); + } + + return ($sd, $cd, $sd ne '' ? 1 : 0, $cd ne '' ? 1 : 0); +} + +sub HMCCU_DetectSCDev ($;$$$$) +{ + my ($clHash, $sc, $sd, $cc, $cd) = @_; + $sc //= ''; + $sd //= ''; + $cc //= ''; + $cd //= ''; + + # Count matching roles to prevent ambiguous definitions + my ($rsc, $rsd, $rcc, $rcd) = ('', '', '', ''); + # Priorities + my ($ccp, $scp) = (0, 0); + # Number of matching roles + my $rsdCnt = $sc ne '' && $sd ne '' ? 1 : 0; + my $rcdCnt = $cc ne '' && $cd ne '' ? 1 : 0; + + my $defRole = $HMCCU_DEF_ROLE->{$clHash->{ccusubtype}}; + my $resRole; + + foreach my $roleDef (split(',', $clHash->{hmccu}{role})) { + my ($rc, $role) = split(':', $roleDef); + + next if (!defined($role) || (defined($defRole) && $role ne $defRole)); + + if (defined($role) && exists($HMCCU_STATECONTROL->{$role}) && $HMCCU_STATECONTROL->{$role}{F} & 2) { + my $nsd = HMCCU_DetectSCDatapoint ($HMCCU_STATECONTROL->{$role}{S}, $clHash->{ccuif}); + if ($sd eq '' && $nsd ne '') { + # If state datapoint is defined for this role + if ($sc ne '' && $rc eq $sc) { + # If channel of current role matches state channel, use datapoint specified + # in $HMCCU_STATECONTROL as state datapoint + $rsc = $sc; + $rsd = $nsd; + $clHash->{ccurolestate} = $role; + $rsdCnt = 1; + } + else { + # If state channel is not defined or role channel doesn't match state channel, + # assign state channel and datapoint considering role priority + if ($HMCCU_STATECONTROL->{$role}{P} > $scp) { + # Priority of this role is higher than the previous priority + $scp = $HMCCU_STATECONTROL->{$role}{P}; + $rsc = $rc; + $rsd = $nsd; + $rsdCnt = 1; + $clHash->{ccurolestate} = $role; + } + elsif ($HMCCU_STATECONTROL->{$role}{P} == $scp) { + # Priority of this role is equal to previous priority. We found more + # than 1 matching roles. We use the first matching role/channel, but count + # the number of matching roles. + if ($rsc eq '') { + $rsc = $rc; + $rsd = $nsd; + $clHash->{ccurolestate} = $role; + } + $rsdCnt++; + } + } + } + if ($cd eq '' && $HMCCU_STATECONTROL->{$role}{C} ne '') { + my $ncd = HMCCU_DetectSCDatapoint ($HMCCU_STATECONTROL->{$role}{C}, $clHash->{ccuif}); + if ($cc ne '' && $rc eq $cc) { + $rcc = $cc; + $rcd = $ncd; + $clHash->{ccurolectrl} = $role; + $rcdCnt = 1; + } + else { + # If control channel is not defined or role channel doesn't match control channel, + # assign control channel and datapoint considering role priority + if ($HMCCU_STATECONTROL->{$role}{P} > $scp) { + # Priority of this role is higher than the previous priority + $scp = $HMCCU_STATECONTROL->{$role}{P}; + $rcc = $rc; + $rcd = $ncd; + $rcdCnt = 1; + $clHash->{ccurolectrl} = $role; + } + elsif ($HMCCU_STATECONTROL->{$role}{P} == $scp) { + # Priority of this role is equal to previous priority. We found more + # than 1 matching roles. We use the first matching role/channel, but count + # the number of matching roles. + if ($rcc eq '') { + $rcc = $rc; + $rcd = $ncd; + $clHash->{ccurolectrl} = $role; + } + $rcdCnt++; + } + } + } + } + } + + ($sc, $sd) = ($rsc, $rsd) if ($rsdCnt > 0 && $sd eq ''); + ($cc, $cd) = ($rcc, $rcd) if ($rcdCnt > 0 && $cd eq ''); + + return ($sc, $sd, $cc, $cd, $rsdCnt, $rcdCnt); +} + +###################################################################### +# Detect roles, channel and datapoint to be used for controlling and +# displaying the state of a device or channel identified by its +# address. +# +# The function returns a hash reference with the following structure: +# +# int stateRoleCount: Number of stateRole entries +# int controlRoleCount: Number of controlRole entries +# int rolePatternCount: Number of 4-channel role patterns +# hash stateRole: Hash with state roles, key is channel number +# hash controlRole: Hash with control roles, key is channel number +# hash rolePattern: Hash with 4-channel role patterns +# string defMod: Default module 'HMCCUDEV', 'HMCCUCHN' or '' +# string defAdd: Device address (append channel number fpr HMCCUCHN) +# int defSCh: Default state channel or -1 +# int defCCh: Default control channel or -1 +# int defSDP: Default state datapoint with channel +# int defCDP: Default control datapoint with channel +# int level: Detection level +# 0 = device type not detected +# 1 = device type detected with single known role => HMCCUCHN +# 2 = device detected with multiple identical channels (i.e. switch +# or remote with more than 1 button) => Multiple HMCCUCHNs +# 3 = device detected with multiple channels with different known +# roles (i.e. roles KEY and THERMALCONTROL) => HMCCUDEV +# 4 = device type detected with different state and control role +# (>=2 different channels) => HMCCUDEV +# 5 = device type detected with one or more 4-channel-groups (1xState,3xControl) +# +# Structure of stateRole / controlRole hashes: +# int : Channel number (key) +# string {}{role}: Channel role +# string {}{datapoint}: State or control datapoint +# int {}{priority}: Priority of role/datapoint +# +# Structure of rolePattern hash (detection level = 5) +# int : Number of first channel of a group +# string {}{stateRole}: Role of state channel +# string {}{controlRole}: Role of control channel +# string {}{stateDatapoint}: The state datapoint +# string {}{controlDatapoint}: The control datapoint +###################################################################### + +sub HMCCU_DetectDevice ($$$) +{ + my ($ioHash, $address, $iface) = @_; + + my @allRoles = (); + my @stateRoles = (); + my @controlRoles = (); + my ($prioState, $prioControl) = (-1, -1); + + if (!defined($address)) { + HMCCU_Log ($ioHash, 2, "Parameter address not defined ".stacktraceAsString(undef)); + return undef; + } + + my ($devAdd, $devChn) = HMCCU_SplitChnAddr ($address); + + my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $address, $iface); + if (!defined($devDesc)) { + HMCCU_Log ($ioHash, 2, "Can't get device description for $address ".stacktraceAsString(undef)); + return undef; + } + + # Identify known roles + if ($devDesc->{_addtype} eq 'dev') { + foreach my $child (split(',', $devDesc->{CHILDREN})) { + $devDesc = HMCCU_GetDeviceDesc ($ioHash, $child, $devDesc->{_interface}) // next; + push @allRoles, $devDesc->{TYPE}; + HMCCU_IdentifyRole ($ioHash, $devDesc, $iface, \@stateRoles, \@controlRoles); + } + } + elsif ($devDesc->{_addtype} eq 'chn') { + HMCCU_IdentifyRole ($ioHash, $devDesc, $iface, \@stateRoles, \@controlRoles); + } + + # Count roles and unique roles + my $stateRoleCnt = scalar(@stateRoles); + my $ctrlRoleCnt = scalar(@controlRoles); + my %uniqStateRoles; + my %uniqCtrlRoles; + $uniqStateRoles{$_->{role}}++ for @stateRoles; + $uniqCtrlRoles{$_->{role}}++ for @controlRoles; + my $cntUniqStateRoles = scalar(keys %uniqStateRoles); + my $cntUniqCtrlRoles = scalar(keys %uniqCtrlRoles); + + # Build device information to be returned + my %di = ( + stateRoleCount => $stateRoleCnt, controlRoleCount => $ctrlRoleCnt, + uniqueStateRoleCount => $cntUniqStateRoles, uniqueControlRoleCount => $cntUniqCtrlRoles, + defMod => '', defSCh => -1, defCCh => -1, defSDP => '', defCDP => '', + level => 0 + ); + my $p = -1; + foreach my $sr (@stateRoles) { + $di{stateRole}{$sr->{channel}}{role} = $sr->{role}; + $di{stateRole}{$sr->{channel}}{datapoint} = $sr->{datapoint}; + $di{stateRole}{$sr->{channel}}{priority} = $sr->{priority}; + if ($sr->{priority} > $p) { + $di{defSCh} = $sr->{channel}; + $p = $sr->{priority}; + } + } + $p = -1; + foreach my $cr (@controlRoles) { + $di{controlRole}{$cr->{channel}}{role} = $cr->{role}; + $di{controlRole}{$cr->{channel}}{datapoint} = $cr->{datapoint}; + $di{controlRole}{$cr->{channel}}{priority} = $cr->{priority}; + if ($cr->{priority} > $p) { + $di{defCCh} = $cr->{channel}; + $p = $cr->{priority}; + } + } + + # Determine parameters for device definition + if ($stateRoleCnt == 1 && $ctrlRoleCnt == 0) { + # Type 1: One channel with statedatapoint, but no controldatapoint (read only) => HMCCUCHN + $di{defSCh} = $stateRoles[0]->{channel}; + $di{defMod} = 'HMCCUCHN'; + $di{defAdd} = "$devAdd:$di{defSCh}"; + $di{level} = 1; + } + elsif ($stateRoleCnt == 0 && $ctrlRoleCnt == 1) { + # Type 1: One channel with controldatapoint, but no statedatapoint (write only) => HMCCUCHN + $di{defCCh} = $controlRoles[0]->{channel}; + $di{defMod} = 'HMCCUCHN'; + $di{defAdd} = "$devAdd:$di{defCCh}"; + $di{level} = 1; + } + elsif ($stateRoleCnt == 1 && $ctrlRoleCnt == 1) { + $di{defSCh} = $stateRoles[0]->{channel}; + $di{defCCh} = $controlRoles[0]->{channel}; + if ($stateRoles[0]->{channel} == $controlRoles[0]->{channel}) { + # Type 1: One channel with controldatapoint and statedatapoint (read + write)=> HMCCUCHN + $di{defMod} = 'HMCCUCHN'; + $di{defAdd} = "$devAdd:$di{defCCh}"; + $di{level} = 1; + } + else { + # Type 4: Two different channels for controldatapoint and statedatapoint (read + write) => HMCCUDEV + $di{defMod} = 'HMCCUDEV'; + $di{defAdd} = $devAdd; + $di{level} = 4; + } + } + elsif ($stateRoleCnt > 1 || $ctrlRoleCnt > 1) { + # Multiple channels found + if ($cntUniqStateRoles == 1 && $cntUniqCtrlRoles == 0 || + $cntUniqStateRoles == 0 && $cntUniqCtrlRoles == 1 || +# $cntUniqCtrlRoles > 1 || + ( + $cntUniqStateRoles == 1 && $cntUniqCtrlRoles == 1 && $stateRoles[0]->{role} eq $controlRoles[0]->{role} + ) + ) { + # Type 2: Device with multiple identical channels + $di{defSCh} = $cntUniqStateRoles == 1 ? $stateRoles[0]->{channel} : -1; + $di{defCCh} = $cntUniqCtrlRoles == 1 ? $controlRoles[0]->{channel} : -1; + $di{defMod} = 'HMCCUCHN'; + $di{defAdd} = $devAdd; + $di{level} = 2; + } + else { + # Type 3: Device with multiple different channel roles + $di{defMod} = 'HMCCUDEV'; + $di{defAdd} = $devAdd; + $di{level} = 3; + $di{rolePatternCount} = 0; + + # Try to find channel role pattern with 4 channels. + # If no pattern can be found, default channels depend on role priorities + my $rolePatterns = HMCCU_DetectRolePattern (\@allRoles, + '^(?!([A-Z]+_VIRTUAL))([A-Z]+)[A-Z_]+(,\g2_VIRTUAL_[A-Z_]+){3}$', 4, 4); + if (defined($rolePatterns)) { + ROLEPATTERN: foreach my $rp (keys %$rolePatterns) { + $di{rolePatternCount} += $rolePatterns->{$rp}{c}; + + # A role pattern is a comma separated list of channel roles + my @patternRoles = split(',', $rp); + + # Check if all roles of a pattern role are supported (TODO: move this check to HMCCU_DetectRolePattern) + PATTERNROLE: foreach my $pr (@patternRoles) { + next ROLEPATTERN if (!exists($HMCCU_STATECONTROL->{$pr})); + } + + foreach my $firstChannel (split(',', $rolePatterns->{$rp}{i})) { + # state/control channel is the first channel with a state/control datapoint + my $i = 0; + foreach my $pr (@patternRoles) { + if ($HMCCU_STATECONTROL->{$pr}{S} ne '') { + $di{rolePattern}{$firstChannel}{stateRole} = $pr; + $di{rolePattern}{$firstChannel}{stateChannel} = $firstChannel+$i; + $di{rolePattern}{$firstChannel}{stateDatapoint} = $HMCCU_STATECONTROL->{$pr}{S}; + $di{defSCh} = $firstChannel+$i; + last; + } + $i++; + } + $i = 0; + foreach my $pr (@patternRoles) { + if ($HMCCU_STATECONTROL->{$pr}{C} ne '') { + $di{rolePattern}{$firstChannel}{controlRole} = $pr; + $di{rolePattern}{$firstChannel}{controlChannel} = $firstChannel+$i; + $di{rolePattern}{$firstChannel}{controlDatapoint} = $HMCCU_STATECONTROL->{$pr}{C}; + $di{defCCh} = $firstChannel+$i; + last; + } + $i++; + } + } + } + + $di{level} = 5 if (exists($di{rolePattern}) && scalar(keys %{$di{rolePattern}}) > 0); + } + } + } + + $di{defSDP} = $di{defSCh}.'.'.$di{stateRole}{$di{defSCh}}{datapoint} if ($di{defSCh} != -1); + $di{defCDP} = $di{defCCh}.'.'.$di{controlRole}{$di{defCCh}}{datapoint} if ($di{defCCh} != -1); + + return \%di; +} + +###################################################################### +# Identify a channel role +###################################################################### + +sub HMCCU_IdentifyRole ($$$$$) +{ + my ($ioHash, $devDesc, $iface, $stateRoles, $controlRoles) = @_; + + my $t = $devDesc->{TYPE}; # Channel role + + if (exists($HMCCU_STATECONTROL->{$t})) { + my ($a, $c) = HMCCU_SplitChnAddr ($devDesc->{ADDRESS}); + my $p = $HMCCU_STATECONTROL->{$t}{P}; + + # State datapoint must be readable and/or event + my $sDP = HMCCU_DetectSCDatapoint ($HMCCU_STATECONTROL->{$t}{S}, $iface); + push @$stateRoles, { 'channel' => $c, 'role' => $t, 'datapoint' => $sDP, 'priority' => $p } + if ($sDP ne '' && HMCCU_IsValidParameter ($ioHash, $devDesc, 'VALUES', $sDP, 5)); + + # Control datapoint must be writeable + my $cDP = HMCCU_DetectSCDatapoint ($HMCCU_STATECONTROL->{$t}{C}, $iface); + push @$controlRoles, { 'channel' => $c, 'role' => $t, 'datapoint' => $cDP, 'priority' => $p } + if ($cDP ne ''&& HMCCU_IsValidParameter ($ioHash, $devDesc, 'VALUES', $cDP, 2)); + } +} + +###################################################################### +# Detect role patterns +# +# Parameters: +# $roles - Array reference containing a list of channel roles +# $regMatch - Regular expression describing the pattern +# $minPatternLen - Minimum number of roles in the pattern +# $maxPatternLen - Maximum number of roles in the pattern +# $minOcc - Minimum number of occurrences of the pattern +# +# Example expression for matching groups of 1 TRANSMITTER and 3 +# virtual RECEIVER channels (default): +# +# '^(?!([A-Z]+_VIRTUAL))([A-Z]+)[A-Z_]+(,\g2_VIRTUAL_[A-Z_]+){3}$' +# +# Return hash reference with role patterns or undef on error. +# Role pattern hash (key = pattern): +# c - Occurrences of the pattern +# i - Comma separated list of the starting positions of the pattern +###################################################################### + +sub HMCCU_DetectRolePattern ($;$$$$) +{ + my ($roles, $regMatch, $minPatternLen, $maxPatternLen, $minOcc) = @_; + $regMatch //= '^(?!([A-Z]+_VIRTUAL))([A-Z]+)[A-Z_]+(,\g2_VIRTUAL_[A-Z_]+){3}$'; + $minPatternLen //= 2; + $minOcc //= 1; + $minOcc = HMCCU_Max ($minOcc, 1); + + my $n = scalar(@$roles); + my $skip = 1; + + return undef if ($n-$skip < $minPatternLen); + $maxPatternLen //= int(($n-$skip)/$minOcc); + return undef if ($maxPatternLen < $minPatternLen); + + my %patternList; + + for (my $patternLen=$minPatternLen; $patternLen<=$maxPatternLen; $patternLen++) { + # Create list of patterns + my @p = (); + for (my $j=$skip; $j<=$n-$patternLen; $j++) { + my $k=$j+$patternLen-1; + my $patStr = join(',',@$roles[$j..$k]); + push @p, { i => $j, p => $patStr } if ($patStr =~ /$regMatch/); + } + # Count patterns + foreach my $first (@p) { + next if (exists($patternList{$first->{p}})); + my $cnt = 0; + my @c = (); + foreach my $t (@p) { + if ($t->{p} eq $first->{p}) { + push @c, $t->{i}; + $cnt++; + } + } + if ($cnt >= $minOcc) { + $patternList{$first->{p}}{c} = $cnt; + $patternList{$first->{p}}{i} = join(',',@c); + } } } - # For devices of type HMCCUCHN extract channel numbers from CCU device address - if ($type eq 'HMCCUCHN') { - $sc = $hash->{ccuaddr}; - $sc =~ s/^[\*]*[0-9A-Z]+://; - $cc = $sc; - } - - # Try to find state channel - my $c = -1; - if ($sc eq '' && $sd ne '') { - $c = HMCCU_FindDatapoint ($hash, $hash->{ccutype}, -1, $sd, 3); - $sc = $c if ($c >= 0); - } - - # Try to find control channel - if ($cc eq '' && $cd ne '') { - $c = HMCCU_FindDatapoint ($hash, $hash->{ccutype}, -1, $cd, 3); - $cc = $c if ($c >= 0); - } - - # By default set control channel and datapoint to state channel and datapoint - $cc = $sc if ($cc eq ''); - $cd = $sd if ($cd eq ''); + return \%patternList; +} - return ($sc, $sd, $cc, $cd); +###################################################################### +# Select state or control datapoint from HMCCU_STATECONTROL definition +# considering interface specified in definition. +# Return datapoint or empty string. +###################################################################### + +sub HMCCU_DetectSCDatapoint ($$) +{ + my ($dpSpec, $iface) = @_; + + if (defined($dpSpec) && $dpSpec ne '') { + foreach my $dp (split(',', $dpSpec)) { + my ($i, $d) = split(':', $dp); + return $dp if (!defined($d)); + return $d if ($i eq $iface); + } + } + + return ''; } ###################################################################### # Get attribute ccuflags. -# Default value is 'null'. With version 4.2.009 flag extrpc is -# substituted by procrpc. +# Default value is 'null'. With version 4.4 flags intrpc and extrpc +# are substituted by procrpc. ###################################################################### sub HMCCU_GetFlags ($) @@ -5762,7 +8521,7 @@ sub HMCCU_GetFlags ($) my ($name) = @_; my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - $ccuflags =~ s/extrpc/procrpc/g; + $ccuflags =~ s/(extrpc|intrpc)/procrpc/g; return $ccuflags; } @@ -5776,7 +8535,7 @@ sub HMCCU_IsFlag ($$) my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); return $ccuflags =~ /$flag/ ? 1 : 0; -} +} ###################################################################### # Get reading format considering default attribute @@ -5786,20 +8545,18 @@ sub HMCCU_IsFlag ($$) sub HMCCU_GetAttrReadingFormat ($$) { - my ($clhash, $iohash) = @_; + my ($clHash, $ioHash) = @_; - my $clname = $clhash->{NAME}; - my $ioname = $iohash->{NAME}; - my $rfdef = ''; - - if (exists ($clhash->{ccutype}) && $clhash->{ccutype} =~ /^HM-CC-VG/) { + my $rfdef; + + if (HMCCU_IsFlag ($ioHash, 'updGroupMembers') && exists($clHash->{ccutype}) && $clHash->{ccutype} =~ /^HM-CC-VG/) { $rfdef = 'name'; } else { - $rfdef = AttrVal ($ioname, 'ccudef-readingformat', 'datapoint'); + $rfdef = AttrVal ($ioHash->{NAME}, 'ccudef-readingformat', 'datapoint'); } - - return AttrVal ($clname, 'ccureadingformat', $rfdef); + + return AttrVal ($clHash->{NAME}, 'ccureadingformat', $rfdef); } ###################################################################### @@ -5810,405 +8567,43 @@ sub HMCCU_GetAttrReadingFormat ($$) sub HMCCU_GetAttrStripNumber ($) { my ($hash) = @_; - my $fnc = "GetAttrStripNumber"; - - my $snDef = 'null'; + my $type = $hash->{TYPE}; - if ($hash->{TYPE} ne 'HMCCU') { - my $ioHash = HMCCU_GetHash ($hash); - if (defined ($ioHash)) { - $snDef = AttrVal ($ioHash->{NAME}, 'ccudef-stripnumber', 'null'); + my %strip = ( + 'BLIND' => '0', 'DIMMER' => '0' + ); + + my $ioHash = HMCCU_GetHash ($hash); + my $snDef = defined($ioHash) ? AttrVal ($ioHash->{NAME}, 'ccudef-stripnumber', '1') : '1'; + + if (defined($hash->{hmccu}{role}) && $hash->{hmccu}{role} ne '') { + foreach my $cr (split(',', $hash->{hmccu}{role})) { + my ($c, $r) = split(':', $cr); + if (exists($strip{$r})) { + $snDef = $strip{$r}; + last; + } } } - else { - $snDef = AttrVal ($hash->{NAME}, 'ccudef-stripnumber', 'null'); - } - my $stripnumber = AttrVal ($hash->{NAME}, 'stripnumber', $snDef); - - HMCCU_Trace ($hash, 2, $fnc, "stripnumber = $stripnumber"); - - return $stripnumber; + return AttrVal ($hash->{NAME}, 'stripnumber', $snDef); } ###################################################################### -# Get attributes substitute and substexcl considering default -# attribute ccudef-substitute defined in I/O device. +# Get attribute substitute considering default attribute +# ccudef-substitute defined in I/O device. # Substitute ${xxx} by datapoint value. ###################################################################### -sub HMCCU_GetAttrSubstitute ($$) +sub HMCCU_GetAttrSubstitute ($;$) { my ($clhash, $iohash) = @_; - my $fnc = "GetAttrSubstitute"; - - my $clname = $clhash->{NAME}; - my $ioname = $iohash->{NAME}; - my $substdef = AttrVal ($ioname, 'ccudef-substitute', ''); - my $subst = AttrVal ($clname, 'substitute', $substdef); + my $substdef = defined($iohash) ? AttrVal ($iohash->{NAME}, 'ccudef-substitute', '') : ''; + my $subst = AttrVal ($clhash->{NAME}, 'substitute', $substdef); $subst .= ";$substdef" if ($subst ne $substdef && $substdef ne ''); - HMCCU_Trace ($clhash, 2, $fnc, "subst = $subst"); - return $subst if ($subst !~ /\$\{.+\}/); - - $subst = HMCCU_SubstVariables ($clhash, $subst, undef); - - HMCCU_Trace ($clhash, 2, $fnc, "subst_vars = $subst"); - - return $subst; -} - -###################################################################### -# Clear RPC queue -###################################################################### - -sub HMCCU_ResetRPCQueue ($$) -{ - my ($hash, $port) = @_; - my $name = $hash->{NAME}; - - my $rpcqueue = AttrVal ($name, 'rpcqueue', '/tmp/ccuqueue'); - my $clkey = 'CB'.$port; - - if (HMCCU_QueueOpen ($hash, $rpcqueue."_".$port."_".$hash->{CCUNum})) { - HMCCU_QueueReset ($hash); - while (defined (HMCCU_QueueDeq ($hash))) { } - HMCCU_QueueClose ($hash); - } - $hash->{hmccu}{rpc}{$clkey}{queue} = '' if (exists ($hash->{hmccu}{rpc}{$clkey}{queue})); -} - -###################################################################### -# Process RPC server event -###################################################################### - -sub HMCCU_ProcessEvent ($$) -{ - my ($hash, $event) = @_; - my $name = $hash->{NAME}; - my $rh = \%{$hash->{hmccu}{rpc}}; - - return undef if (!defined ($event) || $event eq ''); - - my @t = split (/\|/, $event); - my $tc = scalar (@t); - - # Update statistic counters - if (exists ($hash->{hmccu}{ev}{$t[0]})) { - $hash->{hmccu}{evtime} = time (); - $hash->{hmccu}{ev}{total}++; - $hash->{hmccu}{ev}{$t[0]}++; - $hash->{hmccu}{evtimeout} = 0 if ($hash->{hmccu}{evtimeout} == 1); - } - else { - my $errtok = $t[0]; - $errtok =~ s/([\x00-\xFF])/sprintf("0x%X ",ord($1))/eg; - return HMCCU_Log ($hash, 2, "Received unknown event from CCU: ".$errtok); - } - - # Check event syntax - return HMCCU_Log ($hash, 2, "Wrong number of parameters in event $event") - if (exists ($rpceventargs{$t[0]}) && ($tc-1) != $rpceventargs{$t[0]}); - - if ($t[0] eq 'EV') { - # - # Update of datapoint - # Input: EV|Adress|Datapoint|Value - # Output: EV, DevAdd, ChnNo, Reading='', Value - # - return HMCCU_Log ($hash, 2, "Invalid channel ".$t[1]) - if (!HMCCU_IsValidChannel ($hash, $t[1], $HMCCU_FL_ADDRESS)); - my ($add, $chn) = split (/:/, $t[1]); - return ($t[0], $add, $chn, $t[2], $t[3]); - } - elsif ($t[0] eq 'SL') { - # - # RPC server enters server loop - # Input: SL|Pid|Servername - # Output: SL, Servername, Pid - # - my $clkey = $t[2]; - return HMCCU_Log ($hash, 0, "Received SL event for unknown RPC server $clkey") - if (!exists ($rh->{$clkey})); - Log3 $name, 0, "HMCCU: Received SL event. RPC server $clkey enters server loop"; - $rh->{$clkey}{loop} = 1 if ($rh->{$clkey}{pid} == $t[1]); - return ($t[0], $clkey, $t[1]); - } - elsif ($t[0] eq 'IN') { - # - # RPC server initialized - # Input: IN|INIT|State|Servername - # Output: IN, Servername, Running, NotRunning, ClientsUpdated, UpdateErrors - # - my $clkey = $t[3]; - my $norun = 0; - my $run = 0; - return HMCCU_Log ($hash, 0, "Received IN event for unknown RPC server $clkey") - if (!exists ($rh->{$clkey})); - Log3 $name, 0, "HMCCU: Received IN event. RPC server $clkey initialized."; - $rh->{$clkey}{state} = $rh->{$clkey}{pid} != 0 ? "running" : "initialized"; - - # Check if all RPC servers were initialized. Set overall status - foreach my $ser (keys %{$rh}) { - $norun++ if ($rh->{$ser}{state} ne "running" && $rh->{$ser}{pid} != 0); - $norun++ if ($rh->{$ser}{state} ne "initialized" && $rh->{$ser}{pid} == 0); - $run++ if ($rh->{$ser}{state} eq "running"); - } - HMCCU_SetRPCState ($hash, 'running') if ($norun == 0); - $hash->{hmccu}{rpcinit} = $run; - return ($t[0], $clkey, $run, $norun); - } - elsif ($t[0] eq 'EX') { - # - # RPC server shutdown - # Input: EX|SHUTDOWN|Pid|Servername - # Output: EX, Servername, Pid, Flag, Run - # - my $clkey = $t[3]; - my $run = 0; - return HMCCU_Log ($hash, 0, "Received EX event for unknown RPC server $clkey") - if (!exists ($rh->{$clkey})); - - Log3 $name, 0, "HMCCU: Received EX event. RPC server $clkey terminated."; - my $f = $hash->{RPCState} eq "restarting" ? 2 : 1; - delete $rh->{$clkey}; - - # Check if all RPC servers were terminated. Set overall status - foreach my $ser (keys %{$rh}) { - $run++ if ($rh->{$ser}{state} ne "inactive"); - } - if ($run == 0) { - HMCCU_SetRPCState ($hash, 'inactive') if ($f == 1); - $hash->{RPCPID} = '0'; - } - $hash->{hmccu}{rpccount} = $run; - $hash->{hmccu}{rpcinit} = $run; - return ($t[0], $clkey, $t[2], $f, $run); - } - elsif ($t[0] eq 'ND') { - # - # CCU device added - # Input: ND|C/D|Address|Type|Version|Firmware|RxMode - # Output: ND, DevAdd, C/D, Type, Version, Firmware, RxMode - # - return ($t[0], $t[2], $t[1], $t[3], $t[4], $t[5], $t[6]); - } - elsif ($t[0] eq 'DD' || $t[0] eq 'RA') { - # - # CCU device added, deleted or readded - # Input: {DD,RA}|Address - # Output: {DD,RA}, DevAdd - # - return ($t[0], $t[1]); - } - elsif ($t[0] eq 'UD') { - # - # CCU device updated - # Input: UD|Address|Hint - # Output: UD, DevAdd, Hint - # - return ($t[0], $t[1], $t[2]); - } - elsif ($t[0] eq 'RD') { - # - # CCU device replaced - # Input: RD|Address1|Address2 - # Output: RD, Address1, Address2 - # - return ($t[0], $t[1], $t[2]); - } - elsif ($t[0] eq 'ST') { - # - # Statistic data. Store snapshots of sent and received events. - # Input: ST|nTotal|nEV|nND|nDD|nRD|nRA|nUD|nIN|nSL|nEX - # Output: ST, ... - # - my @stkeys = ('total', 'EV', 'ND', 'DD', 'RD', 'RA', 'UD', 'IN', 'SL', 'EX'); - for (my $i=0; $i<10; $i++) { - $hash->{hmccu}{evs}{$stkeys[$i]} = $t[$i+1]; - $hash->{hmccu}{evr}{$stkeys[$i]} = $hash->{hmccu}{ev}{$stkeys[$i]}; - } - return @t; - } - else { - my $errtok = $t[0]; - $errtok =~ s/([\x00-\xFF])/sprintf("0x%X ",ord($1))/eg; - Log3 $name, 2, "HMCCU: Received unknown event from CCU: ".$errtok; - } - - return undef; -} - -###################################################################### -# Timer function for reading RPC queue -###################################################################### - -sub HMCCU_ReadRPCQueue ($) -{ - my ($hash) = @_; - my $name = $hash->{NAME}; - my $eventno = 0; - my $f = 0; - my @newdevices; - my @deldevices; - my @termpids; - my $newcount = 0; - my $devcount = 0; - my %events = (); - my %devices = (); - - my $ccuflags = HMCCU_GetFlags ($name); - my $rpcinterval = AttrVal ($name, 'rpcinterval', 5); - my $rpcqueue = AttrVal ($name, 'rpcqueue', '/tmp/ccuqueue'); - my $rpcevtimeout = AttrVal ($name, 'rpcevtimeout', $HMCCU_TIMEOUT_EVENT); - my $maxevents = $rpcinterval*10; - $maxevents = 50 if ($maxevents > 50); - $maxevents = 10 if ($maxevents < 10); - - my @portlist = HMCCU_GetRPCPortList ($hash); - foreach my $port (@portlist) { - my $clkey = 'CB'.$port; - next if (!exists ($hash->{hmccu}{rpc}{$clkey}{queue})); - my $queuename = $hash->{hmccu}{rpc}{$clkey}{queue}; - next if ($queuename eq ''); - if (!HMCCU_QueueOpen ($hash, $queuename)) { - Log3 $name, 1, "HMCCU: Can't open file queue $queuename"; - next; - } - - my $element = HMCCU_QueueDeq ($hash); - while (defined ($element)) { - Log3 $name, 2, "HMCCU: Event = $element" if ($ccuflags =~ /logEvents/); - my ($et, @par) = HMCCU_ProcessEvent ($hash, $element); - if (defined ($et)) { - if ($et eq 'EV') { - $events{$par[0]}{$par[1]}{$par[2]} = $par[3]; - $eventno++; - last if ($eventno == $maxevents); - } - elsif ($et eq 'ND') { -# push (@newdevices, $par[1]); - $newcount++ if (!exists ($hash->{hmccu}{dev}{$par[0]})); -# $hash->{hmccu}{dev}{$par[1]}{chntype} = $par[3]; - $devices{$par[0]}{flag} = 'N'; - $devices{$par[0]}{version} = $par[3]; - if ($par[1] eq 'D') { - $devices{$par[0]}{addtype} = 'dev'; - $devices{$par[0]}{type} = $par[2]; - $devices{$par[0]}{firmware} = $par[4]; - $devices{$par[0]}{rxmode} = $par[5]; - } - else { - $devices{$par[0]}{addtype} = 'chn'; - $devices{$par[0]}{usetype} = $par[2]; - } - $devcount++; - } - elsif ($et eq 'DD') { -# push (@deldevices, $par[0]); - $devices{$par[0]}{flag} = 'D'; - $devcount++; -# $delcount++; - } - elsif ($et eq 'RD') { - $devices{$par[0]}{flag} = 'R'; - $devices{$par[0]}{newaddr} = $par[1]; - $devcount++; - } - elsif ($et eq 'SL') { - InternalTimer (gettimeofday()+$HMCCU_INIT_INTERVAL1, - 'HMCCU_RPCRegisterCallback', $hash, 0); - $f = -1; - last; - } - elsif ($et eq 'EX') { - push (@termpids, $par[1]); - $f = $par[2]; - last; - } - } - - last if ($f == -1); - - # Read next element from queue - $element = HMCCU_QueueDeq ($hash); - } - - HMCCU_QueueClose ($hash); - } - - # Update readings - HMCCU_UpdateMultipleDevices ($hash, \%events) if ($eventno > 0); - - # Update device table and client device parameter - HMCCU_UpdateDeviceTable ($hash, \%devices) if ($devcount > 0); - - return if ($f == -1); - - # Check if events from CCU timed out - if ($hash->{hmccu}{evtime} > 0 && time()-$hash->{hmccu}{evtime} > $rpcevtimeout && - $hash->{hmccu}{evtimeout} == 0) { - $hash->{hmccu}{evtimeout} = 1; - $hash->{ccustate} = HMCCU_TCPConnect ($hash->{host}, $HMCCU_REGA_PORT{$hash->{prot}}) ne '' ? 'timeout' : 'unreachable'; - Log3 $name, 2, "HMCCU: Received no events from CCU since $rpcevtimeout seconds"; - DoTrigger ($name, "No events from CCU since $rpcevtimeout seconds"); - } - else { - $hash->{ccustate} = 'active' if ($hash->{ccustate} ne 'active'); - } - - my @hm_pids; - my @hm_tids; - HMCCU_IsRPCServerRunning ($hash, \@hm_pids, \@hm_tids); - my $nhm_pids = scalar (@hm_pids); - my $nhm_tids = scalar (@hm_tids); - Log3 $name, 1, "HMCCU: Externally launched RPC server(s) detected. f=$f" if ($nhm_tids > 0); - - if ($f > 0) { - # At least one RPC server has been stopped. Update PID list - $hash->{RPCPID} = $nhm_pids > 0 ? join(',',@hm_pids) : '0'; - Log3 $name, 0, "HMCCU: RPC server(s) with PID(s) ".join(',',@termpids)." shut down. f=$f"; - - # Output statistic counters - foreach my $cnt (sort keys %{$hash->{hmccu}{ev}}) { - Log3 $name, 3, "HMCCU: Eventcount $cnt = ".$hash->{hmccu}{ev}{$cnt}; - } - } - - if ($f == 2 && $nhm_pids == 0) { - # All RPC servers terminated and restart flag set - if ($ccuflags !~ /(extrpc|procrpc)/) { - return if (HMCCU_StartIntRPCServer ($hash)); - } - Log3 $name, 0, "HMCCU: Restart of RPC server failed"; - } - - if ($nhm_pids > 0) { - # Reschedule reading of RPC queues if at least one RPC server is running - InternalTimer (gettimeofday()+$rpcinterval, 'HMCCU_ReadRPCQueue', $hash, 0); - } - else { - # No more RPC servers active - Log3 $name, 0, "HMCCU: Periodical check found no RPC Servers"; - # Deregister existing callbacks - HMCCU_RPCDeRegisterCallback ($hash); - - # Cleanup hash variables - my @clkeylist = keys %{$hash->{hmccu}{rpc}}; - foreach my $clkey (@clkeylist) { - delete $hash->{hmccu}{rpc}{$clkey}; - } - $hash->{hmccu}{rpccount} = 0; - $hash->{hmccu}{rpcinit} = 0; - - $hash->{RPCPID} = '0'; - $hash->{RPCPRC} = 'none'; - - HMCCU_SetRPCState ($hash, 'inactive'); - Log3 $name, 0, "HMCCU: All RPC servers stopped"; - DoTrigger ($name, "All RPC servers stopped"); - } + return $subst !~ /\$\{.+\}/ ? $subst : HMCCU_SubstVariables ($clhash, $subst, undef); } ###################################################################### @@ -6221,14 +8616,13 @@ sub HMCCU_HMCommand ($$$) { my ($cl_hash, $cmd, $mode) = @_; my $cl_name = $cl_hash->{NAME}; - my $fnc = "HMCommand"; my $io_hash = HMCCU_GetHash ($cl_hash); - my $ccureqtimeout = AttrVal ($io_hash->{NAME}, "ccuReqTimeout", $HMCCU_TIMEOUT_REQUEST); + my $ccureqtimeout = AttrVal ($io_hash->{NAME}, 'ccuReqTimeout', $HMCCU_TIMEOUT_REQUEST); my $url = HMCCU_BuildURL ($io_hash, 'rega'); my $value; - HMCCU_Trace ($cl_hash, 2, $fnc, "URL=$url, cmd=$cmd"); + HMCCU_Trace ($cl_hash, 2, "URL=$url, cmd=$cmd"); my $param = { url => $url, timeout => $ccureqtimeout, data => $cmd, method => "POST" }; $param->{sslargs} = { SSL_verify_mode => 0 }; @@ -6236,21 +8630,23 @@ sub HMCCU_HMCommand ($$$) if ($err eq '') { $value = $response; - $value =~ s/(.*)<\/xml>//; - $value =~ s/\r//g; - HMCCU_Trace ($cl_hash, 2, $fnc, "Response=$response, Value=".(defined ($value) ? $value : "undef")); + if (defined($value)) { + $value =~ s/(.*)<\/xml>//; + $value =~ s/\r//g; + } + HMCCU_Trace ($cl_hash, 2, "Response=$response, Value=".(defined($value) ? $value : "undef")); } else { HMCCU_Log ($io_hash, 2, "Error during HTTP request: $err"); - HMCCU_Trace ($cl_hash, 2, $fnc, "Response=$response"); + HMCCU_Trace ($cl_hash, 2, "Response=".(defined($response) ? $response : 'undef')); return undef; } if ($mode == 1) { - return (defined ($value) && $value ne 'null') ? $value : undef; + return (defined($value) && $value ne 'null') ? $value : undef; } else { - return (defined ($value) && $value ne '' && $value ne 'null') ? $value : undef; + return (defined($value) && $value ne '' && $value ne 'null') ? $value : undef; } } @@ -6260,25 +8656,24 @@ sub HMCCU_HMCommand ($$$) sub HMCCU_HMCommandNB ($$$) { - my ($cl_hash, $cmd, $cbfunc) = @_; - my $cl_name = $cl_hash->{NAME}; - my $fnc = "HMCommandNB"; + my ($clHash, $cmd, $cbFunc) = @_; + my $clName = $clHash->{NAME}; - my $io_hash = HMCCU_GetHash ($cl_hash); - my $ccureqtimeout = AttrVal ($io_hash->{NAME}, "ccuReqTimeout", $HMCCU_TIMEOUT_REQUEST); - my $url = HMCCU_BuildURL ($io_hash, 'rega'); + my $ioHash = HMCCU_GetHash ($clHash); + my $ccureqtimeout = AttrVal ($ioHash->{NAME}, 'ccuReqTimeout', $HMCCU_TIMEOUT_REQUEST); + my $url = HMCCU_BuildURL ($ioHash, 'rega'); - HMCCU_Trace ($cl_hash, 2, $fnc, "URL=$url"); + HMCCU_Trace ($clHash, 2, "URL=$url"); - if (defined ($cbfunc)) { + if (defined($cbFunc)) { my $param = { url => $url, timeout => $ccureqtimeout, data => $cmd, method => "POST", - callback => $cbfunc, devhash => $cl_hash }; + callback => $cbFunc, devhash => $clHash }; $param->{sslargs} = { SSL_verify_mode => 0 }; HttpUtils_NonblockingGet ($param); } else { my $param = { url => $url, timeout => $ccureqtimeout, data => $cmd, method => "POST", - callback => \&HMCCU_HMCommandCB, devhash => $cl_hash }; + callback => \&HMCCU_HMCommandCB, devhash => $clHash }; $param->{sslargs} = { SSL_verify_mode => 0 }; HttpUtils_NonblockingGet ($param); } @@ -6292,10 +8687,9 @@ sub HMCCU_HMCommandCB ($$$) { my ($param, $err, $data) = @_; my $hash = $param->{devhash}; - my $fnc = "HMCommandCB"; HMCCU_Log ($hash, 2, "Error during CCU request. $err") if ($err ne ''); - HMCCU_Trace ($hash, 2, $fnc, "URL=".$param->{url}."
Response=$data"); + HMCCU_Trace ($hash, 2, "URL=".$param->{url}."
Response=$data"); } ###################################################################### @@ -6311,22 +8705,26 @@ sub HMCCU_HMCommandCB ($$$) # Return script output or error message starting with "ERROR:". ###################################################################### -sub HMCCU_HMScriptExt ($$$$$) +sub HMCCU_HMScriptExt ($$;$$$) { my ($hash, $hmscript, $params, $cbFunc, $cbParam) = @_; my $name = $hash->{NAME}; my $code = $hmscript; my $scrname = ''; - return HMCCU_LogError ($hash, 2, "CCU host name not defined") if (!exists ($hash->{host})); + if ($hash->{TYPE} ne 'HMCCU') { + HMCCU_Log ($hash, 2, stacktraceAsString(undef)); + } + + return HMCCU_LogError ($hash, 2, 'CCU host name not defined') if (!exists($hash->{host})); my $host = $hash->{host}; - my $ccureqtimeout = AttrVal ($hash->{NAME}, "ccuReqTimeout", $HMCCU_TIMEOUT_REQUEST); + my $ccureqtimeout = AttrVal ($hash->{NAME}, 'ccuReqTimeout', $HMCCU_TIMEOUT_REQUEST); if ($hmscript =~ /^!(.*)$/) { # Internal script $scrname = $1; - return "ERROR: Can't find internal script $scrname" if (!exists ($HMCCU_SCRIPTS->{$scrname})); + return "ERROR: Can't find internal script $scrname" if (!exists($HMCCU_SCRIPTS->{$scrname})); $code = $HMCCU_SCRIPTS->{$scrname}{code}; } elsif ($hmscript =~ /^\[(.*)\]$/) { @@ -6346,7 +8744,7 @@ sub HMCCU_HMScriptExt ($$$$$) } # Check and replace variables - if (defined ($params)) { + if (defined($params)) { my @parnames = keys %{$params}; if ($scrname ne '') { if (scalar (@parnames) != $HMCCU_SCRIPTS->{$scrname}{parameters}) { @@ -6369,37 +8767,33 @@ sub HMCCU_HMScriptExt ($$$$$) } } - HMCCU_Trace ($hash, 2, "HMScriptEx", $code); + HMCCU_Trace ($hash, 2, $code); # Execute script on CCU my $url = HMCCU_BuildURL ($hash, 'rega'); - if (defined ($cbFunc)) { + if (defined($cbFunc)) { + # Non blocking my $param = { url => $url, timeout => $ccureqtimeout, data => $code, method => "POST", callback => $cbFunc, ioHash => $hash }; - if (defined ($cbParam)) { - foreach my $p (keys %{$cbParam}) { - $param->{$p} = $cbParam->{$p}; - } + if (defined($cbParam)) { + foreach my $p (keys %{$cbParam}) { $param->{$p} = $cbParam->{$p}; } } $param->{sslargs} = { SSL_verify_mode => 0 }; HttpUtils_NonblockingGet ($param); return '' } + + # Blocking + my $param = { url => $url, timeout => $ccureqtimeout, data => $code, method => "POST" }; + $param->{sslargs} = { SSL_verify_mode => 0 }; + my ($err, $response) = HttpUtils_BlockingGet ($param); + if ($err eq '') { + $response =~ s/.*<\/xml>//; + $response =~ s/\r//g; + return $response; + } else { - my $param = { url => $url, timeout => $ccureqtimeout, data => $code, method => "POST" }; - $param->{sslargs} = { SSL_verify_mode => 0 }; - my ($err, $response) = HttpUtils_BlockingGet ($param); - - if ($err eq '') { - my $output = $response; - $output =~ s/.*<\/xml>//; - $output =~ s/\r//g; - return $output; - } - else { - HMCCU_Log ($hash, 2, "HMScript failed. $err"); - return "ERROR: HMScript failed. $err"; - } + return HMCCU_LogError ($hash, 2, "HMScript failed. $err"); } } @@ -6407,30 +8801,12 @@ sub HMCCU_HMScriptExt ($$$$$) # Bulk update of reading considering attribute substexcl. ###################################################################### -sub HMCCU_BulkUpdate ($$$$) +sub HMCCU_BulkUpdate ($$$;$) { my ($hash, $reading, $orgval, $subval) = @_; - my $name = $hash->{NAME}; + $subval //= $orgval; - my $excl = AttrVal ($name, 'substexcl', ''); -# -# For later use: Suppress reading update -# -# my $suppress = AttrVal ($name, 'ccusuppress', ''); -# -# if ($suppress ne '') { -# my $ct = time(); -# my @srules = split (";", $suppress); -# -# foreach my $sr (@srules) { -# my ($rnexp, $to) = split (":", $sr); -# next if (!defined ($to)); -# if ($reading =~ /$rnexp/) { -# my $rt = ReadingsTimestamp ($name, $reading, ''); -# return if ($rt ne '' && $ct-time_str2num($rt) < $to); -# } -# } -# } + my $excl = AttrVal ($hash->{NAME}, 'substexcl', ''); readingsBulkUpdate ($hash, $reading, ($excl ne '' && $reading =~ /$excl/ ? $orgval : $subval)); } @@ -6444,17 +8820,13 @@ sub HMCCU_GetDatapoint ($@) { my ($cl_hash, $param, $noupd) = @_; my $cl_name = $cl_hash->{NAME}; - my $fnc = "GetDatapoint"; my $value = ''; - my $io_hash = HMCCU_GetHash ($cl_hash); - return (-3, $value) if (!defined ($io_hash)); + my $io_hash = HMCCU_GetHash ($cl_hash) // return (-3, $value); return (-4, $value) if ($cl_hash->{TYPE} ne 'HMCCU' && $cl_hash->{ccudevstate} eq 'deleted'); my $readingformat = HMCCU_GetAttrReadingFormat ($cl_hash, $io_hash); my $substitute = HMCCU_GetAttrSubstitute ($cl_hash, $io_hash); - my ($statechn, $statedpt, $controlchn, $controldpt) = HMCCU_GetSpecialDatapoints ( - $cl_hash, '', 'STATE', '', ''); my $ccuget = HMCCU_GetAttribute ($io_hash, $cl_hash, 'ccuget', 'Value'); my $ccureqtimeout = AttrVal ($io_hash->{NAME}, "ccuReqTimeout", $HMCCU_TIMEOUT_REQUEST); @@ -6468,22 +8840,22 @@ sub HMCCU_GetDatapoint ($@) } elsif ($flags == $HMCCU_FLAGS_NCD) { $cmd = 'Write((dom.GetObject(ID_CHANNELS)).Get("'.$nam.'").DPByHssDP("'.$dpt.'").'.$ccuget.'())'; - ($add, $chn) = HMCCU_GetAddress ($io_hash, $nam, '', ''); + ($add, $chn) = HMCCU_GetAddress ($io_hash, $nam); } - HMCCU_Trace ($cl_hash, 2, $fnc, "CMD=$cmd, param=$param, ccuget=$ccuget"); + HMCCU_Trace ($cl_hash, 2, "CMD=$cmd, param=$param, ccuget=$ccuget"); $value = HMCCU_HMCommand ($cl_hash, $cmd, 1); - if (defined ($value) && $value ne '' && $value ne 'null') { - if (!defined ($noupd) || $noupd == 0) { + if (defined($value) && $value ne '' && $value ne 'null') { + if (!defined($noupd) || $noupd == 0) { $value = HMCCU_UpdateSingleDatapoint ($cl_hash, $chn, $dpt, $value); } else { my $svalue = HMCCU_ScaleValue ($cl_hash, $chn, $dpt, $value, 0); $value = HMCCU_Substitute ($svalue, $substitute, 0, $chn, $dpt); } - HMCCU_Trace ($cl_hash, 2, $fnc, "Value of $chn.$dpt = $value"); + HMCCU_Trace ($cl_hash, 2, "Value of $chn.$dpt = $value"); return (1, $value); } else { @@ -6494,23 +8866,30 @@ sub HMCCU_GetDatapoint ($@) ###################################################################### # Set multiple values of parameter set. -# Parameter params is a hash reference. Keys are datapoint names. -# Parameter address must be a channel address. +# Parameter params is a hash reference. Keys are parameter names. +# Parameter address must be a device or a channel address. +# If no paramSet is specified, VALUES is used by default. ###################################################################### -sub HMCCU_SetMultipleParameters ($$$) +sub HMCCU_SetMultipleParameters ($$$;$) { - my ($clHash, $address, $params) = @_; - + my ($clHash, $address, $params, $paramSet) = @_; + $paramSet //= 'VALUES'; + $address =~ s/:d$//; + my ($add, $chn) = HMCCU_SplitChnAddr ($address); - return -1 if (!defined ($chn)); + return (-1, undef) if ($paramSet eq 'VALUES' && !defined($chn)); foreach my $p (sort keys %$params) { - return -8 if (!HMCCU_IsValidDatapoint ($clHash, $clHash->{ccutype}, $chn, $p, 2)); + HMCCU_Trace ($clHash, 2, "Parameter=$address.$paramSet.$p Value=$params->{$p}"); + return (-8, undef) if ( + ($paramSet eq 'VALUES' && !HMCCU_IsValidDatapoint ($clHash, $clHash->{ccutype}, $chn, $p, 2)) || + ($paramSet eq 'MASTER' && !HMCCU_IsValidParameter ($clHash, $address, $paramSet, $p)) + ); $params->{$p} = HMCCU_ScaleValue ($clHash, $chn, $p, $params->{$p}, 1); } - - return HMCCU_RPCRequest ($clHash, "putParamset", $address, 'VALUES', $params); + + return HMCCU_RPCRequest ($clHash, 'putParamset', $address, $paramSet, $params); } ###################################################################### @@ -6519,11 +8898,11 @@ sub HMCCU_SetMultipleParameters ($$$) # datapoint specifications in format: # no.interface.{address|fhemdev}:channelno.datapoint # Parameter no defines the command order. +# Return value < 0 on error. ###################################################################### sub HMCCU_SetMultipleDatapoints ($$) { my ($clHash, $params) = @_; - my $fnc = "SetMultipleDatapoints"; my $mdFlag = $clHash->{TYPE} eq 'HMCCU' ? 1 : 0; my $ioHash; @@ -6531,8 +8910,7 @@ sub HMCCU_SetMultipleDatapoints ($$) { $ioHash = $clHash; } else { - $ioHash = HMCCU_GetHash ($clHash); - return -3 if (!defined ($ioHash)); + $ioHash = HMCCU_GetHash ($clHash) // return -3; } my $ioName = $ioHash->{NAME}; @@ -6546,62 +8924,56 @@ sub HMCCU_SetMultipleDatapoints ($$) { # Check address. dev is either a device address or a FHEM device name my ($no, $int, $addchn, $dpt) = split (/\./, $p); - return -1 if (!defined ($dpt)); + return -1 if (!defined($dpt)); my ($dev, $chn) = split (':', $addchn); - return -1 if (!defined ($chn)); + return -1 if (!defined($chn)); my $add = $dev; # Get hash of FHEM device if ($mdFlag) { - return -1 if (!exists ($defs{$dev})); - $clHash = $defs{$dev}; + $clHash = $defs{$dev} // return -1; ($add, undef) = HMCCU_SplitChnAddr ($clHash->{ccuaddr}); } # Device has been deleted or is disabled - return -4 if (exists ($clHash->{ccudevstate}) && $clHash->{ccudevstate} eq 'deleted'); + return -4 if (exists($clHash->{ccudevstate}) && $clHash->{ccudevstate} eq 'deleted'); return -21 if (IsDisabled ($clHash->{NAME})); - HMCCU_Trace ($clHash, 2, $fnc, "dpt=$p, value=$v"); + HMCCU_Trace ($clHash, 2, "dpt=$p, value=$v"); # Check client device type and datapoint my $clType = $clHash->{TYPE}; my $ccuType = $clHash->{ccutype}; return -1 if ($clType ne 'HMCCUCHN' && $clType ne 'HMCCUDEV'); - return -8 if (!HMCCU_IsValidDatapoint ($clHash, $ccuType, $chn, $dpt, 2)); + if (!HMCCU_IsValidDatapoint ($clHash, $ccuType, $chn, $dpt, 2)) { + HMCCU_Trace ($clHash, 2, "Invalid datapoint $chn $dpt"); + return -8; + } my $ccuVerify = AttrVal ($clName, 'ccuverify', 0); my $ccuChange = AttrVal ($clName, 'ccuSetOnChange', 'null'); - # Build device address list considering group devices - my @addrList = $clHash->{ccuif} eq 'fhem' ? split (',', $clHash->{ccugroup}) : ($add); - return -1 if (scalar (@addrList) < 1); - - foreach my $a (@addrList) { - # Override address and interface of group device with address of group members - if ($clHash->{ccuif} eq 'fhem') { - ($add, undef) = HMCCU_SplitChnAddr ($a); - $int = HMCCU_GetDeviceInterface ($ioHash, $a, ''); - return -20 if ($int eq ''); - } - - if ($ccuType eq 'HM-Dis-EP-WM55' && $dpt eq 'SUBMIT') { - $v = HMCCU_EncodeEPDisplay ($v); + if ($ccuType =~ /^HM-Dis-EP-WM55/ && $dpt eq 'SUBMIT') { + $v = HMCCU_EncodeEPDisplay ($v); + } + else { + if ($v =~ /^[\$\%]{1,2}([0-9]{1,2}\.[A-Z0-9_]+)$/ && exists($clHash->{hmccu}{dp}{"$1"})) { + $v = HMCCU_SubstVariables ($clHash, $v, undef); } else { $v = HMCCU_ScaleValue ($clHash, $chn, $dpt, $v, 1); } + } - my $dptType = HMCCU_GetDatapointAttr ($ioHash, $ccuType, $chn, $dpt, 'type'); - $v = "'".$v."'" if (defined ($dptType) && $dptType == $HMCCU_TYPE_STRING); - my $c = '(datapoints.Get("'.$int.'.'.$add.':'.$chn.'.'.$dpt.'")).State('.$v.");\n"; + my $dptType = HMCCU_GetDatapointAttr ($ioHash, $ccuType, $chn, $dpt, 'type'); + $v = "'".$v."'" if (defined($dptType) && $dptType == $HMCCU_TYPE_STRING); + my $c = '(datapoints.Get("'.$int.'.'.$add.':'.$chn.'.'.$dpt.'")).State('.$v.");\n"; - if ($dpt =~ /$ccuChange/) { - $cmd .= 'if((datapoints.Get("'.$int.'.'.$add.':'.$chn.'.'.$dpt.'")).Value() != '.$v.") {\n$c}\n"; - } - else { - $cmd .= $c; - } + if ($dpt =~ /$ccuChange/) { + $cmd .= 'if((datapoints.Get("'.$int.'.'.$add.':'.$chn.'.'.$dpt.'")).Value() != '.$v.") {\n$c}\n"; + } + else { + $cmd .= $c; } } @@ -6610,122 +8982,16 @@ sub HMCCU_SetMultipleDatapoints ($$) { HMCCU_HMCommandNB ($clHash, $cmd, undef); return 0; } - else { - # Execute command (blocking) - my $response = HMCCU_HMCommand ($clHash, $cmd, 1); - return defined ($response) ? 0 : -2; - # Datapoint verification ??? - } + + # Execute command (blocking) + my $response = HMCCU_HMCommand ($clHash, $cmd, 1); + return defined($response) ? 0 : -2; + # Datapoint verification ??? } -###################################################################### -# Set datapoint on CCU. -# Parameter param is a valid CCU or FHEM datapoint specification: -# [ccu:]address:channelnumber.datapoint -# [ccu:]channelname.datapoint -# hmccu:hmccudev_name.channelnumber.datapoint -# hmccu:hmccuchn_name.datapoint -###################################################################### - -# sub HMCCU_SetDatapoint ($$$) -# { -# my ($hash, $param, $value) = @_; -# my $fnc = "SetDatapoint"; -# my $type = $hash->{TYPE}; -# -# my $hmccu_hash = HMCCU_GetHash ($hash); -# return -3 if (!defined ($hmccu_hash)); -# return -4 if (exists ($hash->{ccudevstate}) && $hash->{ccudevstate} eq 'deleted'); -# my $name = $hmccu_hash->{NAME}; -# my $cdname = $hash->{NAME}; -# -# my $ccureqtimeout = AttrVal ($name, "ccuReqTimeout", $HMCCU_TIMEOUT_REQUEST); -# my $ccuflags = HMCCU_GetFlags ($name); -# my $readingformat = HMCCU_GetAttrReadingFormat ($hash, $hmccu_hash); -# my $ccuverify = AttrVal ($cdname, 'ccuverify', 0); -# -# HMCCU_Trace ($hash, 2, $fnc, "param=$param, value=$value"); -# -# if ($param =~ /^hmccu:.+$/) { -# my @t = split (/\./, $param); -# return -1 if (scalar (@t) < 2 || scalar (@t) > 3); -# my $fhdpt = pop @t; -# my ($fhadd, $fhchn) = HMCCU_GetAddress ($hmccu_hash, $t[0], '', ''); -# $fhchn = $t[1] if (scalar (@t) == 2); -# return -1 if ($fhadd eq '' || $fhchn eq ''); -# $param = "$fhadd:$fhchn.$fhdpt"; -# } -# elsif ($param =~ /^ccu:(.+)$/) { -# $param = $1; -# } -# -# my $cmd = ''; -# my ($int, $add, $chn, $dpt, $nam, $flags) = HMCCU_ParseObject ($hmccu_hash, $param, -# $HMCCU_FLAG_INTERFACE); -# return -1 if ($flags != $HMCCU_FLAGS_IACD && $flags != $HMCCU_FLAGS_NCD); -# -# if ($hash->{ccutype} eq 'HM-Dis-EP-WM55' && $dpt eq 'SUBMIT') { -# $value = HMCCU_EncodeEPDisplay ($value); -# } -# else { -# $value = HMCCU_ScaleValue ($hash, $chn, $dpt, $value, 1); -# } -# -# my $dpttype = HMCCU_GetDatapointAttr ($hmccu_hash, $hash->{ccutype}, $chn, $dpt, 'type'); -# if (defined ($dpttype) && $dpttype == $HMCCU_TYPE_STRING) { -# $value = "'".$value."'"; -# } -# -# if ($flags == $HMCCU_FLAGS_IACD) { -# # $cmd = '(datapoints.Get("'.$int.'.'.$add.':'.$chn.'.'.$dpt.'")).State('.$value.')'; -# $nam = HMCCU_GetChannelName ($hmccu_hash, $add.":".$chn, ''); -# } -# elsif ($flags == $HMCCU_FLAGS_NCD) { -# # $cmd = '(dom.GetObject(ID_CHANNELS)).Get("'.$nam.'").DPByHssDP("'.$dpt.'").State('.$value.')'; -# ($add, $chn) = HMCCU_GetAddress ($hmccu_hash, $nam, '', ''); -# } -# -# if ($type eq 'HMCCUDEV' && $hash->{ccuif} eq 'fhem' && $hash->{ccutype} ne 'n/a' && exists ($hash->{ccugroup})) { -# foreach my $gaddr (split (',', $hash->{ccugroup})) { -# $cmd .= '(datapoints.Get("'.$int.'.'.$gaddr.':'.$chn.'.'.$dpt.'")).State('.$value.");\n"; -# } -# } -# else { -# $cmd = '(datapoints.Get("'.$int.'.'.$add.':'.$chn.'.'.$dpt.'")).State('.$value.')'; -# } -# -# my $addr = $add.":".$chn; -# -# if ($ccuflags =~ /nonBlocking/) { -# HMCCU_HMCommandNB ($hash, $cmd, undef); -# return 0; -# } -# -# # Execute command (blocking) -# my $response = HMCCU_HMCommand ($hash, $cmd, 1); -# HMCCU_Trace ($hash, 2, $fnc, -# "Addr=$addr Name=$nam
". -# "Script response = \n".(defined ($response) ? $response: 'undef')."
". -# "Script = \n".$cmd); -# return -2 if (!defined ($response)); -# -# # Verify setting of datapoint value or update reading with new datapoint value -# if (HMCCU_IsValidDatapoint ($hash, $hash->{ccutype}, $addr, $dpt, 1)) { -# if ($ccuverify == 1) { -# my ($rc, $result) = HMCCU_GetDatapoint ($hash, $param, 0); -# return $rc; -# } -# elsif ($ccuverify == 2) { -# HMCCU_UpdateSingleDatapoint ($hash, $chn, $dpt, $value); -# } -# } -# -# return 0; -# } - ###################################################################### # Scale, spread and/or shift datapoint value. -# Mode: 0 = Get/Divide, 1 = Set/Multiply +# Mode: 0 = Get/Multiply, 1 = Set/Divide # Supports reversing of value if value range is specified. Syntax for # Rule is: # [ChannelNo.]Datapoint:Factor @@ -6734,59 +9000,121 @@ sub HMCCU_SetMultipleDatapoints ($$) { # an error original value is returned. ###################################################################### -sub HMCCU_ScaleValue ($$$$$) +sub HMCCU_ScaleValue ($$$$$;$) { - my ($hash, $chnno, $dpt, $value, $mode) = @_; + my ($hash, $chnno, $dpt, $value, $mode, $paramSet) = @_; + $chnno //= ''; + $paramSet //= 'VALUES'; my $name = $hash->{NAME}; - + my $ioHash = HMCCU_GetHash ($hash); + + # Only numeric values allowed + return $value if (!HMCCU_IsFltNum ($value)); + + # Get parameter definition and min/max values + my $min; + my $max; + my $unit; + my $ccuaddr = $hash->{ccuaddr}; + $ccuaddr .= ':'.$chnno if ($hash->{TYPE} eq 'HMCCUDEV' && $chnno ne ''); + my $paramDef = HMCCU_GetParamDef ($ioHash, $ccuaddr, $paramSet, $dpt); + if (defined($paramDef)) { + $min = $paramDef->{MIN} if (defined($paramDef->{MIN}) && $paramDef->{MIN} ne ''); + $max = $paramDef->{MAX} if (defined($paramDef->{MAX}) && $paramDef->{MAX} ne ''); + $unit = $paramDef->{UNIT}; + $unit = '100%' if ($dpt eq 'LEVEL' && !defined($unit)); + } + else { + HMCCU_Trace ($hash, 2, "Can't get parameter definion for addr=$hash->{ccuaddr} chn=$chnno"); + } + + # Default values can be overriden by attribute my $ccuscaleval = AttrVal ($name, 'ccuscaleval', ''); - return $value if ($ccuscaleval eq ''); - my @sl = split (',', $ccuscaleval); - foreach my $sr (@sl) { - my $f = 1.0; - my @a = split (':', $sr); - my $n = scalar (@a); - next if ($n != 2 && $n != 5); + HMCCU_Trace ($hash, 2, "chnno=$chnno, dpt=$dpt, value=$value, mode=$mode"); + + if ($ccuscaleval ne '') { + HMCCU_Trace ($hash, 2, "ccuscaleval"); + my @sl = split (',', $ccuscaleval); + foreach my $sr (@sl) { + my $f = 1.0; + my @a = split (':', $sr); + my $n = scalar (@a); + next if ($n != 2 && $n != 5); - my $rev = 0; - my $dn = $a[0]; - my $cn = $chnno; - if ($dn =~ /^\!(.+)$/) { - # Invert - $dn = $1; - $rev = 1; - } - if ($dn =~ /^([0-9]{1,2})\.(.+)$/) { - # Compare channel number - $cn = $1; - $dn = $2; - } - next if ($dpt ne $dn || ($chnno ne '' && $cn ne $chnno)); + my $rev = 0; + my $dn = $a[0]; + my $cn = $chnno; + if ($dn =~ /^\!(.+)$/) { + # Invert + $dn = $1; + $rev = 1; + } + if ($dn =~ /^([0-9]{1,2})\.(.+)$/) { + # Compare channel number + $cn = $1; + $dn = $2; + } + next if ($dpt ne $dn || ($chnno ne '' && $cn ne $chnno)); - if ($n == 2) { - $f = ($a[1] == 0.0) ? 1.0 : $a[1]; - return ($mode == 0) ? $value/$f : $value*$f; + if ($n == 2) { + $f = ($a[1] == 0.0) ? 1.0 : $a[1]; + $value = ($mode == 0) ? $value/$f : $value*$f; + } + elsif ($a[1] <= $a[2] && $a[3] <= $a[4] && ( + ($mode == 0 && $value >= $a[1] && $value <= $a[2]) || + ($mode == 1 && $value >= $a[3] && $value <= $a[4]) + )) { + # Reverse value + if ($rev) { + my $dr = ($mode == 0) ? $a[1]+$a[2] : $a[3]+$a[4]; + $value = $dr-$value; + } + + my $d1 = $a[2]-$a[1]; + my $d2 = $a[4]-$a[3]; + if ($d1 != 0.0 && $d2 != 0.0) { + $f = $d1/$d2; + $value = ($mode == 0) ? $value/$f+$a[3] : ($value-$a[3])*$f; + } + } + } + + # Align value with min/max boundaries for set mode + if ($mode == 1 && defined($min) && defined($max)) { + $value = HMCCU_MinMax ($value, $min, $max); + } + + HMCCU_Trace ($hash, 2, "Attribute scaled value of $dpt = $value"); + + return int($value) == $value ? int($value) : $value; + } + + if ($dpt =~ /^RSSI_/) { + # Subtract 256 from Rega value (Rega bug) + $value = abs ($value) == 65535 || $value == 0 ? 'N/A' : ($value > 0 ? $value-256 : $value); + } + elsif ($dpt =~ /^(P[0-9]_)?ENDTIME/) { + if ($mode == 0) { + my $hh = sprintf ("%02d", int($value/60)); + my $mm = sprintf ("%02d", $value%60); + $value = "$hh:$mm"; } else { - # Do not scale if value out of range or interval wrong - return $value if ($a[1] > $a[2] || $a[3] > $a[4]); - return $value if ($mode == 0 && ($value < $a[1] || $value > $a[2])); - return $value if ($mode == 1 && ($value < $a[3] || $value > $a[4])); - - # Reverse value - if ($rev) { - my $dr = ($mode == 0) ? $a[1]+$a[2] : $a[3]+$a[4]; - $value = $dr-$value; - } - - my $d1 = $a[2]-$a[1]; - my $d2 = $a[4]-$a[3]; - return $value if ($d1 == 0.0 || $d2 == 0.0); - $f = $d1/$d2; - return ($mode == 0) ? $value/$f+$a[3] : ($value-$a[3])*$f; - } + my ($hh, $mm) = split (':', $value); + $mm //= 0; + $value = $hh*60+$mm; + } } + elsif (defined($unit) && $unit =~ /^([0-9]+)%$/) { + my $f = $1; + $min //= 0; + $max //= 1.0; + $value = ($mode == 0) ? HMCCU_MinMax ($value, $min, $max)*$f : + HMCCU_MinMax($value, $min*$f, $max*$f)/$f; + } + + HMCCU_Trace ($hash, 2, "Auto scaled value of $dpt = $value"); return $value; } @@ -6800,29 +9128,25 @@ sub HMCCU_ScaleValue ($$$$$) sub HMCCU_GetVariables ($$) { my ($hash, $pattern) = @_; - my $name = $hash->{NAME}; + + my $response = HMCCU_HMScriptExt ($hash, '!GetVariables'); + return (-2, $response) if ($response eq '' || $response =~ /^ERROR:.*/); + + my %readings; my $count = 0; my $result = ''; - my $ccureadings = AttrVal ($name, 'ccureadings', HMCCU_IsFlag ($name, "noReadings") ? 0 : 1); - - my $response = HMCCU_HMScriptExt ($hash, "!GetVariables", undef, undef, undef); - return (-2, $response) if ($response eq '' || $response =~ /^ERROR:.*/); - - readingsBeginUpdate ($hash) if ($ccureadings); - foreach my $vardef (split /[\n\r]+/, $response) { + # Array values: 0=varName, 1=varType, 2=varValue my @vardata = split /=/, $vardef; - next if (@vardata != 3); - next if ($vardata[0] !~ /$pattern/); + next if (@vardata != 3 || $vardata[0] !~ /$pattern/); my $rn = HMCCU_CorrectName ($vardata[0]); - my $value = HMCCU_FormatReadingValue ($hash, $vardata[2], $vardata[0]); - readingsBulkUpdate ($hash, $rn, $value) if ($ccureadings); + $readings{$rn} = HMCCU_FormatReadingValue ($hash, $vardata[2], $vardata[0]); $result .= $vardata[0].'='.$vardata[2]."\n"; $count++; } - - readingsEndUpdate ($hash, 1) if ($ccureadings); + + HMCCU_UpdateReadings ($hash, \%readings); return ($count, $result); } @@ -6835,9 +9159,9 @@ sub HMCCU_UpdateVariables ($) { my ($hash) = @_; - if (exists ($hash->{hmccu}{ccuvarspat})) { + if (exists($hash->{hmccu}{ccuvarspat})) { HMCCU_GetVariables ($hash, $hash->{hmccu}{ccuvarspat}); - InternalTimer (gettimeofday ()+$hash->{hmccu}{ccuvarsint}, "HMCCU_UpdateVariables", $hash); + InternalTimer (gettimeofday ()+$hash->{hmccu}{ccuvarsint}, 'HMCCU_UpdateVariables', $hash); } } @@ -6854,33 +9178,33 @@ sub HMCCU_SetVariable ($$$$$) my ($hash, $varname, $value, $vartype, $params) = @_; my $name = $hash->{NAME}; - my $ccureqtimeout = AttrVal ($name, "ccuReqTimeout", $HMCCU_TIMEOUT_REQUEST); + my $ccureqtimeout = AttrVal ($name, 'ccuReqTimeout', $HMCCU_TIMEOUT_REQUEST); my %varfnc = ( - "bool" => "!CreateBoolVariable", "list", "!CreateListVariable", - "number" => "!CreateNumericVariable", "text", "!CreateStringVariable" + 'bool' => '!CreateBoolVariable', 'list' => '!CreateListVariable', + 'number' => '!CreateNumericVariable', 'text' => '!CreateStringVariable' ); - if (!defined ($vartype)) { + if (!defined($vartype)) { my $cmd = qq(dom.GetObject("$varname").State("$value")); - my $response = HMCCU_HMCommand ($hash, $cmd, 1); - return HMCCU_Log ($hash, 1, "CMD=$cmd", -2) if (!defined ($response)); + my $response = HMCCU_HMCommand ($hash, $cmd, 1) // + return HMCCU_Log ($hash, 1, "CMD=$cmd", -2); } else { - return -18 if (!exists ($varfnc{$vartype})); + return -18 if (!exists($varfnc{$vartype})); # Set default values for variable attributes - $params->{name} = $varname if (!exists ($params->{name})); - $params->{init} = $value if (!exists ($params->{init})); - $params->{unit} = "" if (!exists ($params->{unit})); - $params->{desc} = "" if (!exists ($params->{desc})); - $params->{min} = "0" if ($vartype eq 'number' && !exists ($params->{min})); - $params->{max} = "65000" if ($vartype eq 'number' && !exists ($params->{max})); - $params->{list} = $value if ($vartype eq 'list' && !exists ($params->{list})); - $params->{valtrue} = "ist wahr" if ($vartype eq 'bool' && !exists ($params->{valtrue})); - $params->{valfalse} = "ist falsch" if ($vartype eq 'bool' && !exists ($params->{valfalse})); + $params->{name} = $varname if (!exists ($params->{name})); + $params->{init} = $value if (!exists ($params->{init})); + $params->{unit} = '' if (!exists ($params->{unit})); + $params->{desc} = '' if (!exists ($params->{desc})); + $params->{min} = '0' if ($vartype eq 'number' && !exists ($params->{min})); + $params->{max} = '65000' if ($vartype eq 'number' && !exists ($params->{max})); + $params->{list} = $value if ($vartype eq 'list' && !exists ($params->{list})); + $params->{valtrue} = 'ist wahr' if ($vartype eq 'bool' && !exists ($params->{valtrue})); + $params->{valfalse} = 'ist falsch' if ($vartype eq 'bool' && !exists ($params->{valfalse})); - my $rc = HMCCU_HMScriptExt ($hash, $varfnc{$vartype}, $params, undef, undef); + my $rc = HMCCU_HMScriptExt ($hash, $varfnc{$vartype}, $params); return HMCCU_Log ($hash, 1, $rc, -2) if ($rc =~ /^ERROR:.*/); } @@ -6891,49 +9215,44 @@ sub HMCCU_SetVariable ($$$$$) # Update all datapoints / readings of device or channel considering # attribute ccureadingfilter. # Parameter $ccuget can be 'State', 'Value' or 'Attr'. -# Return 1 on success, <= 0 on error +# Return 1 on success, < 0 on error ###################################################################### -sub HMCCU_GetUpdate ($$$) +sub HMCCU_GetUpdate ($$;$$) { - my ($cl_hash, $addr, $ccuget) = @_; - my $name = $cl_hash->{NAME}; - my $type = $cl_hash->{TYPE}; - my $fnc = "GetUpdate"; + my ($clHash, $addr, $filter, $ccuget) = @_; + $filter //= '.*'; + $ccuget //= 'Value'; + my $name = $clHash->{NAME}; + my $type = $clHash->{TYPE}; - my $disable = AttrVal ($name, 'disable', 0); - return 1 if ($disable == 1); - - my $hmccu_hash = HMCCU_GetHash ($cl_hash); - return -3 if (!defined ($hmccu_hash)); - return -4 if ($type ne 'HMCCU' && $cl_hash->{ccudevstate} eq 'deleted'); + return 1 if (AttrVal ($name, 'disable', 0) == 1); + my $ioHash = HMCCU_GetHash ($clHash) // return -3; + return -4 if ($type ne 'HMCCU' && $clHash->{ccudevstate} eq 'deleted'); my $nam = ''; my $list = ''; my $script = ''; + $ccuget = HMCCU_GetAttribute ($ioHash, $clHash, 'ccuget', 'Value') if ($ccuget eq 'Attr'); - $ccuget = HMCCU_GetAttribute ($hmccu_hash, $cl_hash, 'ccuget', 'Value') if ($ccuget eq 'Attr'); - - if (HMCCU_IsValidChannel ($hmccu_hash, $addr, $HMCCU_FL_ADDRESS)) { - $nam = HMCCU_GetChannelName ($hmccu_hash, $addr, ''); + if (HMCCU_IsValidChannel ($ioHash, $addr, $HMCCU_FL_ADDRESS)) { + $nam = HMCCU_GetChannelName ($ioHash, $addr); return -1 if ($nam eq ''); my ($stadd, $stchn) = split (':', $addr); - my $stnam = HMCCU_GetChannelName ($hmccu_hash, "$stadd:0", ''); + my $stnam = HMCCU_GetChannelName ($ioHash, "$stadd:0"); $list = $stnam eq '' ? $nam : $stnam . "," . $nam; - $script = "!GetDatapointsByChannel"; + $script = '!GetDatapointsByChannel'; } - elsif (HMCCU_IsValidDevice ($hmccu_hash, $addr, $HMCCU_FL_ADDRESS)) { - $nam = HMCCU_GetDeviceName ($hmccu_hash, $addr, ''); + elsif (HMCCU_IsValidDevice ($ioHash, $addr, $HMCCU_FL_ADDRESS)) { + $nam = HMCCU_GetDeviceName ($ioHash, $addr); return -1 if ($nam eq ''); - $list = $nam if ($cl_hash->{ccuif} ne 'fhem'); - $script = "!GetDatapointsByDevice"; + $script = '!GetDatapointsByDevice'; # Consider members of group device - if ($type eq 'HMCCUDEV' && - ($cl_hash->{ccuif} eq 'VirtualDevices' || $cl_hash->{ccuif} eq 'fhem') && - exists ($cl_hash->{ccugroup})) { - foreach my $gd (split (",", $cl_hash->{ccugroup})) { - $nam = HMCCU_GetDeviceName ($hmccu_hash, $gd, ''); + if ($type eq 'HMCCUDEV' && $clHash->{ccuif} eq 'VirtualDevices' && HMCCU_IsFlag ($ioHash, 'updGroupMembers') && + exists($clHash->{ccugroup}) && $clHash->{ccugroup} ne '') { + foreach my $gd (split (',', $clHash->{ccugroup})) { + $nam = HMCCU_GetDeviceName ($ioHash, $gd); $list .= ','.$nam if ($nam ne ''); } } @@ -6942,60 +9261,21 @@ sub HMCCU_GetUpdate ($$$) return -1; } - if (HMCCU_IsFlag ($hmccu_hash->{NAME}, 'nonBlocking')) { - HMCCU_HMScriptExt ($hmccu_hash, $script, { list => $list, ccuget => $ccuget }, - \&HMCCU_UpdateCB, undef); - return 1; - } - else { - my $response = HMCCU_HMScriptExt ($hmccu_hash, $script, - { list => $list, ccuget => $ccuget }, undef, undef); - HMCCU_Trace ($cl_hash, 2, $fnc, "Addr=$addr Name=$nam Script=$script
". - "Script response = \n".$response); - return -2 if ($response eq '' || $response =~ /^ERROR:.*/); - - HMCCU_UpdateCB ({ ioHash => $hmccu_hash }, undef, $response); + if (HMCCU_IsFlag ($ioHash->{NAME}, 'nonBlocking')) { + # Non blocking request + HMCCU_HMScriptExt ($ioHash, $script, { list => $list, ccuget => $ccuget }, + \&HMCCU_UpdateCB, { filter => $filter }); return 1; } -# my @dpdef = split /\n/, $response; -# my $count = pop (@dpdef); -# return -10 if (!defined ($count) || $count == 0); -# -# my %events = (); -# foreach my $dp (@dpdef) { -# my ($chnname, $dpspec, $value) = split /=/, $dp; -# next if (!defined ($value)); -# my ($iface, $chnadd, $dpt) = split /\./, $dpspec; -# next if (!defined ($dpt)); -# my ($add, $chn) = ('', ''); -# if ($iface eq 'sysvar' && $chnadd eq 'link') { -# ($add, $chn) = HMCCU_GetAddress ($hmccu_hash, $chnname, '', ''); -# } -# else { -# ($add, $chn) = HMCCU_SplitChnAddr ($chnadd); -# } -# next if ($chn eq ''); -# $events{$add}{$chn}{$dpt} = $value; -# } - -# if ($cl_hash->{ccuif} eq 'fhem') { -# # Calculate datapoints of virtual group device -# if ($cl_hash->{ccutype} ne 'n/a') { -# foreach my $da (split (",", $cl_hash->{ccugroup})) { -# foreach my $cn (keys %{$events{$da}}) { -# foreach my $dp (keys %{$events{$da}{$cn}}) { -# if (defined ($events{$da}{$cn}{$dp})) { -# $events{$cl_hash->{ccuaddr}}{$cn}{$dp} = $events{$da}{$cn}{$dp} -# } -# } -# } -# } -# } -# } - -# HMCCU_UpdateMultipleDevices ($hmccu_hash, \%events); + # Blocking request + my $response = HMCCU_HMScriptExt ($ioHash, $script, + { list => $list, ccuget => $ccuget }); + HMCCU_Trace ($clHash, 2, "Addr=$addr Name=$nam Script=$script
". + "Script response = \n".$response); + return -2 if ($response eq '' || $response =~ /^ERROR:.*/); + HMCCU_UpdateCB ({ ioHash => $ioHash, filter => $filter }, undef, $response); return 1; } @@ -7011,14 +9291,14 @@ sub HMCCU_UpdateCB ($$$) { my ($param, $err, $data) = @_; - if (!exists ($param->{ioHash})) { - Log3 1, undef, "HMCCU: Missing parameter ioHash in update callback"; + if (!exists($param->{ioHash})) { + Log3 1, undef, 'HMCCU: Missing parameter ioHash in update callback'; return; } my $hash = $param->{ioHash}; - my $logcount = 0; - $logcount = 1 if (exists ($param->{logCount}) && $param->{logCount} == 1); + my $filter = $param->{filter} // '.*'; + my $logcount = exists($param->{logCount}) && $param->{logCount} == 1 ? 1 : 0; my $count = 0; my @dpdef = split /[\n\r]+/, $data; @@ -7029,109 +9309,54 @@ sub HMCCU_UpdateCB ($$$) my %events = (); foreach my $dp (@dpdef) { my ($chnname, $dpspec, $value) = split /=/, $dp; - next if (!defined ($value)); + next if (!defined($value)); my ($iface, $chnadd, $dpt) = split /\./, $dpspec; - next if (!defined ($dpt)); + next if (!defined($dpt) || $dpt !~ /$filter/); my ($add, $chn) = ('', ''); if ($iface eq 'sysvar' && $chnadd eq 'link') { - ($add, $chn) = HMCCU_GetAddress ($hash, $chnname, '', ''); + ($add, $chn) = HMCCU_GetAddress ($hash, $chnname); } else { ($add, $chn) = HMCCU_SplitChnAddr ($chnadd); } next if ($chn eq ''); - $events{$add}{$chn}{$dpt} = $value; + $events{$add}{$chn}{VALUES}{$dpt} = $value; } my $c_ok = HMCCU_UpdateMultipleDevices ($hash, \%events); - my $c_err = 0; - $c_err = HMCCU_Max($param->{devCount}-$c_ok, 0) if (exists ($param->{devCount})); + my $c_err = exists($param->{devCount}) ? HMCCU_Max($param->{devCount}-$c_ok, 0) : 0; HMCCU_Log ($hash, 2, "Update success=$c_ok failed=$c_err") if ($logcount); } ###################################################################### -# Get multiple datapoints of channels and update readings of client -# devices. -# Returncodes: -1 = Invalid channel/datapoint in list -# -2 = CCU script execution failed -# -3 = Cannot detect IO device -# On success number of updated readings is returned. +# Execute RPC request +# Parameters: +# $method - RPC request method. Use listParamset or listRawParamset +# as an alias for getParamset if readings should not be updated. +# $address - Device address. +# $paramset - paramset name: VALUE, MASTER, LINK, ... If not defined +# request does not affect a parameter set +# $parref - Hash reference with parameter/value pairs or array +# reference with parameter values (optional). +# $filter - Regular expression for filtering response (default = .*). +# Return (retCode, result). +# retCode = 0 - Success +# retCode < 0 - Error, result contains error message ###################################################################### -sub HMCCU_GetChannel ($$) +sub HMCCU_RPCRequest ($$$$;$$) { - my ($hash, $chnref) = @_; - my $name = $hash->{NAME}; - my $type = $hash->{TYPE}; - my $count = 0; - my $chnlist = ''; - my $result = ''; - my %chnpars; - my %objects = (); - my $objcount = 0; - - my $ccuget = AttrVal ($name, 'ccuget', 'Value'); - - # Build channel list. Datapoints and substitution rules are ignored. - foreach my $chndef (@$chnref) { - my ($channel, $substitute) = split /\s+/, $chndef; - next if (!defined ($channel) || $channel =~ /^#/ || $channel eq ''); - my ($int, $add, $chn, $dpt, $nam, $flags) = HMCCU_ParseObject ($hash, $channel, - $HMCCU_FLAG_INTERFACE | $HMCCU_FLAG_DATAPOINT); - if ($flags == $HMCCU_FLAGS_IACD || $flags == $HMCCU_FLAGS_NCD) { - $nam = HMCCU_GetChannelName ($hash, $add.':'.$chn, '') if ($flags == $HMCCU_FLAGS_IACD); - $chnlist = $chnlist eq '' ? $nam : $chnlist.','.$nam; - $chnpars{$nam}{sub} = $substitute; - $chnpars{$nam}{dpt} = $dpt; - } - else { - return (-1, $result); - } - } - - return (0, $result) if ($chnlist eq ''); - - my $response = HMCCU_HMScriptExt ($hash, "!GetChannel", - { list => $chnlist, ccuget => $ccuget }, undef, undef); - return (-2, $result) if ($response eq '' || $response =~ /^ERROR:.*/); - - # Output format is Channelname=Interface.Channeladdress.Datapoint=Value - foreach my $dpdef (split /[\n\r]+/, $response) { - my ($chnname, $dptaddr, $value) = split /=/, $dpdef; - next if (!defined ($value)); - my ($iface, $chnaddr, $dpt) = split /\./, $dptaddr; - next if (!defined ($dpt)); - my ($add, $chn) = HMCCU_SplitChnAddr ($chnaddr); - next if ($chn eq ''); - if (defined ($chnpars{$chnname}{dpt}) && $chnpars{$chnname}{dpt} ne '') { - next if ($dpt !~ $chnpars{$chnname}{dpt}); - } - - $value = HMCCU_Substitute ($value, $chnpars{$chnname}{sub}, 0, $chn, $dpt); - - $objects{$add}{$chn}{$dpt} = $value; - $objcount++; - } - - $count = HMCCU_UpdateMultipleDevices ($hash, \%objects) if ($objcount > 0); - - return ($count, $result); -} - -sub HMCCU_RPCRequest ($$$$$;$) -{ - my ($clHash, $method, $address, $key, $parref, $filter) = @_; + my ($clHash, $method, $address, $paramset, $parref, $filter) = @_; + $filter //= '.*'; my $name = $clHash->{NAME}; my $type = $clHash->{TYPE}; - my $fnc = "RPCRequest"; - my $reqMethod = $method eq 'listParamset' ? 'getParamset' : $method; - $filter = '.*' if (!defined ($filter)); + my $reqMethod = $method eq 'listParamset' || $method eq 'listRawParamset' || + $method eq 'getRawParamset' ? 'getParamset' : $method; my $addr = ''; my $result = ''; - my $ioHash = HMCCU_GetHash ($clHash); - return (-3, $result) if (!defined ($ioHash)); + my $ioHash = HMCCU_GetHash ($clHash) // return (-3, $result); return (-4, $result) if ($type ne 'HMCCU' && $clHash->{ccudevstate} eq 'deleted'); # Get flags and attributes @@ -7145,37 +9370,53 @@ sub HMCCU_RPCRequest ($$$$$;$) my ($int, $add, $chn, $dpt, $nam, $flags) = HMCCU_ParseObject ($ioHash, $address, $HMCCU_FLAG_FULLADDR); return (-1, $result) if (!($flags & $HMCCU_FLAG_ADDRESS)); - $addr = $add; - $addr .= ':'.$chn if ($flags & $HMCCU_FLAG_CHANNEL); + $addr = $flags & $HMCCU_FLAG_CHANNEL ? "$add:$chn" : $add; # Get RPC type and port for interface of device address my ($rpcType, $rpcPort) = HMCCU_GetRPCServerInfo ($ioHash, $int, 'type,port'); - return (-9, '') if (!defined ($rpcType) || !defined ($rpcPort)); + return (-9, '') if (!defined($rpcType) || !defined($rpcPort)); # Search RPC device, do not create one my ($rpcDevice, $save) = HMCCU_GetRPCDevice ($ioHash, 0, $int); return (-17, $result) if ($rpcDevice eq ''); my $rpcHash = $defs{$rpcDevice}; - # Build parameter array - my @parArray = ($addr, $key); - if (defined ($parref)) { - foreach my $k (keys %{$parref}) { push @parArray, "$k=$parref->{$k}"; }; + # Build parameter array: (Address, Paramset [, Parameter ...]) + # Paramset := VALUE | MASTER | LINK or any paramset supported by device + # Parameter := Name=Value[:Type] + my @parArray = ($addr); + push (@parArray, $paramset) if (defined($paramset)); + if (defined($parref)) { + if (ref($parref) eq 'HASH') { + foreach my $k (keys %{$parref}) { + my ($pv, $pt) = split (':', $parref->{$k}); + if (!defined($pt)) { + my $paramDef = HMCCU_GetParamDef ($ioHash, $addr, $paramset, $k); + $pt = defined($paramDef) && defined($paramDef->{TYPE}) && $paramDef->{TYPE} ne '' ? + $paramDef->{TYPE} : 'STRING'; + } + $pv .= ":$pt"; + push @parArray, "$k=$pv"; + } + } + elsif (ref($parref) eq 'ARRAY') { + push @parArray, @$parref; + } } # Submit RPC request - my $reqResult = HMCCURPCPROC_SendRequest ($rpcHash, $reqMethod, @parArray); - return (-5, "Function not available") if (!defined ($reqResult)); + my $reqResult = HMCCURPCPROC_SendRequest ($rpcHash, $reqMethod, @parArray) // + return (-5, 'RPC function not available'); - HMCCU_Trace ($clHash, 2, $fnc, - "Dump of RPC request $method $addr. Result type=".ref($reqResult)."
". + HMCCU_Trace ($clHash, 2, + "Dump of RPC request $method $paramset $addr. Result type=".ref($reqResult)."
". HMCCU_RefToString ($reqResult)); my $parCount = 0; - - if (ref ($reqResult) eq 'HASH') { - if (exists ($reqResult->{faultString})) { - HMCCU_Log ($rpcHash, 1, $reqResult->{faultString}); + if (ref($reqResult) eq 'HASH') { + if (exists($reqResult->{faultString})) { + HMCCU_Log ($rpcHash, 1, "Error in request $reqMethod ".join(' ', @parArray).': '. + $reqResult->{faultString}); return (-2, $reqResult->{faultString}); } else { @@ -7189,367 +9430,181 @@ sub HMCCU_RPCRequest ($$$$$;$) if ($method eq 'listParamset') { $result = join ("\n", map { $_ =~ /$filter/ ? $_.'='.$reqResult->{$_} : () } keys %$reqResult); } + elsif ($method eq 'listRawParamset' || $method eq 'getRawParamset') { + $result = $reqResult; + } + elsif ($method eq 'getDeviceDescription') { + $result = ''; + foreach my $k (sort keys %$reqResult) { + if (ref($reqResult->{$k}) eq 'ARRAY') { + $result .= "$k=".join(',', @{$reqResult->{$k}})."\n"; + } + else { + $result .= "$k=".$reqResult->{$k}."\n"; + } + } + } elsif ($method eq 'getParamsetDescription') { - my @operFlags = ('', 'R', 'W', 'RW', 'E', 'RE', 'WE', 'RWE'); + my %operFlags = ( 1 => 'R', 2 => 'W', 4 => 'E' ); $result = join ("\n", - map { $_.': '.$reqResult->{$_}->{TYPE}." [".$operFlags[$reqResult->{$_}->{OPERATIONS}]."]" } keys %$reqResult); + map { + $_.': '. + $reqResult->{$_}->{TYPE}. + " [".HMCCU_BitsToStr(\%operFlags,$reqResult->{$_}->{OPERATIONS})."]". + " FLAGS=".sprintf("%#b", $reqResult->{$_}->{FLAGS}). + " RANGE=".$reqResult->{$_}->{MIN}."-".$reqResult->{$_}->{MAX}. + " DFLT=".$reqResult->{$_}->{DEFAULT}. + " UNIT=".$reqResult->{$_}->{UNIT} + } sort keys %$reqResult); } - elsif ($method eq 'getParamset') { - readingsBeginUpdate ($clHash) if ($ccureadings); - - foreach my $key (sort keys %$reqResult) { - next if ($key !~ /$filter/); - my $value = $reqResult->{$key}; - $result .= "$key=$value\n"; - if ($ccureadings) { - $value = HMCCU_FormatReadingValue ($clHash, $value, $key); - $value = HMCCU_Substitute ($value, $substitute, 0, $chn, $key); - my @readings = HMCCU_GetReadingName ($clHash, $int, $add, $chn, $key, $nam, $readingformat); - foreach my $rn (@readings) { - next if ($rn eq ''); - $rn = "R-".$rn; - readingsBulkUpdate ($clHash, $rn, $value); - } - } - } - - readingsEndUpdate ($clHash, 1) if ($ccureadings); + else { + $result = $reqResult; } return (0, $result); } -###################################################################### -# Get RPC paramSet or paramSetDescription -###################################################################### - -sub HMCCU_RPCGetConfig ($$$$) -{ - my ($hash, $param, $mode, $filter) = @_; - my $name = $hash->{NAME}; - my $type = $hash->{TYPE}; - my $fnc = "RPCGetConfig"; - my $method = $mode eq 'listParamset' ? 'getParamset' : $mode; - - my $addr; - my $result = ''; - my $res = ''; - - my $hmccu_hash = HMCCU_GetHash ($hash); - return (-3, $result) if (!defined ($hmccu_hash)); - return (-4, $result) if ($type ne 'HMCCU' && $hash->{ccudevstate} eq 'deleted'); - - my $hmccuflags = HMCCU_GetFlags ($hmccu_hash->{NAME}); - my $ccuflags = HMCCU_GetFlags ($name); - my $ccureadings = AttrVal ($name, 'ccureadings', $ccuflags =~ /noReadings/ ? 0 : 1); - my $readingformat = HMCCU_GetAttrReadingFormat ($hash, $hmccu_hash); - my $substitute = HMCCU_GetAttrSubstitute ($hash, $hmccu_hash); - - my ($int, $add, $chn, $dpt, $nam, $flags) = HMCCU_ParseObject ($hmccu_hash, $param, - $HMCCU_FLAG_FULLADDR); - return (-1, '') if (!($flags & $HMCCU_FLAG_ADDRESS)); - $addr = $add; - $addr .= ':'.$chn if ($flags & $HMCCU_FLAG_CHANNEL); - - my ($rpctype, $port) = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'type,port'); - return (-9, '') if (!defined ($rpctype) || !defined ($port)); - - # Search RPC device - my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hmccu_hash, 0, $int); - return (-17, '') if ($rpcdev eq ''); - my $rpchash = $defs{$rpcdev}; - - if ($rpctype eq 'B') { - HMCCU_Trace ($hash, 2, $fnc, "Method=$method Addr=$addr Port=$port"); - if ($hmccuflags =~ /(extrpc|procrpc)/) { - $res = HMCCURPCPROC_SendRequest ($rpchash, $method, $BINRPC_STRING, $addr, - $BINRPC_STRING, "MASTER"); - } - } - else { - $res = HMCCURPCPROC_SendRequest ($rpchash, $method, "$addr:STRING", "MASTER:STRING"); - } - - return (-5, "Function not available") if (!defined ($res)); - - if (defined ($res)) { - HMCCU_Trace ($hash, 2, $fnc, - "Dump of RPC request $method $addr. Result type=".ref($res)."
". - HMCCU_RefToString ($res)); - } - - if (ref ($res) eq 'HASH') { - my $parcount = scalar (keys %$res); - if (exists ($res->{faultString})) { - Log3 $name, 1, "HMCCU: ".$res->{faultString}; - return (-2, $res->{faultString}); - } - elsif ($parcount == 0) { - return (-5, "CCU returned no data"); - } - } - else { - return (-2, defined ($RPC::XML::ERROR) ? $RPC::XML::ERROR : ''); - } - - if ($mode eq 'getParamsetDescription') { - foreach my $key (sort keys %$res) { - my $oper = ''; - $oper .= 'R' if ($res->{$key}->{OPERATIONS} & 1); - $oper .= 'W' if ($res->{$key}->{OPERATIONS} & 2); - $oper .= 'E' if ($res->{$key}->{OPERATIONS} & 4); - $result .= $key.": ".$res->{$key}->{TYPE}." [".$oper."]\n"; - } - } - elsif ($mode eq 'listParamset') { - foreach my $key (sort keys %$res) { - next if ($key !~ /$filter/); - my $value = $res->{$key}; - $result .= "$key=$value\n"; - } - } - else { - readingsBeginUpdate ($hash) if ($ccureadings); - - foreach my $key (sort keys %$res) { - next if ($key !~ /$filter/); - my $value = $res->{$key}; - $result .= "$key=$value\n"; - next if (!$ccureadings); - - $value = HMCCU_FormatReadingValue ($hash, $value, $key); - $value = HMCCU_Substitute ($value, $substitute, 0, $chn, $key); - my @readings = HMCCU_GetReadingName ($hash, $int, $add, $chn, $key, $nam, $readingformat); - foreach my $rn (@readings) { - next if ($rn eq ''); - $rn = "R-".$rn; - readingsBulkUpdate ($hash, $rn, $value); - } - } - - readingsEndUpdate ($hash, 1) if ($ccureadings); - } - - return (0, $result); -} - -###################################################################### -# Set RPC paramSet -###################################################################### - -sub HMCCU_RPCSetConfig ($$$) -{ - my ($hash, $param, $parref) = @_; - my $name = $hash->{NAME}; - my $type = $hash->{TYPE}; - - my $addr; - my $res; - - my $hmccu_hash = HMCCU_GetHash ($hash); - return -3 if (!defined ($hmccu_hash)); - return -4 if ($type ne 'HMCCU' && $hash->{ccudevstate} eq 'deleted'); - - my $hmccuflags = HMCCU_GetFlags ($hmccu_hash->{NAME}); - my $ccuflags = HMCCU_GetFlags ($name); - - my ($int, $add, $chn, $dpt, $nam, $flags) = HMCCU_ParseObject ($hmccu_hash, $param, - $HMCCU_FLAG_FULLADDR); - return -1 if (!($flags & $HMCCU_FLAG_ADDRESS)); - $addr = $add; - $addr .= ':'.$chn if ($flags & $HMCCU_FLAG_CHANNEL); - - my ($rpctype, $port) = HMCCU_GetRPCServerInfo ($hmccu_hash, $int, 'type,port'); - return -9 if (!defined ($rpctype) || !defined ($port)); - - if ($ccuflags =~ /trace/) { - my $ps = ''; - foreach my $p (keys %$parref) { - $ps .= ", ".$p."=".$parref->{$p}; - } - Log3 $name, 2, "HMCCU: RPCSetConfig: addr=$addr".$ps; - } - - # Search RPC device - my ($rpcdev, $save) = HMCCU_GetRPCDevice ($hmccu_hash, 0, $int); - return -17 if ($rpcdev eq ''); - my $rpchash = $defs{$rpcdev}; - - if ($rpctype eq 'B') { - # Rebuild parameter hash for binary encoding - my %binpar; - foreach my $e (keys %$parref) { - $binpar{$e}{T} = $BINRPC_STRING; - $binpar{$e}{V} = $parref->{$e}; - } - - if ($hmccuflags =~ /(extrpc|procrpc)/) { - $res = HMCCURPCPROC_SendRequest ($rpchash, "putParamset", $BINRPC_STRING, $addr, - $BINRPC_STRING, "MASTER", $BINRPC_STRUCT, \%binpar); - } - } - else { - $res = HMCCURPCPROC_SendRequest ($rpchash, "putParamset", $addr, "MASTER", $parref); - } - - return -5 if (! defined ($res)); - return HMCCU_Log ($hash, 1, "HMCCU: RPC request failed. ".$res->{faultString}, -2) - if (ref ($res) && exists ($res->{faultString})); - - return 0; -} - -###################################################################### -# *** FILEQUEUE FUNCTIONS *** -###################################################################### - -###################################################################### -# Open file queue -###################################################################### - -sub HMCCU_QueueOpen ($$) -{ - my ($hash, $queue_file) = @_; - - my $idx_file = $queue_file . '.idx'; - $queue_file .= '.dat'; - my $mode = '0666'; - - umask (0); - - $hash->{hmccu}{queue}{block_size} = 64; - $hash->{hmccu}{queue}{seperator} = "\n"; - $hash->{hmccu}{queue}{sep_length} = length $hash->{hmccu}{queue}{seperator}; - - $hash->{hmccu}{queue}{queue_file} = $queue_file; - $hash->{hmccu}{queue}{idx_file} = $idx_file; - - $hash->{hmccu}{queue}{queue} = new IO::File $queue_file, O_CREAT | O_RDWR, oct($mode) or return 0; - $hash->{hmccu}{queue}{idx} = new IO::File $idx_file, O_CREAT | O_RDWR, oct($mode) or return 0; - - ### Default ptr to 0, replace it with value in idx file if one exists - $hash->{hmccu}{queue}{idx}->sysseek(0, SEEK_SET); - $hash->{hmccu}{queue}{idx}->sysread($hash->{hmccu}{queue}{ptr}, 1024); - $hash->{hmccu}{queue}{ptr} = '0' unless $hash->{hmccu}{queue}{ptr}; - - if($hash->{hmccu}{queue}{ptr} > -s $queue_file) - { - $hash->{hmccu}{queue}{idx}->truncate(0) or return 0; - $hash->{hmccu}{queue}{idx}->sysseek(0, SEEK_SET); - $hash->{hmccu}{queue}{idx}->syswrite('0') or return 0; - } - - return 1; -} - -###################################################################### -# Close file queue -###################################################################### - -sub HMCCU_QueueClose ($) -{ - my ($hash) = @_; - - if (exists ($hash->{hmccu}{queue})) { - $hash->{hmccu}{queue}{idx}->close(); - $hash->{hmccu}{queue}{queue}->close(); - delete $hash->{hmccu}{queue}; - } -} - -sub HMCCU_QueueReset ($) -{ - my ($hash) = @_; - - $hash->{hmccu}{queue}{idx}->truncate(0) or return 0; - $hash->{hmccu}{queue}{idx}->sysseek(0, SEEK_SET); - $hash->{hmccu}{queue}{idx}->syswrite('0') or return 0; - - $hash->{hmccu}{queue}{queue}->sysseek($hash->{hmccu}{queue}{ptr} = 0, SEEK_SET); - - return 1; -} - -###################################################################### -# Put value in file queue -###################################################################### - -sub HMCCU_QueueEnq ($$) -{ - my ($hash, $element) = @_; - - return 0 if (!exists ($hash->{hmccu}{queue})); - - $hash->{hmccu}{queue}{queue}->sysseek(0, SEEK_END); - $element =~ s/$hash->{hmccu}{queue}{seperator}//g; - $hash->{hmccu}{queue}{queue}->syswrite($element.$hash->{hmccu}{queue}{seperator}) or return 0; - - return 1; -} - -###################################################################### -# Return next value in file queue -###################################################################### - -sub HMCCU_QueueDeq ($) -{ - my ($hash) = @_; - my $name = $hash->{NAME}; - my $sep_length = $hash->{hmccu}{queue}{sep_length}; - my $element = ''; - - return undef if (!exists ($hash->{hmccu}{queue})); - - $hash->{hmccu}{queue}{queue}->sysseek($hash->{hmccu}{queue}{ptr}, SEEK_SET); - - my $i; - while($hash->{hmccu}{queue}{queue}->sysread($_, $hash->{hmccu}{queue}{block_size})) { - $i = index($_, $hash->{hmccu}{queue}{seperator}); - if($i != -1) { - $element .= substr($_, 0, $i); - $hash->{hmccu}{queue}{ptr} += $i + $sep_length; - $hash->{hmccu}{queue}{queue}->sysseek($hash->{hmccu}{queue}{ptr}, SEEK_SET); - last; - } - else { - # Seperator not found, go back 'sep_length' spaces to ensure we don't miss it between reads - Log3 $name, 2, "HMCCU: HMCCU_QueueDeq seperator not found"; - $element .= substr($_, 0, -$sep_length, ''); - $hash->{hmccu}{queue}{ptr} += $hash->{hmccu}{queue}{block_size} - $sep_length; - $hash->{hmccu}{queue}{queue}->sysseek($hash->{hmccu}{queue}{ptr}, SEEK_SET); - } - } - - ## If queue seek pointer is at the EOF, truncate the queue file - if($hash->{hmccu}{queue}{queue}->sysread($_, 1) == 0) - { - $hash->{hmccu}{queue}{queue}->truncate(0) or return undef; - $hash->{hmccu}{queue}{queue}->sysseek($hash->{hmccu}{queue}{ptr} = 0, SEEK_SET); - } - - ## Set idx file contents to point to the current seek position in queue file - $hash->{hmccu}{queue}{idx}->truncate(0) or return undef; - $hash->{hmccu}{queue}{idx}->sysseek(0, SEEK_SET); - $hash->{hmccu}{queue}{idx}->syswrite($hash->{hmccu}{queue}{ptr}) or return undef; - - return ($element ne '') ? $element : undef; -} - ###################################################################### # *** HELPER FUNCTIONS *** ###################################################################### +sub HMCCU_SetIfDef { $_[0] = $_[1] if (defined($_[1])) } +sub HMCCU_SetIfEx { $_[0] = $_[1] if (exists($_[1])) } +sub HMCCU_SetVal { $_[0] = defined $_[1] && $_[1] ne '' ? $_[1] : $_[2] } + +###################################################################### +# Return Prefix.Value if value is defined. Otherwise default. +###################################################################### + +sub HMCCU_DefStr ($;$$) +{ + my ($v, $p, $d) = @_; + $p //= ''; + $d //= ''; + + return defined($v) && $v ne '' ? $p.$v : $d; +} + +###################################################################### +# Get unique array elements +###################################################################### + +sub HMCCU_Unique +{ + my %e; + return grep { !$e{$_}++ } @_; +} + +sub HMCCU_IsArrayElement +{ + my $e = shift; + return grep { $_ eq $e } @_; +} + +###################################################################### +# Convert string from ISO-8859-1 to UTF-8 +###################################################################### + +sub HMCCU_ISO2UTF ($) +{ + my ($t) = @_; + + return encode("UTF-8", decode("iso-8859-1", $t)); +} + +###################################################################### +# Check for floating point number +###################################################################### + +sub HMCCU_IsFltNum ($;$) +{ + my ($value, $flag) = @_; + $flag //= 0; + + return $flag ? + (defined($value) && $value =~ /^[+-]?\d*\.?\d+?$/ ? 1 : 0) : + (defined($value) && $value =~ /^[+-]?\d*\.?\d+(?:(?:e|E)\d+)?$/ ? 1 : 0); +} + +###################################################################### +# Check for integer number +###################################################################### + +sub HMCCU_IsIntNum ($) +{ + my ($value) = @_; + + return defined($value) && $value =~ /^[+-]?[0-9]+$/ ? 1 : 0; +} + +###################################################################### +# Get device state from maintenance channel 0 +# Update corresponding readings. +# Default is unknown for each reading +###################################################################### + +sub HMCCU_UpdateDeviceStates ($) +{ + my ($clHash) = @_; + + # Datapoints related to reading 'devstate' + my %stName = ( + '0.CONFIG_PENDING' => 'cfgPending', + '0.DEVICE_IN_BOOTLOADER' => 'boot', + '0.STICKY_UNREACH' => 'stickyUnreach', + '0.UPDATE_PENDING' => 'updPending', + ); + + # Datapoints to be converted to readings + my %newReadings = ( + '0.AES_KEY' => 'sign', + '0.RSSI_DEVICE' => 'rssidevice', + '0.RSSI_PEER' => 'rssipeer', + '0.LOW_BAT' => 'battery', + '0.LOWBAT' => 'battery', + '0.UNREACH' => 'activity' + ); + + # The new readings + my %readings = (); + + if (exists($clHash->{hmccu}{dp})) { + # Create the new readings + foreach my $dp (keys %newReadings) { + if (exists($clHash->{hmccu}{dp}{$dp}) && exists($clHash->{hmccu}{dp}{$dp}{VALUES})) { + if (exists($clHash->{hmccu}{dp}{$dp}{VALUES}{SVAL})) { + $readings{$newReadings{$dp}} = $clHash->{hmccu}{dp}{$dp}{VALUES}{SVAL}; + } + elsif (exists($clHash->{hmccu}{dp}{$dp}{VALUES}{VAL})) { + $readings{$newReadings{$dp}} = $clHash->{hmccu}{dp}{$dp}{VALUES}{VAL}; + } + } + } + + # Calculate the device state Reading + my @states = (); + foreach my $dp (keys %stName) { + push @states, $stName{$dp} if (exists($clHash->{hmccu}{dp}{$dp}) && + exists($clHash->{hmccu}{dp}{$dp}{VALUES}) && + defined($clHash->{hmccu}{dp}{$dp}{VALUES}{VAL}) && + $clHash->{hmccu}{dp}{$dp}{VALUES}{VAL} =~ /^(1|true)$/); + } + $readings{devstate} = scalar(@states) > 0 ? join(',', @states) : 'ok'; + + HMCCU_UpdateReadings ($clHash, \%readings, 2); + } +} + ###################################################################### # Determine HomeMatic state considering datapoint values specified # in attributes ccudef-hmstatevals and hmstatevals. # Return (reading, channel, datapoint, value) ###################################################################### -sub HMCCU_GetHMState ($$$) +sub HMCCU_GetHMState ($$;$) { my ($name, $ioname, $defval) = @_; my @hmstate = ('hmstate', undef, undef, $defval); - my $fnc = "GetHMState"; my $clhash = $defs{$name}; my $cltype = $clhash->{TYPE}; @@ -7571,14 +9626,11 @@ sub HMCCU_GetHMState ($$$) # Substitute variables $hmstatevals = HMCCU_SubstVariables ($clhash, $hmstatevals, undef); - my @rulelist = split (";", $hmstatevals); - foreach my $rule (@rulelist) { + foreach my $rule (split (';', $hmstatevals)) { my ($dptexpr, $subst) = split ('!', $rule, 2); my $dp = ''; next if (!defined ($dptexpr) || !defined ($subst)); - HMCCU_Trace ($clhash, 2, $fnc, "rule=$rule, dptexpr=$dptexpr, subst=$subst"); foreach my $d (keys %{$clhash->{hmccu}{dp}}) { - HMCCU_Trace ($clhash, 2, $fnc, "Check $d match $dptexpr"); if ($d =~ /$dptexpr/) { $dp = $d; last; @@ -7586,14 +9638,44 @@ sub HMCCU_GetHMState ($$$) } next if ($dp eq ''); my ($chn, $dpt) = split (/\./, $dp); - my $value = HMCCU_FormatReadingValue ($clhash, $clhash->{hmccu}{dp}{$dp}{VAL}, $hmstate[0]); - my ($rc, $newvalue) = HMCCU_SubstRule ($value, $subst, 0); - return ($hmstate[0], $chn, $dpt, $newvalue) if ($rc); + if (exists($clhash->{hmccu}{dp}{$dp}) && exists($clhash->{hmccu}{dp}{$dp}{VALUES}{VAL})) { + my $value = HMCCU_FormatReadingValue ($clhash, $clhash->{hmccu}{dp}{$dp}{VALUES}{VAL}, $hmstate[0]); + my ($rc, $newvalue) = HMCCU_SubstRule ($value, $subst, 0); + return ($hmstate[0], $chn, $dpt, $newvalue) if ($rc); + } } return @hmstate; } +###################################################################### +# Calculate time difference in seconds between current time and +# specified timestamp +###################################################################### + +sub HMCCU_GetTimeSpec ($) +{ + my ($ts) = @_; + + return $ts if (HMCCU_IsFltNum ($ts, 1)); + return -1 if ($ts !~ /^[0-9]{2}:[0-9]{2}$/ && $ts !~ /^[0-9]{2}:[0-9]{2}:[0-9]{2}$/); + + my (undef, $h, $m, $s) = GetTimeSpec ($ts); + return -1 if (!defined($h)); + + $s += $h*3600+$m*60; + my @lt = localtime; + my $cs = $lt[2]*3600+$lt[1]*60+$lt[0]; + $s += 86400 if ($cs > $s); + + return ($s-$cs); +} + +###################################################################### +# Get minimum or maximum of 2 values +# Align value with boundaries +###################################################################### + sub HMCCU_Min ($$) { my ($a, $b) = @_; @@ -7608,26 +9690,13 @@ sub HMCCU_Max ($$) return $a > $b ? $a : $b; } -###################################################################### -# Calculate time difference in seconds between current time and -# specified timestamp -###################################################################### - -sub HMCCU_GetTimeSpec ($) +sub HMCCU_MinMax ($$$) { - my ($ts) = @_; - - return -1 if ($ts !~ /^[0-9]{2}:[0-9]{2}$/ && $ts !~ /^[0-9]{2}:[0-9]{2}:[0-9]{2}$/); - - my (undef, $h, $m, $s) = GetTimeSpec ($ts); - return -1 if (!defined ($h)); - - $s += $h*3600+$m*60; - my @lt = localtime; - my $cs = $lt[2]*3600+$lt[1]*60+$lt[0]; - $s += 86400 if ($cs > $s); - - return ($s-$cs); + my ($v, $min, $max) = @_; + $min = $v if (!defined($min) || $min eq ''); + $max = $min if (!defined($max) || $max eq ''); + + return HMCCU_Max (HMCCU_Min ($v, $max), $min); } ###################################################################### @@ -7645,22 +9714,22 @@ sub HMCCU_BuildURL ($$) my $url = ''; my $username = ''; my $password = ''; - my ($erruser, $encuser) = getKeyValue ($name."_username"); - my ($errpass, $encpass) = getKeyValue ($name."_password"); - if (!defined ($erruser) && !defined ($errpass) && defined ($encuser) && defined ($encpass)) { + my ($erruser, $encuser) = getKeyValue ($name.'_username'); + my ($errpass, $encpass) = getKeyValue ($name.'_password'); + if (!defined($erruser) && !defined($errpass) && defined($encuser) && defined($encpass)) { $username = HMCCU_Decrypt ($encuser); $password = HMCCU_Decrypt ($encpass); } my $auth = ($username ne '' && $password ne '') ? "$username:$password".'@' : ''; if ($backend eq 'rega') { - $url = $hash->{prot}."://$auth".$hash->{host}.":". - $HMCCU_REGA_PORT{$hash->{prot}}."/tclrega.exe"; + $url = $hash->{prot}."://$auth".$hash->{host}.':'. + $HMCCU_REGA_PORT{$hash->{prot}}.'/tclrega.exe'; } else { ($url) = HMCCU_GetRPCServerInfo ($hash, $backend, 'url'); - if (defined ($url)) { - if (exists ($HMCCU_RPC_SSL{$backend})) { + if (defined($url)) { + if (exists($HMCCU_RPC_SSL{$backend})) { my $p = $hash->{prot} eq 'https' ? '4' : ''; $url =~ s/^http:\/\//$hash->{prot}:\/\/$auth/; $url =~ s/:([0-9]+)/:$p$1/; @@ -7679,7 +9748,7 @@ sub HMCCU_BuildURL ($$) # Calculate special readings. Requires hash of client device, channel # number and datapoint. Supported functions: # dewpoint, absolute humidity, increasing/decreasing counters, -# minimum/maximum, average, sum. +# minimum/maximum, average, sum, set. # Return readings array with reading/value pairs. ###################################################################### @@ -7687,26 +9756,27 @@ sub HMCCU_CalculateReading ($$) { my ($cl_hash, $chkeys) = @_; my $name = $cl_hash->{NAME}; - my $fnc = "HMCCU_CalculateReading"; + my %parCount = ('dewpoint' => 2, 'abshumidity' => 2, 'equ' => 1, + 'max' => 1, 'min' => 1, 'inc' => 1, 'dec' => 1, 'avg' => 1, + 'sum' => 1, 'or' => 1, 'and' => 1, 'set' => 1); my @result = (); my $ccucalculate = AttrVal ($name, 'ccucalculate', ''); return @result if ($ccucalculate eq ''); - my @calclist = split (/[;\n]+/, $ccucalculate); - foreach my $calculation (@calclist) { + foreach my $calculation (split (/[;\n]+/, $ccucalculate)) { my ($vt, $rn, $dpts) = split (':', $calculation, 3); - next if (!defined ($dpts)); + if (!defined($dpts) || !exists($parCount{$vt})) { + HMCCU_Log ($cl_hash, 2, "Error in reading calculation expression $calculation. Ignored."); + next; + } my $tmpdpts = ",$dpts,"; $tmpdpts =~ s/[\$\%\{\}]+//g; - HMCCU_Trace ($cl_hash, 2, $fnc, "vt=$vt, rn=$rn, dpts=$dpts, tmpdpts=$tmpdpts"); + HMCCU_Trace ($cl_hash, 2, "vt=$vt, rn=$rn, dpts=$dpts, tmpdpts=$tmpdpts"); my $f = 0; foreach my $chkey (@$chkeys) { - if ($tmpdpts =~ /,$chkey,/) { - $f = 1; - last; - } + if ($tmpdpts =~ /,$chkey,/) { $f = 1; last; } } next if ($f == 0); my @dplist = split (',', $dpts); @@ -7715,30 +9785,21 @@ sub HMCCU_CalculateReading ($$) my $newdpts = HMCCU_SubstVariables ($cl_hash, $dpts, undef); my @pars = split (',', $newdpts); my $pc = scalar (@pars); - next if ($pc != scalar(@dplist)); + if ($pc != scalar(@dplist) || $pc < $parCount{$vt}) { + HMCCU_Log ($cl_hash, 2, "Wrong number of parameters in reading calculation expression $calculation"); + next; + } $f = 0; for (my $i=0; $i<$pc; $i++) { $pars[$i] =~ s/^#//; - if ($pars[$i] eq $dplist[$i]) { - $f = 1; - last; - } + if ($pars[$i] eq $dplist[$i]) { $f = 1; last; } } next if ($f); if ($vt eq 'dewpoint' || $vt eq 'abshumidity') { # Dewpoint and absolute humidity - next if ($pc < 2); my ($tmp, $hum) = @pars; - if ($tmp >= 0.0) { - $a = 7.5; - $b = 237.3; - } - else { - $a = 7.6; - $b = 240.7; - } - + my ($a, $b) = $tmp >= 0.0 ? (7.5, 237.3) : (7.6, 240.7); my $sdd = 6.1078*(10.0**(($a*$tmp)/($b+$tmp))); my $dd = $hum/100.0*$sdd; if ($dd != 0.0) { @@ -7755,7 +9816,6 @@ sub HMCCU_CalculateReading ($$) } elsif ($vt eq 'equ') { # Set reading to value if all variables have the same value - next if ($pc < 1); my $curval = shift @pars; my $f = 1; foreach my $newval (@pars) { @@ -7765,7 +9825,6 @@ sub HMCCU_CalculateReading ($$) } elsif ($vt eq 'min' || $vt eq 'max') { # Minimum or maximum values - next if ($pc < 1); my $curval = $pc > 1 ? shift @pars : ReadingsVal ($name, $rn, 0); foreach my $newval (@pars) { $curval = $newval if ($vt eq 'min' && $newval < $curval); @@ -7775,7 +9834,6 @@ sub HMCCU_CalculateReading ($$) } elsif ($vt eq 'inc' || $vt eq 'dec') { # Increasing or decreasing values without reset - next if ($pc < 1); my $newval = shift @pars; my $oldval = ReadingsVal ($name, $rn."_old", 0); my $curval = ReadingsVal ($name, $rn, 0); @@ -7788,7 +9846,6 @@ sub HMCCU_CalculateReading ($$) } elsif ($vt eq 'avg') { # Average value - next if ($pc < 1); if ($pc == 1) { my $newval = shift @pars; my $cnt = ReadingsVal ($name, $rn."_cnt", 0); @@ -7806,31 +9863,26 @@ sub HMCCU_CalculateReading ($$) } elsif ($vt eq 'sum') { # Sum of values - next if ($pc < 1); my $curval = $pc > 1 ? 0 : ReadingsVal ($name, $rn, 0); - foreach my $newval (@pars) { - $curval += $newval; - } + foreach my $newval (@pars) { $curval += $newval; } push (@result, $rn, $curval); } elsif ($vt eq 'or') { # Logical OR - next if ($pc < 1); my $curval = $pc > 1 ? 0 : ReadingsVal ($name, $rn, 0); - foreach my $newval (@pars) { - $curval |= $newval; - } + foreach my $newval (@pars) { $curval |= $newval; } push (@result, $rn, $curval); } elsif ($vt eq 'and') { # Logical AND - next if ($pc < 1); my $curval = $pc > 1 ? 1 : ReadingsVal ($name, $rn, 1); - foreach my $newval (@pars) { - $curval &= $newval; - } + foreach my $newval (@pars) { $curval &= $newval; } push (@result, $rn, $curval); } + elsif ($vt eq 'set') { + # Set reading to value + push (@result, $rn, join('', @pars)); + } } return @result; @@ -7843,12 +9895,11 @@ sub HMCCU_CalculateReading ($$) sub HMCCU_Encrypt ($) { my ($istr) = @_; - my $ostr = ''; - my $id = getUniqueId(); - return '' if (!defined ($id) || $id eq ''); + my $id = getUniqueId() // return ''; my $key = $id; + my $ostr = ''; foreach my $c (split //, $istr) { my $k = chop($key); if ($k eq '') { @@ -7868,13 +9919,12 @@ sub HMCCU_Encrypt ($) sub HMCCU_Decrypt ($) { my ($istr) = @_; - my $ostr = ''; - my $id = getUniqueId(); - return '' if (!defined ($id) || $id eq ''); + my $id = getUniqueId() // return ''; my $key = $id; - for my $c (map { pack('C', hex($_)) } ($istr =~ /(..)/g)) { + my $ostr = ''; + foreach my $c (map { pack('C', hex($_)) } ($istr =~ /(..)/g)) { my $k = chop($key); if ($k eq '') { $key = $id; @@ -7895,11 +9945,35 @@ sub HMCCU_Decrypt ($) sub HMCCU_DeleteReadings ($$) { my ($hash, $rnexp) = @_; - - $rnexp = '.*' if (!defined ($rnexp)); + $rnexp //= '.*'; + my @readlist = keys %{$hash->{READINGS}}; foreach my $rd (@readlist) { - delete ($hash->{READINGS}{$rd}) if ($rd ne 'state' && $rd ne 'control' && $rd =~ /$rnexp/); + readingsDelete ($hash, $rd) if ($rd ne 'state' && $rd ne 'control' && $rd =~ /$rnexp/); + } +} + +###################################################################### +# Update readings from hash +# If flag & 1, consider reading update attributes +# If flag & 2, skip Begin/End Update +###################################################################### + +sub HMCCU_UpdateReadings ($$;$) +{ + my ($hash, $readings, $flag) = @_; + $flag //= 1; + my $name = $hash->{NAME}; + + my $ccureadings = $flag & 1 ? + AttrVal ($name, 'ccureadings', HMCCU_IsFlag ($name, 'noReadings') ? 0 : 1) : 1; + + if ($ccureadings) { + readingsBeginUpdate ($hash) if (!($flag & 2)); + foreach my $rn (keys %{$readings}) { + readingsBulkUpdate ($hash, $rn, $readings->{$rn}); + } + readingsEndUpdate ($hash, 1) if (!($flag & 2)); } } @@ -7908,7 +9982,7 @@ sub HMCCU_DeleteReadings ($$) # # Parameters: # -# msg := parameter=value[,...] +# msg := parameter:value[,...] # # text1-3=Text # icon1-3=IconName @@ -7922,10 +9996,8 @@ sub HMCCU_DeleteReadings ($$) sub HMCCU_EncodeEPDisplay ($) { - my ($msg) = @_; - - # set defaults - $msg = '' if (!defined ($msg)); + my ($msg) = @_; + $msg //= ''; my %disp_icons = ( ico_off => '0x80', ico_on => '0x81', ico_open => '0x82', ico_closed => '0x83', @@ -7943,22 +10015,19 @@ sub HMCCU_EncodeEPDisplay ($) sig_off => '0xF0', sig_red => '0xF1', sig_green => '0xF2', sig_orange => '0xF3' ); + my %conf = ( + sound => 'snd_off', signal => 'sig_off', repeat => 1, pause => 10 + ); + # Parse command string my @text = ('', '', ''); my @icon = ('', '', ''); - my %conf = (sound => 'snd_off', signal => 'sig_off', repeat => 1, pause => 10); foreach my $tok (split (',', $msg)) { - my ($par, $val) = split ('=', $tok); - next if (!defined ($val)); - if ($par =~ /^text([1-3])$/) { - $text[$1-1] = substr ($val, 0, 12); - } - elsif ($par =~ /^icon([1-3])$/) { - $icon[$1-1] = $val; - } - elsif ($par =~ /^(sound|pause|repeat|signal)$/) { - $conf{$1} = $val; - } + my ($par, $val) = split (':', $tok); + next if (!defined($val)); + if ($par =~ /^text([1-3])$/) { $text[$1-1] = substr ($val, 0, 12); } + elsif ($par =~ /^icon([1-3])$/) { $icon[$1-1] = $val; } + elsif ($par =~ /^(sound|pause|repeat|signal)$/) { $conf{$1} = $val; } } my $cmd = '0x02,0x0A'; @@ -7968,7 +10037,7 @@ sub HMCCU_EncodeEPDisplay ($) $cmd .= ',0x12'; # Hex code - if ($text[$c] =~ /^0x[0-9A-F]{2}$/) { + if ($text[$c] =~ /^0x[0-9A-F]{2}$/i) { $cmd .= ','.$text[$c]; } # Predefined text code #0-9 @@ -7978,13 +10047,11 @@ sub HMCCU_EncodeEPDisplay ($) # Convert string to hex codes else { $text[$c] =~ s/\\_/ /g; - foreach my $ch (split ('', $text[$c])) { - $cmd .= sprintf (",0x%02X", ord ($ch)); - } + foreach my $ch (split ('', $text[$c])) { $cmd .= sprintf (",0x%02X", ord($ch)); } } # Icon - if ($icon[$c] ne '' && exists ($disp_icons{$icon[$c]})) { + if ($icon[$c] ne '' && exists($disp_icons{$icon[$c]})) { $cmd .= ',0x13,'.$disp_icons{$icon[$c]}; } } @@ -7994,30 +10061,20 @@ sub HMCCU_EncodeEPDisplay ($) # Sound my $snd = $disp_sounds{snd_off}; - $snd = $disp_sounds{$conf{sound}} if (exists ($disp_sounds{$conf{sound}})); + $snd = $disp_sounds{$conf{sound}} if (exists($disp_sounds{$conf{sound}})); $cmd .= ',0x14,'.$snd.',0x1C'; # Repeat - my $rep = $conf{repeat} if ($conf{repeat} >= 0 && $conf{repeat} <= 15); - $rep = 1 if ($rep < 0); - $rep = 15 if ($rep > 15); - if ($rep == 0) { - $cmd .= ',0xDF'; - } - else { - $cmd .= sprintf (",0x%02X", 0xD0+$rep-1); - } + my $rep = HMCCU_AdjustValue ($conf{repeat}, 0, 15); + $cmd .= $rep == 0 ? ',0xDF' : sprintf (",0x%02X", 0xD0+$rep-1); $cmd .= ',0x1D'; # Pause - my $pause = $conf{pause}; - $pause = 1 if ($pause < 1); - $pause = 160 if ($pause > 160); - $cmd .= sprintf (",0xE%1X,0x16", int(($pause-1)/10)); + $cmd .= sprintf (",0xE%1X,0x16", int((HMCCU_AdjustValue ($conf{pause}, 1, 160)-1)/10)); # Signal my $sig = $disp_signals{sig_off}; - $sig = $disp_signals{$conf{signal}} if (exists ($disp_signals{$conf{signal}})); + $sig = $disp_signals{$conf{signal}} if(exists($disp_signals{$conf{signal}})); $cmd .= ','.$sig.',0x03'; return $cmd; @@ -8028,49 +10085,69 @@ sub HMCCU_EncodeEPDisplay ($) # Supports reference to ARRAY, HASH and SCALAR and scalar values. ###################################################################### -sub HMCCU_RefToString ($) +sub HMCCU_RefToString ($;$) { - my ($r) = @_; - - my $result = ''; - - if (ref ($r) eq 'ARRAY') { - $result .= "[\n"; - foreach my $e (@$r) { - $result .= "," if ($result ne '['); - $result .= HMCCU_RefToString ($e); - } - $result .= "\n]"; + my ($r, $l) = @_; + $r //= ''; + $l //= 0; + my $s1 = ' ' x ($l*2); + my $s2 = ' ' x (($l+1)*2); + + if (ref($r) eq 'ARRAY') { + my $result = "[\n"; + $result .= join (",\n", map { $s2.HMCCU_RefToString($_, $l+1) } @$r); + return "$result\n$s1]"; } - elsif (ref ($r) eq 'HASH') { - $result .= "{\n"; - foreach my $k (sort keys %$r) { - $result .= "," if ($result ne '{'); - $result .= "$k=".HMCCU_RefToString ($r->{$k}); - } - $result .= "\n}"; + elsif (ref($r) eq 'HASH') { + my $result .= "{\n"; + $result .= join (",\n", map { $s2."$_=".HMCCU_RefToString($r->{$_}, $l+1) } sort keys %$r); + return "$result\n$s1}"; } - elsif (ref ($r) eq 'SCALAR') { - $result .= $$r; + elsif (ref($r) eq 'SCALAR') { + return $$r; } else { - $result .= $r; + return $r; } +} + +###################################################################### +# Convert bitmask to string +###################################################################### + +sub HMCCU_BitsToStr ($$) +{ + my ($chrMap, $bMask) = @_; - return $result; + my $r = ''; + foreach my $bVal (sort keys %$chrMap) { $r .= $chrMap->{$bVal} if ($bMask & $bVal); } + return $r; +} + +###################################################################### +# Ensure that value is within range low-high +###################################################################### + +sub HMCCU_AdjustValue ($$$) +{ + my ($value, $low, $high); + + return $low if ($value < $low); + return $high if ($value > $high); + return $value; } ###################################################################### # Match string with regular expression considering illegal regular -# expressions. Return parameter e if regular expression is incorrect. +# expressions. +# Return parameter e if regular expression is incorrect. ###################################################################### sub HMCCU_ExprMatch ($$$) { my ($t, $r, $e) = @_; - my $x = eval { $t =~ /$r/ }; - return $e if (!defined ($x)); + my $x = eval { $t =~ /$r/ } // $e; return "$x" eq '' ? 0 : 1; } @@ -8078,8 +10155,7 @@ sub HMCCU_ExprNotMatch ($$$) { my ($t, $r, $e) = @_; - my $x = eval { $t !~ /$r/ }; - return $e if (!defined ($x)); + my $x = eval { $t !~ /$r/ } // $e; return "$x" eq '' ? 0 : 1; } @@ -8090,40 +10166,31 @@ sub HMCCU_ExprNotMatch ($$$) sub HMCCU_GetDutyCycle ($) { my ($hash) = @_; - my $name = $hash->{NAME}; my $dc = 0; - my @rpcports = HMCCU_GetRPCPortList ($hash); + my $interfaces = HMCCU_GetRPCInterfaceList ($hash); + my %readings; - readingsBeginUpdate ($hash); - - foreach my $port (@rpcports) { + foreach my $port (values %$interfaces) { next if ($port != 2001 && $port != 2010); - my $url = HMCCU_BuildURL ($hash, $port); - next if (!defined ($url)); + my $url = HMCCU_BuildURL ($hash, $port) // next; my $rpcclient = RPC::XML::Client->new ($url); - my $response = $rpcclient->simple_request ("listBidcosInterfaces"); - next if (!defined ($response) || ref($response) ne 'ARRAY'); + my $response = $rpcclient->simple_request ('listBidcosInterfaces'); + next if (!defined($response) || ref($response) ne 'ARRAY'); foreach my $iface (@$response) { - next if (ref ($iface) ne 'HASH'); - next if (!exists ($iface->{DUTY_CYCLE})); + next if (ref($iface) ne 'HASH' || !exists($iface->{DUTY_CYCLE})); $dc++; - my $type; - if (exists ($iface->{TYPE})) { - $type = $iface->{TYPE} - } - else { - ($type) = HMCCU_GetRPCServerInfo ($hash, $port, 'name'); - } - readingsBulkUpdate ($hash, "iface_addr_$dc", $iface->{ADDRESS}); - readingsBulkUpdate ($hash, "iface_conn_$dc", $iface->{CONNECTED}); - readingsBulkUpdate ($hash, "iface_type_$dc", $type); - readingsBulkUpdate ($hash, "iface_ducy_$dc", $iface->{DUTY_CYCLE}); + my ($type) = exists($iface->{TYPE}) ? + ($iface->{TYPE}) : HMCCU_GetRPCServerInfo ($hash, $port, 'name'); + $readings{"iface_addr_$dc"} = $iface->{ADDRESS}; + $readings{"iface_conn_$dc"} = $iface->{CONNECTED}; + $readings{"iface_type_$dc"} = $type; + $readings{"iface_ducy_$dc"} = $iface->{DUTY_CYCLE}; } } - readingsEndUpdate ($hash, 1); - + HMCCU_UpdateReadings ($hash, \%readings); + return $dc; } @@ -8139,9 +10206,9 @@ sub HMCCU_TCPPing ($$$) if ($timeout > 0) { my $t = time (); - while (time () < $t+$timeout) { - return 1 if (HMCCU_TCPConnect ($addr, $port) ne ''); - sleep (20); + while (time() < $t+$timeout) { + return 1 if (HMCCU_TCPConnect ($addr, $port, 1) ne ''); + sleep ($HMCCU_CCU_PING_SLEEP); } return 0; @@ -8156,15 +10223,15 @@ sub HMCCU_TCPPing ($$$) # Return empty string on error or local IP address on success. ###################################################################### -sub HMCCU_TCPConnect ($$) +sub HMCCU_TCPConnect ($$;$) { - my ($addr, $port) = @_; + my ($addr, $port, $timeout) = @_; - my $socket = IO::Socket::INET->new (PeerAddr => $addr, PeerPort => $port); + my $socket = IO::Socket::INET->new (PeerAddr => $addr, PeerPort => $port, Timeout => $timeout); if ($socket) { my $ipaddr = $socket->sockhost (); close ($socket); - return $ipaddr if (defined ($ipaddr)); + return $ipaddr if (defined($ipaddr)); } return ''; @@ -8177,30 +10244,32 @@ sub HMCCU_TCPConnect ($$) sub HMCCU_GetIdFromIP ($$) { my ($ip, $default) = @_; + return $default if (!defined($ip)); - my @ipseg = split (/\./, $ip); - if (scalar (@ipseg) == 4) { - return sprintf ("%03d%03d", $ipseg[2], $ipseg[3]); + if ($ip =~ /:[0-9]{1,4}$/) { + # Looks like an IPv6 address + $ip =~ s/://g; + my $ip1 = int(hex('0x'.substr($ip,-4))/256) // 0; + my $ip2 = hex('0x'.substr($ip,-4))%256 // 0; + return $ip1 > 0 || $ip2 > 0 ? sprintf("%03d%03d", $ip1, $ip2) : $default; } else { - return $default; + my @ipseg = split (/\./, $ip); + return scalar(@ipseg) == 4 ? sprintf ("%03d%03d", $ipseg[2], $ipseg[3]) : $default; } } ###################################################################### # Resolve hostname. -# Return value defip if hostname can't be resolved. +# Return value defIP if hostname can't be resolved. ###################################################################### sub HMCCU_ResolveName ($$) { - my ($hname, $defip) = @_; + my ($host, $defIP) = @_; - my $ip = $defip; - my $addrnum = inet_aton ($hname); - $ip = inet_ntoa ($addrnum) if (defined ($addrnum)); - - return $ip; + my $addrNum = inet_aton ($host); + return defined($addrNum) ? inet_ntoa ($addrNum) : $defIP; } ###################################################################### @@ -8211,6 +10280,7 @@ sub HMCCU_ResolveName ($$) sub HMCCU_CorrectName ($) { my ($rn) = @_; + $rn =~ s/\:/\./g; $rn =~ s/[^A-Za-z\d_\.-]+/_/g; return $rn; @@ -8247,319 +10317,6 @@ sub HMCCU_MaxHashEntries ($$) return \%result; } -###################################################################### -# *** SUBPROCESS PART *** -###################################################################### - -# Child process. Must be global to allow access by RPC callbacks -my $hmccu_child; - -# Queue file -my %child_queue; -my $cpqueue = \%child_queue; - -# Statistic data of child process -my %child_hash = ( - "total", 0, - "writeerror", 0, - "EV", 0, - "ND", 0, - "DD", 0, - "RD", 0, - "RA", 0, - "UD", 0, - "IN", 0, - "EX", 0, - "SL", 0 -); -my $cphash = \%child_hash; - - -###################################################################### -# Subprocess: Write event to parent process -###################################################################### - -sub HMCCU_CCURPC_Write ($$) -{ - my ($et, $msg) = @_; - my $name = $hmccu_child->{devname}; - - $cphash->{total}++; - $cphash->{$et}++; - - HMCCU_QueueEnq ($cpqueue, $et."|".$msg); -} - -###################################################################### -# Subprocess: Initialize RPC server. Return 1 on success. -###################################################################### - -sub HMCCU_CCURPC_OnRun ($) -{ - $hmccu_child = shift; - my $name = $hmccu_child->{devname}; - my $serveraddr = $hmccu_child->{serveraddr}; - my $serverport = $hmccu_child->{serverport}; - my $callbackport = $hmccu_child->{callbackport}; - my $queuefile = $hmccu_child->{queue}; - my $clkey = "CB".$serverport; - my $ccurpc_server; - - # Create, open and reset queue file - Log3 $name, 0, "CCURPC: $clkey Creating file queue $queuefile"; - if (!HMCCU_QueueOpen ($cpqueue, $queuefile)) { - Log3 $name, 0, "CCURPC: $clkey Can't create queue"; - return 0; - } - - # Reset event queue - HMCCU_QueueReset ($cpqueue); - while (defined (HMCCU_QueueDeq ($cpqueue))) { } - - # Create RPC server - Log3 $name, 0, "CCURPC: Initializing RPC server $clkey"; - $ccurpc_server = RPC::XML::Server->new (port=>$callbackport); - if (!ref($ccurpc_server)) - { - Log3 $name, 0, "CCURPC: Can't create RPC callback server on port $callbackport. Port in use?"; - return 0; - } - else { - Log3 $name, 0, "CCURPC: Callback server created listening on port $callbackport"; - } - - # Format of signature: - # string par1 ... parN - - # Callback for events - # Parameters: Server, InterfaceId, Address, ValueKey, Value - Log3 $name, 1, "CCURPC: $clkey Adding callback for events"; - $ccurpc_server->add_method ( - { name=>"event", - signature=> ["string string string string string","string string string string int","string string string string double","string string string string boolean","string string string string i4"], - code=>\&HMCCU_CCURPC_EventCB - } - ); - - # Callback for new devices - # Parameters: Server, InterfaceId, DeviceDescriptions[] - Log3 $name, 1, "CCURPC: $clkey Adding callback for new devices"; - $ccurpc_server->add_method ( - { name=>"newDevices", - signature=>["string string array"], - code=>\&HMCCU_CCURPC_NewDevicesCB - } - ); - - # Callback for deleted devices - # Parameters: Server, InterfaceId, Addresses[] - Log3 $name, 1, "CCURPC: $clkey Adding callback for deleted devices"; - $ccurpc_server->add_method ( - { name=>"deleteDevices", - signature=>["string string array"], - code=>\&HMCCU_CCURPC_DeleteDevicesCB - } - ); - - # Callback for modified devices - # Parameters: Server, InterfaceId, Address, Hint - Log3 $name, 1, "CCURPC: $clkey Adding callback for modified devices"; - $ccurpc_server->add_method ( - { name=>"updateDevice", - signature=>["string string string int"], - code=>\&HMCCU_CCURPC_UpdateDeviceCB - } - ); - - # Callback for replaced devices - # Parameters: Server, InterfaceId, OldAddress, NewAddress - Log3 $name, 1, "CCURPC: $clkey Adding callback for replaced devices"; - $ccurpc_server->add_method ( - { name=>"replaceDevice", - signature=>["string string string string"], - code=>\&HMCCU_CCURPC_ReplaceDeviceCB - } - ); - - # Callback for readded devices - # Parameters: Server, InterfaceId, Addresses[] - Log3 $name, 1, "CCURPC: $clkey Adding callback for readded devices"; - $ccurpc_server->add_method ( - { name=>"replaceDevice", - signature=>["string string array"], - code=>\&HMCCU_CCURPC_ReaddDeviceCB - } - ); - - # Dummy implementation, always return an empty array - # Parameters: Server, InterfaceId - Log3 $name, 1, "CCURPC: $clkey Adding callback for list devices"; - $ccurpc_server->add_method ( - { name=>"listDevices", - signature=>["string string"], - code=>\&HMCCU_CCURPC_ListDevicesCB - } - ); - - # Enter server loop - HMCCU_CCURPC_Write ("SL", "$$|$clkey"); - - Log3 $name, 0, "CCURPC: $clkey Entering server loop"; - $ccurpc_server->server_loop; - Log3 $name, 0, "CCURPC: $clkey Server loop terminated"; - - # Server loop exited by SIGINT - HMCCU_CCURPC_Write ("EX", "SHUTDOWN|$$|$clkey"); - - return 1; -} - -###################################################################### -# Subprocess: Called when RPC server loop is terminated -###################################################################### - -sub HMCCU_CCURPC_OnExit () -{ - # Output statistics - foreach my $et (sort keys %child_hash) { - Log3 $hmccu_child->{devname}, 2, "CCURPC: Eventcount $et = ".$cphash->{$et}; - } -} - -###################################################################### -# Subprocess: Callback for new devices -###################################################################### - -sub HMCCU_CCURPC_NewDevicesCB ($$$) -{ - my ($server, $cb, $a) = @_; - my $devcount = scalar (@$a); - my $name = $hmccu_child->{devname}; - my $c = 0; - my $msg = ''; - - Log3 $name, 2, "CCURPC: $cb NewDevice received $devcount device specifications"; - foreach my $dev (@$a) { - my $msg = ''; - if ($dev->{ADDRESS} =~ /:[0-9]{1,2}$/) { - $msg = "C|".$dev->{ADDRESS}."|".$dev->{TYPE}."|".$dev->{VERSION}."|null|null"; - } - else { - # Wired devices do not have a RX_MODE attribute - my $rx = exists ($dev->{RX_MODE}) ? $dev->{RX_MODE} : 'null'; - $msg = "D|".$dev->{ADDRESS}."|".$dev->{TYPE}."|".$dev->{VERSION}."|". - $dev->{FIRMWARE}."|".$rx; - } - HMCCU_CCURPC_Write ("ND", $msg); - } - - return; -} - -###################################################################### -# Subprocess: Callback for deleted devices -###################################################################### - -sub HMCCU_CCURPC_DeleteDevicesCB ($$$) -{ - my ($server, $cb, $a) = @_; - my $name = $hmccu_child->{devname}; - my $devcount = scalar (@$a); - - Log3 $name, 2, "CCURPC: $cb DeleteDevice received $devcount device addresses"; - foreach my $dev (@$a) { - HMCCU_CCURPC_Write ("DD", $dev); - } - - return; -} - -###################################################################### -# Subprocess: Callback for modified devices -###################################################################### - -sub HMCCU_CCURPC_UpdateDeviceCB ($$$$) -{ - my ($server, $cb, $devid, $hint) = @_; - - HMCCU_CCURPC_Write ("UD", $devid."|".$hint); - - return; -} - -###################################################################### -# Subprocess: Callback for replaced devices -###################################################################### - -sub HMCCU_CCURPC_ReplaceDeviceCB ($$$$) -{ - my ($server, $cb, $devid1, $devid2) = @_; - - HMCCU_CCURPC_Write ("RD", $devid1."|".$devid2); - - return; -} - -###################################################################### -# Subprocess: Callback for readded devices -###################################################################### - -sub HMCCU_CCURPC_ReaddDevicesCB ($$$) -{ - my ($server, $cb, $a) = @_; - my $name = $hmccu_child->{devname}; - my $devcount = scalar (@$a); - - Log3 $name, 2, "CCURPC: $cb ReaddDevice received $devcount device addresses"; - foreach my $dev (@$a) { - HMCCU_CCURPC_Write ("RA", $dev); - } - - return; -} - -###################################################################### -# Subprocess: Callback for handling CCU events -###################################################################### - -sub HMCCU_CCURPC_EventCB ($$$$$) -{ - my ($server, $cb, $devid, $attr, $val) = @_; - my $name = $hmccu_child->{devname}; - - HMCCU_CCURPC_Write ("EV", $devid."|".$attr."|".$val); - if (($cphash->{EV} % 500) == 0) { - Log3 $name, 3, "CCURPC: $cb Received 500 events from CCU since last check"; - my @stkeys = ('total', 'EV', 'ND', 'DD', 'RD', 'RA', 'UD', 'IN', 'SL', 'EX'); - my $msg = ''; - foreach my $stkey (@stkeys) { - $msg .= '|' if ($msg ne ''); - $msg .= $cphash->{$stkey}; - } - HMCCU_CCURPC_Write ("ST", $msg); - } - - # Never remove this statement! - return; -} - -###################################################################### -# Subprocess: Callback for list devices -###################################################################### - -sub HMCCU_CCURPC_ListDevicesCB ($$) -{ - my ($server, $cb) = @_; - my $name = $hmccu_child->{devname}; - - $cb = "unknown" if (!defined ($cb)); - Log3 $name, 1, "CCURPC: $cb ListDevices. Sending init to HMCCU"; - HMCCU_CCURPC_Write ("IN", "INIT|1|$cb"); - - return RPC::XML::array->new(); -} - - 1; @@ -8573,24 +10330,27 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)
    The module provides an interface between FHEM and a Homematic CCU2. HMCCU is the I/O device for the client devices HMCCUDEV and HMCCUCHN. The module requires the - additional Perl modules IO::File, RPC::XML::Client, RPC::XML::Server and SubProcess - (part of FHEM). + additional Perl modules IO::File, RPC::XML::Client, RPC::XML::Server.

    Define

      - define <name> HMCCU [<Protocol>://]<HostOrIP> [<ccu-number>] [waitforccu=<timeout>] - [ccudelay=<delay>] [delayedinit=<delay>] + define <name> HMCCU [<Protocol>://]<HostOrIP> [<ccu-number>] [nosync] + [waitforccu=<timeout>] [ccudelay=<delay>] [delayedinit=<delay>]

      Example:
      - define myccu HMCCU https://192.168.1.10 ccudelay=180 + define myccu HMCCU https://192.168.1.10 nosync ccudelay=180

      - The parameter HostOrIP is the hostname or IP address of a Homematic CCU2 or CCU3. Optionally + The parameter HostOrIP is the hostname or IP address of a Homematic CCU. Optionally the protocol 'http' or 'https' can be specified. Default protocol is 'http'.
      If you have more than one CCU you can specifiy a unique CCU number with parameter ccu-number. + If there's only one CCU, the default ccu-number is 1. Note: The ports used for the RPC + servers depend on the ccu-number. See attribute rpcserverport for more information.
      + The optionnosync prevents reading CCU config during interactive device definition. This + option is ignored during FHEM start.
      With option waitforccu HMCCU will wait for the specified time if CCU is not reachable. Parameter timeout should be a multiple of 20 in seconds. Warning: This option will - block the start of FHEM for timeout seconds.
      + block the start of FHEM for a maximum of timeout seconds. The default value is 1.
      The option ccudelay specifies the time for delayed initialization of CCU environment if the CCU is not reachable during FHEM startup (i.e. in case of a power failure). The default value for delay is 180 seconds. Increase this value if your CCU needs more time to start up @@ -8606,12 +10366,7 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)
    • Optionally enable automatic start of RPC servers with attribute 'rpcserver'

    Then start with the definition of client devices using modules HMCCUDEV (CCU devices) - and HMCCUCHN (CCU channels) or with command 'get devicelist create'.
    - Maybe it's helpful to set the following FHEM standard attributes for the HMCCU I/O - device:

    -
      -
    • Shortcut for RPC server control: eventMap /rpcserver on:on/rpcserver off:off/
    • -
    + and HMCCUCHN (CCU channels) or with command 'get create'.

@@ -8619,12 +10374,12 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) Set

  • set <name> ackmessages
    - Acknowledge device was unreachable messages in CCU. + Acknowledge "device was unreachable" messages in CCU.

  • set <name> authentication [<username> <password>]
    - Set credentials for CCU authentication. Authentication must be activated by setting - attribute ccuflags to 'authenticate'.
    - When executing this command without arguments credentials are deleted. + Set credentials for CCU authentication. Authentication must be activated by setting flag + 'authenticate' in attribute 'ccuflags'.
    + When executing this command without arguments, the credentials are deleted.

  • set <name> clear [<reading-exp>]
    Delete readings matching specified reading name expression. Default expression is '.*'. @@ -8633,14 +10388,11 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)
  • set <name> cleardefaults
    Clear default attributes imported from file.

  • -
  • set <name> datapoint [<FHEM-DevSpec>] [FHEM-Device[,...]][<channel-number>].<datapoint>=<value>
    +
  • set <name> datapoint <FHEM-DevSpec> [<channel-number>].<datapoint>=<value>
    Set datapoint values on multiple devices. If FHEM-Device is of type HMCCUDEV a channel-number must be specified. The channel number is ignored for devices of type HMCCUCHN.

  • -
  • set <name> defaults
    - Set default attributes for I/O device. -

  • set <name> delete <ccuobject> [<objecttype>]
    Delete object in CCU. Default object type is OT_VARDP. Valid object types are
    OT_DEVICE=device, OT_VARDP=variable. @@ -8653,7 +10405,7 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)

  • set <name> hmscript {<script-file>|'!'<function>|'['<code>']'} [dump] [<parname>=<value> [...]]
    - Execute Homematic script on CCU. If script code contains parameter in format $parname + Execute Homematic script on CCU. If script code contains parameters in format $parname they are substituted by corresponding command line parameters parname.
    If output of script contains lines in format Object=Value readings in existing corresponding FHEM devices will be set. Object can be the name of a CCU system @@ -8696,49 +10448,49 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)
  • get <name> aggregation {<rule>|all}
    Process aggregation rule defined with attribute ccuaggregate.

  • +
  • get <name> ccuConfig
    + Read configuration of CCU (devices, channels, programs). This command is executed automatically + after the definition of an I/O device. It must be executed manually after + module HMCCU is reloaded or after devices have changed in CCU (added, removed or + renamed).
    + If a RPC server is running HMCCU will raise events "count devices added in CCU" or + "count devices deleted in CCU". It's recommended to set up a notification + which reacts with execution of command 'get ccuConfig' on these events. +

  • +
  • get <name> ccuDevices
    + Show table of CCU devices including channel roles supported by HMCCU auto detection. +

  • get <name> ccumsg {service|alarm}
    Query active service or alarm messages from CCU. Generate FHEM event for each message.

  • -
  • get <name> configdesc {<device>|<channel>}
    - Get configuration parameter description of CCU device or channel (similar - to device settings in CCU). Not every CCU device or channel provides a configuration - parameter description. So result may be empty. +
  • get <name> create <devexp> [p=<prefix>] [s=<suffix>] [f=<format>] + [noDefaults] [save] [<attr>=<value> [...]]
    + Create client devices for all CCU devices and channels matching specified regular + expression. Parameter devexp is a regular expression for CCU device or channel + names. HMCCU automatically creates the appropriate client device (HMCCUCHN or HMCCUDEV)
    + With options 'p' and 's' a prefix and/or a suffix for the FHEM device + name can be specified. The option 'f' with parameter format + defines a template for the FHEM device names. Prefix, suffix and format can contain + format identifiers which are substituted by corresponding values of the CCU device or + channel:
    + %n = CCU object name (channel or device)
    + %a = CCU address
    + In addition a list of default attributes for the created client devices can be specified. + If option 'noDefaults' is specified, HMCCU does not set default attributes for a device. + Option 'save' will save FHEM config after device definition.

  • +
  • get <name> createDev <devname>
    + Simplified version of 'get create'. Doesn't accept a regular expression for device name. +

  • +
  • get <name> detectDev <devname>
    + Diagnostics command. Try to auto-detect device and display the result. Add this information + to your post in FHEM forum, if a device is not created as expected. +

  • get <name> defaults
    List device types and channels with default attributes available.

  • -
  • get <name> deviceinfo <device-name> [{State | Value}]
    - List device channels and datapoints. If option 'State' is specified the device is - queried directly. Otherwise device information from CCU is listed. -

  • -
  • get <name> devicelist [dump]
    - Read list of devices and channels from CCU. This command is executed automatically - after the definition of an I/O device. It must be executed manually after - module HMCCU is reloaded or after devices have changed in CCU (added, removed or - renamed). With option 'dump' devices are displayed in browser window. If a RPC - server is running HMCCU will raise events "count devices added in CCU" or - "count devices deleted in CCU". It's recommended to set up a notification - which reacts with execution of command 'get devicelist' on these events. -

  • -
  • get <name> devicelist create <devexp> [t={chn|dev|all}] - [p=<prefix>] [s=<suffix>] [f=<format>] [defattr] [duplicates] - [save] [<attr>=<value> [...]]
    - With option 'create' HMCCU will automatically create client devices for all CCU devices - and channels matching specified regular expression. With option t=chn or t=dev (default) - the creation of devices is limited to CCU channels or devices.
    - Optionally a prefix and/or a - suffix for the FHEM device name can be specified. The parameter format - defines a template for the FHEM device names. Prefix, suffix and format can contain - format identifiers which are substituted by corresponding values of the CCU device or - channel: %n = CCU object name (channel or device), %d = CCU device name, %a = CCU address. - In addition a list of default attributes for the created client devices can be specified. - If option 'defattr' is specified HMCCU tries to set default attributes for device. - With option 'duplicates' HMCCU will overwrite existing devices and/or create devices - for existing device addresses. Option 'save' will save FHEM config after device definition. -

  • -
  • get <name> dump {datapoints|devtypes} [<filter>]
    - Dump all Homematic devicetypes or all devices including datapoints currently - defined in FHEM. +
  • get <name> deviceinfo <device-name-or-address> [extended]
    + List device channels, datapoints and the device description.

  • get <name> dutycycle
    Read CCU interface and gateway information. For each interface/gateway the following @@ -8748,7 +10500,7 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) iface_conn_n = interface connection state (1=connected, 0=disconnected)
    iface_ducy_n = duty cycle of interface (0-100)

  • -
  • get <name> exportdefaults <filename> [csv] [all]
    +
  • get <name> exportdefaults <filename> [all]
    Export default attributes into file. If option all is specified, also defaults imported by customer will be exported.

  • @@ -8759,17 +10511,16 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) With parameter type-expr one can filter displayed firmware versions by Homematic device type.
    -
  • get <name> parfile [<parfile>]
    - Get values of all channels / datapoints specified in parfile. The parameter - parfile can also be defined as an attribute. The file must contain one channel / - definition per line. -

    - The syntax of Parfile entries is: -

    - {[<interface>.]<channel-address> | <channel-name>} - [.<datapoint-expr>] [<subst-rules>] -

    - Empty lines or lines starting with a # are ignored. +
  • get <name> internal <parameter>
    + Show internal values. Valid parameters are:
    +
      +
    • interfaces - RPC interfaces
    • +
    • groups - Virtual CCU device groups
    • +
    • versions - Versions of HMCCU modules
    • +
    +

  • +
  • get <name> paramsetDesc {<device>|<channel>}
    + Get parameter set description of CCU device or channel.

  • get <name> rpcstate
    Check if RPC server process is running. @@ -8785,7 +10536,8 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) time consuming.

  • get <name> vars <regexp>
    - Get CCU system variables matching regexp and store them as readings. + Get CCU system variables matching regexp and store them as readings. Use attribute + ccuGetVars to fetch variables periodically.

@@ -8838,23 +10590,20 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) Example: Find devices with low batteries. Generate reading in HTML format.
name=battery,filter:name=.*,read:(LOWBAT|LOW_BAT),if:any=yes,else:no,prefix:batt_,coll:NAME!All batteries OK,html:/home/battery.cfg

+
  • ccudef-attributes {<attrName>=<attrValue>[;...] | none}
    + Define attributes which are assigned to newly defined HMCCUDEV or HMCCUCHN devices. By default the following + attributes will be assigned:
    + room=Homematic
    + If attribute is set to 'none', no attributes will be assigned to new devices. +

  • ccudef-hmstatevals <subst-rule[;...]>
    Set global rules for calculation of reading hmstate.

  • -
  • ccudef-readingfilter <filter-rule[;...]>
    - Set global reading/datapoint filter. This filter is added to the filter specified by - client device attribute 'ccureadingfilter'. -

  • ccudef-readingformat {name | address | datapoint | namelc | addresslc | datapointlc}
    Set global reading format. This format is the default for all readings except readings of virtual device groups.

  • -
  • ccudef-readingname <old-readingname-expr>:[+]<new-readingname> - [;...]
    - Set global rules for reading name substitution. These rules are added to the rules - specified by client device attribute 'ccureadingname'. -

  • ccudef-stripnumber [<datapoint-expr>!]{0|1|2|-n|%fmt}[;...]
    Set global formatting rules for numeric datapoint or config parameter values. Default value is 2 (strip trailing zeroes).
    @@ -8874,8 +10623,10 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) seperated list of the following strings:
    ackState - Acknowledge command execution by setting STATE to error or success.
    dptnocheck - Do not check within set or get commands if datapoint is valid
    - intrpc - Use internal RPC server. This is the default.
    - extrpc - Same as procrpc (see below)
    + intrpc - No longer supported.
    + extrpc - No longer supported.
    + logCommand - Write all set and get commands of all devices to log file with verbose level 3.
    + logEnhanced - Messages in FHEM logfile will contain line number and process ID.
    logEvents - Write events from CCU into FHEM logfile
    logPong - Write log message when receiving pong event if verbose level is at least 3.
    noEvents - Ignore events / device updates sent by CCU. No readings will be updated!
    @@ -8887,7 +10638,7 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) server start HMCCU will create a HMCCURPCPROC device for each interface confiugured in attribute 'rpcinterface'
    reconnect - Automatically reconnect to CCU when events timeout occurred.
    - Flags intrpc, extrpc and procrpc cannot be combined. + updGroupMembers - Update readings of group members in virtual devices.

  • ccuget {State | Value}
    Set read access method for CCU channel datapoints. Method 'State' is slower than @@ -8895,7 +10646,7 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) is queried. Default is 'Value'. Method for write access to datapoints is always 'State'.

  • -
  • ccuGetVars <interval>[<pattern>]
    +
  • ccuGetVars <interval>:[<pattern>]
    Read CCU system variables periodically and update readings. If pattern is specified only variables matching this expression are stored as readings.

  • @@ -8909,9 +10660,23 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) Deprecated. Readings are written by default. To deactivate readings set flag noReadings in attribute ccuflags.
    -
  • parfile <filename>
    - Define parameter file for command 'get parfile'. -

  • +
  • createDeviceGroup <pattern>
    + The commands "get create" and "get createDev" will automatically set the group + attribute for newly created devices to the specified pattern if multiple FHEM + devices were created for a single CCU device. This will happen i.e. for remote controls + with mutliple keys or HmIP-Wired multi-switches.
    + The parameter pattern supports the following placeholders:
    + %n - replaced by CCU device name
    + %a - replaced by CCU device address
    + %t - replaced by CCU device type
    + Example: A remote with 4 channels named 'Light_Control' should be created in FHEM. Using + command "get createDev" will define one HMCCUCHN device per channel. Our naming scheme + for automatically assigned groups should be "ccuDeviceType ccuDeviceName".
    +
    +		attr myIODev createDeviceGroup "%t %n" 
    +		get myIODev createDev Light_Control
    +		
    +

  • rpcinterfaces <interface>[,...]
    Specify list of CCU RPC interfaces. HMCCU will register a RPC server for each interface. Either interface BidCos-RF or HmIP-RF (HmIP only) is default. Valid interfaces are:

    @@ -8924,35 +10689,12 @@ sub HMCCU_CCURPC_ListDevicesCB ($$)
  • CUxD (Port 8701)
  • VirtualDevice (Port 9292)
  • -
    -
  • rpcinterval <Seconds>
    - Specifiy how often RPC queue is read. Default is 5 seconds. Only relevant if internal - RPC server is used (deprecated).

  • rpcPingCCU <interval>
    Send RPC ping request to CCU every interval seconds. If interval is 0 ping requests are disabled. Default value is 300 seconds. If attribut ccuflags is set to logPong a log message with level 3 is created when receiving a pong event.

  • -
  • rpcport <value[,...]>
    - Deprecated, use attribute 'rpcinterfaces' instead. Specify list of RPC ports on CCU. - Either port 2001 or 2010 (HmIP only) is default. Valid RPC ports are:

    -
      -
    • 2000 = Wired components
    • -
    • 2001 = BidCos-RF (wireless 868 MHz components with BidCos protocol)
    • -
    • 2003 = Homegear (experimental)
    • -
    • 2010 = HM-IP (wireless 868 MHz components with IPv6 protocol)
    • -
    • 7000 = HVL (Homematic Virtual Layer devices)
    • -
    • 8701 = CUxD (only supported with external RPC server HMCCURPC)
    • -
    • 9292 = CCU group devices (especially heating groups)
    • -
    -

  • -
  • rpcqueue <queue-file>
    - Specify name of RPC queue file. This parameter is only a prefix (including the - pathname) for the queue files with extension .idx and .dat. Default is - /tmp/ccuqueue. If FHEM is running on a SD card it's recommended that the queue - files are placed on a RAM disk. -

  • rpcserver {on | off}
    Specify if RPC server is automatically started on FHEM startup.

  • @@ -8967,13 +10709,11 @@ sub HMCCU_CCURPC_ListDevicesCB ($$) value for base-port is 5400.
    The value ccu-number is only relevant if more than one CCU is connected to FHEM. Example: If base-port is 5000, protocol is BidCos (rpc-port 2001) and only - one CCU is connected the resulting RPC server port is 5000+2001+(10*0) = 7001. + one CCU is connected the resulting RPC server port is 5000+2001+(10*1) = 7010.
  • substitute <subst-rule>:<substext>[,...]
    Define substitions for datapoint values. Syntax of subst-rule is

    [[<channelno.>]<datapoint>[,...]!]<{#n1-m1|regexp1}>:<text1>[,...] -
    - Substitutions for parfile values must be specified in parfiles.

  • stripchar <character>
    Strip the specified character from variable or device name in set commands. This diff --git a/fhem/FHEM/88_HMCCUCHN.pm b/fhem/FHEM/88_HMCCUCHN.pm index ddb9c1609..2f026276e 100644 --- a/fhem/FHEM/88_HMCCUCHN.pm +++ b/fhem/FHEM/88_HMCCUCHN.pm @@ -4,13 +4,13 @@ # # $Id$ # -# Version 4.3.011 +# Version 5.0 # -# (c) 2020 zap (zap01 t-online de) +# (c) 2021 zap (zap01 t-online de) # ###################################################################### # Client device for Homematic channels. -# Requires modules 88_HMCCU.pm, HMCCUConf.pm +# Requires module 88_HMCCU.pm ###################################################################### package main; @@ -19,13 +19,19 @@ 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 212981850'; + ###################################################################### # Initialize module ###################################################################### @@ -34,19 +40,24 @@ sub HMCCUCHN_Initialize ($) { my ($hash) = @_; - $hash->{DefFn} = "HMCCUCHN_Define"; - $hash->{SetFn} = "HMCCUCHN_Set"; - $hash->{GetFn} = "HMCCUCHN_Get"; - $hash->{AttrFn} = "HMCCUCHN_Attr"; + $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,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; + $hash->{AttrList} = 'IODev ccucalculate '. + 'ccuflags:multiple-strict,ackState,logCommand,noReadings,trace,showMasterReadings,showLinkReadings,showDeviceReadings,showServiceReadings '. + 'ccureadingfilter:textField-long statedatapoint controldatapoint '. + 'ccureadingformat:name,namelc,address,addresslc '. + 'ccureadingname:textField-long ccuSetOnChange ccuReadingPrefix '. + 'ccuscaleval ccuverify:0,1,2 ccuget:State,Value '. + 'disable:0,1 hmstatevals:textField-long statevals substitute:textField-long '. + 'substexcl stripnumber peer:textField-long traceFilter '. $readingFnAttributes; } ###################################################################### @@ -58,76 +69,87 @@ sub HMCCUCHN_Define ($@) my ($hash, $a, $h) = @_; my $name = $hash->{NAME}; - my $usage = "Usage: define $name HMCCUCHN {device} ['readonly'] ['defaults'] [iodev={iodevname}]"; + my $usage = "Usage: define $name HMCCUCHN {device} ['readonly'] ['noDefaults'|'defaults'] [iodev={iodevname}]"; return $usage if (@$a < 3); - my $devname = shift @$a; - my $devtype = shift @$a; - my $devspec = shift @$a; + my ($devname, $devtype, $devspec) = splice (@$a, 0, 3); + my $ioHash; - my $hmccu_hash = undef; - + 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->{channels} = 1; - $hash->{statevals} = 'devstate'; + $hash->{readonly} = 'no'; + $hash->{hmccu}{channels} = 1; + $hash->{hmccu}{nodefaults} = $init_done ? 0 : 1; + $hash->{hmccu}{semDefaults} = 0; + $hash->{hmccu}{detect} = 0; # Parse optional command line parameters my $n = 0; - my $arg = shift @$a; - while (defined ($arg)) { + while (my $arg = shift @$a) { 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; } + 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++; - $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}}; + 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 - $hmccu_hash = HMCCU_FindIODevice ($devspec); + $ioHash = HMCCU_FindIODevice ($devspec); } if ($init_done) { # Interactive define command while CCU not ready or no IO device defined - if (!defined ($hmccu_hash)) { + if (!defined($ioHash)) { 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"; - } + 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 ($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); + 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 ($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; + 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; + } } ###################################################################### @@ -136,29 +158,89 @@ sub HMCCUCHN_Define ($@) # 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 ($hmccu_hash, $dev_hash) = @_; - my $devspec = $dev_hash->{hmccu}{devspec}; +sub HMCCUCHN_InitDevice ($$) +{ + my ($ioHash, $devHash) = @_; + my $name = $devHash->{NAME}; + my $devspec = $devHash->{hmccu}{devspec}; - return 1 if (!HMCCU_IsValidChannel ($hmccu_hash, $devspec, 7)); + return 1 if (!HMCCU_IsValidChannel ($ioHash, $devspec, 7)); - my ($di, $da, $dn, $dt, $dc) = HMCCU_GetCCUDeviceParam ($hmccu_hash, $devspec); - return 1 if (!defined ($da)); + 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 ($dev_hash, $hmccu_hash->{NAME}, undef)); + return 2 if (!HMCCU_AssignIODevice ($devHash, $ioHash->{NAME})); - $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'; + $devHash->{ccuif} = $di; + $devHash->{ccuaddr} = $da; + $devHash->{ccuname} = $dn; + $devHash->{ccutype} = $dt; + $devHash->{ccudevstate} = 'active'; - return 0; + my $rc = 0; + + if ($init_done) { + 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) { + if (!HMCCU_SetDefaultAttributes ($devHash)) { + HMCCU_SetDefaults ($devHash); + } + } + } + else { + $rc = -2; + } + + HMCCU_GetUpdate ($devHash, $da, 'Value'); + } + + 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); } ###################################################################### @@ -168,27 +250,38 @@ sub HMCCUCHN_InitDevice ($$) { sub HMCCUCHN_Attr ($@) { my ($cmd, $name, $attrname, $attrval) = @_; - my $hash = $defs{$name}; + my $clHash = $defs{$name}; + my $ioHash = HMCCU_GetHash ($clHash); - if ($cmd eq "set") { - return "Missing attribute value" if (!defined ($attrval)); + if ($cmd eq 'set') { + return 'Missing attribute value' if (!defined($attrval)); if ($attrname eq 'IODev') { - $hash->{IODev} = $defs{$attrval}; + $clHash->{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]; + return 'Attribute statevals ignored. Device is read only' if ($clHash->{readonly} eq 'yes'); + return '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 "Invalid value $attrval" if (!HMCCU_SetSCDatapoints ($clHash, $attrname, $attrval, $role)); + } } - elsif ($cmd eq "del") { - if ($attrname eq 'statevals') { - $hash->{statevals} = "devstate"; + 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) { + 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); + } } } @@ -203,259 +296,79 @@ sub HMCCUCHN_Set ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; - my $opt = shift @$a; + my $opt = shift @$a // return 'No set command specified'; + my $lcopt = lc($opt); - return "No set command specified" if (!defined ($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 $rocmds = "clear defaults:noArg"; - my $rwcmds = "clear config control datapoint defaults:noArg rpcparameter devstate"; + 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'; - # 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)$/); + # 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); - 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"; + # 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 ''); } - 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)); + 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 ($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 ($lcopt eq 'datapoint') { + return HMCCU_ExecuteSetDatapointCommand ($hash, $a, $h); } - 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 ($lcopt eq 'toggle') { + return HMCCU_ExecuteToggleCommand ($hash); } - 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 (exists($hash->{hmccu}{roleCmds}{set}{$opt})) { + return HMCCU_ExecuteRoleCommand ($ioHash, $hash, 'set', $opt, $a, $h); } elsif ($opt eq 'clear') { - my $rnexp = shift @$a; - HMCCU_DeleteReadings ($hash, $rnexp); - return HMCCU_SetState ($hash, "OK"); + return HMCCU_ExecuteSetClearCommand ($hash, $a); } - 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 ($lcopt =~ /^(config|values)$/) { + return HMCCU_ExecuteSetParameterCommand ($ioHash, $hash, $lcopt, $a, $h); } - 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 ($lcopt =~ '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 ($opt eq 'defaults') { - $rc = HMCCU_SetDefaults ($hash); + elsif ($lcopt eq 'defaults') { + my $mode = shift @$a // 'update'; + my $rc = 0; + if ($mode ne 'old') { + $rc = HMCCU_SetDefaultAttributes ($hash, { mode => $mode, role => undef, roleChn => undef }); + } + $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: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); + return "Unknown argument $opt choose one of $syntax"; } } @@ -467,133 +380,76 @@ sub HMCCUCHN_Get ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; - my $opt = shift @$a; - - return "No get command specified" if (!defined ($opt)); + 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})); + !defined ($hash->{IODev}) || AttrVal ($name, "disable", 0) == 1); - my $disable = AttrVal ($name, "disable", 0); - return undef if ($disable == 1); + my $ioHash = $hash->{IODev}; + my $ioName = $ioHash->{NAME}; - my $hmccu_hash = $hash->{IODev}; - if (HMCCU_IsRPCStateBlocking ($hmccu_hash)) { - return undef if ($opt eq '?'); - return "HMCCUCHN: CCU busy"; - } + 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 ($sc, $sd, $cc, $cd) = HMCCU_GetSpecialDatapoints ($hash, '', 'STATE', '', ''); my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - my $ccureadings = AttrVal ($name, "ccureadings", 1); - my $result = ''; - my $rc; + # 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, "set $name $opt ".join (' ', @$a)) if ($ccuflags =~ /logCommand/); + HMCCU_Log ($hash, 3, "get $name $opt ".join (' ', @$a)) + if ($opt ne '?' && $ccuflags =~ /logCommand/ || HMCCU_IsFlag ($ioName, '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 ($lcopt eq 'datapoint') { + my $objname = shift @$a // return HMCCU_SetError ($hash, "Usage: get $name datapoint {datapoint}"); + return HMCCU_SetError ($hash, -8, $objname) 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; + my ($rc, $result) = HMCCU_GetDatapoint ($hash, $objname, 0); + return $rc < 0 ? HMCCU_SetError ($hash, $rc, $result) : $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 ($lcopt eq 'deviceinfo') { + my $extended = shift @$a; + my ($devAddr, undef) = HMCCU_SplitChnAddr ($ccuaddr); + return HMCCU_ExecuteGetDeviceInfoCommand ($ioHash, $hash, $devAddr, defined($extended) ? 1 : 0); } - 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 ($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 ($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 ($lcopt eq 'extvalues') { + my $filter = shift @$a; + my $rc = HMCCU_GetUpdate ($hash, $ccuaddr, $filter); + return $rc < 0 ? HMCCU_SetError ($hash, $rc) : 'OK'; } - 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 ($lcopt eq 'paramsetdesc') { + my $result = HMCCU_ParamsetDescToStr ($ioHash, $hash); + return defined($result) ? $result : HMCCU_SetError ($hash, "Can't get device model"); } - 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); + elsif (exists($hash->{hmccu}{roleCmds}{get}{$opt})) { + return HMCCU_ExecuteRoleCommand ($ioHash, $hash, 'get', $opt, $a, $h); } 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; + return "Unknown argument $opt choose one of $syntax"; } } @@ -602,176 +458,218 @@ sub HMCCUCHN_Get ($@) =pod =item device -=item summary controls HMCCU client devices for Homematic CCU2 - FHEM integration +=item summary controls HMCCU client devices for Homematic CCU2/3 - FHEM integration =begin html

    HMCCUCHN

      The module implements Homematic CCU channels as client devices for HMCCU. A HMCCU I/O device must - exist before a client device can be defined. If a CCU channel is not found execute command - 'get devicelist' in I/O device. -

      + exist before a client device can be defined. If a CCU channel is not found, execute command + 'get ccuConfig' in I/O device. This will synchronize devices and channels between CCU + and HMCCU. +

      Define

        define <name> HMCCUCHN {<channel-name> | <channel-address>} - [readonly] [defaults] [iodev=<iodev-name>] + [readonly] [defaults|noDefaults] [iodev=<iodev-name>]

        - If option 'readonly' is specified no set command will be available. With option 'defaults' - some default attributes depending on CCU device type will be set. Default attributes are only - available for some device types.
        - The define command accepts a CCU2 channel name or channel address as parameter. + If option 'readonly' is specified no set command will be available. With option 'noDefaults' + no default attributes will be set during interactive device definition.
        + The define command accepts a CCU channel name or channel address as parameter.

        Examples:
        define window_living HMCCUCHN WIN-LIV-1 readonly
        define temp_control HMCCUCHN BidCos-RF.LEQ1234567:1

        - The interface part of a channel address must not be specified. The default is 'BidCos-RF'. - Channel addresses can be found with command 'get deviceinfo <devicename>' executed - in I/O device. + The interface part of a channel address is optional. Channel addresses can be found with command + 'get deviceinfo <CCU-DeviceName>' executed in I/O device.

        + Internals:
        +
          +
        • ccuaddr: Address of channel in CCU
        • +
        • ccudevstate: State of device in CCU (active/inactive/dead)
        • +
        • ccuif: Interface of device
        • +
        • ccuname: Name of channel in CCU
        • +
        • ccurole: Role of channel
        • +
        • ccusubtype: Homematic subtype of device (different from ccutype for HmIP devices)
        • +
        • ccutype: Homematic type of device
        • +
        • readonly: Indicates whether FHEM device is writeable
        • +
        • receiver: List of peered devices with role 'receiver'. If no FHEM device exists for a receiver, the + name of the CCU device is displayed preceeded by 'ccu:'
        • +
        • sender: List of peered devices with role 'sender'. If no FHEM device exists for a sender, the + name of the CCU device is displayed preceeded by 'ccu:'
        • +

      Set

        -
      • set <name> clear [<reading-exp>]
        +
      • set <name> clear [<reading-exp>|reset]
        Delete readings matching specified reading name expression. Default expression is '.*'. - Readings 'state' and 'control' are not deleted. + Readings 'state' and 'control' are not deleted. With option 'reset' all readings + and all internally stored device parameter values are deleted.

      • -
      • set <name> config [device] [<rpcport>] <parameter>=<value>] - [...]
        - Set config parameters of CCU channel. This is equal to setting device parameters in CCU. - Valid parameters can be listed by using commands 'get configdesc' or 'get configlist'. - With option 'device' specified parameters are set in device instead of channel. +
      • set <name> config [device|<receiver>] <parameter>=<value>[:<type>] [...]
        + Set multiple config (parameter set MASTER) or link (parameter set LINKS) parameters. + If neither 'device' nor receiver is specified, configuration parameters of + current channel are set. With option 'device' configuration parameters of the device + are set.
        + If a receiver is specified, parameters will be set for the specified link. + Parameter receiver is the name of a FHEM device of type HMCCUDEV or HMCCUCHN or + a channel address or a CCU channel name. For FHEM devices of type HMCCUDEV the number + of the linked channel must be specified.
        + Parameter parameter must be a valid configuration parameter. + If type is not specified, it's taken from parameter set definition. If type + cannot be determined, the default type STRING is used. + Valid types are STRING, BOOL, INTEGER, FLOAT, DOUBLE.

        + Example 1: Set device parameter AES
        + set myDev config device AES=1
        + Example 2: Set channel parameters MIN and MAX with type definition
        + set myDev config MIN=0.5:FLOAT MAX=10.0:FLOAT
        + Example 3: Set link parameter. DEV_PARTNER is a HMCCUDEV device, so channel number (3) is required
        + set myDev config DEV_PARTNER:3 MYPARAM=1

      • -
      • set <name> datapoint <datapoint> <value> [...]
        - Set datapoint values of a CCU channel. If parameter value contains special - character \_ it's substituted by blank. -

        +
      • set <name> control <value>
        + Set value of control datapoint. This command is available only on command line + for compatibility reasons. It should not be used any more. +

      • +
      • set <name> datapoint <datapoint> <value> | <datapoint>=<value> [...]
        + Set datapoint values of a CCU channel. If value contains blank characters it must be + enclosed in double quotes. This command is only available, if channel contains a writeable datapoint.

        Examples:
        set temp_control datapoint SET_TEMPERATURE 21
        - set temp_control datapoint AUTO_MODE 1 SET_TEMPERATURE 21 + set temp_control datapoint AUTO_MODE 1 SET_TEMPERATURE=21

      • -
      • set <name> defaults
        +
      • set <name> defaults ['reset'|'old'|'update']
        Set default attributes for CCU device type. Default attributes are only available for - some device types and for some channels of a device type. -

      • -
      • set <name> devstate <value>
        - Set state of a CCU device channel. The state datapoint of a channel must be defined - by setting attribute 'statedatapoint' to a valid datapoint name. -

        - Example:
        - set light_entrance devstate true + some device types and for some channels of a device type. If option 'reset' is specified, + the following attributes are deleted before the new attributes are set: + 'ccureadingname', 'ccuscaleval', 'eventMap', 'substexcl', 'webCmd', 'widgetOverride'. + During update to version 4.4 it's recommended to use option 'reset'. With option 'old' + the attributes are set according to HMCCU 4.3 defaults mechanism.

      • set <name> down [<value>]
        - Decrement value of datapoint LEVEL. This command is only available if channel contains - a datapoint LEVEL. Default for value is 10. -

      • -
      • set <name> <statevalue>
        - Set state of a CCU device channel to StateValue. The state datapoint of a channel - must be defined by setting attribute 'statedatapoint'. The available state values must - be defined by setting attribute 'statevals'. -

        - Example: Turn switch on
        - - attr myswitch statedatapoint STATE
        - attr myswitch statevals on:true,off:false
        - set myswitch on -
        -

      • -
      • set <name> toggle
        - Toggle state datapoint between values defined by attribute 'statevals'. This command is - only available if attribute 'statevals' is set. Toggling supports more than two state - values. -

        - Example: Toggle blind actor
        - - attr myswitch statedatapoint LEVEL
        - attr myswitch statevals up:100,down:0
        - set myswitch toggle -
        + [dimmer, blind] Decrement value of datapoint LEVEL. This command is only available + if channel contains a datapoint LEVEL. Default for value is 20.

      • set <name> on-for-timer <ontime>
        - Switch device on for specified number of seconds. This command is only available if - channel contains a datapoint ON_TIME. The attribute 'statevals' must contain at least a - value for 'on'. The attribute 'statedatapoint' must be set to a writeable datapoint. -

        + [switch] Switch device on for specified number of seconds. This command is only available if + channel contains a datapoint ON_TIME. Parameter ontime can be specified + in seconds or in format HH:MM:SS

        Example: Turn switch on for 300 seconds
        - - attr myswitch statedatapoint STATE
        - attr myswitch statevals on:true,off:false
        - set myswitch on-for-timer 300 -
        + set myswitch on-for-timer 300

      • set <name> on-till <timestamp>
        - Switch device on until timestamp. Parameter timestamp can be a time in + [switch,dimmer] Switch device on until timestamp. Parameter timestamp can be a time in format HH:MM or HH:MM:SS. This command is only available if channel contains a datapoint - ON_TIME. The attribute 'statevals' must contain at least a value for 'on'. The Attribute - 'statedatapoint' must be set to a writeable datapoint. + ON_TIME.

      • set <name> pct <value> [<ontime> [<ramptime>]]
        - Set datapoint LEVEL of a channel to the specified value. Optionally a ontime + [dimmer] Set datapoint LEVEL of a channel to the specified value. Optionally a ontime and a ramptime (both in seconds) can be specified. This command is only available if channel contains at least a datapoint LEVEL and optionally datapoints ON_TIME and RAMP_TIME. The parameter ontime can be specified in seconds or as timestamp in format HH:MM or HH:MM:SS. If ontime is 0 it's ignored. This syntax can be used to - modify the ramp time only. -

        + modify the ramp time only.

        Example: Turn dimmer on for 600 second. Increase light to 100% over 10 seconds
        - - attr myswitch statedatapoint LEVEL
        - attr myswitch statevals on:100,off:0
        - set myswitch pct 100 600 10 -
        + set myswitch pct 100 600 10

      • -
      • set <name> rpcparameter { VALUES | MASTER } <parameter>=<value> [...]
        - Set multiple datapoints or config parameters by using RPC interface instead of Rega. - Supports attribute 'ccuscaleval' for datapoints. Parameter parameter must be a valid - datapoint or config parameter name. +
      • set <name> readingFilter <datapoint-list>
        + Set attribute ccureadingfilter by selecting a list of datapoints. Parameter datapoint-list + is a comma seperated list of datapoints. +

      • +
      • set <name> stop
        + [blind] Set datapoint STOP of a channel to true. This command is only available, if the + channel contains a datapoint STOP. +

      • +
      • set <name> toggle
        + Toggle state datapoint between values defined by attribute 'statevals' or by channel role. This command is + only available if state values can be detected or are defined by using attribute + 'statevals'. Toggling supports more than two state values.

        + Example: Toggle blind actor between states 'open', 'half' and 'close'
        + + attr myswitch statevals open:100,half:50,close:0
        + set myswitch toggle +

      • set <name> up [<value>]
        - Increment value of datapoint LEVEL. This command is only available if channel contains - a datapoint LEVEL. Default for value is 10. + [blind,dimmer] Increment value of datapoint LEVEL. This command is only available + if channel contains a datapoint LEVEL. Default for value is 20.

      • +
      • set <name> values <parameter>=<value>[:<type>] [...]
        + Set multiple datapoint values (parameter set VALUES). Parameter parameter + must be a valid datapoint name. If type is not specified, it's taken from + parameter set definition. The default type is STRING. + Valid types are STRING, BOOL, INTEGER, FLOAT, DOUBLE. +

      Get

        -
      • get <name> config [device] [<filter-expr>]
        - Get configuration parameters of CCU channel. If attribute 'ccureadings' is 0 results - are displayed in browser window. Parameters can be filtered by filter-expr. - Parameters to be stored as readings must be part of 'ccureadingfilter'. If option - 'device' is specified parameters of device are read. -

      • -
      • get <name> configdesc [device]
        - Get description of configuration parameters of CCU channel or device if option 'device' - is specified. -

      • -
      • get <name> configlist [device] [<filter-expr>]
        - Get configuration parameters of CCU channel. Parameters can be filtered by - filter-expr. With option 'device' device parameters are listed. +
      • get <name> config [<filter-expr>]
        + Get configuration parameters of device and channel. If filter-expr is specified, + only parameters matching the expression are stored as readings.
        + Values related to configuration or link parameters are stored as readings beginning + with "R-" for MASTER parameter set and "L-" for LINK parameter set. + Prefixes "R-" and "L-" can be modified with attribute 'ccuReadingPrefix'. Whether parameters are + stored as readings or not, can be controlled by setting the following flags in + attribute ccuflags:
        +
          +
        • noReadings: Do not store any reading.
        • +
        • showMasterReadings: Store configuration readings of parameter set 'MASTER' of current channel.
        • +
        • showDeviceReadings: Store configuration readings of device and value readings of channel 0.
        • +
        • showLinkReadings: Store readings of links.
        • +
        • showServiceReadings: Store readings of parameter set 'SERVICE'
        • +
        + If non of the flags is set, only readings belonging to parameter set VALUES (datapoints) + are stored.

      • get <name> datapoint <datapoint>
        - Get value of a CCU channel datapoint. + Get value of a CCU channel datapoint. Format of datapoint is ChannelNo.DatapointName. + For HMCCUCHN devices the ChannelNo is not needed. This command is only available if a + readable datapoint exists.

      • get <name> defaults
        - Display default attributes for CCU device type. + This command has been removed in version 4.4. The default attributes are included in the + output of command 'get deviceInfo'.

      • -
      • get <name> deviceinfo [{State | Value}]
        - Display all channels and datapoints of device with datapoint values and types. +
      • get <name> deviceInfo ['extended']
        + Display information about device type and channels:
        +
          +
        • all channels and datapoints of device with datapoint values and types
        • +
        • statedatapoint and controldatapoint
        • +
        • device and channel description
        • +
        • default attributes (if device is not supported by built in functionality)
        • +
        + The output of this command is helpful to gather information about new / not yet supported devices. + Please add this information to your post in the FHEM forum, if you have a question about + the integration of a new device. See also command 'get paramsetDesc'.

      • -
      • get <name> devstate
        - Get state of CCU device. Default datapoint STATE can be changed by setting - attribute 'statedatapoint'. Command will fail if state datapoint does not exist in - channel. +
      • get <name> extValues [<filter-expr>]
        + Update all readings for all parameters of parameter set VALUES (datapoints) and connected system + variables by using CCU Rega (Homematic script). + If filter-expr is specified, only datapoints matching the expression are stored as readings. +

      • +
      • get <name> paramsetDesc
        + Display description of parameter sets of channel and device. The output of this command + is helpful to gather information about new / not yet supported devices. Please add this + information to your post in the FHEM forum, if you have a question about + the integration of a new device. See also command 'get deviceInfo'.

      • -
      • get <name> update [{State | Value}]
        - Update all datapoints / readings of channel. With option 'State' the device is queried. - This request method is more accurate but slower then 'Value'. +
      • get <name> update [<filter-expr>]
        + Update all readings for all parameters of all parameter sets (MASTER, LINK, VALUES). + If filter-expr is specified, only parameters matching the expression are stored as readings. +

      • +
      • get <name> values [<filter-expr>]
        + Update all readings for all parameters of parameter set VALUES (datapoints). + If filter-expr is specified, only parameters matching the expression are stored as readings. +

      • +
      • get <name> weekProgram [<program-number>|all]
        + Display week programs. This command is only available if a device supports week programs.

      @@ -782,6 +680,7 @@ sub HMCCUCHN_Get ($@) To reduce the amount of events it's recommended to set attribute 'event-on-change-reading' to '.*'.

      +
    • ccucalculate <value-type>:<reading>[:<dp-list>[;...]
      Calculate special values like dewpoint based on datapoints specified in dp-list. The result is stored in reading. For datapoints in dp-list @@ -799,43 +698,34 @@ sub HMCCUCHN_Get ($@) Example:
      dewpoint:taupunkt:1.TEMPERATURE,1.HUMIDITY

    • -
    • ccuflags {ackState, logCommand, nochn0, trace}
      + +
    • ccuflags {ackState, logCommand, noReadings, showDeviceReadings, showLinkReadings, showConfigReadings, trace}
      Control behaviour of device:
      ackState: Acknowledge command execution by setting STATE to error or success.
      logCommand: Write get and set commands to FHEM log with verbose level 3.
      - nochn0: Prevent update of status channel 0 datapoints / readings.
      + noReadings: Do not update readings
      + showDeviceReadings: Show readings of device and channel 0.
      + showLinkReadings: Show link readings.
      + showMasterReadings: Show configuration readings.
      + showServiceReadings: Show service readings (HmIP only)
      trace: Write log file information for operations related to this device.

    • +
    • ccuget {State | Value}
      Set read access method for CCU channel datapoints. Method 'State' is slower than 'Value' because each request is sent to the device. With method 'Value' only CCU is queried. Default is 'Value'.

    • -
    • ccureadings {0 | 1}
      - If set to 1 values read from CCU will be stored as readings. Default is 1. -

    • +
    • ccureadingfilter <filter-rule[;...]>
      Only datapoints matching specified expression RegExp are stored as readings.
      Syntax for filter-rule is either:
      - [N:]{<channel-name>}!RegExp> or:
      - [N:][<channel-number>.]<RegExp>
      + [N:]{<channel-name-expr>}!RegExp> or:
      + [N:][<channel-number>[,<channel-number>].]<RegExp>
      If channel-name or channel-number is specified the following rule applies only to this channel.
      If a rule starts with 'N:' the filter is negated which means that a reading is - stored if rule doesn't match.
      - The following table describes the dependencies between this attribute and attribute - ccudef-readingfilter in I/O device. The filtering of readings depends on which attribute - is set.
      - - - - - - -
      ccureadingfilter
      Device
      ccudef-readingfilter
      I/O Device
      Update Readings/Datapoints
      not setnot setall readings
      not setsetonly readings from ccudef-readingfilter
      setnot setonly readings from ccureadingfilter
      setsetboth readings from ccureadingfilter and ccudef-readingfilter
      - So if ccudef-readingfilter is set in I/O device one must also set ccureadingfilter to - get updates for additional, device specific readings. Please keep in mind, that readings updates - are also affected by attributes event-on-change-reading and event-on-update-reading.

      + stored if rule doesn't match.

      Examples:
      attr mydev ccureadingfilter .*
      @@ -843,13 +733,21 @@ sub HMCCUCHN_Get ($@) attr mydev ccureadingfilter MyBlindChannel!^LEVEL$

    • -
    • ccureadingformat {address[lc] | name[lc] | datapoint[lc]}
      - Set format of reading names. Default for virtual device groups is 'name'. The default for all - other device types is 'datapoint'. If set to 'address' format of reading names + +
    • ccureadingformat {address[lc] | name[lc] | datapoint[lc] | <format-string>}
      + Set format of reading names. Default for virtual device groups and HMCCUCHN devices is 'name'. + The default for HMCCUDEV is 'datapoint'. If set to 'address' format of reading names is channel-address.datapoint. If set to 'name' format of reading names is - channel-name.datapoint. If set to 'datapoint' format is channel-number.datapoint. With - suffix 'lc' reading names are converted to lowercase. + channel-name.datapoint. If set to 'datapoint' format is channel-number.datapoint. + For HMCCUCHN devices the channel part is ignored. With suffix 'lc' reading names are converted + to lowercase. The reading format can also contain format specifiers %a (address), + %n (name) and %c (channel). Use %A, %N, %C for conversion to upper case.

      + Example:
      + + attr mydev ccureadingformat HM_%c_%N +

    • +
    • ccureadingname <old-readingname-expr>:[+]<new-readingname>[,...];[;...]
      Set alternative or additional reading names or group readings. Only part of old reading name matching old-readingname-exptr is substituted by new-readingname. @@ -870,6 +768,16 @@ sub HMCCUCHN_Get ($@) attr mydev ccureadingname [1-4].PRESSED_SHORT:+pressed

    • + +
    • ccuReadingPrefix <paramset>:<prefix>[,...]
      + Set reading name prefix for parameter sets. Default values for parameter sets are:
      + VALUES (state values): No prefix
      + MASTER (configuration parameters): 'R-'
      + LINK (links parameters): 'L-'
      + PEER (peering parameters): 'P-'
      + SERVICE (service parameters): S-
      +

    • +
    • ccuscaleval <[channelno.]datapoint>:<factor>[,...]
      ccuscaleval <[!][channelno.]datapoint>:<min>:<max>:<minn>:<maxn>[,...]
      @@ -886,6 +794,7 @@ sub HMCCUCHN_Get ($@) attr myblind ccuscale !LEVEL:0:1:0:100

    • +
    • ccuSetOnChange <expression>
      Check if datapoint value will be changed by set command before changing datapoint value. This attribute can reduce the traffic between CCU and devices. It presumes that datapoint @@ -896,18 +805,16 @@ sub HMCCUCHN_Get ($@) corresponding reading will be set to the new value directly after setting a datapoint in CCU without any verification.

    • +
    • controldatapoint <datapoint>
      - Set datapoint for device control. Can be use to realize user defined control elements for - setting control datapoint. For example if datapoint of thermostat control is - SET_TEMPERATURE one can define a slider for setting the destination temperature with - following attributes:

      - attr mydev controldatapoint SET_TEMPERATURE
      - attr mydev webCmd control
      - attr mydev widgetOverride control:slider,10,1,25 + Set datapoint for device control by commands 'set control' and 'set toggle'. + This attribute must be set if control datapoint cannot be detected automatically.

    • +
    • disable {0 | 1}
      Disable client device.

    • +
    • hmstatevals <subst-rule>[;...]
      Define building rules and substitutions for reading hmstate. Syntax of subst-rule is
      @@ -929,6 +836,7 @@ sub HMCCUCHN_Get ($@) Optionally the name of the HomeMatic state reading can be specified at the beginning of the attribute in format =<reading>;. The default reading name is 'hmstate'.

    • +
    • peer <datapoints>:<condition>: {ccu:<object>=<value>|hmccu:<object>=<value>| fhem:<command>}
      @@ -961,19 +869,27 @@ sub HMCCUCHN_Get ($@) # Set 1.STATE of device LEQ1234567 to true if current level is different from old level
      attr mydev peer 1.LEVEL:$1.LEVEL != $$1.LEVEL:ccu:LEQ1234567:1.STATE=true

    • +
    • statedatapoint <datapoint>
      - Set state datapoint used by some commands like 'set devstate'. + Set datapoint used for displaying device state. This attribute must be set, if + state datapoint cannot be detected automatically.

    • -
    • statevals <text>:<text>[,...]
      - Define substitution for values of set commands. The parameters text are available - as set commands. + +
    • statevals <new-command>:<control-datapoint-value>[,...]
      + Define set commands for control datapoint. This attribute should only be used if the device + type is not recognized by HMCCU. Using this attribute for automatically detected devices + could lead to problems!

      - Example:
      + Example: controldatapoint of a device is STATE. Device is not recognized by HMCCU:
      + # Define 2 new commands on and off representing the possible states of STATE:
      attr my_switch statevals on:true,off:false
      - set my_switch on + # After attr the commands on and off are available:
      + set my_switch on
      + set my_switch off

    • +
    • stripnumber [<datapoint-expr>!]{0|1|2|-n|%fmt}[;...]
      Remove trailing digits or zeroes from floating point numbers, round or format numbers. If attribute is negative (-0 is valid) floating point values are rounded @@ -990,15 +906,20 @@ sub HMCCUCHN_Get ($@) attr myDev stripnumber TEMPERATURE!%.2f degree

    • +
    • substexcl <reading-expr>
      Exclude values of readings matching reading-expr from substitution. This is helpful for reading 'control' if the reading is used for a slider widget and the corresponding datapoint is assigned to attribute statedatapoint and controldatapoint.

    • +
    • substitute <subst-rule>[;...]
      - Define substitutions for datapoint/reading values. Syntax of subst-rule is

      - [[<channelno>.]<datapoint>[,...]!]<{#n1-m1|regexp}>:<text>[,...] + Define substitutions for datapoint/reading values. This attribute is helpful / necessary if + a device is not automatically detected by HMCCU.
      + Syntax of subst-rule is

      + [[<type>:][<channelno>.]<datapoint>[,...]!]<{#n1-m1|regexp}>:<text>[,...]

      + Parameter type is a valid channel type/role, i.e. "SHUTTER_CONTACT". Parameter text can contain variables in format ${varname}. The variable ${value} is substituted by the original datapoint value. All other variables must match with a valid @@ -1016,7 +937,11 @@ sub HMCCUCHN_Get ($@) attr my_dim substitute LEVEL!#0-0:off,#1-100:on -
    • +
      + +
    • traceFilter <filter-expr>
      + Trace only function calls which are maching filter-expr. +

    diff --git a/fhem/FHEM/88_HMCCUDEV.pm b/fhem/FHEM/88_HMCCUDEV.pm index 4e28afa52..8dfb0cfbf 100644 --- a/fhem/FHEM/88_HMCCUDEV.pm +++ b/fhem/FHEM/88_HMCCUDEV.pm @@ -4,29 +4,35 @@ # # $Id$ # -# Version 4.3.012 +# Version 5.0 # -# (c) 2020 zap (zap01 t-online de) +# (c) 2021 zap (zap01 t-online de) # ###################################################################### # Client device for Homematic devices. -# Requires modules 88_HMCCU.pm, HMCCUConf.pm +# 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_Delete ($$); sub HMCCUDEV_InitDevice ($$); +sub HMCCUDEV_Undef ($$); +sub HMCCUDEV_Rename ($$); sub HMCCUDEV_Set ($@); sub HMCCUDEV_Get ($@); sub HMCCUDEV_Attr ($@); +my $HMCCUDEV_VERSION = '5.0 212981850'; + ###################################################################### # Initialize module ###################################################################### @@ -35,20 +41,24 @@ sub HMCCUDEV_Initialize ($) { my ($hash) = @_; - $hash->{DefFn} = "HMCCUDEV_Define"; - $hash->{DeleteFn} = "HMCCUDEV_Delete"; - $hash->{SetFn} = "HMCCUDEV_Set"; - $hash->{GetFn} = "HMCCUDEV_Get"; - $hash->{AttrFn} = "HMCCUDEV_Attr"; + $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,logCommand,nochn0,trace ccureadingfilter:textField-long ". - "ccureadingformat:name,namelc,address,addresslc,datapoint,datapointlc ". - "ccureadingname:textField-long ". - "ccureadings:0,1 ccuget:State,Value ccuscaleval ccuSetOnChange ccuverify:0,1,2 disable:0,1 ". - "hmstatevals:textField-long statevals substexcl substitute:textField-long statechannel ". - "statedatapoint controldatapoint stripnumber peer:textField-long ". + $hash->{AttrList} = 'IODev ccuaggregate:textField-long ccucalculate:textField-long '. + 'ccuflags:multiple-strict,ackState,logCommand,noReadings,trace,showMasterReadings,showLinkReadings,showDeviceReadings,showServiceReadings '. + '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 statedatapoint '. + 'controlchannel controldatapoint stripnumber peer:textField-long traceFilter '. $readingFnAttributes; } @@ -61,10 +71,10 @@ sub HMCCUDEV_Define ($@) my ($hash, $a, $h) = @_; my $name = $hash->{NAME}; - my $usage = "Usage: define $name HMCCUDEV {device|'virtual'} [state-channel] ". - "['readonly'] ['defaults'] [iodev={iodev-name}] [address={virtual-device-no}]". - "[{groupexp=regexp|group={device|channel}[,...]]"; - return $usage if (scalar (@$a) < 3); + 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", @@ -74,84 +84,85 @@ sub HMCCUDEV_Define ($@) "No matching CCU devices found", "Type of virtual device not defined", "Device type not found", - "Too many virtual devices" + "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 = shift @$a; - my $devtype = shift @$a; - my $devspec = shift @$a; + 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})); - my $hmccu_hash = undef; - # Store some definitions for delayed initialization - $hash->{hmccu}{devspec} = $devspec; - $hash->{hmccu}{groupexp} = $h->{groupexp} if (exists ($h->{groupexp})); - $hash->{hmccu}{group} = $h->{group} if (exists ($h->{group})); - - if (exists ($h->{address})) { - if ($init_done || $devspec ne 'virtual') { - return "Option address not allowed"; - } - else { - $hash->{hmccu}{address} = $h->{address}; - } - } - else { - return "Option address not specified" if (!$init_done && $devspec eq 'virtual'); - } - - # Defaults - $hash->{statevals} = 'devstate'; + $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; + $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})); # Parse optional command line parameters foreach my $arg (@$a) { - if ($arg eq 'readonly') { $hash->{statevals} = $arg; } - elsif ($arg eq 'defaults') { - HMCCU_SetDefaults ($hash) if ($init_done); - } - elsif ($arg =~ /^[0-9]+$/) { $attr{$name}{statechannel} = $arg; } - else { return $usage; } + 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 "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}}; + 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 - $hmccu_hash = $devspec eq 'virtual' ? HMCCU_GetHash (0) : HMCCU_FindIODevice ($devspec); + $ioHash = $devspec eq 'virtual' ? HMCCU_GetHash (0) : HMCCU_FindIODevice ($devspec); } if ($init_done) { # Interactive define command while CCU not ready - if (!defined ($hmccu_hash)) { + if (!defined($ioHash)) { 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"; - } + 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 ($hmccu_hash) || $hmccu_hash->{ccustate} ne 'active') { - Log3 $name, 2, "HMCCUDEV: [$devname] Cannot detect IO device, maybe CCU not ready. Trying later ..."; -# readingsSingleUpdate ($hash, "state", "Pending", 1); + 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 ($hmccu_hash, $hash); - return $errmsg[$rc] if ($rc > 0); - - return undef; + 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; + } } ###################################################################### @@ -165,137 +176,149 @@ sub HMCCUDEV_Define ($@) # 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 ($hmccu_hash, $dev_hash) = @_; - my $name = $dev_hash->{NAME}; - my $devspec = $dev_hash->{hmccu}{devspec}; + 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'; - if ($devspec eq 'virtual') { - my $no = 0; - if (exists ($dev_hash->{hmccu}{address})) { - # Only true during FHEM start - $no = $dev_hash->{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 ($hmccu_hash, $va, 1)) { - $no = $i; - last; + my $rc = 0; + + if ($init_done) { + 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}; + if (!HMCCU_SetDefaultAttributes ($devHash, { + mode => 'update', role => undef, roleChn => $chn, + })) { + HMCCU_Log ($devHash, 2, "No role attributes found for channel $chn"); + HMCCU_SetDefaults ($devHash); } } - return 7 if ($no == 0); - $dev_hash->{DEF} .= " address=$no"; } - $dev_hash->{ccuif} = 'fhem'; - $dev_hash->{ccuaddr} = sprintf ("VIR%07d", $no); - $dev_hash->{ccuname} = $name; - } - else { - return 1 if (!HMCCU_IsValidDevice ($hmccu_hash, $devspec, 7)); + else { + $rc = -2; # Device type not known by HMCCU + } - my ($di, $da, $dn, $dt, $dc) = HMCCU_GetCCUDeviceParam ($hmccu_hash, $devspec); - return 1 if (!defined ($da)); - $gdname = $dn; - - $dev_hash->{ccuif} = $di; - $dev_hash->{ccuaddr} = $da; - $dev_hash->{ccuname} = $dn; - $dev_hash->{ccutype} = $dt; - $dev_hash->{channels} = $dc; + # Update readings + HMCCU_GetUpdate ($devHash, $da, 'Value'); } - + # Parse group options - if ($dev_hash->{ccuif} eq 'VirtualDevices' || $dev_hash->{ccuif} eq 'fhem') { + if ($devHash->{ccuif} eq 'VirtualDevices') { my @devlist = (); - if (exists ($dev_hash->{hmccu}{groupexp})) { + if (exists ($devHash->{hmccu}{groupexp})) { # Group devices specified by name expression - $gdcount = HMCCU_GetMatchingDevices ($hmccu_hash, $dev_hash->{hmccu}{groupexp}, 'dev', \@devlist); + $gdcount = HMCCU_GetMatchingDevices ($ioHash, $devHash->{hmccu}{groupexp}, 'dev', \@devlist); return 4 if ($gdcount == 0); } - elsif (exists ($dev_hash->{hmccu}{group})) { + elsif (exists ($devHash->{hmccu}{group})) { # Group devices specified by comma separated name list - my @gdevlist = split (",", $dev_hash->{hmccu}{group}); - $dev_hash->{ccugroup} = '' if (@gdevlist > 0); + my @gdevlist = split (',', $devHash->{hmccu}{group}); + $devHash->{ccugroup} = '' if (scalar(@gdevlist) > 0); foreach my $gd (@gdevlist) { - my ($gda, $gdc, $gdo) = ('', '', '', ''); - - return 1 if (!HMCCU_IsValidDevice ($hmccu_hash, $gd, 7)); - - ($gda, $gdc) = HMCCU_GetAddress ($hmccu_hash, $gd, '', ''); - $gdo = $gda; - $gdo .= ':'.$gdc if ($gdc ne ''); - push @devlist, $gdo; + 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 ($hmccu_hash, $gdname); + @devlist = HMCCU_GetGroupMembers ($ioHash, $gdname); $gdcount = scalar (@devlist); } - - return 3 if ($gdcount == 0); - $dev_hash->{ccugroup} = join (',', @devlist); - if ($devspec eq 'virtual') { - my $dev = shift @devlist; - my $devtype = HMCCU_GetDeviceType ($hmccu_hash, $dev, 'n/a'); - my $devna = $devtype eq 'n/a' ? 1 : 0; - for my $d (@devlist) { - if (HMCCU_GetDeviceType ($hmccu_hash, $d, 'n/a') ne $devtype) { - $devna = 1; - last; - } - } - - my $rc = 0; - if ($devna) { - $dev_hash->{ccutype} = 'n/a'; - $dev_hash->{statevals} = 'readonly'; - $rc = HMCCU_CreateDevice ($hmccu_hash, $dev_hash->{ccuaddr}, $name, undef, $dev); - } - else { - $dev_hash->{ccutype} = $devtype; - $rc = HMCCU_CreateDevice ($hmccu_hash, $dev_hash->{ccuaddr}, $name, $devtype, $dev); - } - return $rc+4 if ($rc > 0); - - # Set default attributes - $attr{$name}{ccureadingformat} = 'name'; - } + $devHash->{ccugroup} = join (',', @devlist) if (scalar(@devlist) > 0); } - # Inform HMCCU device about client device - return 2 if (!HMCCU_AssignIODevice ($dev_hash, $hmccu_hash->{NAME}, undef)); - -# readingsSingleUpdate ($dev_hash, "state", "Initialized", 1); - $dev_hash->{ccudevstate} = 'active'; - - return 0; + return $rc; } ###################################################################### # Delete device ###################################################################### -sub HMCCUDEV_Delete ($$) +sub HMCCUDEV_Undef ($$) { - my ($hash, $name) = @_; - - if ($hash->{ccuif} eq 'fhem') { - HMCCU_DeleteDevice ($hash); - } + 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 ###################################################################### @@ -303,30 +326,56 @@ sub HMCCUDEV_Delete ($$) sub HMCCUDEV_Attr ($@) { my ($cmd, $name, $attrname, $attrval) = @_; - my $hash = $defs{$name}; + my $clHash = $defs{$name}; + my $ioHash = HMCCU_GetHash ($clHash); - if ($cmd eq "set") { - return "Missing attribute value" if (!defined ($attrval)); + if ($cmd eq 'set') { + return "Missing value of attribute $attrname" if (!defined($attrval)); if ($attrname eq 'IODev') { - $hash->{IODev} = $defs{$attrval}; + $clHash->{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 ($attrname eq 'statevals') { + return 'Attribute statevals ignored. Device is read only' if ($clHash->{readonly} eq 'yes'); + return '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 "Attribute value must be in format channel.datapoint"; + } + } + else { + return "Attribute value must be a valid channel number" if (!HMCCU_IsIntNum ($attrval)); + $chn = $attrval; + } + + my $role = HMCCU_GetChannelRole ($clHash, $chn); + return "Invalid value $attrval" if (!HMCCU_SetSCDatapoints ($clHash, $attrname, $attrval, $role)); + } + } + 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) { + # 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, "Cannot set default state- and/or control datapoints") + if ($rsd == 0 && $rcd == 0); } } } - elsif ($cmd eq "del") { - if ($attrname eq "statevals") { - $hash->{statevals} = "devstate"; - } - } - + return; } @@ -338,315 +387,76 @@ sub HMCCUDEV_Set ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; - my $opt = shift @$a; + my $opt = shift @$a // return 'No set command specified'; + my $lcopt = lc($opt); - return "No set command specified" if (!defined ($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); - # Get I/O device, check device state - return undef if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending' || - !defined ($hash->{IODev})); - my $hmccu_hash = $hash->{IODev}; - my $hmccu_name = $hmccu_hash->{NAME}; + my $ioHash = $hash->{IODev}; + my $ioName = $ioHash->{NAME}; + return ($opt eq '?' ? undef : 'Cannot perform set commands. CCU busy') + if (HMCCU_IsRPCStateBlocking ($ioHash)); - # Handle read only and disabled devices - return undef if ($hash->{statevals} eq 'readonly' && $opt ne '?' - && $opt !~ /^(clear|config|defaults)$/); - my $disable = AttrVal ($name, "disable", 0); - return undef if ($disable == 1); - - # Check if CCU is busy - if (HMCCU_IsRPCStateBlocking ($hmccu_hash)) { - return undef if ($opt eq '?'); - return "HMCCUDEV: CCU busy"; - } - - # Get parameters of current device - 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/); + # Build set command syntax + my $syntax = 'clear defaults:reset,update,old'; - if ($opt eq 'datapoint') { - my $usage = "Usage: set $name datapoint [{channel-number}.]{datapoint} {value} [...]"; - my %dpval; - my $i = 0; - - while (my $objname = shift @$a) { - my $objvalue = shift @$a; - $i += 1; - - if ($ccutype eq 'HM-Dis-EP-WM55' && !defined ($objvalue)) { - $objvalue = ''; - foreach my $t (keys %{$h}) { - if ($objvalue eq '') { - $objvalue = $t.'='.$h->{$t}; - } - else { - $objvalue .= ','.$t.'='.$h->{$t}; - } - } - } - - return HMCCU_SetError ($hash, $usage) if (!defined ($objvalue) || $objvalue eq ''); - - if ($objname =~ /^([0-9]+)\..+$/) { - my $chn = $1; - return HMCCU_SetError ($hash, -7) if ($chn >= $hash->{channels}); - } - else { - return HMCCU_SetError ($hash, -11) if ($sc eq ''); - $objname = $sc.'.'.$objname; - } - - 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)); + # 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 ''); } - elsif ($opt eq 'control') { - return HMCCU_SetError ($hash, -12) if ($cc eq ''); - return HMCCU_SetError ($hash, -14) if ($cd eq ''); - return HMCCU_SetError ($hash, -7) if ($cc >= $hash->{channels}); - - 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:$cc.$cd" => HMCCU_Substitute ($objvalue, $statevals, 1, undef, '') } - ); - return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); + + # 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 ($opt =~ /^($hash->{statevals})$/) { - my $cmd = $1; - my $objvalue = ($cmd ne 'devstate') ? $cmd : shift @$a; - - return HMCCU_SetError ($hash, -11) if ($sc eq ''); - return HMCCU_SetError ($hash, -13) if ($sd eq ''); - return HMCCU_SetError ($hash, "Usage: set $name devstate {value}") if (!defined ($objvalue)); - - $objvalue =~ s/\\_/%20/g; - $rc = HMCCU_SetMultipleDatapoints ($hash, - { "001.$ccuif.$ccuaddr:$sc.$sd" => HMCCU_Substitute ($objvalue, $statevals, 1, undef, '') } - ); - return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); + elsif ($lcopt eq 'datapoint') { + return HMCCU_ExecuteSetDatapointCommand ($hash, $a, $h); } - elsif ($opt eq 'toggle') { - return HMCCU_SetError ($hash, -15) if ($statevals eq '' || !exists($hash->{statevals})); - return HMCCU_SetError ($hash, -11) if ($sc eq ''); - return HMCCU_SetError ($hash, -13) if ($sd eq ''); - return HMCCU_SetError ($hash, -8) if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $sc, $sd, 2)); - - my $tstates = $hash->{statevals}; - $tstates =~ s/devstate\|//; - my @states = split /\|/, $tstates; - my $stc = scalar (@states); - - my $objname = $ccuif.'.'.$ccuaddr.':'.$sc.'.'.$sd; - - # Read current value of datapoint without updating reading - ($rc, $result) = HMCCU_GetDatapoint ($hash, $objname, 1); - Log3 $name, 2, "HMCCU: set toggle: GetDatapoint returned $rc, $result" - if ($ccuflags =~ /trace/); - 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 ($lcopt eq 'toggle') { + return HMCCU_ExecuteToggleCommand ($hash); } - elsif ($opt eq 'pct' || $opt eq 'up' || $opt eq 'down') { - return HMCCU_SetError ($hash, -11) if ($sc eq '' && $cc eq ''); - my $chn; - if (HMCCU_IsValidDatapoint ($hash, $ccutype, $cc, "LEVEL", 2)) { - $chn = $cc; - } - elsif (HMCCU_IsValidDatapoint ($hash, $ccutype, $sc, "LEVEL", 2)) { - $chn = $sc; - } - else { - return HMCCU_SetError ($hash, "Can't find LEVEL datapoint for device type $ccutype") - } - - 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, $chn, "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:$chn.ON_TIME"} = $timespec if ($timespec > 0); - } - - # Set ramp time - $dpval{"002.$ccuif.$ccuaddr:$chn.RAMP_TIME"} = $ramptime if (defined ($ramptime)); - - # Set level - $dpval{"003.$ccuif.$ccuaddr:$chn.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:$chn.LEVEL"; - - ($rc, $result) = HMCCU_GetDatapoint ($hash, $objname, 1); - Log3 $name, 2, "HMCCU: set $opt: GetDatapoint returned $rc, $result" - if ($ccuflags =~ /trace/); - 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 (exists($hash->{hmccu}{roleCmds}{set}{$opt})) { + return HMCCU_ExecuteRoleCommand ($ioHash, $hash, 'set', $opt, $a, $h); } - 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, -11) if ($sc eq ''); - return HMCCU_SetError ($hash, -13) if ($sd eq ''); - return HMCCU_SetError ($hash, "Can't find ON_TIME datapoint for device type") - if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $sc, "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:$sc.ON_TIME" => $timespec, - "002.$ccuif.$ccuaddr:$sc.$sd" => HMCCU_Substitute ("on", $statevals, 1, undef, '') - }); - return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); + elsif ($lcopt eq 'clear') { + return HMCCU_ExecuteSetClearCommand ($hash, $a); } - elsif ($opt eq 'clear') { - my $rnexp = shift @$a; - HMCCU_DeleteReadings ($hash, $rnexp); - return HMCCU_SetState ($hash, "OK"); + elsif ($lcopt =~ /^(config|values)$/) { + return HMCCU_ExecuteSetParameterCommand ($ioHash, $hash, $opt, $a, $h); } - elsif ($opt eq 'config') { - return HMCCU_SetError ($hash, "Usage: set $name config [{channel-number}] {parameter}={value} [...]") - if ((scalar keys %{$h}) < 1); - my $objname = $ccuaddr; - - # Channel number is optional because parameter can be related to device or channel - if ((scalar @$a) > 0 && $$a[0] =~ /^([0-9]{1,2})$/) { - return HMCCU_SetError ($hash, -7) if ($1 >= $hash->{channels}); - $objname .= ':'.$1; - } - - my ($rc, $res) = HMCCU_RPCRequest ($hash, "putParamset", $objname, "MASTER", $h); - return HMCCU_SetError ($hash, HMCCU_Min(0, $rc)); + elsif ($lcopt =~ '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 ($opt eq 'rpcparameter') { - return HMCCU_SetError ($hash, "Usage: set $name rpcparameter { channel VALUES | [channel] MASTER } {parameter}={value} [...]") - if ((scalar keys %{$h}) < 1); - - my $key; - my $chn; - - while (my $p = shift @$a) { - if (uc($p) =~ /^(MASTER|VALUES)$/ && !defined ($key)) { - $key = uc($p); - } - elsif ($p =~ /^([0-9]+)$/ && !defined ($chn)) { - HMCCU_SetError ($hash, -7) if ($p >= $hash->{channels}); - $chn = $p; - } + elsif ($lcopt eq 'defaults') { + my $mode = shift @$a // 'update'; + my $rc = 0; + if ($mode ne 'old') { + $rc = HMCCU_SetDefaultAttributes ($hash, { mode => $mode, role => undef, roleChn => undef }); } - - $key = 'VALUES' if (!defined ($key)); - my $addr = defined ($chn) ? "$ccuaddr:$chn" : $ccuaddr; - - if ($key eq 'VALUES') { - ($rc, $result) = HMCCU_SetMultipleParameters ($hash, $addr, $h); - } - elsif ($key eq 'MASTER') { - ($rc, $result) = HMCCU_RPCRequest ($hash, "putParamset", $addr, $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') { - my $rc = HMCCU_SetDefaults ($hash); - return HMCCU_SetError ($hash, $rc == 0 ? "No default attributes found" : "OK"); + $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 config defaults:noArg"; - - if ($hash->{statevals} ne 'readonly') { - $retmsg .= " control datapoint rpcparameter"; - if ($sc ne '') { - $retmsg .= " devstate"; - if ($hash->{statevals} ne '') { - my @cmdlist = split /\|/,$hash->{statevals}; - shift @cmdlist; - $retmsg .= ':'.join(',',@cmdlist) if (@cmdlist > 0); - foreach my $sv (@cmdlist) { - $retmsg .= ' '.$sv.':noArg'; - } - $retmsg .= " toggle:noArg"; - $retmsg .= " on-for-timer on-till" - if (HMCCU_IsValidDatapoint ($hash, $hash->{ccutype}, $sc, "ON_TIME", 2)); - $retmsg .= " pct up down" - if (HMCCU_IsValidDatapoint ($hash, $hash->{ccutype}, $sc, "LEVEL", 2) || - HMCCU_IsValidDatapoint ($hash, $hash->{ccutype}, $cc, "LEVEL", 2)); - } - } - } - return AttrTemplate_Set ($hash, $retmsg, $name, $opt, @$a); + return "Unknown argument $opt choose one of $syntax"; } } @@ -658,191 +468,102 @@ sub HMCCUDEV_Get ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; - my $opt = shift @$a; - - return "No get command specified" if (!defined ($opt)); + my $opt = shift @$a // return 'No get command specified'; + my $lcopt = lc($opt); # Get I/O device - return undef if (!defined ($hash->{ccudevstate}) || $hash->{ccudevstate} eq 'pending' || - !defined ($hash->{IODev})); - my $hmccu_hash = $hash->{IODev}; - my $hmccu_name = $hmccu_hash->{NAME}; - - # Handle disabled devices - my $disable = AttrVal ($name, "disable", 0); - return undef if ($disable == 1); + 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 - if (HMCCU_IsRPCStateBlocking ($hmccu_hash)) { - return undef if ($opt eq '?'); - return "HMCCUDEV: CCU 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 $ccureadings = AttrVal ($name, 'ccureadings', 1); - my ($sc, $sd, $cc, $cd) = HMCCU_GetSpecialDatapoints ($hash, '', 'STATE', '', ''); - - my $result = ''; - my $rc; - - # Virtual devices only support command get update - if ($ccuif eq 'fhem' && $opt ne 'update') { - return "HMCCUDEV: Unknown argument $opt, choose one of update:noArg"; - } + # 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, "set $name $opt ".join (' ', @$a)) if ($ccuflags =~ /logCommand/); + HMCCU_Log ($hash, 3, "get $name $opt ".join (' ', @$a)) + if ($opt ne '?' && $ccuflags =~ /logCommand/ || HMCCU_IsFlag ($ioName, 'logCommand')); - if ($opt eq 'devstate') { - return HMCCU_SetError ($hash, -11) if ($sc eq ''); - return HMCCU_SetError ($hash, -13) if ($sd eq ''); - return HMCCU_SetError ($hash, -8) if (!HMCCU_IsValidDatapoint ($hash, $ccutype, $sc, $sd, 1)); - - my $objname = $ccuif.'.'.$ccuaddr.':'.$sc.'.'.$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 [{channel-number}.]{datapoint}") - if (!defined ($objname)); + if ($lcopt 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->{channels}); + 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 ''); $objname = $sc.'.'.$objname; } - return HMCCU_SetError ($hash, -8) + return HMCCU_SetError ($hash, -8, $objname) if (!HMCCU_IsValidDatapoint ($hash, $ccutype, undef, $objname, 1)); $objname = $ccuif.'.'.$ccuaddr.':'.$objname; - ($rc, $result) = HMCCU_GetDatapoint ($hash, $objname, 0); + my ($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 $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'}]"); - } - - if ($hash->{ccuif} ne 'fhem') { - $rc = HMCCU_GetUpdate ($hash, $ccuaddr, $ccuget); - return HMCCU_SetError ($hash, $rc) if ($rc < 0); - } - else { - # Update all devices belonging to group - my @vdevs = split (",", $hash->{ccugroup}); - foreach my $vd (@vdevs) { - $rc = HMCCU_GetUpdate ($hash, $vd, $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'}]"); - } - $result = HMCCU_GetDeviceInfo ($hash, $ccuaddr, $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 =~ /^([0-9]{1,2})$/) { - return HMCCU_SetError ($hash, -7) if ($1 >= $hash->{channels}); - $ccuobj .= ':'.$1; - $par = shift @$a; - } - } - $par = '.*' if (!defined ($par)); - - my ($rc, $res) = HMCCU_RPCRequest ($hash, "getParamset", $ccuobj, "MASTER", undef, $par); -# my ($rc, $res) = HMCCU_RPCGetConfig ($hash, $ccuobj, "getParamset", $par); - return HMCCU_SetError ($hash, $rc, $res) if ($rc < 0); - HMCCU_SetState ($hash, "OK") if (exists ($hash->{STATE}) && $hash->{STATE} eq "Error"); - return $ccureadings ? undef : $res; - } - elsif ($opt eq 'configlist') { - my $ccuobj = $ccuaddr; - my $par = shift @$a; - if (defined ($par)) { - if ($par =~ /^([0-9]{1,2})$/) { - return HMCCU_SetError ($hash, -7) if ($1 >= $hash->{channels}); - $ccuobj .= ':'.$1; - $par = shift @$a; - } - } - $par = '.*' if (!defined ($par)); - - my ($rc, $res) = HMCCU_RPCRequest ($hash, "listParamset", $ccuobj, "MASTER", undef, $par); -# my ($rc, $res) = HMCCU_RPCGetConfig ($hash, $ccuobj, "listParamset", $par); - return HMCCU_SetError ($hash, $rc, $res) if ($rc < 0); - HMCCU_SetState ($hash, "OK") if (exists ($hash->{STATE}) && $hash->{STATE} eq "Error"); - return $res; - } - elsif ($opt eq 'configdesc') { - my $ccuobj = $ccuaddr; - my $par = shift @$a; - if (defined ($par)) { - if ($par =~ /^([0-9]{1,2})$/) { - return HMCCU_SetError ($hash, -7) if ($1 >= $hash->{channels}); - $ccuobj .= ':'.$1; - } - else { - return HMCCU_SetError ($hash, -7) if ($1 >= $hash->{channels}); - } - } - - my ($rc, $res) = HMCCU_RPCRequest ($hash, "getParamsetDescription", $ccuobj, "MASTER", undef); -# my ($rc, $res) = HMCCU_RPCGetConfig ($hash, $ccuobj, "getParamsetDescription", undef); - return HMCCU_SetError ($hash, $rc, $res) if ($rc < 0); - HMCCU_SetState ($hash, "OK") if (exists ($hash->{STATE}) && $hash->{STATE} eq "Error"); - return $res; - } - elsif ($opt eq 'defaults') { - $result = HMCCU_GetDefaults ($hash, 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 { - 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 configlist configdesc deviceinfo:noArg"; - $retmsg .= ' devstate:noArg' if ($sc ne ''); - - return $retmsg; + return "Unknown argument $opt choose one of $syntax"; } } - 1; =pod =item device -=item summary controls HMCCU client devices for Homematic CCU2 - FHEM integration +=item summary controls HMCCU client devices for Homematic CCU - FHEM integration =begin html @@ -857,23 +578,18 @@ sub HMCCUDEV_Get ($@) Define

      - define <name> HMCCUDEV {<device> | 'virtual'} [<statechannel>] - [readonly] [defaults] [{group={device|channel}[,...]|groupexp=regexp] - [iodev=<iodev-name>] + define <name> HMCCUDEV <device> [<controlchannel>] + [readonly] [defaults|noDefaults] [forceDev] [iodev=<iodev-name>]

      If option 'readonly' is specified no set command will be available. With option 'defaults' some default attributes depending on CCU device type will be set. Default attributes are only available for some device types. The option is ignored during FHEM start. - Parameter statechannel corresponds to attribute 'statechannel'.
      - A HMCCUDEV device supports CCU group devices. The CCU devices or channels related to a group - device are specified by using options 'group' or 'groupexp' followed by the names or - addresses of the CCU devices or channels. By using 'groupexp' one can specify a regular - expression for CCU device or channel names. Since version 4.2.009 of HMCCU HMCCUDEV - is able to detect members of group devices automatically. So options 'group' or - 'groupexp' are no longer necessary to define a group device.
      - It's also possible to group any kind of CCU devices without defining a real group - in CCU by using option 'virtual' instead of a CCU device specification. -

      + Option 'forceDev' must be specified to define a HMCCUDEV device even if the preferred and + recommended type is HMCCUCHN.
      + Parameter controlchannel corresponds to attribute 'controlchannel'. If a device + has several identical channels, some commands need to know the channel number for + controlling the device.
      +
      Examples:
      # Simple device by using CCU device name
      @@ -882,11 +598,24 @@ sub HMCCUDEV_Get ($@) define temp_control HMCCUDEV BidCos-RF.LEQ1234567 1
      # Simple read only device by using CCU device address and with default attributes
      define temp_sensor HMCCUDEV BidCos-RF.LEQ2345678 1 readonly defaults - # Group device by using CCU group device and 3 group members
      - define heating_living HMCCUDEV GRP-LIV group=WIN-LIV,HEAT-LIV,THERM-LIV
      -
      -
    +

    + Internals:
    +
      +
    • ccuaddr: Address of device in CCU
    • +
    • ccudevstate: State of device in CCU (active/inactive/dead)
    • +
    • ccuif: Interface of device
    • +
    • ccuname: Name of device in CCU
    • +
    • ccurole: Role of device
    • +
    • ccusubtype: Homematic subtype of device (different from ccutype for HmIP devices)
    • +
    • ccutype: Homematic type of device
    • +
    • readonly: Indicates whether FHEM device is writeable
    • +
    • receiver: List of peered devices with role 'receiver'. If no FHEM device exists for a receiver, the + name of the CCU device is displayed preceeded by 'ccu:'
    • +
    • sender: List of peered devices with role 'sender'. If no FHEM device exists for a sender, the + name of the CCU device is displayed preceeded by 'ccu:'
    • +
    +
    @@ -900,6 +629,10 @@ sub HMCCUDEV_Get ($@) Set configuration parameter of CCU device or channel. Valid parameters can be listed by using command 'get configdesc'.

  • +
  • set <name> control <value>
    + Set value of control datapoint. This command is available for compatibility reasons. + It should not be used any more. +

  • set <name> datapoint [<channel-number>.]<datapoint> <value> [...]
    Set datapoint values of a CCU device channel. If channel number is not specified @@ -909,13 +642,13 @@ sub HMCCUDEV_Get ($@) set temp_control datapoint 2.SET_TEMPERATURE 21
    set temp_control datapoint 2.AUTO_MODE 1 2.SET_TEMPERATURE 21

  • -
  • set <name> defaults
    +
  • set <name> defaults ['reset'|'old'|'update']
    Set default attributes for CCU device type. Default attributes are only available for - some device types. -

  • -
  • set <name> devstate <value>
    - Set state of a CCU device channel. Channel and state datapoint must be defined as - attribute 'statedatapoint'. If value contains string \_ it is substituted by blank. + some device types and for some channels of a device type. If option 'reset' is specified, + the following attributes are deleted before the new attributes are set: + 'ccureadingname', 'ccuscaleval', 'eventMap', 'substexcl', 'webCmd', 'widgetOverride'. + During update to version 4.4 it's recommended to use option 'reset'. With option 'old' + the attributes are set according to HMCCU 4.3 defaults mechanism.

  • set <name> down [<value>]
    see HMCCUCHN @@ -929,13 +662,11 @@ sub HMCCUDEV_Get ($@)
  • set <name> pct <value;> [<ontime> [<ramptime>]]
    see HMCCUCHN

  • -
  • set <name> rpcparameter { <channel> VALUES | [<channel>] MASTER } <parameter>=<value> [...]
    - Set multiple datapoints or config parameters by using RPC interface instead of Rega. - Supports attribute 'ccuscaleval' for datapoints. Method VALUES (setting datapoints) - requires a channel number. For method MASTER (setting parameters) a channel number - is optional (setting device parameters). Parameter parameter must be a valid - datapoint or config parameter name. -

  • +
  • set <name> readingFilter <datapoint-list>
    + Set attribute ccureadingfilter by selecting a list of datapoints. Parameter datapoint-list + is a comma seperated list of datapoints. The datapoints must be specifed in format + "channel-number.datapoint-name". +

  • set <name> <statevalue>
    State datapoint of a CCU device channel is set to 'statevalue'. State channel and state datapoint must be defined as attribute 'statedatapoint'. Values for statevalue @@ -995,19 +726,11 @@ sub HMCCUDEV_Get ($@) Get

      -
    • get <name> config [<channel-number>] [<filter-expr>]
      - Get configuration parameters of CCU device. If attribute 'ccureadings' is set to 0 +
    • get <name> config [<filter-expr>]
      + Get configuration parameters of CCU device and all its channels. If ccuflag noReadings is set parameters are displayed in browser window (no readings set). Parameters can be filtered by filter-expr.

    • -
    • get <name> configdesc [<channel-number>] [<rpcport>]
      - Get description of configuration parameters for CCU device. Default value for Parameter - rpcport is 2001 (BidCos-RF). Other valid values are 2000 (wired) and 2010 (HMIP). -

    • -
    • get <name> configlist [<channel-number>] [<filter-expr>]
      - Display configuration parameters of CCU device. Parameters can be filtered by - filter-expr. -

    • get <name> datapoint [<channel-number>.]<datapoint>
      Get value of a CCU device datapoint. If channel-number is not specified state channel is used. @@ -1015,16 +738,23 @@ sub HMCCUDEV_Get ($@)
    • get <name> defaults
      see HMCCUCHN

    • -
    • get <name> deviceinfo [{State | Value}]
      - Display all channels and datapoints of device with datapoint values and types. +
    • get <name> deviceinfo ['extended']
      + Display information about device and channels:
      +
        +
      • all channels and datapoints of device with datapoint values and types
      • +
      • statedatapoint and controldatapoint
      • +
      • device and channel description
      • +

    • -
    • get <name> devstate
      - Get state of CCU device. Attribute 'statechannel' must be set. Default state datapoint - STATE can be modified by attribute 'statedatapoint'. -

    • -
    • get <name> update [{State | Value}]
      +
    • get <name> extValues [<filter-expr>]
      + see HMCCUCHN +

    • +
    • get <name> update [{State | Value}]
      see HMCCUCHN

    • +
    • get <name> weekProgram [<program-number>|all]
      + Display week programs. This command is only available if a device supports week programs. +

    @@ -1042,9 +772,6 @@ sub HMCCUDEV_Get ($@)
  • ccuget {State | Value}
    see HMCCUCHN

  • -
  • ccureadings {0 | 1}
    - see HMCCUCHN -

  • ccureadingfilter <filter-rule[,...]>
    see HMCCUCHN

  • @@ -1064,6 +791,9 @@ sub HMCCUDEV_Get ($@)
  • ccuverify {0 | 1 | 2}
    see HMCCUCHN

  • +
  • controlchannel <channel-number>
    + Channel used for setting device states. +

  • controldatapoint <channel-number.datapoint>
    Set channel number and datapoint for device control. see HMCCUCHN @@ -1075,15 +805,14 @@ sub HMCCUDEV_Get ($@) see HMCCUCHN

  • peer [<datapoints>:<condition>: - {ccu:<object>=<value>|hmccu:<object>=<value>|fhem:<command>}
    + {ccu:<object>=<value>|hmccu:<object>=<value>}
    see HMCCUCHN

  • statechannel <channel-number>
    - Channel for setting device state by devstate command. Deprecated, use attribute - 'statedatapoint' instead. + Channel for getting device state. Deprecated, use attribute 'statedatapoint' instead.

  • statedatapoint [<channel-number>.]<datapoint>
    - Set state channel and state datapoint for setting device state by devstate command. + Set state channel and state datapoint. Default is STATE. If 'statedatapoint' is not defined at least attribute 'statechannel' must be set.

  • @@ -1099,6 +828,9 @@ sub HMCCUDEV_Get ($@)
  • substitute <subst-rule>[;...]
    see HMCCUCHN

  • +
  • traceFilter <filter-expr>
    + see HMCCUCHN +

  • diff --git a/fhem/FHEM/88_HMCCURPCPROC.pm b/fhem/FHEM/88_HMCCURPCPROC.pm index eb3349561..05c75dda6 100755 --- a/fhem/FHEM/88_HMCCURPCPROC.pm +++ b/fhem/FHEM/88_HMCCURPCPROC.pm @@ -4,11 +4,11 @@ # # $Id$ # -# Version 1.9.001 +# Version 5.0 # # Subprocess based RPC Server module for HMCCU. # -# (c) 2019 by zap (zap01 t-online de) +# (c) 2021 by zap (zap01 t-online de) # ############################################################################## # @@ -17,6 +17,7 @@ # RPC::XML::Client # RPC::XML::Server # +# ND deaktiviert in Read und Write!! ############################################################################## @@ -25,17 +26,20 @@ package main; use strict; use warnings; +# use Data::Dumper; use RPC::XML::Client; use RPC::XML::Server; use SetExtensions; +require "$attr{global}{modpath}/FHEM/88_HMCCU.pm"; + ###################################################################### # Constants ###################################################################### # HMCCURPC version -my $HMCCURPCPROC_VERSION = '1.9.001'; +my $HMCCURPCPROC_VERSION = '5.0 212981850'; # Maximum number of events processed per call of Read() my $HMCCURPCPROC_MAX_EVENTS = 100; @@ -61,6 +65,9 @@ my $HMCCURPCPROC_TIMEOUT_CONNECTION = 1; # Timeout for TriggerIO() in seconds my $HMCCURPCPROC_TIMEOUT_WRITE = 0.001; +# Timeout for reading from Socket +my $HMCCURPCPROC_TIMEOUT_READ = 0.005; + # Timeout for accepting incoming connections in seconds (0 = default) my $HMCCURPCPROC_TIMEOUT_ACCEPT = 1; @@ -99,25 +106,40 @@ my $BINRPC_ERROR = 0x42696EFF; # BinRPC datatype mapping my %BINRPC_TYPE_MAPPING = ( - "BOOL" => $BINRPC_BOOL, - "INTEGER" => $BINRPC_INTEGER, - "STRING" => $BINRPC_STRING, - "FLOAT" => $BINRPC_DOUBLE, - "DOUBLE" => $BINRPC_DOUBLE, - "BASE64" => $BINRPC_BASE64, - "ARRAY" => $BINRPC_ARRAY, - "STRUCT" => $BINRPC_STRUCT + 'BOOL' => $BINRPC_BOOL, + 'INTEGER' => $BINRPC_INTEGER, + 'STRING' => $BINRPC_STRING, + 'FLOAT' => $BINRPC_DOUBLE, + 'DOUBLE' => $BINRPC_DOUBLE, + 'BASE64' => $BINRPC_BASE64, + 'ARRAY' => $BINRPC_ARRAY, + 'STRUCT' => $BINRPC_STRUCT ); # Read/Write flags for RPC methods (0=Read, 1=Write) my %RPC_METHODS = ( - 'putParamset' => 1, - 'getParamset' => 0, + 'putParamset' => 1, + 'getParamset' => 0, 'getParamsetDescription' => 0, - 'setValue' => 1, - 'getValue' => 0 + 'setValue' => 1, + 'getValue' => 0 ); +# RPC event types +# EV = Event +# ND = New device +# DD = Delete device +# RD = Replace device +# RA = Readded device +# UD = Update device +# IN = Init RPC connection +# EX = Exit RPC process +# SL = Server loop +# ST = Statistics (not in list of event types) +# TO = Timeout +my @RPC_EVENT_TYPES = ('EV', 'ND', 'DD', 'RD', 'RA', 'UD', 'IN', 'EX', 'SL', 'TO'); + + ###################################################################### # Functions ###################################################################### @@ -127,6 +149,7 @@ sub HMCCURPCPROC_Initialize ($); sub HMCCURPCPROC_Define ($$); sub HMCCURPCPROC_InitDevice ($$); sub HMCCURPCPROC_Undef ($$); +sub HMCCURPCPROC_Rename ($$); sub HMCCURPCPROC_DelayedShutdown ($); sub HMCCURPCPROC_Shutdown ($); sub HMCCURPCPROC_Attr ($@); @@ -137,6 +160,11 @@ sub HMCCURPCPROC_SetError ($$$); sub HMCCURPCPROC_SetState ($$); sub HMCCURPCPROC_ProcessEvent ($$); +# RPC information +sub HMCCURPCPROC_GetDeviceDesc ($;$); +sub HMCCURPCPROC_GetParamsetDesc ($;$); +sub HMCCURPCPROC_GetPeers ($;$); + # RPC server control functions sub HMCCURPCPROC_CheckProcessState ($$); sub HMCCURPCPROC_CleanupIO ($); @@ -153,6 +181,8 @@ sub HMCCURPCPROC_RPCPing ($); sub HMCCURPCPROC_RPCServerStarted ($); sub HMCCURPCPROC_RPCServerStopped ($); sub HMCCURPCPROC_SendRequest ($@); +sub HMCCURPCPROC_SendXMLRequest ($@); +sub HMCCURPCPROC_SendBINRequest ($@); sub HMCCURPCPROC_SetRPCState ($$$$); sub HMCCURPCPROC_StartRPCServer ($); sub HMCCURPCPROC_StopRPCServer ($$); @@ -160,6 +190,7 @@ sub HMCCURPCPROC_TerminateProcess ($); # Helper functions sub HMCCURPCPROC_GetAttribute ($$$$); +sub HMCCURPCPROC_GetKey ($); sub HMCCURPCPROC_HexDump ($$); # RPC server functions @@ -167,6 +198,10 @@ sub HMCCURPCPROC_ProcessRequest ($$); sub HMCCURPCPROC_HandleConnection ($$$$); sub HMCCURPCPROC_SendQueue ($$$$); sub HMCCURPCPROC_SendData ($$); +sub HMCCURPCPROC_ReceiveData ($$); +sub HMCCURPCPROC_ReadFromSocket ($$$); +sub HMCCURPCPROC_DataAvailableOnSocket ($$); +sub HMCCURPCPROC_WriteToSocket ($$$); sub HMCCURPCPROC_Write ($$$$); sub HMCCURPCPROC_WriteStats ($$); sub HMCCURPCPROC_NewDevicesCB ($$$); @@ -212,21 +247,24 @@ sub HMCCURPCPROC_Initialize ($) { my ($hash) = @_; - $hash->{DefFn} = "HMCCURPCPROC_Define"; - $hash->{UndefFn} = "HMCCURPCPROC_Undef"; - $hash->{SetFn} = "HMCCURPCPROC_Set"; - $hash->{GetFn} = "HMCCURPCPROC_Get"; - $hash->{ReadFn} = "HMCCURPCPROC_Read"; - $hash->{AttrFn} = "HMCCURPCPROC_Attr"; - $hash->{ShutdownFn} = "HMCCURPCPROC_Shutdown"; - $hash->{DelayedShutdownFn} = "HMCCURPCPROC_DelayedShutdown"; + $hash->{version} = $HMCCURPCPROC_VERSION; + + $hash->{DefFn} = 'HMCCURPCPROC_Define'; + $hash->{UndefFn} = 'HMCCURPCPROC_Undef'; + $hash->{RenameFn} = 'HMCCURPCPROC_Rename'; + $hash->{SetFn} = 'HMCCURPCPROC_Set'; + $hash->{GetFn} = 'HMCCURPCPROC_Get'; + $hash->{ReadFn} = 'HMCCURPCPROC_Read'; + $hash->{AttrFn} = 'HMCCURPCPROC_Attr'; + $hash->{ShutdownFn} = 'HMCCURPCPROC_Shutdown'; + $hash->{DelayedShutdownFn} = 'HMCCURPCPROC_DelayedShutdown'; $hash->{parseParams} = 1; - $hash->{AttrList} = "ccuflags:multiple-strict,expert,logEvents,ccuInit,queueEvents,noEvents,noInitialUpdate,statistics". - " rpcMaxEvents rpcQueueSend rpcQueueSize rpcMaxIOErrors". - " rpcServerAddr rpcServerPort rpcWriteTimeout rpcAcceptTimeout". - " rpcConnTimeout rpcStatistics rpcEventTimeout rpcPingCCU ". + $hash->{AttrList} = 'ccuflags:multiple-strict,expert,logEvents,ccuInit,queueEvents,noEvents,noInitialUpdate,statistics'. + ' rpcMaxEvents rpcQueueSend rpcQueueSize rpcMaxIOErrors'. + ' rpcServerAddr rpcServerPort rpcReadTimeout rpcWriteTimeout rpcAcceptTimeout'. + ' rpcConnTimeout rpcStatistics rpcEventTimeout rpcPingCCU '. $readingFnAttributes; } @@ -238,23 +276,24 @@ sub HMCCURPCPROC_Define ($$) { my ($hash, $a, $h) = @_; my $name = $hash->{NAME}; - my $hmccu_hash; + my $ioHash; my $ioname = ''; my $rpcip = ''; my $iface; - my $usage = "Usage: define $name HMCCURPCPROC { CCUHost | iodev={device} } { RPCPort | RPCInterface }"; + my $usage = 'Usage: define $name HMCCURPCPROC { CCUHost | iodev={device} } { RPCPort | RPCInterface }'; + my $errSource = "HMCCURPCPROC [$name]"; $hash->{version} = $HMCCURPCPROC_VERSION; - if (exists ($h->{iodev})) { + if (exists($h->{iodev})) { $ioname = $h->{iodev}; - return $usage if (scalar (@$a) < 3); - return "HMCCU I/O device $ioname not found" if (!exists ($defs{$ioname})); - return "Device $ioname is not a HMCCU device" if ($defs{$ioname}->{TYPE} ne 'HMCCU'); - $hmccu_hash = $defs{$ioname}; - if (scalar (@$a) < 4) { - $hash->{host} = $hmccu_hash->{host}; - $hash->{prot} = $hmccu_hash->{prot}; + return $usage if (scalar(@$a) < 3); + return "$errSource HMCCU I/O device $ioname not found" if (!exists($defs{$ioname})); + return "$errSource Device $ioname is not a HMCCU device" if ($defs{$ioname}->{TYPE} ne 'HMCCU'); + $ioHash = $defs{$ioname}; + if (scalar(@$a) < 4) { + $hash->{host} = $ioHash->{host}; + $hash->{prot} = $ioHash->{prot}; $iface = $$a[2]; } else { @@ -271,7 +310,7 @@ sub HMCCURPCPROC_Define ($$) $rpcip = HMCCU_ResolveName ($hash->{host}, 'N/A'); } else { - return $usage if (scalar (@$a) < 4); + return $usage if (scalar(@$a) < 4); if ($$a[2] =~ /^(https?):\/\/(.+)/) { $hash->{prot} = $1; $hash->{host} = $2; @@ -286,13 +325,11 @@ sub HMCCURPCPROC_Define ($$) # Find IO device foreach my $d (keys %defs) { my $dh = $defs{$d}; - next if (!exists ($dh->{TYPE}) || !exists ($dh->{NAME})); - next if ($dh->{TYPE} ne 'HMCCU'); - if ($dh->{ccuip} eq $rpcip) { - $hmccu_hash = $dh; - last; - } + next if (!exists ($dh->{TYPE}) || !exists ($dh->{NAME}) || $dh->{TYPE} ne 'HMCCU'); + if ($dh->{ccuip} eq $rpcip) { $ioHash = $dh; last; } } + + return $errSource."HMCCU I/O device not found" if (!defined($ioHash)); } # Store some definitions for delayed initialization @@ -301,33 +338,30 @@ sub HMCCURPCPROC_Define ($$) if ($init_done) { # Interactive define command while CCU not ready or no IO device defined - if (!defined ($hmccu_hash)) { + if (!defined($ioHash)) { 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"; - } + return $errSource.($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 ($hmccu_hash) || $hmccu_hash->{ccustate} ne 'active') { - HMCCU_Log ($hash, 2, "Cannot detect IO device, maybe CCU not ready. Trying later ..."); - readingsSingleUpdate ($hash, "state", "Pending", 1); + if ($ioHash->{ccustate} ne 'active') { + HMCCU_Log ($hash, 2, 'CCU not ready. Trying later ...'); + readingsSingleUpdate ($hash, 'state', 'Pending', 1); $hash->{ccudevstate} = 'pending'; return undef; } } # Initialize FHEM device, set IO device - my $rc = HMCCURPCPROC_InitDevice ($hmccu_hash, $hash); - return "Invalid port or interface $iface" if ($rc == 1); - return "Can't assign I/O device $ioname" if ($rc == 2); - return "Invalid local IP address ".$hash->{hmccu}{localaddr} if ($rc == 3); - return "RPC device for CCU/port already exists" if ($rc == 4); - return "Cannot connect to CCU ".$hash->{host}." interface $iface" if ($rc == 5); + my $rc = HMCCURPCPROC_InitDevice ($ioHash, $hash); + return "$errSource Invalid port or interface $iface" if ($rc == 1); + return "$errSource Can't assign I/O device $ioname" if ($rc == 2); + return "$errSource Invalid local IP address ".$hash->{hmccu}{localaddr} if ($rc == 3); + return "$errSource RPC device for CCU/port already exists" if ($rc == 4); + return "$errSource Cannot connect to CCU ".$hash->{host}." interface $iface" if ($rc == 5); return undef; } @@ -344,60 +378,62 @@ sub HMCCURPCPROC_Define ($$) # 5 = Cannot connect to CCU ###################################################################### -sub HMCCURPCPROC_InitDevice ($$) { - my ($hmccu_hash, $dev_hash) = @_; - my $name = $dev_hash->{NAME}; - my $iface = $dev_hash->{hmccu}{devspec}; +sub HMCCURPCPROC_InitDevice ($$) +{ + my ($ioHash, $devHash) = @_; + my $name = $devHash->{NAME}; + my $iface = $devHash->{hmccu}{devspec}; # Check if interface is valid - my ($ifname, $ifport) = HMCCU_GetRPCServerInfo ($hmccu_hash, $iface, 'name,port'); - return 1 if (!defined ($ifname) || !defined ($ifport)); + my ($ifname, $ifport) = HMCCU_GetRPCServerInfo ($ioHash, $iface, 'name,port'); + return 1 if (!defined($ifname) || !defined($ifport)); # Check if RPC device with same interface already exists foreach my $d (keys %defs) { my $dh = $defs{$d}; - next if (!exists ($dh->{TYPE}) || !exists ($dh->{NAME})); + next if (!exists($dh->{TYPE}) || !exists($dh->{NAME})); if ($dh->{TYPE} eq 'HMCCURPCPROC' && $dh->{NAME} ne $name && IsDisabled ($dh->{NAME}) != 1) { - return 4 if ($dev_hash->{host} eq $dh->{host} && exists ($dh->{rpcport}) && + return 4 if ($devHash->{host} eq $dh->{host} && exists ($dh->{rpcport}) && $dh->{rpcport} == $ifport); } } # Detect local IP address and check if CCU is reachable - my $localaddr = HMCCU_TCPConnect ($dev_hash->{host}, $ifport); + my $localaddr = HMCCU_TCPConnect ($devHash->{host}, $ifport); return 5 if ($localaddr eq ''); - $dev_hash->{hmccu}{localaddr} = $localaddr; - $dev_hash->{hmccu}{defaultaddr} = $dev_hash->{hmccu}{localaddr}; + $devHash->{hmccu}{localaddr} = $localaddr; + $devHash->{hmccu}{defaultaddr} = $devHash->{hmccu}{localaddr}; # Get unique ID for RPC server: last 2 segments of local IP address # Do not append random digits because of https://forum.fhem.de/index.php/topic,83544.msg797146.html#msg797146 - my $id1 = HMCCU_GetIdFromIP ($dev_hash->{hmccu}{localaddr}, ''); - my $id2 = HMCCU_GetIdFromIP ($hmccu_hash->{ccuip}, ''); + my $id1 = HMCCU_GetIdFromIP ($devHash->{hmccu}{localaddr}, ''); + my $id2 = HMCCU_GetIdFromIP ($ioHash->{ccuip}, ''); return 3 if ($id1 eq '' || $id2 eq ''); - $dev_hash->{rpcid} = $id1.$id2; + $devHash->{rpcid} = $id1.$id2; # Set I/O device and store reference for RPC device in I/O device - my $ioname = $hmccu_hash->{NAME}; - return 2 if (!HMCCU_AssignIODevice ($dev_hash, $ioname, $ifname)); + my $ioname = $ioHash->{NAME}; + return 2 if (!HMCCU_AssignIODevice ($devHash, $ioname, $ifname)); # Store internals - $dev_hash->{rpcport} = $ifport; - $dev_hash->{rpcinterface} = $ifname; - $dev_hash->{ccuip} = $hmccu_hash->{ccuip}; - $dev_hash->{ccutype} = $hmccu_hash->{ccutype}; - $dev_hash->{CCUNum} = $hmccu_hash->{CCUNum}; - $dev_hash->{ccustate} = $hmccu_hash->{ccustate}; + $devHash->{rpcport} = $ifport; + $devHash->{rpcinterface} = $ifname; + $devHash->{ccuip} = $ioHash->{ccuip}; + $devHash->{ccutype} = $ioHash->{ccutype}; + $devHash->{CCUNum} = $ioHash->{CCUNum}; + $devHash->{ccustate} = $ioHash->{ccustate}; - HMCCU_Log ($dev_hash, 1, "Initialized version $HMCCURPCPROC_VERSION for interface $ifname with I/O device $ioname"); + HMCCU_Log ($devHash, 1, "Initialized version $HMCCURPCPROC_VERSION for interface $ifname with I/O device $ioname"); # Set some attributes if ($init_done) { - $attr{$name}{stateFormat} = "rpcstate/state"; + $attr{$name}{stateFormat} = 'rpcstate/state'; $attr{$name}{verbose} = 2; } - - HMCCURPCPROC_ResetRPCState ($dev_hash); - HMCCURPCPROC_SetState ($dev_hash, 'Initialized'); + + # RPC device ready + HMCCURPCPROC_ResetRPCState ($devHash); + HMCCURPCPROC_SetState ($devHash, 'Initialized'); return 0; } @@ -410,24 +446,37 @@ sub HMCCURPCPROC_Undef ($$) { my ($hash, $arg) = @_; my $name = $hash->{NAME}; - my $hmccu_hash = $hash->{IODev}; + my $ioHash = $hash->{IODev}; my $ifname = $hash->{rpcinterface}; -# HMCCU_Log ($hash, 3, "Undef()"); - # Shutdown RPC server HMCCURPCPROC_StopRPCServer ($hash, $HMCCURPCPROC_INIT_INTERVAL2); # Delete RPC device name in I/O device - if (exists ($hmccu_hash->{hmccu}{interfaces}{$ifname}) && - exists ($hmccu_hash->{hmccu}{interfaces}{$ifname}{device}) && - $hmccu_hash->{hmccu}{interfaces}{$ifname}{device} eq $name) { - delete $hmccu_hash->{hmccu}{interfaces}{$ifname}{device}; + if (exists($ioHash->{hmccu}{interfaces}{$ifname}) && + exists($ioHash->{hmccu}{interfaces}{$ifname}{device}) && + $ioHash->{hmccu}{interfaces}{$ifname}{device} eq $name) { + delete $ioHash->{hmccu}{interfaces}{$ifname}{device}; } return undef; } +###################################################################### +# Rename device +###################################################################### + +sub HMCCURPCPROC_Rename ($$) +{ + my ($newName, $oldName) = @_; + my $hash = $defs{$newName}; + + my $ioHash = $hash->{IODev}; + my $ifName = $hash->{rpcinterface}; + + $ioHash->{hmccu}{interfaces}{$ifName}{device} = $newName; +} + ###################################################################### # Delayed shutdown FHEM ###################################################################### @@ -435,23 +484,21 @@ sub HMCCURPCPROC_Undef ($$) sub HMCCURPCPROC_DelayedShutdown ($) { my ($hash) = @_; - my $hmccu_hash = $hash->{IODev}; + my $ioHash = $hash->{IODev}; my $ifname = $hash->{rpcinterface}; -# HMCCU_Log ($hash, 3, "DelayedShutdown()"); - - my $delay = HMCCU_Max (AttrVal ("global", "maxShutdownDelay", 10)-2, 0); + my $delay = HMCCU_Max (AttrVal ('global', 'maxShutdownDelay', 10)-2, 0); # Shutdown RPC server - if (defined ($hmccu_hash) && exists ($hmccu_hash->{hmccu}{interfaces}{$ifname}{manager}) && - $hmccu_hash->{hmccu}{interfaces}{$ifname}{manager} eq 'HMCCURPCPROC') { - if (!exists ($hash->{hmccu}{delayedShutdown})) { + if (defined($ioHash) && exists($ioHash->{hmccu}{interfaces}{$ifname}{manager}) && + $ioHash->{hmccu}{interfaces}{$ifname}{manager} eq 'HMCCURPCPROC') { + if (!exists($hash->{hmccu}{delayedShutdown})) { $hash->{hmccu}{delayedShutdown} = $delay; HMCCU_Log ($hash, 1, "Graceful shutdown within $delay seconds"); HMCCURPCPROC_StopRPCServer ($hash, $delay); } else { - HMCCU_Log ($hash, 1, "Graceful shutdown already in progress"); + HMCCU_Log ($hash, 1, 'Graceful shutdown already in progress'); } } @@ -465,20 +512,18 @@ sub HMCCURPCPROC_DelayedShutdown ($) sub HMCCURPCPROC_Shutdown ($) { my ($hash) = @_; - my $hmccu_hash = $hash->{IODev}; + my $ioHash = $hash->{IODev}; my $ifname = $hash->{rpcinterface}; -# HMCCU_Log ($hash, 3, "Shutdown()"); - # Shutdown RPC server - if (defined ($hmccu_hash) && exists ($hmccu_hash->{hmccu}{interfaces}{$ifname}{manager}) && - $hmccu_hash->{hmccu}{interfaces}{$ifname}{manager} eq 'HMCCURPCPROC') { + if (defined($ioHash) && exists($ioHash->{hmccu}{interfaces}{$ifname}{manager}) && + $ioHash->{hmccu}{interfaces}{$ifname}{manager} eq 'HMCCURPCPROC') { if (!exists ($hash->{hmccu}{delayedShutdown})) { - HMCCU_Log ($hash, 1, "Immediate shutdown"); + HMCCU_Log ($hash, 1, 'Immediate shutdown'); HMCCURPCPROC_StopRPCServer ($hash, 0); } else { - HMCCU_Log ($hash, 1, "Graceful shutdown"); + HMCCU_Log ($hash, 1, 'Graceful shutdown'); } } @@ -498,7 +543,7 @@ sub HMCCURPCPROC_Attr ($@) my $hash = $defs{$name}; if ($cmd eq 'set') { - if (($attrname eq 'rpcAcceptTimeout' || $attrname eq 'rpcMaxEvents') && $attrval == 0) { + if ($attrname =~ /^(rpcAcceptTimeout|rpcReadTimeout|rpcWriteTimeout)$/ && $attrval == 0) { return "HMCCURPCPROC: [$name] Value for attribute $attrname must be greater than 0"; } elsif ($attrname eq 'rpcServerAddr') { @@ -507,11 +552,8 @@ sub HMCCURPCPROC_Attr ($@) elsif ($attrname eq 'rpcPingCCU') { HMCCU_Log ($hash, 1, "Attribute rpcPingCCU ignored. Please set it in I/O device"); } - elsif ($attrname eq 'ccuflags' && $attrval =~ /reconnect/) { - HMCCU_Log ($hash, 1, "Flag reconnect ignored. Please set it in I/O device"); - } - elsif ($attrname eq 'ccuflags' && $attrval =~ /logPong/) { - HMCCU_Log ($hash, 1, "Flag logPong ignored. Please set it in I/O device"); + elsif ($attrname eq 'ccuflags' && $attrval =~ /(reconnect|logPong)/) { + HMCCU_Log ($hash, 1, "Flag $1 ignored. Please set it in I/O device"); } } elsif ($cmd eq 'del') { @@ -519,7 +561,9 @@ sub HMCCURPCPROC_Attr ($@) $hash->{hmccu}{localaddr} = $hash->{hmccu}{defaultaddr}; } } - + + HMCCU_LogDisplay ($hash, 2, 'Please restart RPC server to apply attribute changes') if ($init_done); + return undef; } @@ -530,16 +574,14 @@ sub HMCCURPCPROC_Attr ($@) sub HMCCURPCPROC_Set ($@) { my ($hash, $a, $h) = @_; - my $hmccu_hash = $hash->{IODev}; + my $ioHash = $hash->{IODev}; my $name = shift @$a; - my $opt = shift @$a; - - return "No set command specified" if (!defined ($opt)); + my $opt = shift @$a // return 'No set command specified'; my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $options = $ccuflags =~ /expert/ ? - "cleanup:noArg deregister:noArg register:noArg rpcrequest rpcserver:on,off" : ""; - my $busyoptions = $ccuflags =~ /expert/ ? "rpcserver:off" : ""; + 'cleanup:noArg deregister:noArg register:noArg rpcrequest rpcserver:on,off' : ''; + my $busyoptions = $ccuflags =~ /expert/ ? 'rpcserver:off' : ''; return "HMCCURPCPROC: CCU busy, choose one of $busyoptions" if ($opt ne 'rpcserver' && HMCCURPCPROC_IsRPCStateBlocking ($hash)); @@ -553,14 +595,14 @@ sub HMCCURPCPROC_Set ($@) my ($rc, $rcmsg) = HMCCURPCPROC_RegisterCallback ($hash, 2); if ($rc) { $hash->{ccustate} = 'active'; - return HMCCURPCPROC_SetState ($hash, "OK"); + return HMCCURPCPROC_SetState ($hash, 'OK'); } else { return HMCCURPCPROC_SetError ($hash, $rcmsg, 2); } } else { - return HMCCURPCPROC_SetError ($hash, "RPC server not running", 2); + return HMCCURPCPROC_SetError ($hash, 'RPC server not running', 2); } } elsif ($opt eq 'deregister') { @@ -569,24 +611,22 @@ sub HMCCURPCPROC_Set ($@) return HMCCURPCPROC_SetState ($hash, "OK"); } elsif ($opt eq 'rpcrequest') { - my $request = shift @$a; - return HMCCURPCPROC_SetError ($hash, "Usage: set $name rpcrequest {request} [{parameter} ...]", 2) - if (!defined ($request)); + my $request = shift @$a // return HMCCURPCPROC_SetError ( + $hash, "Usage: set $name rpcrequest {request} [{parameter} ...]", 2); my $response = HMCCURPCPROC_SendRequest ($hash, $request, @$a); - return HMCCURPCPROC_SetError ($hash, "RPC request failed", 2) if (!defined ($response)); + return HMCCURPCPROC_SetError ($hash, 'RPC request failed', 2) if (!defined($response)); return HMCCU_RefToString ($response); } elsif ($opt eq 'rpcserver') { my $action = shift @$a; - return HMCCURPCPROC_SetError ($hash, "Usage: set $name rpcserver {on|off}", 2) - if (!defined ($action) || $action !~ /^(on|off)$/); + if (!defined($action) || $action !~ /^(on|off)$/); if ($action eq 'on') { - return HMCCURPCPROC_SetError ($hash, "RPC server already running", 2) + return HMCCURPCPROC_SetError ($hash, 'RPC server already running', 2) if ($hash->{RPCState} ne 'inactive' && $hash->{RPCState} ne 'error'); - $hmccu_hash->{hmccu}{interfaces}{$hash->{rpcinterface}}{manager} = 'HMCCURPCPROC'; + $ioHash->{hmccu}{interfaces}{$hash->{rpcinterface}}{manager} = 'HMCCURPCPROC'; my ($rc, $info) = HMCCURPCPROC_StartRPCServer ($hash); if (!$rc) { HMCCURPCPROC_SetRPCState ($hash, 'error', undef, undef); @@ -594,15 +634,14 @@ sub HMCCURPCPROC_Set ($@) } } elsif ($action eq 'off') { - $hmccu_hash->{hmccu}{interfaces}{$hash->{rpcinterface}}{manager} = 'HMCCURPCPROC'; + $ioHash->{hmccu}{interfaces}{$hash->{rpcinterface}}{manager} = 'HMCCURPCPROC'; HMCCURPCPROC_StopRPCServer ($hash, $HMCCURPCPROC_INIT_INTERVAL2); } return undef; } - else { - return "HMCCURPCPROC: Unknown argument $opt, choose one of ".$options; - } + + return "HMCCURPCPROC: Unknown argument $opt, choose one of $options"; } ###################################################################### @@ -614,30 +653,50 @@ sub HMCCURPCPROC_Get ($@) my ($hash, $a, $h) = @_; my $ioHash = $hash->{IODev}; my $name = shift @$a; - my $opt = shift @$a; - - return "No get command specified" if (!defined ($opt)); + my $opt = shift @$a // return 'No get command specified'; my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - my $options = "rpcevents:noArg rpcstate:noArg"; + my $options = 'deviceDesc rpcevents:noArg rpcstate:noArg peers:noArg'; - return "HMCCURPCPROC: CCU busy, choose one of rpcstate:noArg" + return 'HMCCURPCPROC: CCU busy, choose one of rpcstate:noArg' if ($opt ne 'rpcstate' && HMCCURPCPROC_IsRPCStateBlocking ($hash)); my $result = 'Command not implemented'; my $rc; - if ($opt eq 'rpcevents') { - my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); - my $clkey = 'CB'.$hash->{rpcport}.$hash->{rpcid}; + if ($opt eq 'deviceDesc') { + my $address; + my $object = shift @$a; + if (defined($object)) { + if (exists($defs{$object})) { + my $clHash = $defs{$object}; + my $clType = $clHash->{TYPE}; + return HMCCURPCPROC_SetError ($hash, "Illegal device type $clType", 2) + if ($clType ne 'HMCCUCHN' && $clType ne 'HMCCUDEV'); + $address = $clHash->{ccuaddr}; + } + else { + $address = $object; + } + ($address, undef) = HMCCU_SplitChnAddr ($address); + } + HMCCU_ResetDeviceTables ($ioHash, $hash->{rpcinterface}, $address); + my $cd = HMCCURPCPROC_GetDeviceDesc ($hash, $address); + my $cm = HMCCURPCPROC_GetParamsetDesc ($hash, $address); + return "Read $cd channel and device descriptions and $cm device models from CCU"; + } + elsif ($opt eq 'peers') { + my $cp = HMCCURPCPROC_GetPeers ($hash); + return "Read $cp links from CCU"; + } + elsif ($opt eq 'rpcevents') { + my $clkey = HMCCURPCPROC_GetKey ($hash); $result = "Event statistics for server $clkey\n"; $result .= "Average event delay = ".$hash->{hmccu}{rpc}{avgdelay}."\n" if (defined ($hash->{hmccu}{rpc}{avgdelay})); - $result .= "========================================\n"; - $result .= "ET Sent by RPC server Received by FHEM\n"; - $result .= "----------------------------------------\n"; - foreach my $et (@eventtypes) { + $result .= ('=' x 40)."\nET Sent by RPC server Received by FHEM\n".('-' x 40)."\n"; + foreach my $et (@RPC_EVENT_TYPES) { my $snd = exists ($hash->{hmccu}{rpc}{snd}{$et}) ? sprintf ("%7d", $hash->{hmccu}{rpc}{snd}{$et}) : " n/a"; my $rec = exists ($hash->{hmccu}{rpc}{rec}{$et}) ? @@ -646,33 +705,29 @@ sub HMCCURPCPROC_Get ($@) } if ($ccuflags =~ /statistics/ && exists ($hash->{hmccu}{stats}{rcv})) { my $eh = HMCCU_MaxHashEntries ($hash->{hmccu}{stats}{rcv}, 3); - $result .= "========================================\n"; - $result .= "Top Sender\n"; - $result .= "========================================\n"; + $result .= ('=' x 40)."\nTop Sender\n".('=' x 40)."\n"; for (my $i=0; $i<3; $i++) { last if (!exists ($eh->{$i})); my $dn = HMCCU_GetDeviceName ($ioHash, $eh->{$i}{k}, '?'); $result .= "$eh->{$i}{k} / $dn : $eh->{$i}{v}\n"; } } - return $result eq '' ? "No event statistics found" : $result; + return $result eq '' ? 'No event statistics found' : $result; } elsif ($opt eq 'rpcstate') { - my $clkey = 'CB'.$hash->{rpcport}.$hash->{rpcid}; - $result = "PID RPC-Process State \n"; - $result .= "--------------------------------\n"; + my $clkey = HMCCURPCPROC_GetKey ($hash); + $result = "PID RPC-Process State \n".('-' x 32)."\n"; my $sid = defined ($hash->{hmccu}{rpc}{pid}) ? sprintf ("%5d", $hash->{hmccu}{rpc}{pid}) : "N/A "; my $sname = sprintf ("%-10s", $clkey); my $cbport = defined ($hash->{hmccu}{rpc}{cbport}) ? $hash->{hmccu}{rpc}{cbport} : "N/A"; my $addr = defined ($hash->{hmccu}{localaddr}) ? $hash->{hmccu}{localaddr} : "N/A"; - $result .= $sid." ".$sname." ".$hash->{hmccu}{rpc}{state}."\n\n"; - $result .= "Local address = $addr\n"; - $result .= "Callback port = $cbport\n"; + $result .= $sid." ".$sname." ".$hash->{hmccu}{rpc}{state}."\n\n". + "Local address = $addr\n". + "Callback port = $cbport\n"; return $result; } - else { - return "HMCCURPCPROC: Unknown argument $opt, choose one of ".$options; - } + + return "HMCCURPCPROC: Unknown argument $opt, choose one of $options"; } ###################################################################### @@ -683,7 +738,7 @@ sub HMCCURPCPROC_Read ($) { my ($hash) = @_; my $name = $hash->{NAME}; - my $hmccu_hash = $hash->{IODev}; + my $ioHash = $hash->{IODev}; my $eventcount = 0; # Total number of events my $devcount = 0; # Number of DD, ND or RD events @@ -691,65 +746,64 @@ sub HMCCURPCPROC_Read ($) my %events = (); my %devices = (); - HMCCU_Log ($hash, 4, "Read called"); - # Check if child socket exists if (!defined ($hash->{hmccu}{sockchild})) { - HMCCU_Log ($hash, 2, "Child socket does not exist"); + HMCCU_Log ($hash, 2, 'Child socket does not exist'); return; } # Get attributes my $rpcmaxevents = AttrVal ($name, 'rpcMaxEvents', $HMCCURPCPROC_MAX_EVENTS); my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - my $hmccuflags = AttrVal ($hmccu_hash->{NAME}, 'ccuflags', 'null'); + my $hmccuflags = AttrVal ($ioHash->{NAME}, 'ccuflags', 'null'); my $socktimeout = AttrVal ($name, 'rpcWriteTimeout', $HMCCURPCPROC_TIMEOUT_WRITE); # Read events from queue while (1) { my ($item, $err) = HMCCURPCPROC_ReceiveData ($hash->{hmccu}{sockchild}, $socktimeout); - if (!defined ($item)) { + if (!defined($item)) { HMCCU_Log ($hash, 4, "Read stopped after $eventcount events $err"); last; } HMCCU_Log ($hash, 4, "read $item from queue") if ($ccuflags =~ /logEvents/); my ($et, $clkey, @par) = HMCCURPCPROC_ProcessEvent ($hash, $item); - next if (!defined ($et)); + next if (!defined($et)); if ($et eq 'EV') { - $events{$par[0]}{$par[1]}{$par[2]} = $par[3]; + $events{$par[0]}{$par[1]}{VALUES}{$par[2]} = $par[3]; $evcount++; $hash->{ccustate} = 'active' if ($hash->{ccustate} ne 'active'); # Count events per device for statistics - if ($ccuflags =~ /statistics/) { - if (exists ($hash->{hmccu}{stats}{rcv}{$par[0]})) { - $hash->{hmccu}{stats}{rcv}{$par[0]}++; - } - else { - $hash->{hmccu}{stats}{rcv}{$par[0]} = 1; - } - } + $hash->{hmccu}{stats}{rcv}{$par[0]}++ if ($ccuflags =~ /statistics/); } elsif ($et eq 'EX') { # I/O already cleaned up. Leave Read() last; } elsif ($et eq 'ND') { - $devices{$par[0]}{flag} = 'N'; - $devices{$par[0]}{version} = $par[3]; - if ($par[1] eq 'D') { - $devices{$par[0]}{addtype} = 'dev'; - $devices{$par[0]}{type} = $par[2]; - $devices{$par[0]}{firmware} = $par[4]; - $devices{$par[0]}{rxmode} = $par[5]; - } - else { - $devices{$par[0]}{addtype} = 'chn'; - $devices{$par[0]}{usetype} = $par[2]; - } - $devcount++; +# HMCCU_Log ($hash, 2, "ND: ".join(';', @par)); +# $devices{$par[0]}{flag} = 'N'; +# $devices{$par[0]}{version} = $par[3]; +# $devices{$par[0]}{paramsets} = $par[6]; +# if ($par[1] eq 'D') { +# $devices{$par[0]}{addtype} = 'dev'; +# $devices{$par[0]}{type} = $par[2]; +# $devices{$par[0]}{firmware} = $par[4]; +# $devices{$par[0]}{rxmode} = $par[5]; +# $devices{$par[0]}{children} = $par[10]; +# } +# else { +# $devices{$par[0]}{addtype} = 'chn'; +# $devices{$par[0]}{usetype} = $par[2]; +# $devices{$par[0]}{sourceroles} = $par[7]; +# $devices{$par[0]}{targetroles} = $par[8]; +# $devices{$par[0]}{direction} = $par[9]; +# $devices{$par[0]}{parent} = $par[11]; +# $devices{$par[0]}{aes} = $par[12]; +# } +# $devcount++; } elsif ($et eq 'DD') { $devices{$par[0]}{flag} = 'D'; @@ -769,11 +823,9 @@ sub HMCCURPCPROC_Read ($) } # Update device table and client device readings - HMCCU_UpdateDeviceTable ($hmccu_hash, \%devices) if ($devcount > 0); - HMCCU_UpdateMultipleDevices ($hmccu_hash, \%events) + HMCCU_UpdateDeviceTable ($ioHash, \%devices) if ($devcount > 0); + HMCCU_UpdateMultipleDevices ($ioHash, \%events) if ($evcount > 0 && $ccuflags !~ /noEvents/ && $hmccuflags !~ /noEvents/); - - HMCCU_Log ($hash, 4, "Read finished"); } ###################################################################### @@ -784,9 +836,9 @@ sub HMCCURPCPROC_Read ($) sub HMCCURPCPROC_SetError ($$$) { my ($hash, $text, $level) = @_; - my $msg = defined ($text) ? $text : "unknown error"; + my $msg = defined ($text) ? $text : 'unknown error'; - HMCCURPCPROC_SetState ($hash, "error"); + HMCCURPCPROC_SetState ($hash, 'error'); HMCCU_Log ($hash, (defined($level) ? $level : 1), $msg); return $msg; @@ -800,8 +852,8 @@ sub HMCCURPCPROC_SetState ($$) { my ($hash, $state) = @_; - if (defined ($state)) { - readingsSingleUpdate ($hash, "state", $state, 1); + if (defined($state)) { + readingsSingleUpdate ($hash, 'state', $state, 1); HMCCU_Log ($hash, 4, "Set state to $state"); } @@ -817,23 +869,23 @@ sub HMCCURPCPROC_SetRPCState ($$$$) { my ($hash, $state, $msg, $level) = @_; my $name = $hash->{NAME}; - my $hmccu_hash = $hash->{IODev}; + my $ioHash = $hash->{IODev}; - return undef if (exists ($hash->{RPCState}) && $hash->{RPCState} eq $state); + return undef if (exists($hash->{RPCState}) && $hash->{RPCState} eq $state); $hash->{hmccu}{rpc}{state} = $state; $hash->{RPCState} = $state; - readingsSingleUpdate ($hash, "rpcstate", $state, 1); + readingsSingleUpdate ($hash, 'rpcstate', $state, 1); HMCCURPCPROC_SetState ($hash, 'busy') if ($state ne 'running' && $state ne 'inactive' && $state ne 'error' && ReadingsVal ($name, 'state', '') ne 'busy'); - HMCCU_Log ($hash, (defined($level) ? $level : 1), $msg) if (defined ($msg)); + HMCCU_Log ($hash, (defined($level) ? $level : 1), $msg) if (defined($msg)); HMCCU_Log ($hash, 4, "Set rpcstate to $state"); # Set state of interface in I/O device - HMCCU_SetRPCState ($hmccu_hash, $state, $hash->{rpcinterface}); + HMCCU_SetRPCState ($ioHash, $state, $hash->{rpcinterface}); return undef; } @@ -846,9 +898,7 @@ sub HMCCURPCPROC_ResetRPCState ($) { my ($hash) = @_; - HMCCU_Log ($hash, 4, "Reset RPC state"); - - $hash->{RPCPID} = "0"; + $hash->{RPCPID} = '0'; $hash->{hmccu}{rpc}{pid} = undef; $hash->{hmccu}{rpc}{clkey} = undef; $hash->{hmccu}{evtime} = 0; @@ -865,8 +915,8 @@ sub HMCCURPCPROC_IsRPCStateBlocking ($) { my ($hash) = @_; - return (exists ($hash->{RPCState}) && - ($hash->{RPCState} eq "running" || $hash->{RPCState} eq "inactive")) ? 0 : 1; + return (exists($hash->{RPCState}) && + ($hash->{RPCState} eq 'running' || $hash->{RPCState} eq 'inactive')) ? 0 : 1; } ###################################################################### @@ -879,64 +929,47 @@ sub HMCCURPCPROC_ProcessEvent ($$) my $name = $hash->{NAME}; my $rpcname = 'CB'.$hash->{rpcport}.$hash->{rpcid}; my $rh = \%{$hash->{hmccu}{rpc}}; # Just for code simplification - my $hmccu_hash = $hash->{IODev}; - my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($hmccu_hash); + my $ioHash = $hash->{IODev}; + my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($ioHash); # Number of arguments in RPC events (without event type and clkey) my %rpceventargs = ( - "EV", 4, - "ND", 6, - "DD", 1, - "RD", 2, - "RA", 1, - "UD", 2, - "IN", 2, - "EX", 2, - "SL", 1, - "TO", 1, - "ST", 11 + 'EV', 4, 'ND', 13, 'DD', 1, 'RD', 2, 'RA', 1, 'UD', 2, 'IN', 2, 'EX', 2, 'SL', 1, + 'TO', 1, 'ST', 11 ); - my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - my $ping = AttrVal ($hmccu_hash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); - my $evttimeout = ($ping > 0 && $hash->{rpcinterface} eq $defInterface) ? $ping*2 : - HMCCURPCPROC_GetAttribute ($hash, 'rpcEventTimeout', 'rpcevtimeout', $HMCCURPCPROC_TIMEOUT_EVENT); - return undef if (!defined ($event) || $event eq ''); + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); + my $ping = AttrVal ($ioHash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); + my $evttimeout = ($ping > 0 && $hash->{rpcinterface} eq $defInterface) ? $ping*2 : + HMCCURPCPROC_GetAttribute ($hash, 'rpcEventTimeout', 'rpcevtimeout', $HMCCURPCPROC_TIMEOUT_EVENT); + # Log event HMCCU_Log ($hash, 2, "CCUEvent = $event") if ($ccuflags =~ /logEvents/); # Detect event type and clkey my ($et, $clkey, $evdata) = split (/\|/, $event, 3); - if (!defined ($evdata)) { - HMCCU_Log ($hash, 2, "Syntax error in RPC event data $event"); - return undef; - } + return HMCCU_Log ($hash, 2, "Syntax error in RPC event data $event", undef) + if (!defined($evdata)); # Check for valid server - if ($clkey ne $rpcname) { - HMCCU_Log ($hash, 2, "Received $et event for unknown RPC server $clkey"); - return undef; - } + return HMCCU_Log ($hash, 2, "Received $et event for unknown RPC server $clkey", undef) + if ($clkey ne $rpcname); # Check event type if (!exists ($rpceventargs{$et})) { $et =~ s/([\x00-\xFF])/sprintf("0x%X ",ord($1))/eg; - HMCCU_Log ($hash, 2, "Received unknown event from CCU: ".$et); - return undef; + return HMCCU_Log ($hash, 2, "Received unknown event from CCU: $et", undef); } # Parse event my @t = split (/\|/, $evdata, $rpceventargs{$et}); - my $tc = scalar (@t); + my $tc = scalar(@t); # Check event parameters - if ($tc != $rpceventargs{$et}) { - HMCCU_Log ($hash, 2, "Wrong number of $tc parameters in event $event. Expected ". - $rpceventargs{$et}); - return undef; - } + return HMCCU_Log ($hash, 2, "Wrong number of $tc parameters in event $event. Expected ". + $rpceventargs{$et}, undef) if ($tc != $rpceventargs{$et}); # Update statistic counters $rh->{rec}{$et}++; @@ -953,9 +986,9 @@ sub HMCCURPCPROC_ProcessEvent ($$) $rh->{avgdelay} = $rh->{sumdelay}/$rh->{rec}{$et}; $hash->{ccustate} = 'active' if ($hash->{ccustate} ne 'active'); HMCCU_Log ($hash, 3, "Received CENTRAL event from $clkey. ".$t[2]."=".$t[3]) - if ($t[1] eq 'CENTRAL' && $t[3] eq $rpcname && HMCCU_IsFlag ($hmccu_hash->{NAME}, 'logPong')); + if ($t[1] eq 'CENTRAL' && $t[3] eq $rpcname && HMCCU_IsFlag ($ioHash->{NAME}, 'logPong')); my ($add, $chn) = split (/:/, $t[1]); - return defined ($chn) ? ($et, $clkey, $add, $chn, $t[2], $t[3]) : undef; + return defined($chn) ? ($et, $clkey, $add, $chn, @t[2,3]) : undef; } elsif ($et eq 'SL') { # @@ -977,8 +1010,7 @@ sub HMCCURPCPROC_ProcessEvent ($$) return ($et, $clkey, ($srun == 0 ? 1 : 0), $srun); } else { - HMCCU_Log ($hash, 0, "Received SL event. Wrong PID=".$t[0]." for RPC server $clkey"); - return undef; + return HMCCU_Log ($hash, 0, "Received SL event. Wrong PID=".$t[0]." for RPC server $clkey", undef); } } elsif ($et eq 'IN') { @@ -1006,10 +1038,12 @@ sub HMCCURPCPROC_ProcessEvent ($$) elsif ($et eq 'ND') { # # CCU device added - # Input: ND|clkey|C/D|Address|Type|Version|Firmware|RxMode - # Output: ND, clkey, DevAdd, C/D, Type, Version, Firmware, RxMode + # Input: ND|clkey|C/D|Address|Type|Version|Firmware|RxMode|Paramsets| + # LinkSourceRoles|LinkTargetRoles|Direction|Children|Parent|AESActive + # Output: ND, clkey, DevAdd, C/D, Type, Version, Firmware, RxMode, Paramsets, + # LinkSourceRoles, LinkTargetRoles, Direction, Children, Parent, AESActive # - return ($et, $clkey, $t[1], $t[0], $t[2], $t[3], $t[4], $t[5]); + return ($et, $clkey, @t[1,0,2..12]); } elsif ($et eq 'DD' || $et eq 'RA') { # @@ -1025,7 +1059,7 @@ sub HMCCURPCPROC_ProcessEvent ($$) # Input: UD|clkey|Address|Hint # Output: UD, clkey, DevAdd, Hint # - return ($et, $clkey, $t[0], $t[1]); + return ($et, $clkey, @t[0,1]); } elsif ($et eq 'RD') { # @@ -1033,7 +1067,7 @@ sub HMCCURPCPROC_ProcessEvent ($$) # Input: RD|clkey|Address1|Address2 # Output: RD, clkey, Address1, Address2 # - return ($et, $clkey, $t[0], $t[1]); + return ($et, $clkey, @t[0,1]); } elsif ($et eq 'ST') { # @@ -1044,9 +1078,8 @@ sub HMCCURPCPROC_ProcessEvent ($$) my @res = ($et, $clkey); push (@res, @t); my $total = shift @t; - my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); - for (my $i=0; $i{hmccu}{rpc}{snd}{$eventtypes[$i]} += $t[$i]; + for (my $i=0; $i{hmccu}{rpc}{snd}{$RPC_EVENT_TYPES[$i]} += $t[$i]; } return @res; } @@ -1061,7 +1094,7 @@ sub HMCCURPCPROC_ProcessEvent ($$) $hash->{ccustate} = 'timeout'; if ($hash->{RPCState} eq 'running' && $hash->{rpcport} == $defPort) { # If interface is default interface inform IO device about timeout - HMCCU_EventsTimedOut ($hmccu_hash) + HMCCU_EventsTimedOut ($ioHash) } DoTrigger ($name, "No events from interface $clkey for ".$t[0]." seconds"); } @@ -1079,22 +1112,155 @@ sub HMCCURPCPROC_GetAttribute ($$$$) { my ($hash, $attr, $ioattr, $default) = @_; my $name = $hash->{NAME}; - my $hmccu_hash = $hash->{IODev}; + my $ioHash = $hash->{IODev}; my $value = 'null'; - if (defined ($attr)) { + if (defined($attr)) { $value = AttrVal ($name, $attr, 'null'); return $value if ($value ne 'null'); } - if (defined ($ioattr)) { - $value = AttrVal ($hmccu_hash->{NAME}, $ioattr, 'null'); + if (defined($ioattr)) { + $value = AttrVal ($ioHash->{NAME}, $ioattr, 'null'); return $value if ($value ne 'null'); } return $default; } +###################################################################### +# Get links (sender and receiver) from CCU. +###################################################################### + +sub HMCCURPCPROC_GetPeers ($;$) +{ + my ($hash, $address) = @_; + my $ioHash = $hash->{IODev}; + my $c = 0; + + my $rd = defined($address) ? + HMCCURPCPROC_SendRequest ($hash, 'getLinks', $address) : + HMCCURPCPROC_SendRequest ($hash, 'getLinks'); + + if (!defined($rd)) { + my $msg = defined($address) ? "Can't get peers of device $address" : + "Can't get full list of peers"; + return HMCCU_Log ($hash, 2, $msg, 0); + } + + if (ref($rd) eq 'HASH' && exists($rd->{faultString})) { + return HMCCU_Log ($hash, 2, "Can't get peers. ".$rd->{faultString}, 0); + } + elsif (ref($rd) eq 'ARRAY') { + $c = HMCCU_AddPeers ($ioHash, $rd, $hash->{rpcinterface}); + } + else { + return HMCCU_Log ($hash, 2, 'Unexpected response from getLinks', 0); + } + + return $c; +} + +###################################################################### +# Get RPC device descriptions from CCU recursively. Add devices to +# IO device. +# If address is not specified, fetch description of all devices. +# Return number of devices and channels read from CCU. +###################################################################### + +sub HMCCURPCPROC_GetDeviceDesc ($;$) +{ + my ($hash, $address) = @_; + my $ioHash = $hash->{IODev}; + my $c = 0; + + my $rd = defined($address) ? + HMCCURPCPROC_SendRequest ($hash, 'getDeviceDescription', $address) : + HMCCURPCPROC_SendRequest ($hash, 'listDevices'); + + if (!defined($rd)) { + my $msg = defined($address) ? "Can't get description of device $address" : + "Can't get full list of device descriptions"; + return HMCCU_Log ($hash, 2, $msg); + } + + if (ref($rd) eq 'HASH') { + return HMCCU_Log ($hash, 2, "Can't get device description. ".$rd->{faultString}, 0) + if (exists($rd->{faultString})); + + if (HMCCU_AddDeviceDesc ($ioHash, $rd, 'ADDRESS', $hash->{rpcinterface})) { + $c = 1; + if (defined($rd->{CHILDREN}) && ref($rd->{CHILDREN}) eq 'ARRAY') { + foreach my $child (@{$rd->{CHILDREN}}) { + $c += HMCCURPCPROC_GetDeviceDesc ($hash, $child); + } + } + } + } + elsif (ref($rd) eq 'ARRAY') { + foreach my $dev (@$rd) { + $c++ if (HMCCU_AddDeviceDesc ($ioHash, $dev, 'ADDRESS', $hash->{rpcinterface})); + } + } + + return $c; +} + +###################################################################### +# Get RPC device paramset descriptions from CCU +# Function is called recursively +# Parameters: +# $address - Device or channel address. If not specified, all +# addresses known by IO device are used. +# Return number of devices read from CCU. +###################################################################### + +sub HMCCURPCPROC_GetParamsetDesc ($;$) +{ + my ($hash, $address) = @_; + my $ioHash = $hash->{IODev}; + + my $c = 0; + + if (defined($address)) { + my $devDesc = HMCCU_GetDeviceDesc ($ioHash, $address, $hash->{rpcinterface}); + return 0 if (!defined($devDesc) || !defined($devDesc->{PARAMSETS}) || $devDesc->{PARAMSETS} eq '' || + !exists($devDesc->{_fw_ver})); + + my $chnNo = ($devDesc->{_addtype} eq 'chn') ? $devDesc->{INDEX} : 'd'; + + # Check if model already exists + return 0 if (HMCCU_ExistsDeviceModel ($ioHash, $devDesc->{_model}, $devDesc->{_fw_ver}, $chnNo)); + + # Read all paramset definitions + foreach my $ps (split (',', $devDesc->{PARAMSETS})) { + my $rm = HMCCURPCPROC_SendRequest ($hash, "getParamsetDescription", $address, $ps); + if (defined($rm) && ref($rm) eq 'HASH' && !exists($rm->{faultString})) { + HMCCU_AddDeviceModel ($ioHash, $rm, $devDesc->{_model}, $devDesc->{_fw_ver}, $ps, $chnNo); + } + else { + HMCCU_Log ($hash, 2, "Can't get description of paramset $ps for address $address"); + } + } + + $c = 1; + + # Read paramset definitions of childs + if (defined($devDesc->{CHILDREN}) && $devDesc->{CHILDREN} ne '') { + foreach my $child (split (',', $devDesc->{CHILDREN})) { + $c += HMCCURPCPROC_GetParamsetDesc ($hash, $child); + } + } + } + else { + foreach my $a (HMCCU_GetDeviceAddresses ($ioHash, $hash->{rpcinterface}, "_addtype=dev")) { + $c += HMCCURPCPROC_GetParamsetDesc ($hash, $a); + } + } + + return $c; +} + ###################################################################### # Register callback for specified CCU interface port. # Parameter force: @@ -1110,35 +1276,34 @@ sub HMCCURPCPROC_RegisterCallback ($$) { my ($hash, $force) = @_; my $name = $hash->{NAME}; - my $hmccu_hash = $hash->{IODev}; + my $ioHash = $hash->{IODev}; my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); my $port = $hash->{rpcport}; my $serveraddr = $hash->{host}; my $localaddr = $hash->{hmccu}{localaddr}; - my $clkey = 'CB'.$port.$hash->{rpcid}; + my $clkey = HMCCURPCPROC_GetKey ($hash); return (0, "RPC server $clkey not in state working") if ($hash->{hmccu}{rpc}{state} ne 'working' && $force == 0); + return (0, "CCU port $port not reachable") + if ($force == 2 && !HMCCU_TCPConnect ($hash->{host}, $port)); - if ($force == 2) { - return (0, "CCU port $port not reachable") if (!HMCCU_TCPConnect ($hash->{host}, $port)); - } - - my $cburl = HMCCU_GetRPCCallbackURL ($hmccu_hash, $localaddr, $hash->{hmccu}{rpc}{cbport}, $clkey, $port); - my $clurl = HMCCU_BuildURL ($hmccu_hash, $port); - my ($rpctype) = HMCCU_GetRPCServerInfo ($hmccu_hash, $port, 'type'); - return (0, "Can't get RPC parameters for ID $clkey") if (!defined ($cburl) || !defined ($clurl) || !defined ($rpctype)); + my $cburl = HMCCU_GetRPCCallbackURL ($ioHash, $localaddr, $hash->{hmccu}{rpc}{cbport}, $clkey, $port); + my $clurl = HMCCU_BuildURL ($ioHash, $port); + my ($rpctype) = HMCCU_GetRPCServerInfo ($ioHash, $port, 'type'); + return (0, "Can't get RPC parameters for ID $clkey") + if (!defined($cburl) || !defined($clurl) || !defined($rpctype)); - $hash->{hmccu}{rpc}{port} = $port; + $hash->{hmccu}{rpc}{port} = $port; $hash->{hmccu}{rpc}{clurl} = $clurl; $hash->{hmccu}{rpc}{cburl} = $cburl; HMCCU_Log ($hash, 2, "Registering callback $cburl of type $rpctype with ID $clkey at $clurl"); my $rc = HMCCURPCPROC_SendRequest ($hash, "init", "$cburl:STRING", "$clkey:STRING"); - if (defined ($rc)) { + if (defined($rc)) { return (1, $ccuflags !~ /ccuInit/ ? 'running' : 'registered'); } else { @@ -1154,10 +1319,10 @@ sub HMCCURPCPROC_DeRegisterCallback ($$) { my ($hash, $force) = @_; my $name = $hash->{NAME}; - my $hmccu_hash = $hash->{IODev}; + my $ioHash = $hash->{IODev}; my $port = $hash->{rpcport}; - my $clkey = 'CB'.$port.$hash->{rpcid}; + my $clkey = HMCCURPCPROC_GetKey ($hash); my $localaddr = $hash->{hmccu}{localaddr}; my $cburl = ''; my $clurl = ''; @@ -1166,24 +1331,24 @@ sub HMCCURPCPROC_DeRegisterCallback ($$) return (0, "RPC server $clkey not in state registered or running") if ($rpchash->{state} ne 'registered' && $rpchash->{state} ne 'running' && $force == 0); - $cburl = $rpchash->{cburl} if (exists ($rpchash->{cburl})); - $clurl = $rpchash->{clurl} if (exists ($rpchash->{clurl})); - $cburl = HMCCU_GetRPCCallbackURL ($hmccu_hash, $localaddr, $rpchash->{cbport}, $clkey, $port) if ($cburl eq ''); - $clurl = HMCCU_BuildURL ($hmccu_hash, $port) if ($clurl eq ''); + $cburl = $rpchash->{cburl} if (exists($rpchash->{cburl})); + $clurl = $rpchash->{clurl} if (exists($rpchash->{clurl})); + $cburl = HMCCU_GetRPCCallbackURL ($ioHash, $localaddr, $rpchash->{cbport}, $clkey, $port) if ($cburl eq ''); + $clurl = HMCCU_BuildURL ($ioHash, $port) if ($clurl eq ''); return (0, "Can't get RPC parameters for ID $clkey") if ($cburl eq '' || $clurl eq ''); HMCCU_Log ($hash, 1, "Deregistering RPC server $cburl with ID $clkey at $clurl"); # Deregister up to 2 times for (my $i=0; $i<2; $i++) { - my $rc = HMCCURPCPROC_SendRequest ($hash, "init", "$cburl:STRING"); + my $rc = HMCCURPCPROC_SendRequest ($hash, "init", "$cburl:STRING". ''); if (defined ($rc)) { HMCCURPCPROC_SetRPCState ($hash, $force == 0 ? 'deregistered' : $rpchash->{state}, "Callback for RPC server $clkey deregistered", 1); - $rpchash->{cburl} = ''; - $rpchash->{clurl} = ''; + $rpchash->{cburl} = ''; + $rpchash->{clurl} = ''; $rpchash->{cbport} = 0; return (1, 'working'); @@ -1200,92 +1365,81 @@ sub HMCCURPCPROC_DeRegisterCallback ($$) sub HMCCURPCPROC_InitRPCServer ($$$$) { - my ($name, $clkey, $callbackport, $prot) = @_; + my ($name, $clkey, $cbPort, $prot) = @_; my $server; # Create binary RPC server if ($prot eq 'B') { - $server->{__daemon} = IO::Socket::INET->new (LocalPort => $callbackport, + $server->{__daemon} = IO::Socket::INET->new (LocalPort => $cbPort, Type => SOCK_STREAM, Reuse => 1, Listen => SOMAXCONN); - if (!($server->{__daemon})) { - HMCCU_Log ($name, 1, "Can't create RPC callback server $clkey on port $callbackport. Port in use?"); - return undef; - } + return HMCCU_Log ($name, 1, "Can't create RPC callback server $clkey. Port $cbPort in use?", undef) + if (!($server->{__daemon})); return $server; } # Create XML RPC server - $server = RPC::XML::Server->new (port => $callbackport); - if (!ref($server)) { - HMCCU_Log ($name, 1, "Can't create RPC callback server $clkey on port $callbackport. Port in use?"); - return undef; - } - HMCCU_Log ($name, 2, "Callback server $clkey created. Listening on port $callbackport"); + $server = RPC::XML::Server->new (port => $cbPort); + return HMCCU_Log ($name, 1, "Can't create RPC callback server $clkey. Port $cbPort in use?", undef) + if (!ref($server)); + HMCCU_Log ($name, 2, "Callback server $clkey created. Listening on port $cbPort"); # Callback for events HMCCU_Log ($name, 4, "Adding callback for events for server $clkey"); - $server->add_method ( - { name=>"event", - signature=> ["string string string string string","string string string string int", - "string string string string double","string string string string boolean", - "string string string string i4"], - code=>\&HMCCURPCPROC_EventCB - } - ); + $server->add_method ({ + name => "event", + signature => ["string string string string string","string string string string int", + "string string string string double","string string string string boolean", + "string string string string i4"], + code => \&HMCCURPCPROC_EventCB + }); # Callback for new devices HMCCU_Log ($name, 4, "Adding callback for new devices for server $clkey"); - $server->add_method ( - { name=>"newDevices", - signature=>["string string array"], - code=>\&HMCCURPCPROC_NewDevicesCB - } - ); + $server->add_method ({ + name => "newDevices", + signature => ["string string array"], + code => \&HMCCURPCPROC_NewDevicesCB + }); # Callback for deleted devices HMCCU_Log ($name, 4, "Adding callback for deleted devices for server $clkey"); - $server->add_method ( - { name=>"deleteDevices", - signature=>["string string array"], - code=>\&HMCCURPCPROC_DeleteDevicesCB - } - ); + $server->add_method ({ + name => "deleteDevices", + signature => ["string string array"], + code => \&HMCCURPCPROC_DeleteDevicesCB + }); # Callback for modified devices HMCCU_Log ($name, 4, "Adding callback for modified devices for server $clkey"); - $server->add_method ( - { name=>"updateDevice", - signature=>["string string string int", "string string string i4"], - code=>\&HMCCURPCPROC_UpdateDeviceCB - } - ); + $server->add_method ({ + name => "updateDevice", + signature => ["string string string int", "string string string i4"], + code => \&HMCCURPCPROC_UpdateDeviceCB + }); # Callback for replaced devices HMCCU_Log ($name, 4, "Adding callback for replaced devices for server $clkey"); - $server->add_method ( - { name=>"replaceDevice", - signature=>["string string string string"], - code=>\&HMCCURPCPROC_ReplaceDeviceCB - } - ); + $server->add_method ({ + name => "replaceDevice", + signature => ["string string string string"], + code => \&HMCCURPCPROC_ReplaceDeviceCB + }); # Callback for readded devices HMCCU_Log ($name, 4, "Adding callback for readded devices for server $clkey"); - $server->add_method ( - { name=>"readdedDevice", - signature=>["string string array"], - code=>\&HMCCURPCPROC_ReaddDeviceCB - } - ); + $server->add_method ({ + name => "readdedDevice", + signature => ["string string array"], + code => \&HMCCURPCPROC_ReaddDeviceCB + }); # Dummy implementation, always return an empty array HMCCU_Log ($name, 4, "Adding callback for list devices for server $clkey"); - $server->add_method ( - { name=>"listDevices", - signature=>["array string"], - code=>\&HMCCURPCPROC_ListDevicesCB - } - ); + $server->add_method ({ + name => "listDevices", + signature => ["array string"], + code => \&HMCCURPCPROC_ListDevicesCB + }); return $server; } @@ -1299,18 +1453,18 @@ sub HMCCURPCPROC_StartRPCServer ($) { my ($hash) = @_; my $name = $hash->{NAME}; - my $hmccu_hash = $hash->{IODev}; - my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($hmccu_hash); + my $ioHash = $hash->{IODev}; + my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($ioHash); # Local IP address and callback ID should be set during device definition - return (0, "Local address and/or callback ID not defined") - if (!exists ($hash->{hmccu}{localaddr}) || !exists ($hash->{rpcid})); + return (0, 'Local address and/or callback ID not defined') + if (!exists($hash->{hmccu}{localaddr}) || !exists($hash->{rpcid})); # Check if RPC server is already running - return (0, "RPC server already running") if (HMCCURPCPROC_CheckProcessState ($hash, 'running')); + return (0, 'RPC server already running') if (HMCCURPCPROC_CheckProcessState ($hash, 'running')); # Get parameters and attributes - my $ping = AttrVal ($hmccu_hash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); + my $ping = AttrVal ($ioHash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); my $localaddr = HMCCURPCPROC_GetAttribute ($hash, undef, 'rpcserveraddr', $hash->{hmccu}{localaddr}); my $rpcserverport = HMCCURPCPROC_GetAttribute ($hash, 'rpcServerPort', 'rpcserverport', $HMCCURPCPROC_SERVER_PORT); my $evttimeout = ($ping > 0 && $hash->{rpcinterface} eq $defInterface) ? @@ -1318,7 +1472,7 @@ sub HMCCURPCPROC_StartRPCServer ($) HMCCURPCPROC_GetAttribute ($hash, 'rpcEventTimeout', 'rpcevtimeout', $HMCCURPCPROC_TIMEOUT_EVENT); my $ccunum = $hash->{CCUNum}; my $rpcport = $hash->{rpcport}; - my ($serveraddr, $interface) = HMCCU_GetRPCServerInfo ($hmccu_hash, $rpcport, 'host,name'); + my ($serveraddr, $interface) = HMCCU_GetRPCServerInfo ($ioHash, $rpcport, 'host,name'); my $clkey = 'CB'.$rpcport.$hash->{rpcid}; $hash->{hmccu}{localaddr} = $localaddr; @@ -1334,12 +1488,10 @@ sub HMCCURPCPROC_StartRPCServer ($) $procpar{ccuflags} = AttrVal ($name, 'ccuflags', 'null'); $procpar{evttimeout} = $evttimeout; $procpar{interface} = $interface; - ($procpar{flags}, $procpar{type}) = HMCCU_GetRPCServerInfo ($hmccu_hash, $rpcport, 'flags,type'); + ($procpar{flags}, $procpar{type}) = HMCCU_GetRPCServerInfo ($ioHash, $rpcport, 'flags,type'); $procpar{name} = $name; $procpar{clkey} = $clkey; - my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); - # Reset state of server processes $hash->{hmccu}{rpc}{state} = 'inactive'; @@ -1352,19 +1504,19 @@ sub HMCCURPCPROC_StartRPCServer ($) $hash->{hmccu}{sockparent} = $sockparent; $hash->{hmccu}{sockchild} = $sockchild; - # Enable FHEM I/O + # Enable FHEM I/O, calculate RPC server port my $pid = $$; $hash->{FD} = fileno $sockchild; $selectlist{"RPC.$name.$pid"} = $hash; + my $callbackport = $rpcserverport+$rpcport+($ccunum*10); # Initialize RPC server - my $err = ''; - my %srvprocpar; - my $callbackport = $rpcserverport+$rpcport+($ccunum*10); +# my $err = ''; +# my %srvprocpar; # Start RPC server process my $rpcpid = fhemFork (); - if (!defined ($rpcpid)) { + if (!defined($rpcpid)) { close ($sockparent); close ($sockchild); return (0, "Can't create RPC server process for interface $interface"); @@ -1390,21 +1542,21 @@ sub HMCCURPCPROC_StartRPCServer ($) $hash->{hmccu}{rpc}{state} = 'initialized'; # Reset statistic counter - foreach my $et (@eventtypes) { + foreach my $et (@RPC_EVENT_TYPES) { $hash->{hmccu}{rpc}{rec}{$et} = 0; $hash->{hmccu}{rpc}{snd}{$et} = 0; } + $hash->{hmccu}{rpc}{sumdelay} = 0; - $hash->{RPCPID} = $rpcpid; # Trigger Timer function for checking successful RPC start - # Timer will be removed before execution if event 'IN' is reveived + # Timer will be removed before first execution if event 'IN' is reveived InternalTimer (gettimeofday()+$HMCCURPCPROC_INIT_INTERVAL3, "HMCCURPCPROC_IsRPCServerRunning", $hash, 0); - HMCCURPCPROC_SetRPCState ($hash, "starting", "RPC server starting", 1); - DoTrigger ($name, "RPC server starting"); + HMCCURPCPROC_SetRPCState ($hash, 'starting', 'RPC server starting', 1); + DoTrigger ($name, 'RPC server starting'); return (1, undef); } @@ -1419,29 +1571,28 @@ sub HMCCURPCPROC_RPCServerStarted ($) { my ($hash) = @_; my $name = $hash->{NAME}; - my $hmccu_hash = $hash->{IODev}; - my $clkey = 'CB'.$hash->{rpcport}.$hash->{rpcid}; + my $ioHash = $hash->{IODev}; + my $clkey = HMCCURPCPROC_GetKey ($hash); my $ifname = $hash->{rpcinterface}; - my $ping = AttrVal ($hmccu_hash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); - my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($hmccu_hash); + my $ping = AttrVal ($ioHash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); + my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($ioHash); # Check if RPC servers are running. Set overall status if (HMCCURPCPROC_CheckProcessState ($hash, 'running')) { $hash->{hmccu}{rpcstarttime} = time (); - HMCCURPCPROC_SetState ($hash, "OK"); + HMCCURPCPROC_SetState ($hash, 'OK'); # Update client devices if interface is managed by HMCCURPCPROC device. # Normally interfaces are managed by HMCCU device. - if ($hmccu_hash->{hmccu}{interfaces}{$ifname}{manager} eq 'HMCCURPCPROC') { - HMCCU_UpdateClients ($hmccu_hash, '.*', 'Attr', 0, $ifname, 1); -# Log3 $name, 2, "HMCCURPCPROC: [$name] Updated devices. Success=$c_ok Failed=$c_err"; + if ($ioHash->{hmccu}{interfaces}{$ifname}{manager} eq 'HMCCURPCPROC') { + HMCCU_UpdateClients ($ioHash, '.*', 'Attr', 0, $ifname, 1); } RemoveInternalTimer ($hash, "HMCCURPCPROC_IsRPCServerRunning"); # Activate heartbeat if interface is default interface and rpcPingCCU > 0 if ($ping > 0 && $ifname eq $defInterface) { - HMCCU_Log ($hash, 1, "Scheduled CCU ping every $ping seconds", undef); + HMCCU_Log ($hash, 1, "Scheduled CCU ping every $ping seconds"); InternalTimer (gettimeofday()+$ping, "HMCCURPCPROC_RPCPing", $hash, 0); } @@ -1460,20 +1611,20 @@ sub HMCCURPCPROC_RPCServerStopped ($) { my ($hash) = @_; my $name = $hash->{NAME}; - my $clkey = 'CB'.$hash->{rpcport}.$hash->{rpcid}; + my $clkey = HMCCURPCPROC_GetKey ($hash); HMCCURPCPROC_CleanupProcess ($hash); HMCCURPCPROC_CleanupIO ($hash); HMCCURPCPROC_ResetRPCState ($hash); - HMCCURPCPROC_SetState ($hash, "OK"); + HMCCURPCPROC_SetState ($hash, 'OK'); RemoveInternalTimer ($hash); DoTrigger ($name, "RPC server $clkey stopped"); # Inform FHEM that instance can be shut down - HMCCU_Log ($hash, 2, "RPC server stopped. Cancel delayed shutdown.", undef); - CancelDelayedShutdown ($name) if (exists ($hash->{hmccu}{delayedShutdown})); + HMCCU_Log ($hash, 2, 'RPC server stopped. Cancel delayed shutdown.'); + CancelDelayedShutdown ($name) if (exists($hash->{hmccu}{delayedShutdown})); } ###################################################################### @@ -1486,18 +1637,18 @@ sub HMCCURPCPROC_CleanupIO ($) my $name = $hash->{NAME}; my $pid = $$; - if (exists ($selectlist{"RPC.$name.$pid"})) { - HMCCU_Log ($hash, 2, "Stop I/O handling", undef); + if (exists($selectlist{"RPC.$name.$pid"})) { + HMCCU_Log ($hash, 2, 'Stop I/O handling'); delete $selectlist{"RPC.$name.$pid"}; delete $hash->{FD} if (defined ($hash->{FD})); } - if (defined ($hash->{hmccu}{sockchild})) { - HMCCU_Log ($hash, 3, "Close child socket", undef); + if (defined($hash->{hmccu}{sockchild})) { + HMCCU_Log ($hash, 3, 'Close child socket'); $hash->{hmccu}{sockchild}->close (); delete $hash->{hmccu}{sockchild}; } - if (defined ($hash->{hmccu}{sockparent})) { - HMCCU_Log ($hash, 3, "Close parent socket", undef); + if (defined($hash->{hmccu}{sockparent})) { + HMCCU_Log ($hash, 3, 'Close parent socket'); $hash->{hmccu}{sockparent}->close (); delete $hash->{hmccu}{sockparent}; } @@ -1511,9 +1662,7 @@ sub HMCCURPCPROC_CleanupIO ($) sub HMCCURPCPROC_TerminateProcess ($) { my ($hash) = @_; - my $clkey = 'CB'.$hash->{rpcport}.$hash->{rpcid}; - -# return 0 if ($hash->{hmccu}{rpc}{state} eq 'inactive'); + my $clkey = HMCCURPCPROC_GetKey ($hash); my $pid = $hash->{hmccu}{rpc}{pid}; if (defined ($pid) && kill (0, $pid)) { @@ -1535,19 +1684,15 @@ sub HMCCURPCPROC_TerminateProcess ($) sub HMCCURPCPROC_CleanupProcess ($) { my ($hash) = @_; - my $clkey = 'CB'.$hash->{rpcport}.$hash->{rpcid}; - -# return 1 if ($hash->{hmccu}{rpc}{state} eq 'inactive'); + my $clkey = HMCCURPCPROC_GetKey ($hash); my $pid = $hash->{hmccu}{rpc}{pid}; - if (defined ($pid) && kill (0, $pid)) { - HMCCU_Log ($hash, 1, "Process $clkey with PID=$pid still running. Killing it.", undef); + if (defined($pid) && kill (0, $pid)) { + HMCCU_Log ($hash, 1, "Process $clkey with PID=$pid still running. Killing it."); kill ('KILL', $pid); sleep (1); - if (kill (0, $pid)) { - HMCCU_Log ($hash, 1, "Can't kill process $clkey with PID=$pid", undef); - return 0; - } + return HMCCU_Log ($hash, 1, "Can't kill process $clkey with PID=$pid", 0) + if (kill (0, $pid)); } HMCCURPCPROC_SetRPCState ($hash, 'inactive', "RPC server process $clkey deleted", 2); @@ -1573,14 +1718,10 @@ sub HMCCURPCPROC_CheckProcessState ($$) { my ($hash, $state) = @_; -# HMCCU_Log ($hash, 3, "CheckProcessState()"); - - my $prcname = 'CB'.$hash->{rpcport}.$hash->{rpcid}; - my $pstate = $hash->{hmccu}{rpc}{state}; if ($state eq 'running' || $state eq '.*') { my $pid = $hash->{hmccu}{rpc}{pid}; - return (defined ($pid) && $pid != 0 && kill (0, $pid) && $pstate =~ /$state/) ? $pid : 0 + return (defined($pid) && $pid != 0 && kill (0, $pid) && $pstate =~ /$state/) ? $pid : 0 } else { return ($pstate =~ /$state/) ? 1 : 0; @@ -1596,15 +1737,14 @@ sub HMCCURPCPROC_IsRPCServerRunning ($) { my ($hash) = @_; - HMCCU_Log ($hash, 2, "Checking if RPC server process is running", undef); + HMCCU_Log ($hash, 2, 'Checking if RPC server process is running'); if (!HMCCURPCPROC_CheckProcessState ($hash, 'running')) { - HMCCU_Log ($hash, 1, "RPC server process not running. Cleaning up", undef); + HMCCU_Log ($hash, 1, 'RPC server process not running. Cleaning up'); HMCCURPCPROC_Housekeeping ($hash); return 0; } - HMCCU_Log ($hash, 2, "RPC server process running", undef); - return 1; + return HMCCU_Log ($hash, 2, 'RPC server process running', 1); } ###################################################################### @@ -1615,7 +1755,7 @@ sub HMCCURPCPROC_Housekeeping ($) { my ($hash) = @_; - HMCCU_Log ($hash, 1, "Housekeeping called. Cleaning up RPC environment", undef); + HMCCU_Log ($hash, 1, 'Housekeeping called. Cleaning up RPC environment'); # Deregister callback URLs in CCU HMCCURPCPROC_DeRegisterCallback ($hash, 0); @@ -1629,18 +1769,16 @@ sub HMCCURPCPROC_Housekeeping ($) ###################################################################### # Stop RPC server processes. -# If function is called by Shutdown, parameter wait must be 0 +# If function is called by Shutdown. If parameter wait can be 0 or +# undef. ###################################################################### sub HMCCURPCPROC_StopRPCServer ($$) { my ($hash, $wait) = @_; - my $clkey = 'CB'.$hash->{rpcport}.$hash->{rpcid}; + $wait //= $HMCCURPCPROC_INIT_INTERVAL2; + my $clkey = HMCCURPCPROC_GetKey ($hash); - HMCCU_Log ($hash, 3, "StopRPCServer()"); - - $wait = $HMCCURPCPROC_INIT_INTERVAL2 if (!defined ($wait)); - if (HMCCURPCPROC_CheckProcessState ($hash, 'running')) { HMCCU_Log ($hash, 1, "Stopping RPC server $clkey"); HMCCURPCPROC_SetState ($hash, "busy"); @@ -1659,7 +1797,7 @@ sub HMCCURPCPROC_StopRPCServer ($$) InternalTimer (gettimeofday()+$wait, "HMCCURPCPROC_Housekeeping", $hash, 0); } else { - HMCCU_Log ($hash, 2, "Cleaning up immediately"); + HMCCU_Log ($hash, 2, 'Cleaning up immediately'); HMCCURPCPROC_Housekeeping ($hash); } @@ -1668,7 +1806,7 @@ sub HMCCURPCPROC_StopRPCServer ($$) return 1; } else { - HMCCU_Log ($hash, 2, "Found no running processes. Cleaning up ..."); + HMCCU_Log ($hash, 2, 'Found no running processes. Cleaning up ...'); HMCCURPCPROC_Housekeeping ($hash); return 0; } @@ -1696,134 +1834,153 @@ sub HMCCURPCPROC_StopRPCServer ($$) # "BASE64" = $BINRPC_BASE64 # "ARRAY" = $BINRPC_ARRAY # "STRUCT" = $BINRPC_STRUCT +# The default parameter type is "STRING". # Return response or undef on error. ###################################################################### sub HMCCURPCPROC_SendRequest ($@) { my ($hash, $request, @param) = @_; - my $name = $hash->{NAME}; - my $ioHash = $hash->{IODev}; my $port = $hash->{rpcport}; - my $rc; - - return HMCCU_Log ($hash, 2, "I/O device not found", undef) if (!defined ($ioHash)); - - my $re = ':('.join('|', keys(%BINRPC_TYPE_MAPPING)).')'; + my $ioHash = $hash->{IODev} // + return HMCCU_Log ($hash, 2, 'I/O device not found', undef); if (HMCCU_IsRPCType ($ioHash, $port, 'A')) { - # Use XMLRPC - my $clurl = HMCCU_BuildURL ($ioHash, $port); - return HMCCU_Log ($hash, 2, "Can't get client URL for port $port", undef) - if (!defined ($clurl)); - - HMCCU_Log ($hash, 4, "Send ASCII RPC request $request to $clurl", undef); - my $rpcclient = RPC::XML::Client->new ($clurl, useragent => [ - ssl_opts => { verify_hostname => 0, SSL_verify_mode => 0 } ]); - - if (exists ($RPC_METHODS{$request})) { - # Read or write parameter sets - my $address = shift @param; - my $key = shift @param; - return HMCCU_Log ($hash, 2, "Missing address or key in RPC request $request", undef) - if (!defined ($key)); - - my %hparam; - - # Write requests have at least one parameters - if ($RPC_METHODS{$request} == 1) { - # Build a parameter hash - while (my $p = shift @param) { - my $pt = "STRING"; - if ($p =~ /${re}/) { - $pt = $1; - $p =~ s/${re}//; - } - my ($pn, $pv) = split ('=', $p, 2); - next if (!defined ($pv)); - $hparam{$pn} = HMCCURPCPROC_EncValue ($pv, $pt); - } - - return HMCCU_Log ($hash, 2, "Missing parameter in RPC request $request", undef) - if (!keys %hparam); - - # Submit write paramset request - $rc = $rpcclient->simple_request ($request, $address, $key, \%hparam); - } - else { - # Submit read paramset request - $rc = $rpcclient->simple_request ($request, $address, $key); - } - } - else { - # RPC commands - my @aparam = (); - - # Build a parameter array - while (my $p = shift @param) { - my $pt = "STRING"; - if ($p =~ /${re}/) { - $pt = $1; - $p =~ s/${re}//; - } - push (@aparam, HMCCURPCPROC_EncValue ($p, $pt)); - } - - # Submit RPC command - $rc = $rpcclient->simple_request ($request, @aparam); - } - - HMCCU_Log ($hash, 2, "RPC request error ".$RPC::XML::ERROR, undef) if (!defined ($rc)); + return HMCCURPCPROC_SendXMLRequest ($hash, $ioHash, $port, $request, @param); } elsif (HMCCU_IsRPCType ($ioHash, $port, 'B')) { - # Use BINRPC - my ($serveraddr) = HMCCU_GetRPCServerInfo ($ioHash, $port, 'host'); - return HMCCU_Log ($ioHash, 2, "Can't get server address for port $port", undef) - if (!defined ($serveraddr)); - - my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); - my $verbose = GetVerbose ($name); - - HMCCU_Log ($hash, 4, "Send binary RPC request $request to $serveraddr:$port", undef); - my $encreq = HMCCURPCPROC_EncodeRequest ($request, \@param); - return HMCCU_Log ($hash, 2, "Error encoding binary request", undef) if ($encreq eq ''); - - # auto-flush on socket - $| = 1; - - # create a connecting socket - my $socket = new IO::Socket::INET (PeerHost => $serveraddr, PeerPort => $port, - Proto => 'tcp'); - return HMCCU_Log ($hash, 2, "Can't create socket for $serveraddr:$port", undef) if (!$socket); - - my $size = $socket->send ($encreq); - if (defined ($size)) { - my $encresp = <$socket>; - $socket->close (); - - if (defined ($encresp)) { - if ($ccuflags =~ /logEvents/ && $verbose >= 4) { - HMCCU_Log ($hash, 4, "Response", undef); - HMCCURPCPROC_HexDump ($name, $encresp); - } - my ($response, $err) = HMCCURPCPROC_DecodeResponse ($encresp); - return $response; - } - else { - return ''; - } - } - - $socket->close (); + return HMCCURPCPROC_SendBINRequest ($hash, $ioHash, $port, $request, @param); } else { - HMCCU_Log ($hash, 2, "Unknown RPC server type", undef); + return HMCCU_Log ($hash, 2, 'Unknown RPC server type', undef); + } +} + +###################################################################### +# Send XML RPC request to CCU +###################################################################### + +sub HMCCURPCPROC_SendXMLRequest ($@) +{ + my ($hash, $ioHash, $port, $request, @param) = @_; + my $name = $hash->{NAME}; + + my $rc; + my $re = ':('.join('|', keys(%BINRPC_TYPE_MAPPING)).')'; + my $clurl = HMCCU_BuildURL ($ioHash, $port) // + return HMCCU_Log ($hash, 2, "Can't get client URL for port $port", undef); + + HMCCU_Log ($hash, 4, "Send ASCII RPC request $request to $clurl"); + my $rpcclient = RPC::XML::Client->new ($clurl, useragent => [ + ssl_opts => { verify_hostname => 0, SSL_verify_mode => 0 } ]); + + if (exists($RPC_METHODS{$request})) { + # Read or write parameter sets + my $address = shift @param // + return HMCCU_Log ($hash, 2, "Missing address in RPC request $request", undef); + my $key = shift @param // + return HMCCU_Log ($hash, 2, "Missing key in RPC request $request", undef); + my %hparam; + + # Write requests have at least one parameter + if ($RPC_METHODS{$request} == 1) { + # Build a parameter hash + while (my $p = shift @param) { + my $pt; + if ($p =~ /${re}/) { $pt = $1; $p =~ s/${re}//; } + my ($pn, $pv) = split ('=', $p, 2); + $hparam{$pn} = HMCCURPCPROC_EncValue ($pv, $pt) if (defined($pv)); + } + + return HMCCU_Log ($hash, 2, "Missing parameter in RPC request $request", undef) + if (!keys %hparam); + + # Submit write paramset request + $rc = $rpcclient->simple_request ($request, $address, $key, \%hparam); + } + else { + # Submit read paramset request + $rc = $rpcclient->simple_request ($request, $address, $key); + } + } + else { + # RPC commands + my @aparam = (); + + # Build a parameter array + while (my $p = shift @param) { + my $pt; + if ($p =~ /${re}/) { $pt = $1; $p =~ s/${re}//; } + push (@aparam, HMCCURPCPROC_EncValue ($p, $pt)); + } + + # Submit RPC command + $rc = $rpcclient->simple_request ($request, @aparam); } + HMCCU_Log ($hash, 2, "RPC request error ".$RPC::XML::ERROR) if (!defined($rc)); return $rc; } +###################################################################### +# Send binary RPC request to CCU +###################################################################### + +sub HMCCURPCPROC_SendBINRequest ($@) +{ + my ($hash, $ioHash, $port, $request, @param) = @_; + my $name = $hash->{NAME}; + + my ($serveraddr) = HMCCU_GetRPCServerInfo ($ioHash, $port, 'host'); + return HMCCU_Log ($ioHash, 2, "Can't get server address for port $port", undef) + if (!defined($serveraddr)); + + my $timeoutRead = AttrVal ($name, 'rpcReadTimeout', $HMCCURPCPROC_TIMEOUT_READ); + my $timeoutWrite = AttrVal ($name, 'rpcWriteTimeout', $HMCCURPCPROC_TIMEOUT_WRITE); + my $ccuflags = AttrVal ($name, 'ccuflags', 'null'); + my $verbose = GetVerbose ($name); + + my $encreq = HMCCURPCPROC_EncodeRequest ($request, \@param); + return HMCCU_Log ($hash, 2, 'Error while encoding binary request', undef) if ($encreq eq ''); + + if ($ccuflags =~ /logEvents/) { + HMCCU_Log ($hash, 4, 'Binary RPC request'); + HMCCURPCPROC_HexDump ($name, $encreq); + } + + # create a connecting socket + my $socket = IO::Socket::INET->new (PeerHost => $serveraddr, PeerPort => $port, Proto => 'tcp', Timeout => 3); + return HMCCU_Log ($hash, 2, "Can't create socket for $serveraddr:$port", undef) if (!$socket); + + $socket->autoflush (1); + $socket->timeout (1); + + my ($bytesWritten, $errmsg) = HMCCURPCPROC_WriteToSocket ($socket, $encreq, $timeoutWrite); + if ($bytesWritten > 0) { + my ($bytesRead, $encresp) = HMCCURPCPROC_ReadFromSocket ($hash, $socket, $timeoutRead); + $socket->close (); + + if ($bytesRead > 0) { + if ($ccuflags =~ /logEvents/) { + HMCCU_Log ($hash, 4, 'Binary RPC response'); + HMCCURPCPROC_HexDump ($name, $encresp); + } + my ($response, $err) = HMCCURPCPROC_DecodeResponse ($encresp); + return HMCCU_Log ($hash, 2, 'Error while decoding binary response', undef) + if (!defined($err) || $err == 0); + return $response; + } + else { + return HMCCU_Log ($hash, 2, "Error while reading response for command $request: $encresp", ''); + } + } + else { + $socket->close (); + return HMCCU_Log ($hash, 2, "No data sent for request $request: $errmsg", undef); + } +} + ###################################################################### # Timer function for RPC Ping ###################################################################### @@ -1831,21 +1988,17 @@ sub HMCCURPCPROC_SendRequest ($@) sub HMCCURPCPROC_RPCPing ($) { my ($hash) = @_; - my $hmccu_hash = $hash->{IODev}; - my $ping = AttrVal ($hmccu_hash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); - my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($hmccu_hash); + my $ioHash = $hash->{IODev}; + my $ping = AttrVal ($ioHash->{NAME}, 'rpcPingCCU', $HMCCURPCPROC_TIME_PING); + return HMCCU_Log ($hash, 1, 'CCU ping disabled') if ($ping == 0); + my ($defInterface, $defPort) = HMCCU_GetDefaultInterface ($ioHash); if ($hash->{rpcinterface} eq $defInterface) { - if ($ping > 0) { - if ($init_done && HMCCURPCPROC_CheckProcessState ($hash, 'running')) { - my $clkey = 'CB'.$hash->{rpcport}.$hash->{rpcid}; - HMCCURPCPROC_SendRequest ($hash, "ping", "$clkey:STRING"); - } - InternalTimer (gettimeofday()+$ping, "HMCCURPCPROC_RPCPing", $hash, 0); - } - else { - HMCCU_Log ($hash, 1, "CCU ping disabled"); + if ($init_done && HMCCURPCPROC_CheckProcessState ($hash, 'running')) { + my $clkey = HMCCURPCPROC_GetKey ($hash); + HMCCURPCPROC_SendRequest ($hash, 'ping', "$clkey:STRING"); } + InternalTimer (gettimeofday()+$ping, "HMCCURPCPROC_RPCPing", $hash, 0); } } @@ -1858,7 +2011,7 @@ sub HMCCURPCPROC_ProcessRequest ($$) my ($server, $connection) = @_; my $name = $server->{hmccu}{name}; my $clkey = $server->{hmccu}{clkey}; - my @methodlist = ('listDevices', 'listMethods', 'system.multicall'); + my @methodlist = ('listDevices', 'listMethods', 'system.listMethods', 'system.multicall'); my $verbose = GetVerbose ($name); # Read request @@ -1866,7 +2019,7 @@ sub HMCCURPCPROC_ProcessRequest ($$) while (my $packet = <$connection>) { $request .= $packet; } - return if (!defined ($request) || $request eq ''); + return if ($request eq ''); if ($server->{hmccu}{ccuflags} =~ /logEvents/ && $verbose >= 4) { HMCCU_Log ($name, 4, "$clkey raw request:"); @@ -1875,10 +2028,11 @@ sub HMCCURPCPROC_ProcessRequest ($$) # Decode request my ($method, $params) = HMCCURPCPROC_DecodeRequest ($request); - return if (!defined ($method)); + return if (!defined($method)); + $method = lc($method); HMCCU_Log ($name, 4, "Request method = $method"); - if ($method eq 'listmethods') { + if ($method eq 'listmethods' || $method eq 'system.listmethods') { $connection->send (HMCCURPCPROC_EncodeResponse ($BINRPC_ARRAY, \@methodlist)); } elsif ($method eq 'listdevices') { @@ -1886,15 +2040,14 @@ sub HMCCURPCPROC_ProcessRequest ($$) $connection->send (HMCCURPCPROC_EncodeResponse ($BINRPC_ARRAY, undef)); } elsif ($method eq 'system.multicall') { - return if (ref ($params) ne 'ARRAY'); + return if (ref($params) ne 'ARRAY'); my $a = $$params[0]; foreach my $s (@$a) { - next if (!exists ($s->{methodName}) || !exists ($s->{params})); - next if ($s->{methodName} ne 'event'); - next if (scalar (@{$s->{params}}) < 4); + next if (!exists($s->{methodName}) || !exists($s->{params}) || + $s->{methodName} ne 'event' || scalar(@{$s->{params}}) < 4); HMCCURPCPROC_EventCB ($server, $clkey, ${$s->{params}}[1], ${$s->{params}}[2], ${$s->{params}}[3]); - HMCCU_Log ($name, 4, "Event ".${$s->{params}}[1]." ".${$s->{params}}[2]." " + HMCCU_Log ($name, 4, 'Event '.${$s->{params}}[1].' '.${$s->{params}}[2].' ' .${$s->{params}}[3]); } } @@ -1924,19 +2077,13 @@ sub HMCCURPCPROC_HandleConnection ($$$$) my $run = 1; my $pid = $$; - my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); - # Initialize RPC server HMCCU_Log ($name, 2, "Initializing RPC server $clkey for interface $iface"); my $rpcsrv = HMCCURPCPROC_InitRPCServer ($name, $clkey, $callbackport, $prot); - if (!defined ($rpcsrv)) { - HMCCU_Log ($name, 1, "Can't initialize RPC server $clkey for interface $iface"); - return; - } - if (!($rpcsrv->{__daemon})) { - HMCCU_Log ($name, 1, "Server socket not found for port $port"); - return; - } + return HMCCU_Log ($name, 1, "Can't initialize RPC server $clkey for interface $iface") + if (!defined($rpcsrv)); + return HMCCU_Log ($name, 1, "Server socket not found for port $port") + if (!($rpcsrv->{__daemon})); # Event queue my @queue = (); @@ -1953,17 +2100,15 @@ sub HMCCURPCPROC_HandleConnection ($$$$) $rpcsrv->{hmccu}{evttime} = time (); # Initialize statistic counters - foreach my $et (@eventtypes) { + foreach my $et (@RPC_EVENT_TYPES, 'total') { $rpcsrv->{hmccu}{rec}{$et} = 0; $rpcsrv->{hmccu}{snd}{$et} = 0; } - $rpcsrv->{hmccu}{rec}{total} = 0; - $rpcsrv->{hmccu}{snd}{total} = 0; # Signal handler $SIG{INT} = sub { $run = 0; HMCCU_Log ($name, 2, "$clkey received signal INT"); }; - HMCCURPCPROC_Write ($rpcsrv, "SL", $clkey, $pid); + HMCCURPCPROC_Write ($rpcsrv, 'SL', $clkey, $pid); HMCCU_Log ($name, 2, "$clkey accepting connections. PID=$pid"); $rpcsrv->{__daemon}->timeout ($acctimeout) if ($acctimeout > 0.0); @@ -1971,7 +2116,7 @@ sub HMCCURPCPROC_HandleConnection ($$$$) while ($run) { if ($evttimeout > 0) { my $difftime = time()-$rpcsrv->{hmccu}{evttime}; - HMCCURPCPROC_Write ($rpcsrv, "TO", $clkey, $difftime) if ($difftime >= $evttimeout); + HMCCURPCPROC_Write ($rpcsrv, 'TO', $clkey, $difftime) if ($difftime >= $evttimeout); } # Send queue entries to parent process @@ -1989,7 +2134,7 @@ sub HMCCURPCPROC_HandleConnection ($$$$) } # Next statement blocks for rpcAcceptTimeout seconds - HMCCU_Log ($name, 5, "RPC server $clkey accepting connections"); + HMCCU_Log ($name, 4, "RPC server $clkey accepting connections"); my $connection = $rpcsrv->{__daemon}->accept (); next if (! $connection); last if (! $run); @@ -2016,7 +2161,7 @@ sub HMCCURPCPROC_HandleConnection ($$$$) HMCCURPCPROC_WriteStats ($rpcsrv, $clkey); # Send exit information - HMCCURPCPROC_Write ($rpcsrv, "EX", $clkey, "SHUTDOWN|$pid"); + HMCCURPCPROC_Write ($rpcsrv, 'EX', $clkey, "SHUTDOWN|$pid"); # Send queue entries to parent process. Resend on error to ensure that EX event is sent my ($c, $m) = HMCCURPCPROC_SendQueue ($sockparent, $socktimeout, \@queue, 0); @@ -2028,7 +2173,7 @@ sub HMCCURPCPROC_HandleConnection ($$$$) } # Log statistic counters - foreach my $et (@eventtypes) { + foreach my $et (@RPC_EVENT_TYPES) { HMCCU_Log ($name, 4, "$clkey event type = $et: ".$rpcsrv->{hmccu}{rec}{$et}); } HMCCU_Log ($name, 2, "Number of I/O errors = $sioerrors"); @@ -2052,7 +2197,7 @@ sub HMCCURPCPROC_SendQueue ($$$$) vec ($win, $fd, 1) = 1; my $nf = select (undef, $win, undef, $socktimeout); if ($nf <= 0) { - $msg = $nf == 0 ? "select found no reader" : $!; + $msg = $nf == 0 ? 'select found no reader' : $!; return (-1, $msg); } @@ -2085,15 +2230,15 @@ sub HMCCURPCPROC_SendData ($$) my $bytes = 0; my $err = ''; - my $size = pack ("N", length ($data)); + my $size = pack ("N", length($data)); my $msg = $size . $data; $bytes = syswrite ($sockparent, $msg); - if (!defined ($bytes)) { + if (!defined($bytes)) { $err = $!; $bytes = 0; } - elsif ($bytes != length ($msg)) { - $err = "Sent incomplete data"; + elsif ($bytes != length($msg)) { + $err = 'send: incomplete data'; } return ($bytes, $err); @@ -2101,36 +2246,28 @@ sub HMCCURPCPROC_SendData ($$) ###################################################################### # Check if file descriptor is readable and read data. -# Return data and error message. +# Return (data, '') on success +# Return (undef, errmsg) on error ###################################################################### sub HMCCURPCPROC_ReceiveData ($$) { - my ($fh, $socktimeout) = @_; + my ($fh, $timeout) = @_; my $header; my $data; my $err = ''; - # Check if data is available - my $fd = fileno ($fh); - my $rin = ''; - vec ($rin, $fd, 1) = 1; - my $nfound = select ($rin, undef, undef, $socktimeout); - if ($nfound < 0) { - return (undef, $!); - } - elsif ($nfound == 0) { - return (undef, "read: no data"); - } + my ($st, $msg) = HMCCURPCPROC_DataAvailableOnSocket ($fh, $timeout); + return (undef, $msg) if ($st <= 0); # Read datagram size my $sbytes = sysread ($fh, $header, 4); - if (!defined ($sbytes)) { + if (!defined($sbytes)) { return (undef, $!); } elsif ($sbytes != 4) { - return (undef, "read: short header"); + return (undef, 'receive: short header'); } # Read datagram @@ -2140,12 +2277,90 @@ sub HMCCURPCPROC_ReceiveData ($$) return (undef, $!); } elsif ($bytes != $size) { - return (undef, "read: incomplete data"); + return (undef, 'receive: incomplete data'); } return ($data, $err); } +###################################################################### +# Read data from socket +# Return (-1, ErrorStr) on error. +# Return (0, 'read: no data') if no data available. +# Return (BytesRead, Data) on success. +###################################################################### + +sub HMCCURPCPROC_ReadFromSocket ($$$) +{ + my ($hash, $socket, $timeout) = @_; + + my $data = ''; + my $totalBytes = 0; + + my ($st, $msg) = HMCCURPCPROC_DataAvailableOnSocket ($socket, $timeout); + while ($st > 0) { + my $buffer; + my $bytes = sysread ($socket, $buffer, 10000); + return (-1, $!) if (!defined($bytes)); + last if ($bytes == 0); + $data .= $buffer; + $totalBytes += $bytes; + ($st, $msg) = HMCCURPCPROC_DataAvailableOnSocket ($socket, $timeout); + } + + return $st < 0 ? ($st, $msg) : ($totalBytes, $data); +} + +###################################################################### +# Check if data is available for reading from socket +###################################################################### + +sub HMCCURPCPROC_DataAvailableOnSocket ($$) +{ + my ($socket, $timeout) = @_; + + my $fd = fileno ($socket); + my $rin = ''; + vec ($rin, $fd, 1) = 1; + + my $nfound = select ($rin, undef, undef, $timeout); + if ($nfound < 0) { + return (-1, $!); + } + elsif ($nfound == 0) { + return (0, 'read: no data'); + } + + return (1, ''); +} + +###################################################################### +# Write data to socket +# Return (-1, ErrorStr) on error. +# Return (0, 'write: no reader') if no reading process on remote host. +# Return (BytesWritten, 'OK') on success. +###################################################################### + +sub HMCCURPCPROC_WriteToSocket ($$$) +{ + my ($socket, $data, $timeout) = @_; + + my $fd = fileno ($socket); + my $win = ''; + vec ($win, $fd, 1) = 1; + my $nfound = select (undef, $win, undef, $timeout); + if ($nfound < 0) { + return (-1, $!); + } + elsif ($nfound == 0) { + return (0, 'write: no reader'); + } + + my $size = syswrite ($socket, $data); + + return defined($size) ? ($size, 'OK') : (-1, $!); +} + ###################################################################### # Write event into queue. ###################################################################### @@ -2155,16 +2370,14 @@ sub HMCCURPCPROC_Write ($$$$) my ($server, $et, $cb, $msg) = @_; my $name = $server->{hmccu}{name}; - if (defined ($server->{hmccu}{eventqueue})) { + if (defined($server->{hmccu}{eventqueue})) { my $queue = $server->{hmccu}{eventqueue}; - my $ev = $et."|".$cb."|".$msg; - + my $ev = "$et|$cb|$msg"; $server->{hmccu}{evttime} = time (); - if (defined ($server->{hmccu}{queuesize}) && - scalar (@{$queue}) >= $server->{hmccu}{queuesize}) { - HMCCU_Log ($name, 1, "$cb maximum queue size reached. Dropping event."); - return; + if (defined($server->{hmccu}{queuesize}) && + scalar(@{$queue}) >= $server->{hmccu}{queuesize}) { + return HMCCU_Log ($name, 1, "$cb maximum queue size reached. Dropping event."); } HMCCU_Log ($name, 2, "Event = $ev") if ($server->{hmccu}{ccuflags} =~ /logEvents/); @@ -2172,7 +2385,7 @@ sub HMCCURPCPROC_Write ($$$$) # Try to send events immediately. Put them in queue if send fails my $rc = 0; my $err = ''; - if ($et ne 'ND' && $server->{hmccu}{ccuflags} !~ /queueEvents/) { + if ($server->{hmccu}{ccuflags} !~ /queueEvents/) { ($rc, $err) = HMCCURPCPROC_SendData ($server->{hmccu}{sockparent}, $ev); HMCCU_Log ($name, 3, "SendData $ev $err") if ($rc == 0); } @@ -2197,14 +2410,12 @@ sub HMCCURPCPROC_WriteStats ($$) my ($server, $clkey) = @_; my $name = $server->{hmccu}{name}; - my @eventtypes = ("EV", "ND", "DD", "RD", "RA", "UD", "IN", "EX", "SL", "TO"); - if (defined ($server->{hmccu}{eventqueue})) { my $queue = $server->{hmccu}{eventqueue}; # Send statistic info my $st = $server->{hmccu}{snd}{total}; - foreach my $et (@eventtypes) { + foreach my $et (@RPC_EVENT_TYPES) { $st .= '|'.$server->{hmccu}{snd}{$et}; $server->{hmccu}{snd}{$et} = 0; } @@ -2226,10 +2437,15 @@ sub HMCCURPCPROC_HexDump ($$) { my ($name, $data) = @_; + if (!defined($data)) { + HMCCU_Log ($name, 4, 'HexDump called without data'); + return; + } + my $offset = 0; foreach my $chunk (unpack "(a16)*", $data) { - my $hex = unpack "H*", $chunk; # hexadecimal magic + my $hex = unpack "H*", $chunk; # hexadecimal $chunk =~ tr/ -~/./c; # replace unprintables $hex =~ s/(.{1,8})/$1 /gs; # insert spaces HMCCU_Log ($name, 4, sprintf "0x%08x (%05u) %-*s %s", $offset, $offset, 36, $hex, $chunk); @@ -2237,12 +2453,30 @@ sub HMCCURPCPROC_HexDump ($$) } } +###################################################################### +# Build RPC server key +###################################################################### + +sub HMCCURPCPROC_GetKey ($) +{ + my ($hash) = @_; + + return 'CB'.$hash->{rpcport}.$hash->{rpcid}; +} + ###################################################################### # Callback functions ###################################################################### ###################################################################### # Callback for new devices +# Message format: +# C|ADDRESS|TYPE|VERSION|null|null|PARAMSETS| +# LINK_SOURCE_ROLES|LINK_TARGET_ROLES|DIRECTION| +# null|PARENT|AES_ACTIVE +# D|ADDRESS|TYPE|VERSION|FIRMWARE|RX_MODE|PARAMSETS| +# null|null|null| +# CHILDREN|null|null ###################################################################### sub HMCCURPCPROC_NewDevicesCB ($$$) @@ -2251,19 +2485,33 @@ sub HMCCURPCPROC_NewDevicesCB ($$$) my $name = $server->{hmccu}{name}; my $devcount = scalar (@$a); - HMCCU_Log ($name, 2, "$cb NewDevice received $devcount device and channel specifications"); + HMCCU_Log ($name, 2, "$cb NewDevice received $devcount device and channel specifications"); + + # Format: + # C/D|Address|Type|Version|Firmware|RxMode|Paramsets| + # LinkSourceRoles|LinkTargetRoles|Direction|Children|Parent|AESActive + foreach my $dev (@$a) { my $msg = ''; - if ($dev->{ADDRESS} =~ /:[0-9]{1,2}$/) { - $msg = "C|".$dev->{ADDRESS}."|".$dev->{TYPE}."|".$dev->{VERSION}."|null|null"; + my $ps = ref($dev->{PARAMSETS}) eq 'ARRAY' ? + join(',', @{$dev->{PARAMSETS}}) : $dev->{PARAMSETS}; + if (defined($dev->{PARENT}) && $dev->{PARENT} ne '') { + my $lsr = ref($dev->{LINK_SOURCE_ROLES}) eq 'ARRAY' ? + join(',', @{$dev->{LINK_SOURCE_ROLES}}) : $dev->{LINK_SOURCE_ROLES}; + my $ltr = ref($dev->{LINK_TARGET_ROLES}) eq 'ARRAY' ? + join(',', @{$dev->{LINK_TARGET_ROLES}}) : $dev->{LINK_TARGET_ROLES}; + $msg = 'C|'.$dev->{ADDRESS}.'|'.$dev->{TYPE}.'|'.$dev->{VERSION}. + '|null|null|'.$ps.'|'.$lsr.'|'.$ltr.'|'.$dev->{DIRECTION}. + '|null|'.$dev->{PARENT}."|".$dev->{AES_ACTIVE}; } else { # Wired devices do not have a RX_MODE attribute my $rx = exists ($dev->{RX_MODE}) ? $dev->{RX_MODE} : 'null'; - $msg = "D|".$dev->{ADDRESS}."|".$dev->{TYPE}."|".$dev->{VERSION}."|". - $dev->{FIRMWARE}."|".$rx; + $msg = 'D|'.$dev->{ADDRESS}.'|'.$dev->{TYPE}.'|'.$dev->{VERSION}."|". + $dev->{FIRMWARE}.'|'.$rx.'|'.$ps.'|null|null|null|'. + join(',',@{$dev->{CHILDREN}}).'|null|null'; } - HMCCURPCPROC_Write ($server, "ND", $cb, $msg); + HMCCURPCPROC_Write ($server, 'ND', $cb, $msg); } return; @@ -2277,11 +2525,11 @@ sub HMCCURPCPROC_DeleteDevicesCB ($$$) { my ($server, $cb, $a) = @_; my $name = $server->{hmccu}{name}; - my $devcount = scalar (@$a); + my $devCount = scalar (@$a); - HMCCU_Log ($name, 2, "$cb DeleteDevice received $devcount device addresses"); + HMCCU_Log ($name, 2, "$cb DeleteDevice received $devCount device addresses"); foreach my $dev (@$a) { - HMCCURPCPROC_Write ($server, "DD", $cb, $dev); + HMCCURPCPROC_Write ($server, 'DD', $cb, $dev); } return; @@ -2297,7 +2545,7 @@ sub HMCCURPCPROC_UpdateDeviceCB ($$$$) my $name = $server->{hmccu}{name}; HMCCU_Log ($name, 2, "$cb updated device $devid with hint $hint"); - HMCCURPCPROC_Write ($server, "UD", $cb, $devid."|".$hint); + HMCCURPCPROC_Write ($server, 'UD', $cb, $devid.'|'.$hint); return; } @@ -2312,7 +2560,7 @@ sub HMCCURPCPROC_ReplaceDeviceCB ($$$$) my $name = $server->{hmccu}{name}; HMCCU_Log ($name, 2, "$cb device $devid1 replaced by $devid2"); - HMCCURPCPROC_Write ($server, "RD", $cb, $devid1."|".$devid2); + HMCCURPCPROC_Write ($server, 'RD', $cb, $devid1.'|'.$devid2); return; } @@ -2325,12 +2573,10 @@ sub HMCCURPCPROC_ReaddDevicesCB ($$$) { my ($server, $cb, $a) = @_; my $name = $server->{hmccu}{name}; - my $devcount = scalar (@$a); + my $devcount = scalar(@$a); HMCCU_Log ($name, 2, "$cb ReaddDevice received $devcount device addresses"); - foreach my $dev (@$a) { - HMCCURPCPROC_Write ($server, "RA", $cb, $dev); - } + foreach my $dev (@$a) { HMCCURPCPROC_Write ($server, 'RA', $cb, $dev); } return; } @@ -2345,7 +2591,7 @@ sub HMCCURPCPROC_EventCB ($$$$$) my $name = $server->{hmccu}{name}; my $etime = time (); - HMCCURPCPROC_Write ($server, "EV", $cb, $etime."|".$devid."|".$attr."|".$val); + HMCCURPCPROC_Write ($server, 'EV', $cb, $etime.'|'.$devid.'|'.$attr.'|'.$val); # Never remove this statement! return; @@ -2358,12 +2604,12 @@ sub HMCCURPCPROC_EventCB ($$$$$) sub HMCCURPCPROC_ListDevicesCB ($$) { my ($server, $cb) = @_; + $cb //= 'unknown'; my $name = $server->{hmccu}{name}; if ($server->{hmccu}{ccuflags} =~ /ccuInit/) { - $cb = "unknown" if (!defined ($cb)); HMCCU_Log ($name, 1, "$cb ListDevices. Sending init to HMCCU"); - HMCCURPCPROC_Write ($server, "IN", $cb, "INIT|1"); + HMCCURPCPROC_Write ($server, 'IN', $cb, 'INIT|1'); } return RPC::XML::array->new (); @@ -2377,7 +2623,7 @@ sub HMCCURPCPROC_ListDevicesCB ($$) # Convert value to RPC data type # Valid types are bool, boolean, int, integer, float, double, string. # If type is undefined, type is detected. If type cannot be detected -# value is returned as is. +# value is returned as it is. ###################################################################### sub HMCCURPCPROC_EncValue ($$) @@ -2385,23 +2631,14 @@ sub HMCCURPCPROC_EncValue ($$) my ($value, $type) = @_; # Try to detect type if type not specified - if (!defined ($type)) { - if (lc($value) =~ /^(true|false)$/) { - $type = 'boolean'; - } - elsif ($value =~ /^[-+]?\d+$/) { - $type = 'integer'; - } - elsif ($value =~ /^[-+]?[0-9]*\.[0-9]+$/) { - # A float must contain at least a dot followed by a digit - $type = 'float'; - } - elsif ($value =~ /[a-zA-Z_ ]/ || $value =~ /^'.+'$/ || $value =~ /^".+"$/) { - $type = 'string'; - } + if (!defined($type)) { + if (lc($value) =~ /^(true|false)$/) { $type = 'boolean'; } + elsif ($value =~ /^[-+]?\d+$/) { $type = 'integer'; } + elsif ($value =~ /^[-+]?[0-9]*\.[0-9]+$/) { $type = 'float'; } + elsif ($value eq '' || $value =~ /^([a-zA-Z_ ]+|'.+'|".+")$/) { $type = 'string'; } } - if (defined ($type)) { + if (defined($type)) { my $lcType = lc($type); if ($lcType =~ /^bool/ && uc($value) =~ /^(TRUE|FALSE|0|1)$/) { return RPC::XML::boolean->new ($value); @@ -2456,6 +2693,7 @@ sub HMCCURPCPROC_EncString ($) ###################################################################### # Encode name +# Encoded data will only contain the length and the name, no type. ###################################################################### sub HMCCURPCPROC_EncName ($) @@ -2472,16 +2710,6 @@ sub HMCCURPCPROC_EncName ($) sub HMCCURPCPROC_EncDouble ($) { my ($v) = @_; - -# my $s = $v < 0 ? -1.0 : 1.0; -# my $l = $v != 0.0 ? log (abs($v))/log (2) : 0.0; -# my $f = $l; -# -# if ($l-int ($l) > 0) { -# $f = ($l < 0) ? -int (abs ($l)+1.0) : int ($l); -# } -# my $e = $f+1; -# my $m = int ($v*2**-$e*0x40000000); my $m = 0; my $e = 0; @@ -2518,7 +2746,7 @@ sub HMCCURPCPROC_EncArray ($) my $r = ''; my $s = 0; - if (defined ($a)) { + if (defined($a)) { while (my $t = shift @$a) { my $e = shift @$a; if ($e) { @@ -2564,7 +2792,7 @@ sub HMCCURPCPROC_EncType ($$) { my ($t, $v) = @_; - return '' if (!defined ($t)); + return '' if (!defined($t)); if ($t == $BINRPC_INTEGER) { return HMCCURPCPROC_EncInteger ($v); @@ -2600,40 +2828,47 @@ sub HMCCURPCPROC_EncType ($$) # type is STRING. # Return encoded data or empty string on error ###################################################################### +# Binary RPC request format: +# +# Offset Size Description +# 0 3 'Bin' +# 3 1 Type: 0=Request, 1=Response +# 4 4 Total length of data: n+4(ml)+4(pc)+p +# 8 4 Length of request method name (ml) +# 12 n Request method name +# 12+n 4 Number of parameters (pc) +# 16+n p Encoded parameters +###################################################################### sub HMCCURPCPROC_EncodeRequest ($$) { my ($method, $args) = @_; + + return '' if (!defined($method) || $method eq ''); # Encode method - my $m = HMCCURPCPROC_EncName ($method); + my $methodEnc = HMCCURPCPROC_EncName ($method); # Encode parameters my $re = ':('.join('|', keys(%BINRPC_TYPE_MAPPING)).')'; - my $r = ''; + my $content = ''; my $s = 0; - if (defined ($args)) { + if (defined($args)) { while (my $p = shift @$args) { - my $pt = "STRING"; - if ($p =~ /${re}/) { - $pt = $1; - $p =~ s/${re}//; - } - my ($e, $t) = split (':', $p); - $r .= HMCCURPCPROC_EncType ($BINRPC_TYPE_MAPPING{uc($pt)}, $p); + my $pt = 'STRING'; + if ($p =~ /${re}/) { $pt = $1; $p =~ s/${re}//; } + my $encType = HMCCURPCPROC_EncType ($BINRPC_TYPE_MAPPING{uc($pt)}, $p); + return '' if ($encType eq ''); + $content .= $encType; $s++; } } - # Method, ParameterCount, Parameters - $r = $m.pack ('N', $s).$r; - - # Identifier, ContentLength, Content - # Ggf. +8 - $r = pack ('NN', $BINRPC_REQUEST, length ($r)+8).$r; + my $header = pack ('NN', $BINRPC_REQUEST, 8+length($method)+length($content)). + $methodEnc.pack('N', $s); - return $r; + return $header.$content; } ###################################################################### @@ -2647,8 +2882,8 @@ sub HMCCURPCPROC_EncodeResponse ($$) if (defined ($t) && defined ($v)) { my $r = HMCCURPCPROC_EncType ($t, $v); - # Ggf. +8 - return pack ('NN', $BINRPC_RESPONSE, length ($r)+8).$r; + # BINRPC is not a standard. Some implementations require an offset of 8 to be added + return pack ('NN', $BINRPC_RESPONSE, length($r)+8).$r; } else { return pack ('NN', $BINRPC_RESPONSE); @@ -2694,7 +2929,7 @@ sub HMCCURPCPROC_DecString ($$) my ($d, $i) = @_; my ($s, $o) = HMCCURPCPROC_DecInteger ($d, $i, 'N'); - if (defined ($s) && $i+$s+4 <= length ($d)) { + if (defined($s) && $i+$s+4 <= length ($d)) { return $s > 0 ? (substr ($d, $i+4, $s), $s+4) : ('', 4); } @@ -2710,7 +2945,7 @@ sub HMCCURPCPROC_DecDouble ($$) { my ($d, $i) = @_; - return (undef, undef) if ($i+8 > length ($d)); + return (undef, undef) if ($i+8 > length($d)); my $m = unpack ('l', reverse (substr ($d, $i, 4))); my $e = unpack ('l', reverse (substr ($d, $i+4, 4))); @@ -2743,11 +2978,11 @@ sub HMCCURPCPROC_DecArray ($$) my @r = (); my ($s, $x) = HMCCURPCPROC_DecInteger ($d, $i, 'N'); - if (defined ($s)) { + if (defined($s)) { my $j = $x; for (my $n=0; $n<$s; $n++) { my ($v, $o) = HMCCURPCPROC_DecType ($d, $i+$j); - return (undef, undef) if (!defined ($o)); + return (undef, undef) if (!defined($o)); push (@r, $v); $j += $o; } @@ -2768,13 +3003,13 @@ sub HMCCURPCPROC_DecStruct ($$) my %r; my ($s, $x) = HMCCURPCPROC_DecInteger ($d, $i, 'N'); - if (defined ($s)) { + if (defined($s)) { my $j = $x; for (my $n=0; $n<$s; $n++) { my ($k, $o1) = HMCCURPCPROC_DecString ($d, $i+$j); - return (undef, undef) if (!defined ($o1)); + return (undef, undef) if (!defined($o1)); my ($v, $o2) = HMCCURPCPROC_DecType ($d, $i+$j+$o1); - return (undef, undef) if (!defined ($o2)); + return (undef, undef) if (!defined($o2)); $r{$k} = $v; $j += $o1+$o2; } @@ -2787,6 +3022,7 @@ sub HMCCURPCPROC_DecStruct ($$) ###################################################################### # Decode any type # Return (element, packetsize) or (undef, undef) +# element could be a scalar, array ref or hash ref. ###################################################################### sub HMCCURPCPROC_DecType ($$) @@ -2849,18 +3085,17 @@ sub HMCCURPCPROC_DecodeRequest ($) return (undef, undef) if (!defined ($method)); $i += $o; - my $c = unpack ('N', substr ($data, $i, 4)); $i += 4; for (my $n=0; $n<$c; $n++) { my ($d, $s) = HMCCURPCPROC_DecType ($data, $i); - return (undef, undef) if (!defined ($d) || !defined ($s)); + return (undef, undef) if (!defined($d)|| !defined($s)); push (@r, $d); $i += $s; } - return (lc ($method), \@r); + return (lc($method), \@r); } ###################################################################### @@ -2873,13 +3108,13 @@ sub HMCCURPCPROC_DecodeResponse ($) { my ($data) = @_; - return (undef, undef) if (length ($data) < 8); + return (undef, 0) if (length($data) < 8); my $id = unpack ('N', substr ($data, 0, 4)); if ($id == $BINRPC_RESPONSE) { # Data my ($result, $offset) = HMCCURPCPROC_DecType ($data, 8); - return ($result, 1); + return ($result, defined($result) ? 1 : 0); } elsif ($id == $BINRPC_ERROR) { # Error @@ -2890,7 +3125,7 @@ sub HMCCURPCPROC_DecodeResponse ($) # elsif ($id == 0x42696E41) { # } - return (undef, undef); + return (undef, 0); } @@ -2958,6 +3193,13 @@ sub HMCCURPCPROC_DecodeResponse ($) Get

      +
    • get <name> devicedesc [<fhem-device>|<address>]
      + Read device and paramset descriptions for current RPC interface from CCU and + store the information in I/O device. The device description is always read from + CCU. The paramset description is only read if it doesn't exist in IO device. + If a HMCCUCHN device or a channel address is specified, the description of the + corresponding device address with all channels is read. +

    • get <name> rpcevent
      Show RPC server events statistics. If attribute ccuflags contains flag 'statistics' the 3 devices which sent most events are listed. @@ -3017,6 +3259,9 @@ sub HMCCURPCPROC_DecodeResponse ($) are forwarded to FHEM. In this case increase this value or increase attribute rpcMaxEvents. Default value is 500.

    • +
    • rpcReadTimeout <seconds>
      + Wait the specified time for socket to become readable. Default value is 0.005 seconds. +
    • rpcServerAddr <ip-address>
      Set local IP address of RPC servers on FHEM system. If attribute is missing the corresponding attribute of I/O device (HMCCU device) is used or IP address is @@ -3033,8 +3278,7 @@ sub HMCCURPCPROC_DecodeResponse ($) is 500.

    • rpcWriteTimeout <seconds>
      - Wait the specified time for socket to become readable or writeable. Default value - is 0.001 seconds. + Wait the specified time for socket to become writeable. Default value is 0.001 seconds.
    diff --git a/fhem/FHEM/HMCCUConf.pm b/fhem/FHEM/HMCCUConf.pm index af4c43784..a51b56511 100644 --- a/fhem/FHEM/HMCCUConf.pm +++ b/fhem/FHEM/HMCCUConf.pm @@ -4,11 +4,11 @@ # # $Id$ # -# Version 4.6.002 +# Version 5.0 # # Configuration parameters for HomeMatic devices. # -# (c) 2019 by zap (zap01 t-online de) +# (c) 2021 by zap (zap01 t-online de) # ######################################################################### @@ -17,10 +17,654 @@ package HMCCUConf; use strict; use warnings; +use vars qw($HMCCU_CONFIG_VERSION); +use vars qw(%HMCCU_DEF_ROLE); +use vars qw(%HMCCU_STATECONTROL); +use vars qw(%HMCCU_READINGS); +use vars qw(%HMCCU_ROLECMDS); +use vars qw(%HMCCU_ATTR); +use vars qw(%HMCCU_CONVERSIONS); use vars qw(%HMCCU_CHN_DEFAULTS); use vars qw(%HMCCU_DEV_DEFAULTS); use vars qw(%HMCCU_SCRIPTS); +$HMCCU_CONFIG_VERSION = '5.0'; + +###################################################################### +# Map subtype to default role. Subtype is only available for HMIP +# devices. +# Used by HMCCU to detect control channel of HMCCUDEV devices. +###################################################################### + +%HMCCU_DEF_ROLE = ( + 'ASIR' => 'ALARM_SWITCH_VIRTUAL_RECEIVER', + 'FSM' => 'SWITCH_VIRTUAL_RECEIVER', + 'PSM' => 'SWITCH_VIRTUAL_RECEIVER', + 'SD' => 'SMOKE_DETECTOR' +); + +###################################################################### +# Channel roles with state and control datapoints +# F: 1=Channel/HMCCUCHN, 2=Device/HMCCUDEV, 3=Both +# S: State datapoint, C: Control datapoint, V: Control values +# P: Priority (used by HMCCUDEV if more than 1 channel role fits) +# 1=lowest priority +###################################################################### + +%HMCCU_STATECONTROL = ( + 'ACCELERATION_TRANSCEIVER' => { + F => 3, S => 'MOTION', C => '', V => '', P => 1 + }, + 'ACCESSPOINT_GENERIC_RECEIVER' => { + F => 3, S => 'VOLTAGE', C => '', V => '', P => 1 + }, + 'ALARM_SWITCH_VIRTUAL_RECEIVER' => { + F => 3, S => 'ACOUSTIC_ALARM_ACTIVE', C => 'ACOUSTIC_ALARM_SELECTION', V => '', P => 2 + }, + 'BLIND' => { + F => 3, S => 'LEVEL', C => 'LEVEL', V => 'open:100,close:0', P => 2 + }, + 'BLIND_TRANSMITTER' => { + F => 3, S => 'LEVEL', C => '', V => '', P => 1 + }, + 'BLIND_VIRTUAL_RECEIVER' => { + F => 3, S => 'LEVEL', C => 'LEVEL', V => 'open:100,close:0', P => 2 + }, + 'BRIGHTNESS_TRANSMITTER' => { + F => 3, S => 'CURRENT_ILLUMINATION', C => '', V => '', P => 2 + }, + 'CLIMATE_TRANSCEIVER' => { + F => 3, S => 'ACTUAL_TEMPERATURE', C => '', V => '', P => 1 + }, + 'CLIMATECONTROL_REGULATOR' => { + F => 3, S => 'LEVEL', C => 'SETPOINT', V => 'on:30.5,off:4.5', P => 2 + }, + 'CLIMATECONTROL_RT_TRANSCEIVER' => { + F => 3, S => 'ACTUAL_TEMPERATURE', C => 'SET_TEMPERATURE', V => 'on:30.5,off:4.5', P => 2 + }, + 'CLIMATECONTROL_VENT_DRIVE' => { + F => 3, S => 'VALVE_STATE', C => '', V => '', P => 2 + }, + 'DIMMER' => { + F => 3, S => 'LEVEL', C => 'LEVEL', V => 'on:100,off:0', P => 2 + }, + 'DIMMER_TRANSMITTER' => { + F => 3, S => 'LEVEL', C => '', V => '', P => 1 + }, + 'DIMMER_VIRTUAL_RECEIVER' => { + F => 3, S => 'LEVEL', C => 'LEVEL', V => 'on:100,off:0', P => 2 + }, + 'DIMMER_WEEK_PROFILE' => { + F => 3, S => 'WEEK_PROGRAM_CHANNEL_LOCKS', C => 'WEEK_PROGRAM_TARGET_CHANNEL_LOCK', V => '', P => 2 + }, + 'DOOR_LOCK_STATE_TRANSMITTER' => { + F => 3, S => 'LOCK_STATE', C => 'LOCK_TARGET_LEVEL', V => 'open:2,unlocked:1,locked:0' + }, + 'ENERGIE_METER_TRANSMITTER' => { + F => 3, S => 'CURRENT', C => '', V => '', P => 1 + }, + 'HEATING_CLIMATECONTROL_TRANSCEIVER' => { + F => 3, S => 'ACTUAL_TEMPERATURE', C => 'SET_POINT_TEMPERATURE', V => 'on:30.5,off:4.5', P => 2 + }, + 'KEY' => { + F => 3, S => 'PRESS_SHORT', C => 'PRESS_SHORT', V => 'pressed:true', P => 1 + }, + 'KEY_TRANSCEIVER' => { + F => 3, S => 'PRESS_SHORT', C => '', V => '', P => 1 + }, + 'LUXMETER' => { + F => 3, S => 'LUX', C => '', V => '', P => 2 + }, + 'MOTION_DETECTOR' => { + F => 3, S => 'MOTION', C => '', V => '', P => 1 + }, + 'MOTIONDETECTOR_TRANSCEIVER' => { + F => 3, S => 'MOTION', C => 'MOTION_DETECTION_ACTIVE', V => 'active:1,inactive:0', P => 2 + }, + 'MULTI_MODE_INPUT_TRANSMITTER' => { + F => 3, S => 'STATE', C => '', V => '', P => 1 + }, + 'PASSAGE_DETECTOR_DIRECTION_TRANSMITTER' => { + F => 3, S => 'CURRENT_PASSAGE_DIRECTION', C => '', V => '', P => 1 + }, + 'POWERMETER' => { + F => 3, S => 'CURRENT', C => '', V => '', P => 1 + }, + 'PRESENCEDETECTOR_TRANSCEIVER' => { + F => 3, S => 'PRESENCE_DETECTION_STATE', C => 'PRESENCE_DETECTION_ACTIVE', V => 'active:1,inactive:0', P => 2 + }, + 'ROTARY_HANDLE_SENSOR' => { + F => 3, S => 'STATE', C => '', V => '', P => 2 + }, + 'ROTARY_HANDLE_TRANSCEIVER' => { + F => 3, S => 'STATE', C => '', V => '', P => 2 + }, + 'SHUTTER_CONTACT' => { + F => 3, S => 'STATE', C => '', V => '', P => 2 + }, + 'SHUTTER_CONTACT_TRANSCEIVER' => { + F => 3, S => 'STATE', C => '', V => '', P => 2 + }, + 'SMOKE_DETECTOR' => { + F => 3, S => 'BidCos-RF:STATE,SMOKE_DETECTOR_ALARM_STATUS', C => 'HmIP-RF:SMOKE_DETECTOR_COMMAND', V => '', P => 2 + }, + 'VIRTUAL_KEY' => { + F => 3, S => 'PRESS_SHORT', C => 'PRESS_SHORT', V => 'pressed:true', P => 1 + }, + 'SHUTTER_TRANSMITTER' => { + F => 3, S => 'LEVEL', C => '', V => '', P => 1 + }, + 'SHUTTER_VIRTUAL_RECEIVER' => { + F => 3, S => 'LEVEL', C => 'LEVEL', V => 'open:100,close:0', P => 2 + }, + 'SWITCH' => { + F => 3, S => 'STATE', C => 'STATE', V => 'on:true,off:false', P => 2 + }, + 'SWITCH_TRANSMITTER' => { + F => 3, S => 'STATE', C => '', V => '', P => 1 + }, + 'SWITCH_VIRTUAL_RECEIVER' => { + F => 3, S => 'STATE', C => 'STATE', V => 'on:true,off:false', P => 2 + }, + 'THERMALCONTROL_TRANSMIT' => { + F => 3, S => 'ACTUAL_TEMPERATURE', C => 'SET_TEMPERATURE', V => 'on:30.5,off:4.5', P => 2 + }, + 'WATER_DETECTION_TRANSMITTER' => { + F => 3, S => 'ALARMSTATE', C => '', V => '', P => 1 + }, + 'WEATHER' => { + F => 3, S => 'TEMPERATURE', C => '', V => '', P => 1 + }, + 'WEATHER_TRANSMIT' => { + F => 3, S => 'ACTUAL_TEMPERATURE', C => '', V => '', P => 1 + } +); + +###################################################################### +# Add or rename readings +# C# = Placeholder for state or control channel number +# DEFAULT should not be used, if a HMCCUDEV device has multiple +# channels with identical datapoints (i.e. LEVEL) +###################################################################### + +%HMCCU_READINGS = ( + 'BLIND' => + '(C#\.)?LEVEL$:+pct', + 'BLIND_TRANSMITTER' => + '(C#\.)?LEVEL$:+pct', + 'BLIND_VIRTUAL_RECEIVER' => + '(C#\.)?LEVEL$:+pct', + 'SHUTTER_TRANSMITTER' => + '(C#\.)?LEVEL$:+pct', + 'SHUTTER_VIRTUAL_RECEIVER' => + '(C#\.)?LEVEL$:+pct', + 'DIMMER' => + '(C#\.)?LEVEL$:+pct', + 'DIMMER_TRANSMITTER' => + '(C#\.)?LEVEL$:+pct;(C#\.)?COLOR$:+color', + 'DIMMER_VIRTUAL_RECEIVER' => + '(C#\.)?LEVEL$:+pct;(C#\.)?COLOR$:+color', + 'DIMMER_WEEK_PROFILE' => + '(C#\.)?WEEK_PROGRAM_CHANNEL_LOCKS$:+progMode', + 'KEY' => + '(C#\.)?PRESS_(SHORT|LONG)$:+pressed', + 'KEY_TRANSCEIVER' => + '(C#\.)?PRESS_(SHORT|LONG)$:+pressed', + 'VIRTUAL_KEY' => + '(C#\.)?PRESS_(SHORT|LONG)$:+pressed', + 'ACCELERATION_TRANSCEIVER' => + '(C#\.)?MOTION:motion', + 'MOTION_DETECTOR' => + '^(C#\.)?BRIGHTNESS$:brightness;(C#\.)?MOTION:motion', + 'MOTIONDETECTOR_TRANSCEIVER' => + '^(C#\.)?ILLUMINATION$:brightness;^(C#\.)?MOTION$:motion;(C#\.)?MOTION_DETECTION_ACTIVE$:detection', + 'PRESENCEDETECTOR_TRANSCEIVER' => + '^(C#\.)?ILLUMINATION$:brightness;(C#\.)?PRESENCE_DETECTION_STATE:presence;(C#\.)?PRESENCE_DETECTION_ACTIVE:detection', + 'WEATHER' => + '(C#\.)?TEMPERATURE$:+measured-temp;'. + '(C#\.)?HUMIDITY$:+humidity', + 'WEATHER_TRANSMIT' => + '(C#\.)?TEMPERATURE$:+measured-temp;'. + '(C#\.)?HUMIDITY$:+humidity', + 'CLIMATE_TRANSCEIVER' => + '(C#\.)?ACTUAL_TEMPERATURE$:+measured-temp;'. + '(C#\.)?ACTUAL_HUMIDITY$:+humidity', + 'THERMALCONTROL_TRANSMIT' => + '(C#\.)?ACTUAL_TEMPERATURE$:+measured-temp;'. + '(C#\.)?ACTUAL_HUMIDITY$:+humidity;'. + '(C#\.)?SET_TEMPERATURE$:+desired-temp', + 'CLIMATECONTROL_RT_TRANSCEIVER' => + '(C#\.)?ACTUAL_TEMPERATURE$:+measured-temp;'. + '(C#\.)?ACTUAL_HUMIDITY$:+humidity;'. + '(C#\.)?SET_TEMPERATURE$:+desired-temp', + 'HEATING_CLIMATECONTROL_TRANSCEIVER' => + '(C#\.)?ACTUAL_TEMPERATURE$:+measured-temp;'. + '(C#\.)?ACTUAL_HUMIDITY$:+humidity;'. + '(C#\.)?SET_POINT_TEMPERATURE$:+desired-temp', + 'CLIMATECONTROL_REGULATOR' => + '(C#\.)?SETPOINT$:+desired-temp', + 'DEFAULT' => + '([0-9]{1,2}\.)?LEVEL$:+pct;'. + '([0-9]{1,2}\.)?SET_TEMPERATURE$:+desired-temp;'. + '^([0-9]{1,2}\.)?(ACTUAL_TEMPERATURE|TEMPERATURE)$:+measured-temp;'. + '([0-9]{1,2}\.)?SET_POINT_TEMPERATURE$:+desired-temp;'. + '([0-9]{1,2}\.)?ACTUAL_HUMIDITY$:+humidity' +); + +###################################################################### +# Set commands related to channel role +# Role => { Command-Definition, ... } +# Command-Defintion: +# Command[:InterfaceExpr] => [No:]Datapoint-Def[:Function] [...]' +# No: +# Execution order of subcommands. By default subcommands are +# executed from left to right. +# Function: +# A Perl function name +# Datapoint-Def: +# Paramset:Datapoints:[Parameter=]FixedValue +# Paramset:Datapoints:?Parameter +# Paramset:Datapoints:?Parameter=Default-Value +# Paramset:Datapoints:#Parameter[=FixedValue,[...]] +# Paramset:Datapoints:*Parameter=Default-Value +# Paramset: +# V=VALUES, M=MASTER (channel), D=MASTER (device), I=INTERNAL +# Datapoints: +# List of parameter names separated by ',' +# Parameter characters: +# ? = any value is accepted +# # = If datapoint is of type ENUM, values are taken from +# parameter set description. Otherwise a list of values must +# be specified after '='. +# * = internal value $hash->{hmccu}{values}{parameterName} +# FixedValue: Parameter values are detected in the following order: +# 1. If command parameter name is identical with controldatapoint, +# option values are taken from controldatapoint definition {V}. The +# FixedValues are used as lookup key into HMCCU_STATECCONTROL. +# The command options are identical to the FixedValues. +# 2. FixedValues are treated as option values. The option +# names are taken from HMCCU_CONVERSIONS by using FixedValues as +# lookup key. +# 3. As a fallback command options and option values are identical. +# If Default-Value is preceeded by + or -, value is added to or +# subtracted from current datapoint value +###################################################################### + +%HMCCU_ROLECMDS = ( + 'MOTIONDETECTOR_TRANSCEIVER' => { + 'detection' => 'V:MOTION_DETECTION_ACTIVE:#detection=inactive,active', + 'reset' => 'V:RESET_MOTION:true' + }, + 'PRESENCEDETECTOR_TRANSCEIVER' => { + 'detection' => 'V:PRESENCE_DETECTION_ACTIVE:#detection=inactive,active', + 'reset' => 'V:RESET_PRESENCE:true' + }, + 'PASSAGE_DETECTOR_DIRECTION_TRANSMITTER' => { + 'detection' => 'M:PASSAGE_DETECTION,CHANNEL_OPERATION_MODE:#inactive,active' + }, + 'SMOKE_DETECTOR' => { + 'command' => 'V:SMOKE_DETECTOR_COMMAND:#command' + }, + 'ALARM_SWITCH_VIRTUAL_RECEIVER' => { + 'opticalAlarm' => 'V:OPTICAL_ALARM_SELECTION:#alarmMode V:ACOUSTIC_ALARM_SELECTION:0 V:DURATION_UNIT:*unit=0 V:DURATION_VALUE:*duration=10', + 'acousticAlarm' => 'V:ACOUSTIC_ALARM_SELECTION:#alarmMode V:OPTICAL_ALARM_SELECTION:0 V:DURATION_UNIT:0 V:DURATION_VALUE:10', + 'duration' => 'I:DURATION_VALUE:?duration I:DURATION_UNIT:#unit' + }, + 'DOOR_LOCK_STATE_TRANSMITTER' => { + 'open' => 'V:LOCK_TARGET_LEVEL:2', + 'unlock' => 'V:LOCK_TARGET_LEVEL:1', + 'lock' => 'V:LOCK_TARGET_LEVEL:0' + }, + 'KEY' => { + 'on' => 'V:PRESS_SHORT:1', + 'off' => 'V:PRESS_SHORT:1', + 'press' => 'V:PRESS_SHORT:1' + }, + 'VIRTUAL_KEY' => { + 'on' => 'V:PRESS_SHORT:1', + 'off' => 'V:PRESS_SHORT:1', + 'press' => 'V:PRESS_SHORT:1' + }, + 'BLIND' => { + 'pct' => 'V:LEVEL:?level', + 'open' => 'V:LEVEL:100', + 'close' => 'V:LEVEL:0', + 'up' => 'V:LEVEL:?delta=+20', + 'down' => 'V:LEVEL:?delta=-20', + 'stop' => 'V:STOP:1' + }, + 'BLIND_VIRTUAL_RECEIVER' => { + 'pct' => 'V:LEVEL:?level', + 'open' => 'V:LEVEL:100', + 'close' => 'V:LEVEL:0', + 'up' => 'V:LEVEL:?delta=+20', + 'down' => 'V:LEVEL:?delta=-20', + 'stop' => 'V:STOP:1' + }, + 'SHUTTER_VIRTUAL_RECEIVER' => { + 'pct' => 'V:LEVEL:?level', + 'open' => 'V:LEVEL:100', + 'close' => 'V:LEVEL:0', + 'up' => 'V:LEVEL:?delta=+20', + 'down' => 'V:LEVEL:?delta=-20', + 'stop' => 'V:STOP:1' + }, + 'SWITCH' => { + 'on' => 'V:STATE:1', + 'off' => 'V:STATE:0', + 'on-for-timer' => 'V:ON_TIME:?duration V:STATE:1', + 'on-till' => 'V:ON_TIME:?time V:STATE:1' + }, + 'SWITCH_VIRTUAL_RECEIVER' => { + 'on' => 'V:STATE:1', + 'off' => 'V:STATE:0', + 'on-for-timer' => 'V:ON_TIME:?duration V:STATE:1', + 'on-till' => 'V:ON_TIME:?time V:STATE:1' + }, + 'DIMMER' => { + 'pct' => '3:V:LEVEL:?level 1:V:ON_TIME:?time=0.0 2:V:RAMP_TIME:?ramp=0.5', + 'on' => 'V:LEVEL:100', + 'off' => 'V:LEVEL:0', + 'on-for-timer' => 'V:ON_TIME:?duration V:LEVEL:100', + 'on-till' => 'V:ON_TIME:?time V:LEVEL:100', + 'up' => 'V:LEVEL:?delta=+10', + 'down' => 'V:LEVEL:?delta=-10', + 'stop' => 'V:RAMP_STOP:1' + }, + 'DIMMER_VIRTUAL_RECEIVER' => { + 'pct' => '5:V:LEVEL:?level 1:V:DURATION_UNIT:0 2:V:ON_TIME,DURATION_VALUE:?time=0.0 3:V:RAMP_TIME_UNIT:0 4:V:RAMP_TIME,RAMP_TIME_VALUE:?ramp=0.5', + 'on' => 'V:LEVEL:100', + 'off' => 'V:LEVEL:0', + 'on-for-timer' => '1:V:DURATION_UNIT:0 2:V:ON_TIME,DURATION_VALUE:?duration 3:V:LEVEL:100', + 'on-till' => '1:V:DURATION_UNIT:0 2:V:ON_TIME,DURATION_VALUE:?time 3:V:LEVEL:100', + 'up' => 'V:LEVEL:?delta=+10', + 'down' => 'V:LEVEL:?delta=-10', + 'color' => 'V:COLOR:#color' + }, + 'DIMMER_WEEK_PROFILE' => { + 'progMode' => 'V:WEEK_PROGRAM_TARGET_CHANNEL_LOCK:#progMode' + }, + 'THERMALCONTROL_TRANSMIT' => { + 'desired-temp' => 'V:SET_TEMPERATURE:?temperature', + 'manu' => 'V:MANU_MODE:?temperature=20', + 'on' => 'V:MANU_MODE:30.5', + 'off' => 'V:MANU_MODE:4.5', + 'auto' => 'V:AUTO_MODE:1', + 'boost' => 'V:BOOST_MODE:1', + 'week-program' => 'D:WEEK_PROGRAM_POINTER:#program', + 'get week-program' => 'D:WEEK_PROGRAM_POINTER:#program:HMCCU_DisplayWeekProgram' + }, + 'CLIMATECONTROL_RT_TRANSCEIVER' => { + 'desired-temp' => 'V:SET_TEMPERATURE:?temperature', + 'manu' => 'V:MANU_MODE:?temperature=20', + 'on' => 'V:MANU_MODE:30.5', + 'off' => 'V:MANU_MODE:4.5', + 'auto' => 'V:AUTO_MODE:1', + 'boost' => 'V:BOOST_MODE:1', + 'week-program:VirtualDevices' => 'D:WEEK_PROGRAM_POINTER:#program', + 'get week-program:VirtualDevices' => 'D:WEEK_PROGRAM_POINTER:#program:HMCCU_DisplayWeekProgram' + }, + 'HEATING_CLIMATECONTROL_TRANSCEIVER' => { + 'desired-temp' => 'V:SET_POINT_TEMPERATURE:?temperature', + 'auto' => 'V:CONTROL_MODE:0', + 'manu' => 'V:CONTROL_MODE:1', + 'holiday' => 'V:CONTROL_MODE:2', + 'boost' => 'V:BOOST_MODE:1', + 'on' => 'V:CONTROL_MODE:1 V:SET_POINT_TEMPERATURE:30.5', + 'off' => 'V:CONTROL_MODE:1 V:SET_POINT_TEMPERATURE:4.5' + }, + 'CLIMATECONTROL_REGULATOR' => { + 'desired-temp' => 'V:SETPOINT:?temperature', + 'on' => 'V:SETPOINT:30.5', + 'off' => 'V:SETPOINT:4.5' + } +); + +###################################################################### +# Channel roles with attributes +# If key '_none_' exists, role doesn't have default attributes +###################################################################### + +%HMCCU_ATTR = ( + 'ACCELERATION_TRANSCEIVER' => { + '_none_' => '' + }, + 'BLIND' => { + 'substexcl' => 'pct', + 'cmdIcon' => 'open:fts_shutter_up stop:fts_shutter_manual close:fts_shutter_down', + 'webCmd' => 'pct:open:close:stop', + 'widgetOverride' => 'pct:slider,0,10,100' + }, + 'BLIND_TRANSMITTER' => { + 'substexcl' => 'pct', + }, + 'BLIND_VIRTUAL_RECEIVER' => { + 'substexcl' => 'pct', + 'cmdIcon' => 'open:fts_shutter_up stop:fts_shutter_manual close:fts_shutter_down', + 'webCmd' => 'pct:open:close:stop', + 'widgetOverride' => 'pct:slider,0,10,100' + }, + 'DIMMER' => { + 'cmdIcon' => 'on:general_an off:general_aus', + 'substexcl' => 'pct', + 'webCmd' => 'pct:on:off', + 'widgetOverride' => 'pct:slider,0,10,100' + }, + 'DIMMER_TRANSMITTER' => { + 'substexcl' => 'pct' + }, + 'DIMMER_VIRTUAL_RECEIVER' => { + 'cmdIcon' => 'on:general_an off:general_aus', + 'substexcl' => 'pct', + 'webCmd' => 'pct:on:off', + 'widgetOverride' => 'pct:slider,0,10,100' + }, + 'DOOR_LOCK_STATE_TRANSMITTER' => { + 'cmdIcon' => 'open:fts_door_open unlock:secur_open lock:secur_lock', + 'webCmd' => 'lock:unlock:open' + }, + 'KEY' => { + 'event-on-update-reading' => 'PRESS.*', + 'cmdIcon' => 'press:taster', + 'webCmd' => 'press' + }, + 'KEY_TRANSCEIVER' => { + 'event-on-update-reading' => 'PRESS.*' + }, + 'MOTION_DETECTOR' => { + '_none_' => '' + }, + 'MOTIONDETECTOR_TRANSCEIVER' => { + 'cmdIcon' => 'reset:rc_BACK', + 'webCmd' => 'detection:reset' + }, + 'MULTI_MODE_INPUT_TRANSMITTER' => { + '_none_' => '' + }, + 'PASSAGE_DETECTOR_DIRECTION_TRANSMITTER' => { + '_none_' => '' + }, + 'PRESENCEDETECTOR_TRANSCEIVER' => { + 'cmdIcon' => 'reset:rc_BACK', + 'webCmd' => 'detection:reset' + }, + 'SHUTTER_CONTACT' => { + '_none_' => '' + }, + 'SHUTTER_CONTACT_TRANSCEIVER' => { + '_none_' => '' + }, + 'SHUTTER_TRANSMITTER' => { + 'substexcl' => 'pct', + }, + 'SHUTTER_VIRTUAL_RECEIVER' => { + 'substexcl' => 'pct', + 'cmdIcon' => 'open:fts_shutter_up stop:fts_shutter_manual close:fts_shutter_down', + 'webCmd' => 'pct:open:close:stop', + 'widgetOverride' => 'pct:slider,0,10,100' + }, + 'SWITCH' => { + 'cmdIcon' => 'on:general_an off:general_aus' + }, + 'SWITCH_VIRTUAL_RECEIVER' => { + 'cmdIcon' => 'on:general_an off:general_aus' + }, + 'THERMALCONTROL_TRANSMIT' => { + 'substexcl' => 'desired-temp', + 'cmdIcon' => 'auto:sani_heating_automatic manu:sani_heating_manual boost:sani_heating_boost on:general_an off:general_aus', + 'webCmd' => 'desired-temp:auto:manu:boost:on:off', + 'widgetOverride' => 'desired-temp:slider,4.5,0.5,30.5,1' + }, + 'CLIMATECONTROL_RT_TRANSCEIVER' => { + 'substexcl' => 'desired-temp', + 'cmdIcon' => 'auto:sani_heating_automatic manu:sani_heating_manual boost:sani_heating_boost on:general_an off:general_aus', + 'webCmd' => 'desired-temp:auto:manu:boost:on:off', + 'widgetOverride' => 'desired-temp:slider,4.5,0.5,30.5,1' + }, + 'HEATING_CLIMATECONTROL_TRANSCEIVER' => { + 'substexcl' => 'desired-temp', + 'cmdIcon' => 'auto:sani_heating_automatic manu:sani_heating_manual boost:sani_heating_boost on:general_an off:general_aus', + 'webCmd' => 'desired-temp:auto:manu:boost:on:off', + 'widgetOverride' => 'desired-temp:slider,4.5,0.5,30.5,1' + }, + 'CLIMATECONTROL_REGULATOR' => { + 'substexcl' => 'desired-temp', + 'cmdIcon' => 'on:general_an off:general_aus', + 'webCmd' => 'desired-temp:on:off', + 'widgetOverride' => 'desired-temp:slider,4.5,0.5,30.5,1' + }, + 'CLIMATECONTROL_VENT_DRIVE' => { + '_none_' => '' + }, + 'WATER_DETECTION_TRANSMITTER' => { + '_none_' => '' + } +); + +###################################################################### +# Value conversions +# Role => { +# Datapoint => { Value => 'Conversion', ... }, +# ... +# } +###################################################################### + +%HMCCU_CONVERSIONS = ( + 'ACCELERATION_TRANSCEIVER' => { + 'MOTION' => { '0' => 'noMotion', 'false' => 'noMotion', '1' => 'motion', 'true' => 'motion' } + }, + 'MOTION_DETECTOR' => { + 'MOTION' => { '0' => 'noMotion', 'false' => 'noMotion', '1' => 'motion', 'true' => 'motion' } + }, + 'MOTIONDETECTOR_TRANSCEIVER' => { + 'MOTION' => { '0' => 'noMotion', 'false' => 'noMotion', '1' => 'motion', 'true' => 'motion' }, + 'MOTION_DETECTION_ACTIVE' => { '0' => 'inactive', 'false' => 'inactive', '1' => 'active', 'true' => 'active' } + }, + 'PRESENCEDETECTOR_TRANSCEIVER' => { + 'PRESENCE_DETECTION_STATE' => { '0' => 'noPresence', 'false' => 'noPresence', '1' => 'presence', 'true' => 'presence' }, + 'PRESENCE_DETECTION_ACTIVE' => { '0' => 'inactive', 'false' => 'inactive', '1' => 'active', 'true' => 'active' } + }, + 'PASSAGE_DETECTOR_DIRECTION_TRANSMITTER' => { + 'PASSAGE_DETECTION' => { '0' => 'inactive', 1 => 'active' }, + 'CHANNEL_OPERATION_MODE' => { '0' => 'inactive', 1 => 'active'} + }, + 'KEY' => { + 'PRESS_SHORT' => { '1' => 'pressed', 'true' => 'pressed' }, + 'PRESS_LONG' => { '1' => 'pressed', 'true' => 'pressed' } + }, + 'KEY_TRANSCEIVER' => { + 'PRESS_SHORT' => { '1' => 'pressed', 'true' => 'pressed' }, + 'PRESS_LONG' => { '1' => 'pressed', 'true' => 'pressed' } + }, + 'VIRTUAL_KEY' => { + 'PRESS_SHORT' => { '1' => 'pressed', 'true' => 'pressed' }, + 'PRESS_LONG' => { '1' => 'pressed', 'true' => 'pressed' } + }, + 'SHUTTER_CONTACT' => { + 'STATE' => { '0' => 'closed', '1' => 'open', 'false' => 'closed', 'true' => 'open' } + }, + 'SHUTTER_CONTACT_TRANSCEIVER' => { + 'STATE' => { '0' => 'closed', '1' => 'open', 'false' => 'closed', 'true' => 'open' } + }, + 'ROTARY_HANDLE_SENSOR' => { + 'STATE' => { '0' => 'closed', '1' => 'tilted', '2' => 'open' } + }, + 'ROTARY_HANDLE_TRANSCEIVER' => { + 'STATE' => { '0' => 'closed', '1' => 'tilted', '2' => 'open' } + }, + 'ALARM_SWITCH_VIRTUAL_RECEIVER' => { + 'STATE' => { '0' => 'ok', '1' => 'alarm', 'false' => 'ok', 'true' => 'alarm' } + }, + 'SWITCH' => { + 'STATE' => { '0' => 'off', 'false' => 'off', '1' => 'on', 'true' => 'on', 'off' => '0', 'on' => '1' }, + }, + 'SWITCH_TRANSMITTER' => { + 'STATE' => { '0' => 'off', 'false' => 'off', '1' => 'on', 'true' => 'on', 'off' => '0', 'on' => '1' }, + }, + 'SWITCH_VIRTUAL_RECEIVER' => { + 'STATE' => { '0' => 'off', 'false' => 'off', '1' => 'on', 'true' => 'on', 'off' => '0', 'on' => '1' }, + }, + 'BLIND' => { + 'LEVEL' => { '0' => 'closed', '100' => 'open', 'close' => '0', 'open' => '100' }, + 'DIRECTION' => { '0' => 'none', '1' => 'up', '2' => 'down' }, + 'WORKING' => { '0' => 'no', 'false' => 'no', '1' => 'yes', 'true' => 'yes' } + }, + 'BLIND_TRANSMITTER' => { + 'LEVEL' => { '0' => 'closed', '100' => 'open', 'close' => '0', 'open' => '100' } + }, + 'BLIND_VIRTUAL_RECEIVER' => { + 'LEVEL' => { '0' => 'closed', '100' => 'open', 'close' => '0', 'open' => '100' }, + 'DIRECTION' => { '0' => 'none', '1' => 'up', '2' => 'down' }, + 'WORKING' => { '0' => 'no', 'false' => 'no', '1' => 'yes', 'true' => 'yes' } + }, + 'SHUTTER_TRANSMITTER' => { + 'LEVEL' => { '0' => 'closed', '100' => 'open', 'close' => '0', 'open' => '100' } + }, + 'SHUTTER_VIRTUAL_RECEIVER' => { + 'LEVEL' => { '0' => 'closed', '100' => 'open', 'close' => '0', 'open' => '100' } + }, + 'DIMMER' => { + 'LEVEL' => { '0' => 'off', '100' => 'on', 'off' => '0', 'on' => '100' }, + 'DIRECTION' => { '0' => 'none', '1' => 'up', '2' => 'down' }, + 'WORKING' => { '0' => 'no', 'false' => 'no', '1' => 'yes', 'true' => 'yes' } + }, + 'DIMMER_TRANSMITTER' => { + 'LEVEL' => { '0' => 'off', '100' => 'on', 'off' => '0', 'on' => '100' }, + 'DIRECTION' => { '0' => 'none', '1' => 'up', '2' => 'down' }, + 'WORKING' => { '0' => 'no', 'false' => 'no', '1' => 'yes', 'true' => 'yes' } + }, + 'DIMMER_VIRTUAL_RECEIVER' => { + 'LEVEL' => { '0' => 'off', '100' => 'on', 'off' => '0', 'on' => '100' }, + 'DIRECTION' => { '0' => 'none', '1' => 'up', '2' => 'down' }, + 'WORKING' => { '0' => 'no', 'false' => 'no', '1' => 'yes', 'true' => 'yes' } + }, + 'THERMALCONTROL_TRANSMIT' => { + 'SET_TEMPERATURE' => { '4.5' => 'off', '30.5' => 'on' }, + 'WINDOW_OPEN_REPORTING' => { '0' => 'closed', '1' => 'open', 'false' => 'closed', 'true' => 'open' } + }, + 'CLIMATECONTROL_RT_TRANSCEIVER' => { + 'SET_TEMPERATURE' => { '4.5' => 'off', '30.5' => 'on' } + }, + 'HEATING_CLIMATECONTROL_TRANSCEIVER' => { + 'SET_POINT_TEMPERATURE' => { '4.5' => 'off', '30.5' => 'on' }, + 'SET_POINT_MODE' => { '0' => 'auto', '1' => 'manual', '2' => 'boost', '3' => 'off' }, + 'WINDOW_STATE' => { '0' => 'closed', '1' => 'open', 'false' => 'closed', 'true' => 'open' } + }, + 'CLIMATECONTROL_REGULATOR' => { + 'SETPOINT' => { '4.5' => 'off', '30.5' => 'on' } + }, + 'WATER_DETECTION_TRANSMITTER' => { + 'ALARMSTATE' => { '0' => 'noAlarm', '1' => 'Alarm', 'false' => 'noAlarm', 'true' => 'alarm' } + }, + 'DEFAULT' => { + 'AES_KEY' => { '0' => 'off', 'false' => 'off', '1' => 'on', 'true' => 'on' }, + 'LOW_BAT' => { '0' => 'ok', 'false' => 'ok', '1' => 'low', 'true' => 'low' }, + 'LOWBAT' => { '0' => 'ok', 'false' => 'ok', '1' => 'low', 'true' => 'low' }, + 'STATE' => { '0' => 'false', '1' => 'true' }, + 'UNREACH' => { '0' => 'alive', 'false' => 'alive', '1' => 'dead', 'true' => 'dead' } + } +); + ###################################################################### # Default attributes for Homematic devices of type HMCCUCHN ###################################################################### @@ -462,11 +1106,8 @@ use vars qw(%HMCCU_SCRIPTS); %HMCCU_DEV_DEFAULTS = ( "CCU2" => { - _description => "HomeMatic CCU2", - "ccudef-readingfilter" => '^(LOW_?BAT|UNREACH)\$', - "ccudef-readingformat" => 'datapoint', - "ccudef-readingname" => '^(.+\.)?AES_KEY\$:sign;^(.+\.)?LOW_?BAT\$:battery;^(.+\.)?BATTERY_STATE\$:batteryLevel;^(.+\.)?UNREACH\$:Activity;^(.+\.)?TEMPERATURE\$:+temperature;^(.+\.)?SET_TEMPERATURE\$:+desired-temp;^(.+\.)?HUMIDITY\$:+humidity;^(.+\.)?LEVEL\$:+pct;^(.+\.)?CONTROL_MODE\$:+controlMode', - "ccudef-substitute" => 'AES_KEY!(0|false):off,(1|true):on;LOWBAT,LOW_BAT!(0|false):ok,(1|true):low;UNREACH!(0|false):alive,(1|true):dead;MOTION!(0|false):noMotion,(1|true):motion;DIRECTION!0:stop,1:up,2:down,3:undefined;WORKING!0:false,1:true;INHIBIT!(0|false):unlocked,(1|true):locked' + _description => "HomeMatic CCU", + "ccudef-readingformat" => 'datapoint' }, "HM-Sec-SCo|HM-Sec-SC|HM-Sec-SC-2|HMIP-SWDO" => { _description => "Tuer/Fensterkontakt optisch und magnetisch", @@ -979,7 +1620,7 @@ use vars qw(%HMCCU_SCRIPTS); eventMap => "/datapoint 1.STATE 1:led-on/datapoint 1.STATE 0:led-off/datapoint 2.STATE 1:sound-on/datapoint 2.STATE 0:sound-off", statedatapoint => "1.STATE", statevals => "on:true,off:false", - substitute => "STATE!(0|false):ledOff,(1|true):ledOn;2.STATE!(0|false):soundOff,(1|true):soundOn" + substitute => "1.STATE!(0|false):ledOff,(1|true):ledOn;2.STATE!(0|false):soundOff,(1|true):soundOn" }, "HM-PB-4Dis-WM" => { _description => "Funk-Display Wandtaster", @@ -1141,7 +1782,7 @@ if (oSV) { code => qq( object osysvar; string ssysvarid; -foreach (ssysvarid, (dom.GetObject(ID_SYSTEM_VARIABLES)).EnumUsedIDs()) { +foreach (ssysvarid, (dom.GetObject(ID_SYSTEM_VARIABLES)).EnumIDs()) { osysvar = dom.GetObject(ssysvarid); Write(osysvar.Name()); if(osysvar.ValueSubType() == 6) { @@ -1160,7 +1801,7 @@ foreach (ssysvarid, (dom.GetObject(ID_SYSTEM_VARIABLES)).EnumUsedIDs()) { parameters => 0, code => qq( string sSysVarId; -foreach (sSysVarId, (dom.GetObject(ID_SYSTEM_VARIABLES)).EnumUsedIDs()) { +foreach (sSysVarId, (dom.GetObject(ID_SYSTEM_VARIABLES)).EnumIDs()) { object oSysVar = dom.GetObject(sSysVarId); Write(oSysVar.Name()); if (oSysVar.ValueSubType() == 6) { @@ -1195,6 +1836,9 @@ string chnid; string sDPId; object odev = (dom.GetObject(ID_DEVICES)).Get("\$devname"); if (odev) { + string intid=odev.Interface(); + string intna=dom.GetObject(intid).Name(); + WriteLine ("D;" # intna # ";" # odev.Address() # ";" # odev.Name() # ";" # odev.HssType()); foreach (chnid, odev.Channels()) { object ochn = dom.GetObject(chnid); if (ochn) {