From 5d7f5bf2b03d30bce77915750b49d05e3090daac Mon Sep 17 00:00:00 2001 From: Michael Maroszek Date: Sat, 17 Oct 2026 09:52:05 +0200 Subject: [PATCH] Complete documented return values, keys and constants - Return TRUE from IPS_Set*/Delete*/profile functions and IPS_RequestAction like documented - RegisterScript creates a read-only script with the default content of the kernel and returns its ID (IPSModuleStrict: whether it was created) - Add CategoryID, InstanceSupportsSearching/InstanceIsSearching/InstanceVisualizationType, ScriptCRC/ScriptSize/ScriptUpdated and URL/Limit/Translation to the IPS_Get* results; hide the internal Class key of IPS_GetModule - Wire up IPS_GetMedia and the other media functions to the MediaManager - SetVisualizationType is protected like in the kernel and is reflected in IPS_GetInstance - Remove ConnectParent/RequireParent/ForceParent from IPSModuleStrict, which are only available for IPSModule - Restore the documented timer signatures without the additional time parameters - Add missing constants and messages registered by the kernel (e.g. MODULETYPE_VISUALIZATION, SCRIPTTYPE_FLOW, DEBUG_FORMAT_*, INSTANCE_VISUALIZATION_TYPE_*, EVENT_DAY_*, IS_STANDBY, IM_REMOVEATTRIBUTE, SM_UPDATE, AM_*, LP_*, TP_*) - Validator: allow empty URLs, require braces for GUIDs like the kernel, fix typo Co-Authored-By: Claude Opus 5.5 --- ConstantStubs.php | 49 ++++ GlobalStubs.php | 58 ++++- KernelStubs.php | 116 +++++++++- MessageStubs.php | 25 ++ ModuleStrictStubs.php | 51 ++-- ModuleStubs.php | 50 ++-- Validator.php | 8 +- tests/DocumentationAlignmentTest.php | 217 ++++++++++++++++++ .../SanitizationDevice/module.php | 10 + .../SanitizationStrictDevice/module.php | 10 + 10 files changed, 520 insertions(+), 74 deletions(-) create mode 100644 tests/DocumentationAlignmentTest.php diff --git a/ConstantStubs.php b/ConstantStubs.php index 8a9bc64..97c8987 100644 --- a/ConstantStubs.php +++ b/ConstantStubs.php @@ -31,12 +31,28 @@ define('EVENTTRIGGERTYPE_ONLIMITDROP', 3); define('EVENTTRIGGERTYPE_ONVALUE', 4); +/* EventConditionOperation */ +define('EVENT_CONDITION_OPERATION_AND', 0); +define('EVENT_CONDITION_OPERATION_OR', 1); + +/* EventDay */ +define('EVENT_DAY_MONDAY', 1); +define('EVENT_DAY_TUESDAY', 2); +define('EVENT_DAY_WEDNESDAY', 4); +define('EVENT_DAY_THURSDAY', 8); +define('EVENT_DAY_FRIDAY', 16); +define('EVENT_DAY_SATURDAY', 32); +define('EVENT_DAY_SUNDAY', 64); +define('EVENT_DAY_ALL', 127); + /* EventType */ define('EVENTTYPE_TRIGGER', 0); define('EVENTTYPE_CYCLIC', 1); define('EVENTTYPE_SCHEDULE', 2); /* MediaType */ +define('MEDIATYPE_IPSVIEW', 0); +define('MEDIATYPE_FORM', 0); define('MEDIATYPE_DASHBOARD', 0); define('MEDIATYPE_IMAGE', 1); define('MEDIATYPE_SOUND', 2); @@ -51,6 +67,17 @@ define('MODULETYPE_DEVICE', 3); define('MODULETYPE_CONFIGURATOR', 4); define('MODULETYPE_DISCOVERY', 5); +define('MODULETYPE_VISUALIZATION', 6); + +/* InstanceVisualizationType */ +define('INSTANCE_VISUALIZATION_TYPE_NONE', 0); +define('INSTANCE_VISUALIZATION_TYPE_HTML', 1); +define('INSTANCE_VISUALIZATION_TYPE_HTML_FULLSCREEN', 2); +define('INSTANCE_VISUALIZATION_TYPE_FORM', 3); + +/* DebugFormat */ +define('DEBUG_FORMAT_TEXT', 0); +define('DEBUG_FORMAT_BINARY', 1); /* ObjectType */ define('OBJECTTYPE_CATEGORY', 0); @@ -63,6 +90,28 @@ /* ScriptType */ define('SCRIPTTYPE_PHP', 0); +define('SCRIPTTYPE_FLOW', 1); +define('SCRIPTTYPE_IPSWORKFLOW', 2); + +/* UserType */ +define('USER_TYPE_LOCAL', 0); +define('USER_TYPE_LDAP', 1); + +/* ReplicationMode */ +define('REPLICATION_MODE_PARALLEL', 0); +define('REPLICATION_MODE_STANDBY', 1); + +/* VariableAction */ +define('VARIABLE_ACTION_NONE', 0); +define('VARIABLE_ACTION_DISABLED', 1); + +/* Color */ +define('COLOR_TRANSPARENT', -1); +define('COLOR_ENCODING_RGB', 0); +define('COLOR_ENCODING_CMYK', 1); +define('COLOR_ENCODING_HSV', 2); +define('COLOR_ENCODING_HSL', 3); +define('COLOR_ENCODING_XY', 4); /* VariableType */ define('VARIABLETYPE_BOOLEAN', 0); diff --git a/GlobalStubs.php b/GlobalStubs.php index cfb4fc0..4153373 100644 --- a/GlobalStubs.php +++ b/GlobalStubs.php @@ -279,41 +279,49 @@ function RequestActionEx(int $VariableID, $Value, $Sender) function IPS_SetParent(int $ID, int $ParentID) { IPS\ObjectManager::setParent($ID, $ParentID); + return true; } function IPS_SetIdent(int $ID, string $Ident) { IPS\ObjectManager::setIdent($ID, $Ident); + return true; } function IPS_SetName(int $ID, string $Name) { IPS\ObjectManager::setName($ID, $Name); + return true; } function IPS_SetInfo(int $ID, string $Info) { IPS\ObjectManager::setInfo($ID, $Info); + return true; } function IPS_SetIcon(int $ID, string $Icon) { IPS\ObjectManager::setIcon($ID, $Icon); + return true; } function IPS_SetPosition(int $ID, int $Position) { IPS\ObjectManager::setPosition($ID, $Position); + return true; } function IPS_SetHidden(int $ID, bool $Hidden) { IPS\ObjectManager::setHidden($ID, $Hidden); + return true; } function IPS_SetDisabled(int $ID, bool $Disabled) { IPS\ObjectManager::setDisabled($ID, $Disabled); + return true; } function IPS_ObjectExists(int $ID) @@ -385,6 +393,7 @@ function IPS_DeleteCategory(int $CategoryID) IPS\ObjectManager::checkNoChildren($CategoryID, 'category'); IPS\CategoryManager::deleteCategory($CategoryID); IPS\ObjectManager::unregisterObject($CategoryID); + return true; } function IPS_CategoryExists(int $CategoryID) @@ -422,6 +431,7 @@ function IPS_DeleteInstance(int $InstanceID) IPS\ObjectManager::checkNoChildren($InstanceID, 'instance'); IPS\InstanceManager::deleteInstance($InstanceID); IPS\ObjectManager::unregisterObject($InstanceID); + return true; } function IPS_InstanceExists(int $InstanceID) @@ -468,11 +478,13 @@ function IPS_HasChanges(int $InstanceID) function IPS_ResetChanges(int $InstanceID) { IPS\InstanceManager::getInstanceInterface($InstanceID)->ResetChanges(); + return true; } function IPS_ApplyChanges(int $InstanceID) { IPS\InstanceManager::getInstanceInterface($InstanceID)->ApplyChanges(); + return true; } function IPS_GetProperty(int $InstanceID, string $Name) @@ -498,22 +510,26 @@ function IPS_GetConfigurationForm(int $InstanceID) function IPS_SetProperty(int $InstanceID, string $Name, $Value) { IPS\InstanceManager::getInstanceInterface($InstanceID)->SetProperty($Name, $Value); + return true; } function IPS_SetConfiguration(int $InstanceID, string $Configuration) { IPS\InstanceManager::getInstanceInterface($InstanceID)->SetConfiguration($Configuration); + return true; } /* Instance Manager - Connections */ function IPS_ConnectInstance(int $InstanceID, int $ParentID) { IPS\InstanceManager::connectInstance($InstanceID, $ParentID); + return true; } function IPS_DisconnectInstance(int $InstanceID) { IPS\InstanceManager::disconnectInstance($InstanceID); + return true; } /* Instance Manager - Searching */ @@ -541,22 +557,26 @@ function IPS_IsSearching(int $InstanceID) function IPS_DisableDebug(int $ID) { IPS\DebugServer::disableDebug($ID); + return true; } function IPS_EnableDebug(int $ID, int $Duration) { IPS\DebugServer::enableDebug($ID, $Duration); + return true; } function IPS_SendDebug(int $SenderID, string $Message, string $Data, int $Format) { IPS\DebugServer::sendDebug($SenderID, $Message, $Data, $Format); + return true; } /* Instance Manager - Actions */ function IPS_RequestAction(int $InstanceID, string $VariableIdent, $Value) { - return IPS\InstanceManager::getInstanceInterface($InstanceID)->RequestAction($VariableIdent, $Value); + IPS\InstanceManager::getInstanceInterface($InstanceID)->RequestAction($VariableIdent, $Value); + return true; } /* Variable Manager */ @@ -574,6 +594,7 @@ function IPS_DeleteVariable(int $VariableID) IPS\ObjectManager::checkNoChildren($VariableID, 'variable'); IPS\VariableManager::deleteVariable($VariableID); IPS\ObjectManager::unregisterObject($VariableID); + return true; } function IPS_VariableExists(int $VariableID) @@ -612,16 +633,19 @@ function IPS_GetVariableList() function IPS_SetVariableCustomAction(int $VariableID, int $ScriptID) { IPS\VariableManager::setVariableCustomAction($VariableID, $ScriptID); + return true; } function IPS_SetVariableCustomProfile(int $VariableID, string $ProfileName) { IPS\VariableManager::setVariableCustomProfile($VariableID, $ProfileName); + return true; } function IPS_SetVariableCustomPresentation(int $VariableID, array $Presentation) { IPS\VariableManager::setVariableCustomPresentation($VariableID, $Presentation); + return true; } /* Script Manager */ @@ -639,6 +663,7 @@ function IPS_DeleteScript(int $ScriptID, bool $DeleteFile) IPS\ObjectManager::checkNoChildren($ScriptID, 'script'); IPS\ScriptManager::deleteScript($ScriptID, $DeleteFile); IPS\ObjectManager::unregisterObject($ScriptID); + return true; } function IPS_ScriptExists(int $ScriptID) @@ -649,11 +674,13 @@ function IPS_ScriptExists(int $ScriptID) function IPS_SetScriptContent(int $ScriptID, string $Content) { IPS\ScriptManager::setScriptContent($ScriptID, $Content); + return true; } function IPS_SetScriptFile(int $ScriptID, string $FilePath) { IPS\ScriptManager::setScriptFile($ScriptID, $FilePath); + return true; } function IPS_GetScript(int $ScriptID) @@ -870,6 +897,7 @@ function IPS_DeleteMedia(int $MediaID, bool $DeleteFile) IPS\ObjectManager::checkNoChildren($MediaID, 'media'); IPS\MediaManager::deleteMedia($MediaID, $DeleteFile); IPS\ObjectManager::unregisterObject($MediaID); + return true; } function IPS_MediaExists(int $MediaID) @@ -879,46 +907,49 @@ function IPS_MediaExists(int $MediaID) function IPS_GetMedia(int $MediaID) { - return []; + return IPS\MediaManager::getMedia($MediaID); } function IPS_GetMediaContent(int $MediaID) { - return ''; + return IPS\MediaManager::getMediaContent($MediaID); } function IPS_GetMediaIDByFile(string $FilePath) { - return 0; + return IPS\MediaManager::getMediaIDByFile($FilePath); } function IPS_GetMediaIDByName(string $Name, int $ParentID) { - return 0; + return IPS\ObjectManager::getObjectIDByNameEx($Name, $ParentID, 5 /* Media */); } function IPS_GetMediaList() { - return []; + return IPS\MediaManager::getMediaList(); } function IPS_GetMediaListByType(int $MediaType) { - return []; + return IPS\MediaManager::getMediaListByType($MediaType); } function IPS_SetMediaCached(int $MediaID, bool $Cached) { + IPS\MediaManager::setMediaCached($MediaID, $Cached); return true; } function IPS_SetMediaContent(int $MediaID, string $Content) { + IPS\MediaManager::setMediaContent($MediaID, $Content); return true; } function IPS_SetMediaFile(int $MediaID, string $FilePath, bool $FileMustExists) { + IPS\MediaManager::setMediaFile($MediaID, $FilePath, $FileMustExists); return true; } @@ -940,6 +971,7 @@ function IPS_DeleteLink(int $LinkID) IPS\ObjectManager::checkNoChildren($LinkID, 'link'); IPS\LinkManager::deleteLink($LinkID); IPS\ObjectManager::unregisterObject($LinkID); + return true; } function IPS_LinkExists(int $LinkID) @@ -965,17 +997,20 @@ function IPS_GetLinkList() function IPS_SetLinkTargetID(int $LinkID, int $ChildID) { IPS\LinkManager::setLinkTargetID($LinkID, $ChildID); + return true; } /* Profile Manager */ function IPS_CreateVariableProfile(string $ProfileName, int $ProfileType) { IPS\ProfileManager::createVariableProfile($ProfileName, $ProfileType); + return true; } function IPS_DeleteVariableProfile(string $ProfileName) { IPS\ProfileManager::deleteVariableProfile($ProfileName); + return true; } function IPS_VariableProfileExists(string $ProfileName) @@ -1001,26 +1036,31 @@ function IPS_GetVariableProfileListByType(int $ProfileType) function IPS_SetVariableProfileAssociation(string $ProfileName, $AssociationValue, string $AssociationName, string $AssociationIcon, int $AssociationColor) { IPS\ProfileManager::setVariableProfileAssociation($ProfileName, $AssociationValue, $AssociationName, $AssociationIcon, $AssociationColor); + return true; } function IPS_SetVariableProfileDigits(string $ProfileName, int $Digits) { IPS\ProfileManager::setVariableProfileDigits($ProfileName, $Digits); + return true; } function IPS_SetVariableProfileIcon(string $ProfileName, string $Icon) { IPS\ProfileManager::setVariableProfileIcon($ProfileName, $Icon); + return true; } function IPS_SetVariableProfileText(string $ProfileName, string $Prefix, string $Suffix) { IPS\ProfileManager::setVariableProfileText($ProfileName, $Prefix, $Suffix); + return true; } function IPS_SetVariableProfileValues(string $ProfileName, float $MinValue, float $MaxValue, float $StepSize) { IPS\ProfileManager::setVariableProfileValues($ProfileName, $MinValue, $MaxValue, $StepSize); + return true; } /* Kernel */ @@ -1297,7 +1337,8 @@ function IPS_ModuleExists(string $ModuleID) function IPS_GetModule(string $ModuleID) { - return IPS\ModuleLoader::getModule($ModuleID); + //The class name is only used internally by the stubs + return array_diff_key(IPS\ModuleLoader::getModule($ModuleID), ['Class' => true]); } function IPS_GetModuleList() @@ -1497,6 +1538,7 @@ function IPS_ExecuteEx(string $Filename, string $Parameter, bool $ShowWindow, bo function IPS_Sleep(int $Milliseconds) { usleep($Milliseconds * 1000); + return true; } /* System Information */ diff --git a/KernelStubs.php b/KernelStubs.php index d808669..c393d16 100644 --- a/KernelStubs.php +++ b/KernelStubs.php @@ -132,6 +132,9 @@ private static function loadModule(string $folder, string $libraryID): void 'Implemented' => $module['implemented'] ?? [], 'LibraryID' => $libraryID, 'Prefix' => $module['prefix'], + 'URL' => $module['url'] ?? '', + 'Limit' => 0, + 'Translation' => $module['translations'] ?? [], 'Class' => str_replace(' ', '', $module['name']) ]; @@ -722,7 +725,9 @@ public static function getCategory(int $CategoryID): array { self::checkCategory($CategoryID); - return []; + return [ + 'CategoryID' => $CategoryID + ]; } public static function getCategoryList(): array @@ -759,11 +764,14 @@ public static function createInstance(int $InstanceID, array $Module): void } self::$instances[$InstanceID] = [ - 'InstanceID' => $InstanceID, - 'ConnectionID' => 0, - 'InstanceStatus' => 101 /* IS_CREATING */, - 'InstanceChanged' => time(), - 'ModuleInfo' => [ + 'InstanceID' => $InstanceID, + 'ConnectionID' => 0, + 'InstanceStatus' => 101 /* IS_CREATING */, + 'InstanceChanged' => TimeManager::getTime(), + 'InstanceSupportsSearching' => false, + 'InstanceIsSearching' => false, + 'InstanceVisualizationType' => 0 /* INSTANCE_VISUALIZATION_TYPE_NONE */, + 'ModuleInfo' => [ 'ModuleID' => $Module['ModuleID'], 'ModuleName' => $Module['ModuleName'], 'ModuleType' => $Module['ModuleType'] @@ -868,6 +876,13 @@ public static function setStatus($InstanceID, $Status): void self::$instances[$InstanceID]['InstanceStatus'] = $Status; } + public static function setVisualizationType(int $InstanceID, int $VisualizationType): void + { + self::checkInstance($InstanceID); + + self::$instances[$InstanceID]['InstanceVisualizationType'] = $VisualizationType; + } + public static function getStatus($InstanceID): int { self::checkInstance($InstanceID); @@ -1325,7 +1340,10 @@ public static function createScript(int $ScriptID, int $ScriptType): void 'ScriptType' => $ScriptType, 'ScriptFile' => $ScriptID . '.ips.php', 'ScriptExecuted' => 0, - 'ScriptIsBroken' => false + 'ScriptIsBroken' => false, + 'ScriptCRC' => '', + 'ScriptSize' => 0, + 'ScriptUpdated' => 0 ]; self::$content[$ScriptID] = ''; @@ -1357,6 +1375,9 @@ public static function setScriptContent(int $ScriptID, string $Content): void self::checkScript($ScriptID); self::$content[$ScriptID] = $Content; + self::$scripts[$ScriptID]['ScriptUpdated'] = TimeManager::getTime(); + self::$scripts[$ScriptID]['ScriptSize'] = strlen($Content); + self::$scripts[$ScriptID]['ScriptCRC'] = strtoupper(hash('crc32b', $Content)); } public static function scriptExists(int $ScriptID): bool @@ -1542,6 +1563,7 @@ public static function reset() class MediaManager { private static $medias = []; + private static $content = []; public static function createMedia(int $MediaID, int $MediaType): void { @@ -1561,6 +1583,7 @@ public static function deleteMedia(int $MediaID, bool $DeleteFile): void { self::checkMedia($MediaID); unset(self::$medias[$MediaID]); + unset(self::$content[$MediaID]); } public static function mediaExists(int $MediaID): bool @@ -1568,6 +1591,84 @@ public static function mediaExists(int $MediaID): bool return isset(self::$medias[$MediaID]); } + public static function getMedia(int $MediaID): array + { + self::checkMedia($MediaID); + + return self::$medias[$MediaID]; + } + + public static function getMediaList(): array + { + return array_keys(self::$medias); + } + + public static function getMediaListByType(int $MediaType): array + { + $result = []; + foreach (self::$medias as $media) { + if ($media['MediaType'] == $MediaType) { + $result[] = $media['MediaID']; + } + } + + return $result; + } + + public static function getMediaIDByFile(string $FilePath): int|false + { + foreach (self::$medias as $media) { + if ($media['MediaFile'] == $FilePath) { + return $media['MediaID']; + } + } + + trigger_error(sprintf('File %s could not be found', $FilePath), E_USER_WARNING); + return false; + } + + //The content is kept in memory instead of a file. Like the kernel, it is passed as Base64 + public static function getMediaContent(int $MediaID): string + { + self::checkMedia($MediaID); + + if (!isset(self::$content[$MediaID])) { + throw new \Exception(sprintf('File #%s does not exist', self::$medias[$MediaID]['MediaFile'])); + } + + return self::$content[$MediaID]; + } + + public static function setMediaContent(int $MediaID, string $Content): void + { + self::checkMedia($MediaID); + + $data = base64_decode($Content); + self::$content[$MediaID] = $Content; + self::$medias[$MediaID]['MediaIsAvailable'] = true; + self::$medias[$MediaID]['MediaSize'] = strlen($data); + self::$medias[$MediaID]['MediaCRC'] = strtoupper(hash('crc32b', $data)); + self::$medias[$MediaID]['MediaUpdated'] = TimeManager::getTime(); + } + + public static function setMediaFile(int $MediaID, string $FilePath, bool $FileMustExists): void + { + self::checkMedia($MediaID); + + if ($FileMustExists && !isset(self::$content[$MediaID]) && !file_exists($FilePath)) { + throw new \Exception(sprintf('File #%s does not exist', $FilePath)); + } + + self::$medias[$MediaID]['MediaFile'] = $FilePath; + } + + public static function setMediaCached(int $MediaID, bool $Cached): void + { + self::checkMedia($MediaID); + + self::$medias[$MediaID]['MediaIsCached'] = $Cached; + } + public static function checkMediaTypeRange(int $MediaType): void { if (($MediaType < 0) || ($MediaType > 5)) { @@ -1585,6 +1686,7 @@ public static function checkMedia(int $MediaID): void public static function reset() { self::$medias = []; + self::$content = []; } } diff --git a/MessageStubs.php b/MessageStubs.php index 236ab97..9b921b8 100644 --- a/MessageStubs.php +++ b/MessageStubs.php @@ -39,6 +39,9 @@ define('OM_CHILDREMOVED', 10413); define('OM_CHANGEIDENT', 10414); define('OM_CHANGEDISABLED', 10415); +define('OM_CHANGELOCKED', 10416); +define('OM_CHANGEHIDDENTITLE', 10417); +define('OM_CHANGEHIDDENMAXIMIZE', 10418); define('IPS_INSTANCEMESSAGE', 10500); define('IM_CREATE', 10501); define('IM_DELETE', 10502); @@ -48,6 +51,7 @@ define('IM_CHANGESETTINGS', 10506); define('IM_CHANGEATTRIBUTE', 10507); define('IM_ADDATTRIBUTE', 10508); +define('IM_REMOVEATTRIBUTE', 10509); define('IM_SEARCHMESSAGE', 10510); define('IM_SEARCHSTART', 10511); define('IM_SEARCHSTOP', 10512); @@ -63,11 +67,13 @@ define('VM_UPDATE', 10603); define('VM_CHANGEPROFILENAME', 10604); define('VM_CHANGEPROFILEACTION', 10605); +define('VM_CHANGELOCKED', 10606); define('IPS_SCRIPTMESSAGE', 10700); define('SM_CREATE', 10701); define('SM_DELETE', 10702); define('SM_CHANGEFILE', 10703); define('SM_BROKEN', 10704); +define('SM_UPDATE', 10705); define('IPS_EVENTMESSAGE', 10800); define('EM_CREATE', 10801); define('EM_DELETE', 10802); @@ -75,6 +81,7 @@ define('EM_CHANGEACTIVE', 10804); define('EM_CHANGELIMIT', 10805); define('EM_CHANGESCRIPT', 10806); +define('EM_CHANGEACTION', 10806); define('EM_CHANGETRIGGER', 10807); define('EM_CHANGETRIGGERVALUE', 10808); define('EM_CHANGETRIGGEREXECUTION', 10809); @@ -104,6 +111,9 @@ define('EM_ADDCONDITIONTIMERULE', 10833); define('EM_REMOVECONDITIONTIMERULE', 10834); define('EM_CHANGECONDITIONTIMERULE', 10835); +define('EM_ADDCONDITIONDAYOFTHEWEEKRULE', 10836); +define('EM_REMOVECONDITIONDAYOFTHEWEEKRULE', 10837); +define('EM_CHANGECONDITIONDAYOFTHEWEEKRULE', 10838); define('IPS_MEDIAMESSAGE', 10900); define('MM_CREATE', 10901); define('MM_DELETE', 10902); @@ -124,6 +134,7 @@ define('SE_UPDATE', 11201); define('SE_EXECUTE', 11202); define('SE_RUNNING', 11203); +define('SE_FLOWSCRIPT', 11204); define('IPS_PROFILEMESSAGE', 11300); define('PM_CREATE', 11301); define('PM_DELETE', 11302); @@ -140,12 +151,26 @@ define('TM_CHANGEINTERVAL', 11403); define('TM_CHANGEPROGRESS', 11404); define('TM_MESSAGE', 11405); +define('IPS_ACTIONMESSAGE', 11500); +define('AM_FORMFIELDCREATE', 11501); +define('AM_FORMFIELDDELETE', 11502); +define('AM_FORMFIELDUPDATE', 11503); +define('AM_FORMRELOAD', 11504); +define('IPS_LICENSEMESSAGE', 11600); +define('LP_UPDATELICENSE', 11601); +define('LP_UPDATESUBSCRIPTION', 11602); +define('IPS_TEMPLATEMESSAGE', 12000); +define('TP_CREATE', 12001); +define('TP_DELETE', 12002); +define('TP_CHANGENAME', 12003); +define('TP_CHANGEVALUES', 12004); define('IS_SBASE', 100); define('IS_CREATING', 101); define('IS_ACTIVE', 102); define('IS_DELETING', 103); define('IS_INACTIVE', 104); define('IS_NOTCREATED', 105); +define('IS_STANDBY', 106); define('IS_EBASE', 200); define('IF_UNKNOWN', 0); define('IF_NEW', 1); diff --git a/ModuleStrictStubs.php b/ModuleStrictStubs.php index 65c4b74..f82a7f5 100644 --- a/ModuleStrictStubs.php +++ b/ModuleStrictStubs.php @@ -43,12 +43,6 @@ public function __construct(int $InstanceID) return $this->getTime(); }); $this->InstanceID = $InstanceID; - - //Route getTime back to the module under test, so it can be overridden like with IPSModule - $this->module->SetStubTimeSource(function () - { - return $this->getTime(); - }); } public function Create(): void @@ -171,11 +165,6 @@ public function getMessages(): array return $this->module->getMessages(); } - public function SetVisualizationType(int $Type): void - { - $this->module->SetVisualizationType($Type); - } - protected function GetIDForIdent(string $Ident): int|false { return $this->module->GetIDForIdent($Ident); @@ -237,26 +226,27 @@ protected function RegisterOnceTimer(string $Ident, string $ScriptText): bool protected function RegisterTimer(string $Ident, int $Milliseconds, string $ScriptText): bool { - $this->module->RegisterTimer($Ident, $Milliseconds, $ScriptText, $this->getTime()); + $this->module->RegisterTimer($Ident, $Milliseconds, $ScriptText); return true; } protected function SetTimerInterval(string $Ident, int $Milliseconds): bool { - $this->module->SetTimerInterval($Ident, $Milliseconds, $this->getTime()); + $this->module->SetTimerInterval($Ident, $Milliseconds); return true; } protected function GetTimerInterval(string $Ident): int { - return $this->module->GetTimerInterval($Ident, $this->getTime()); + return $this->module->GetTimerInterval($Ident); } - protected function RegisterScript(string $Ident, string $Name, string $Content = '', int $Position = 0): bool + protected function RegisterScript(string $Ident, string $Name, string $Content = "module->GetIDForIdent($Ident) !== false); - $this->module->RegisterScript($Ident, $Name, $Content, $Position); - return !$scriptExists; + return $this->WasCreated($Ident, function () use ($Ident, $Name, $Content, $Position) + { + $this->module->RegisterScript($Ident, $Name, $Content, $Position); + }); } protected function RegisterVariableBoolean(string $Ident, string $Name, string|array $ProfileOrPresentation = '', int $Position = 0): bool @@ -396,24 +386,6 @@ protected function SendDataToChildren(string $Data): array return $this->module->SendDataToChildren($Data); } - protected function ConnectParent(string $ModuleID): bool - { - $this->module->ConnectParent($ModuleID); - return true; - } - - protected function RequireParent(string $ModuleID): bool - { - $this->module->RequireParent($ModuleID); - return true; - } - - protected function ForceParent(string $ModuleID): bool - { - $this->module->ForceParent($ModuleID); - return true; - } - protected function SetStatus(int $Status): bool { $this->module->SetStatus($Status); @@ -506,7 +478,12 @@ protected function UpdateFormField(string $Field, string $Parameter, mixed $Valu return true; } - protected function UpdateVisualizationValue(mixed $Value) + protected function SetVisualizationType(int $Type): bool + { + return $this->module->SetVisualizationType($Type); + } + + protected function UpdateVisualizationValue(mixed $Value): bool { $this->module->UpdateVisualizationValue($Value); return true; diff --git a/ModuleStubs.php b/ModuleStubs.php index 8c5b8c3..caa7216 100644 --- a/ModuleStubs.php +++ b/ModuleStubs.php @@ -25,8 +25,6 @@ class IPSModule private $messages = []; private $messageRegistrations = []; - private $visualizationType = 0 /* None */; - public function __construct($InstanceID) { $this->InstanceID = $InstanceID; @@ -230,11 +228,6 @@ public function getMessages() return $this->messages; } - public function SetVisualizationType(int $Type) - { - $this->visualizationType = $Type; - } - public function SetStubTimeSource(callable $timeSource) { // This helper is only available in Stub, not in PHP-SDK @@ -292,7 +285,7 @@ protected function RegisterOnceTimer(string $Ident, string $ScriptText) IPS_RunScriptTextEx($ScriptText, ['TARGET' => $this->InstanceID]); } - protected function RegisterTimer(string $Ident, int $Milliseconds, string $ScriptText, ?int $start = null) + protected function RegisterTimer(string $Ident, int $Milliseconds, string $ScriptText) { if (!$this->IsCreating()) { throw new Exception('Timer registration is only possible in the Create method'); @@ -303,12 +296,11 @@ protected function RegisterTimer(string $Ident, int $Milliseconds, string $Scrip } $this->timers[$Ident] = [ - 'millis' => $Milliseconds, - 'start' => $start ?? $this->getTime() + 'millis' => $Milliseconds ]; } - protected function SetTimerInterval(string $Ident, int $Milliseconds, ?int $start = null) + protected function SetTimerInterval(string $Ident, int $Milliseconds) { if ($Milliseconds < 0) { throw new Exception('Timer interval must not be negative!'); @@ -318,13 +310,10 @@ protected function SetTimerInterval(string $Ident, int $Milliseconds, ?int $star throw new Exception(sprintf('Timer %s does not exist', $Ident)); } - $this->timers[$Ident] = [ - 'millis' => $Milliseconds, - 'start' => $start ?? $this->getTime() - ]; + $this->timers[$Ident]['millis'] = $Milliseconds; } - protected function GetTimerInterval(string $Ident, ?int $currentTime = null) + protected function GetTimerInterval(string $Ident) { if (!isset($this->timers[$Ident])) { throw new Exception(sprintf('Timer %s does not exist', $Ident)); @@ -334,10 +323,26 @@ protected function GetTimerInterval(string $Ident, ?int $currentTime = null) return $this->timers[$Ident]['millis']; } - protected function RegisterScript($Ident, $Name, $Content = '', $Position = 0) + protected function RegisterScript($Ident, $Name, $Content = "InstanceID); + + if ($sid === false) { + $sid = IPS_CreateScript(SCRIPTTYPE_PHP); + + IPS_SetParent($sid, $this->InstanceID); + IPS_SetIdent($sid, $Ident); + IPS_SetName($sid, $Name); + IPS_SetPosition($sid, $Position); + IPS\ObjectManager::setReadOnly($sid, true); + } + + //only update script content if it changed. this fails like the kernel, if the ident is used by another object type + if (IPS_GetScriptContent($sid) != $Content) { + IPS_SetScriptContent($sid, $Content); + } + + return $sid; } protected function RegisterVariableBoolean($Ident, $Name, $ProfileOrPresentation = '', $Position = 0) @@ -784,9 +789,16 @@ protected function UpdateFormField($Field, $Parameter, $Value) { } + protected function SetVisualizationType(int $Type) + { + IPS\InstanceManager::setVisualizationType($this->InstanceID, $Type); + return true; + } + protected function UpdateVisualizationValue($Value) { $this->postMessage(10541, [$Value]); + return true; } protected function GetValue(string $Ident) diff --git a/Validator.php b/Validator.php index eee3a8b..de58fa8 100644 --- a/Validator.php +++ b/Validator.php @@ -32,7 +32,7 @@ protected function validateLibrary($folder): void $this->assertArrayHasKey('url', $library); $this->assertIsString($library['url']); - $this->assertTrue($this->isValidURL($library['url']), 'library url is not a valid'); + $this->assertTrue($this->isValidURL($library['url']), 'library url is not valid'); $this->assertArrayHasKey('version', $library); $this->assertIsString($library['version']); @@ -141,7 +141,8 @@ protected function validateModule($folder): void private function isValidGUID($guid): bool { - return preg_match('/^\{?[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}\}?$/', $guid) == 1; + //Same check as the kernel, which requires the braces + return preg_match('/^\{[A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}\}$/', $guid) == 1; } private function isValidName($name): bool @@ -156,7 +157,8 @@ private function isValidPrefix($name): bool private function isValidURL($name): bool { - return preg_match('/^(?:http:\/\/|https:\/\/)/', $name) == 1; + //The URL may also be empty + return ($name === '') || (preg_match('/^(?:http:\/\/|https:\/\/)/', $name) == 1); } private function ignoreFolders(): array diff --git a/tests/DocumentationAlignmentTest.php b/tests/DocumentationAlignmentTest.php new file mode 100644 index 0000000..44320dd --- /dev/null +++ b/tests/DocumentationAlignmentTest.php @@ -0,0 +1,217 @@ +assertTrue(IPS_SetName($id, 'Name')); + $this->assertTrue(IPS_SetIdent($id, 'Ident')); + $this->assertTrue(IPS_SetPosition($id, 1)); + $this->assertTrue(IPS_SetHidden($id, true)); + $this->assertTrue(IPS_SetParent($id, IPS_CreateCategory())); + + $this->assertTrue(IPS_CreateVariableProfile('Profile', VARIABLETYPE_INTEGER)); + $this->assertTrue(IPS_SetVariableProfileValues('Profile', 0, 10, 1)); + $this->assertTrue(IPS_SetVariableProfileAssociation('Profile', 1, 'One', '', -1)); + $this->assertTrue(IPS_DeleteVariableProfile('Profile')); + + $variable = IPS_CreateVariable(VARIABLETYPE_INTEGER); + $this->assertTrue(IPS_SetVariableCustomPresentation($variable, [])); + $this->assertTrue(IPS_DeleteVariable($variable)); + } + + public function testInstanceFunctionsReturnTrue(): void + { + $id = IPS_CreateInstance(self::DEVICE); + + $this->assertTrue(IPS_SetProperty($id, 'Port', 81)); + $this->assertTrue(IPS_ApplyChanges($id)); + $this->assertTrue(IPS_RequestAction($id, 'Value', 1)); + + //Like in the kernel, the children of an instance need to be deleted first + $this->assertTrue(IPS_DeleteVariable(IPS_GetObjectIDByIdent('Value', $id))); + $this->assertTrue(IPS_DeleteInstance($id)); + } + + /* Keys */ + public function testCategoryContainsID(): void + { + $id = IPS_CreateCategory(); + + $this->assertSame(['CategoryID' => $id], IPS_GetCategory($id)); + } + + public function testInstanceContainsDocumentedKeys(): void + { + $id = IPS_CreateInstance(self::DEVICE); + $instance = IPS_GetInstance($id); + + foreach (['InstanceID', 'ConnectionID', 'InstanceStatus', 'InstanceSupportsSearching', 'InstanceIsSearching', 'InstanceVisualizationType', 'InstanceChanged', 'ModuleInfo'] as $key) { + $this->assertArrayHasKey($key, $instance); + } + $this->assertSame(INSTANCE_VISUALIZATION_TYPE_NONE, $instance['InstanceVisualizationType']); + } + + public function testSetVisualizationTypeIsVisibleInInstance(): void + { + $id = IPS_CreateInstance(self::DEVICE); + $this->assertTrue(SANDEV_CallSetVisualizationType($id, INSTANCE_VISUALIZATION_TYPE_HTML)); + $this->assertSame(INSTANCE_VISUALIZATION_TYPE_HTML, IPS_GetInstance($id)['InstanceVisualizationType']); + + $strict = IPS_CreateInstance(self::STRICT_DEVICE); + $this->assertTrue(SANSTR_CallSetVisualizationType($strict, INSTANCE_VISUALIZATION_TYPE_FORM)); + $this->assertSame(INSTANCE_VISUALIZATION_TYPE_FORM, IPS_GetInstance($strict)['InstanceVisualizationType']); + } + + public function testScriptContainsDocumentedKeys(): void + { + $id = IPS_CreateScript(SCRIPTTYPE_PHP); + IPS_SetScriptContent($id, 'assertSame(13, $script['ScriptSize']); + $this->assertSame(strtoupper(hash('crc32b', 'assertArrayHasKey('ScriptUpdated', $script); + } + + public function testModuleContainsDocumentedKeys(): void + { + $module = IPS_GetModule(self::DEVICE); + + foreach (['ModuleID', 'ModuleName', 'ModuleType', 'Vendor', 'Aliases', 'ParentRequirements', 'ChildRequirements', 'Implemented', 'LibraryID', 'Prefix', 'URL', 'Limit', 'Translation'] as $key) { + $this->assertArrayHasKey($key, $module); + } + $this->assertArrayNotHasKey('Class', $module); + } + + /* Media */ + public function testMediaFunctions(): void + { + $id = IPS_CreateMedia(MEDIATYPE_DOCUMENT); + IPS_SetName($id, 'Manual'); + IPS_SetMediaFile($id, 'media/manual.txt', false); + IPS_SetMediaContent($id, base64_encode('Hello')); + + $media = IPS_GetMedia($id); + $this->assertSame($id, $media['MediaID']); + $this->assertSame('media/manual.txt', $media['MediaFile']); + $this->assertTrue($media['MediaIsAvailable']); + $this->assertSame(5, $media['MediaSize']); + $this->assertSame(base64_encode('Hello'), IPS_GetMediaContent($id)); + + $this->assertSame([$id], IPS_GetMediaList()); + $this->assertSame([$id], IPS_GetMediaListByType(MEDIATYPE_DOCUMENT)); + $this->assertSame([], IPS_GetMediaListByType(MEDIATYPE_IMAGE)); + $this->assertSame($id, IPS_GetMediaIDByFile('media/manual.txt')); + $this->assertSame($id, IPS_GetMediaIDByName('Manual', 0)); + $this->assertFalse(@IPS_GetMediaIDByFile('media/missing.txt')); + + IPS_SetMediaCached($id, true); + $this->assertTrue(IPS_GetMedia($id)['MediaIsCached']); + } + + public function testMediaContentWithoutFile(): void + { + $id = IPS_CreateMedia(MEDIATYPE_IMAGE); + + $this->expectExceptionMessage('File # does not exist'); + IPS_GetMediaContent($id); + } + + /* IPSModule */ + public function testRegisterScript(): void + { + $id = IPS_CreateInstance(self::DEVICE); + $sid = SANDEV_CallRegisterScript($id, 'Script', 'Script', 'assertTrue(IPS_ScriptExists($sid)); + $this->assertSame($id, IPS_GetParent($sid)); + $this->assertTrue(IPS_GetObject($sid)['ObjectIsReadOnly']); + $this->assertSame('assertSame($sid, SANDEV_CallRegisterScript($id, 'Script', 'Script', 'assertSame('assertTrue(SANSTR_CallRegisterScript($strict, 'Script', 'Script')); + $this->assertFalse(SANSTR_CallRegisterScript($strict, 'Script', 'Script')); + $this->assertSame("assertSame(3, (new ReflectionMethod('IPSModule', 'RegisterTimer'))->getNumberOfParameters()); + $this->assertSame(2, (new ReflectionMethod('IPSModule', 'SetTimerInterval'))->getNumberOfParameters()); + $this->assertSame(1, (new ReflectionMethod('IPSModule', 'GetTimerInterval'))->getNumberOfParameters()); + } + + public function testParentFunctionsAreOnlyAvailableForIPSModule(): void + { + foreach (['ConnectParent', 'RequireParent', 'ForceParent'] as $method) { + $this->assertTrue(method_exists('IPSModule', $method)); + $this->assertFalse(method_exists('IPSModuleStrict', $method)); + } + $this->assertTrue(method_exists('IPSModuleStrict', 'UnregisterOAuth')); + } + + /* Constants */ + public function testDocumentedConstantsAreDefined(): void + { + $constants = [ + 'MODULETYPE_VISUALIZATION' => 6, + 'SCRIPTTYPE_FLOW' => 1, + 'SCRIPTTYPE_IPSWORKFLOW' => 2, + 'MEDIATYPE_IPSVIEW' => 0, + 'IS_STANDBY' => 106, + 'IM_REMOVEATTRIBUTE' => 10509, + 'SM_UPDATE' => 10705, + 'INSTANCE_VISUALIZATION_TYPE_FORM' => 3, + 'DEBUG_FORMAT_BINARY' => 1, + 'EVENT_CONDITION_OPERATION_OR' => 1, + 'EVENT_DAY_ALL' => 127, + 'COLOR_TRANSPARENT' => -1, + 'VARIABLE_ACTION_DISABLED' => 1, + 'AM_FORMRELOAD' => 11504, + 'LP_UPDATESUBSCRIPTION' => 11602 + ]; + foreach ($constants as $name => $value) { + $this->assertSame($value, constant($name), $name); + } + } + + /* Validator */ + public function testValidatorRules(): void + { + $validator = new TestCaseSymconValidation('validator'); + $guid = new ReflectionMethod($validator, 'isValidGUID'); + $url = new ReflectionMethod($validator, 'isValidURL'); + + $this->assertTrue($guid->invoke($validator, '{5C0E3E64-7A5B-4C55-9D7C-0B0F2E8A1A20}')); + $this->assertFalse($guid->invoke($validator, '5C0E3E64-7A5B-4C55-9D7C-0B0F2E8A1A20')); + $this->assertFalse($guid->invoke($validator, '{5c0e3e64-7a5b-4c55-9d7c-0b0f2e8a1a20}')); + + $this->assertTrue($url->invoke($validator, '')); + $this->assertTrue($url->invoke($validator, 'https://www.symcon.de/')); + $this->assertFalse($url->invoke($validator, 'www.symcon.de')); + } +} diff --git a/tests/SanitizationStubs/SanitizationDevice/module.php b/tests/SanitizationStubs/SanitizationDevice/module.php index 5fad619..64bdce1 100644 --- a/tests/SanitizationStubs/SanitizationDevice/module.php +++ b/tests/SanitizationStubs/SanitizationDevice/module.php @@ -100,6 +100,16 @@ public function CallReadPropertyFloat(string $Name) return $this->ReadPropertyFloat($Name); } + public function CallRegisterScript(string $Ident, string $Name, string $Content) + { + return $this->RegisterScript($Ident, $Name, $Content); + } + + public function CallSetVisualizationType(int $Type) + { + return $this->SetVisualizationType($Type); + } + protected function getTime() { return 1000; diff --git a/tests/SanitizationStubs/SanitizationStrictDevice/module.php b/tests/SanitizationStubs/SanitizationStrictDevice/module.php index 511766d..400cb61 100644 --- a/tests/SanitizationStubs/SanitizationStrictDevice/module.php +++ b/tests/SanitizationStubs/SanitizationStrictDevice/module.php @@ -30,6 +30,16 @@ public function CallUnregisterVariable(string $Ident): bool return $this->UnregisterVariable($Ident); } + public function CallRegisterScript(string $Ident, string $Name): bool + { + return $this->RegisterScript($Ident, $Name); + } + + public function CallSetVisualizationType(int $Type): bool + { + return $this->SetVisualizationType($Type); + } + protected function getTime(): int { return 1000;