Functions/Hue/Set-HueRule.ps1

function Set-HueRule
{
    <#
    .Synopsis
        Sets a Hue Rule
    .Description
        Sets a Hue Rule. Hue Rules are used to automatically change your Hue Lights and devices when conditions occur.
    .Link
        Get-HueRule
    .Link
        Remove-HueRule
    .Example
        Set-HueRule -Condition {
            "/sensors/55/state/status" -eq "1"
        } -Action {
            Set-HueLight -Name "Sunroom*" -ColorTemperature 420
        } -Name BrightenRoom
    .EXAMPLE
        # Set a rule that when
        Set-HueRule -Condition {
            "/sensors/61/state/buttonevent" -eq "4002"
        } -Action {
            Set-HueLight -RoomName "Sunroom" -Brightness 0.01
        } -Name SunroomDimmerTap
    .EXAMPLE
        Set-HueRule -Condition {
            "/sensors/SunroomDimmerSwitch/state/buttonevent" -eq "4003"
        } -Action {
            Set-HueLight -RoomName "Sunroom" -Off
        } -Name SunroomDimmerHoldDownToTurnOff
    .EXAMPLE
        Set-HueRule -Condition {
            "/sensors/SunroomDimmerSwitch/state/buttonevent" -eq "1003"
        } -Action {
            Set-HueLight -RoomName "Sunroom" -On
        } -Name SunroomDimmerHoldUpToTurnOn
    .EXAMPLE
        Set-HueRule -Condition {
            "/sensors/SunroomDimmerSwitch/state/buttonevent" -eq "1002"
        } -Action {
            Set-HueLight -RoomName "Sunroom" -On -Brightness .8
        } -Name SunroomDimmerTapOn
    .EXAMPLE
        Set-HueRule -Condition {
            "/sensors/SunroomDimmerSwitch/state/buttonevent" -eq "2003"
        } -Action {
            Set-HueLight -RoomName "Sunroom" -BrightnessIncrement .1
        } -Name SunroomDimmerHoldBright
    .EXAMPLE
        Set-HueRule -Condition {
            "/sensors/SunroomDimmerSwitch/state/buttonevent" -eq "2002"
        } -Action {
            Set-HueLight -RoomName "Sunroom" -BrightnessIncrement .05
        } -Name SunroomDimmerTapBright
    .EXAMPLE
        Set-HueRule -Condition {
            "/sensors/SunroomDimmerSwitch/state/buttonevent" -eq "3002"
        } -Action {
            Set-HueLight -RoomName "Sunroom" -BrightnessIncrement -.05
        } -Name SunroomDimmerTapDarken
    .EXAMPLE
        Set-HueRule -Condition {
            "/sensors/SunroomDimmerSwitch/state/buttonevent" -eq "3003"
        } -Action {
            Set-HueLight -RoomName "Sunroom" -BrightnessIncrement -.1
        } -Name SunroomDimmerHoldDarken
    #>

    [OutputType([PSObject])]
    [CmdletBinding(SupportsShouldProcess)]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("Test-ForSlowScript", "", Justification="Impact Incidental")]
    param(
    # The name of the rule.
    [Parameter(Mandatory=$true,Position=0)]
    [string]
    $Name,

    # The condition.
    # If the value is a ScriptBlock, only operators and their surrounding conext will be accepted.
    # Each condition should take the form: `"/resource/id/restOfAddress" -operator "value"`.
    # Rules may have more than one condition.
    # If the address is not a resource followed by a digit, the resource will be looked up by name.
    [Parameter(Mandatory,Position=1)]
    [Alias('Conditions')]
    [PSObject[]]
    $Condition,

    # The action.
    # If this value is a Script Block, only commands from this module that have the parameter -OutputInput may be called.
    [ValidateScript({
        
        $MyModule = Get-Module LightScript
        foreach ($act in $_) {
            if ($act -is [ScriptBlock]) { # If the input is a ScriptBlock
                $ast = $act.Ast
                $expr = $ast.FindAll({param($ast) $ast -is [Management.Automation.Language.CommandExpressionAst]}, $true ) # and has no command expressions
                if ($expr) { throw "Action Script cannot use expressions " }
                $foundCmds = $ast.FindAll({param($ast) $ast -is [Management.Automation.Language.CommandAst]}, $true ) # find all commands.
                foreach ($fc in $foundCmds) { # Ensure each command
                    $thecmd = @($fc.CommandElements)[0]
                    if ($theCmd.Value) {
                        if (-not $MyModule.ExportedCommands[$thecmd.Value]) { # comes from this module
                            throw "Action Script Blocks cannot use commands that are not defined in the module"
                        }
                        if (-not $MyModule.ExportedCommands[$theCmd.Value].Parameters.OutputInput) { # and has the -OutputInput parameter.
                            throw "Only commands that support -OutputInput can be used in a rule action"
                        }
                    } else {
                        throw "Action Script Blocks cannot use commands that are not defined in the module"
                    }
                }
            } else {
                foreach ($requirement in 'address', 'method', 'body') { # Otherwise, check for the required properties.
                    if (-not $act.$requirement) {
                        throw "Requirement $requirment not found in action"
                    }
                }
            }
        }
        return $true
    })]
    [Parameter(Mandatory,Position=2)]
    [Alias('Actions')]
    [PSObject[]]
    $Action,

    # If provided, the schedule will only run on the bridge with a particular device ID
    [Parameter(ValueFromPipelineByPropertyName)]
    [string]
    $DeviceID,

    # If provided, the schedule will only run on the bridge found at the provided IP address
    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('IP')]
    [IPAddress]
    $IPAddress,

    # If set, will disable the rule.
    [switch]
    $Disable
    )

    begin {
        $myCmd          = $MyInvocation.MyCommand
        $camelCaseSpace = [Regex]::new("(?<=[a-z])(?=[A-Z])")
    }

    process {
        $splat = @{}
        #region Copy Parameters from Get-HueBridge
        $MyParameters = @{} + $psBoundParameters # Copy $PSBoundParameters
        foreach ($in in 'Name','ExactMatch','RegularExpression','ID') {
            if (-not $splat.$in -and $myParameters.$in) {
                $splat.$in = $myParameters.$in
            }
        }
        $RuleExists = Get-HueRule @splat

        #region Translate Condition
        $realCondition = @(foreach ($c in $Condition) {
            if ($c -is [string]) {
                $c
            } elseif ($c -is [ScriptBlock]) {
                $tokens = [Management.Automation.PSParser]::Tokenize($c, [ref]$null)
                for ($i =1 ;$i -lt $tokens.Count;$i++) {
                    $isOperator = $tokens[$i].Type -eq 'operator'
                    if ($isOperator) {
                        $nextToken = $tokens[$i + 1]
                        $previousToken = $tokens[$i -1]
                        $value =
                            if ('String', 'Number' -contains $nextToken.Type) {
                                $nextToken.Content
                            } elseif ( 'Variable' -contains $nextToken.Type) {
                                $ExecutionContext.SessionState.PSVariable.Get($nextToken.Content).Value
                            }
                        $address =
                            if ('String', 'Number' -contains $previousToken.Type) {
                                $previousToken.Content
                            } elseif ( 'Variable' -contains $previousToken.Type) {
                                $ExecutionContext.SessionState.PSVariable.Get($previousToken.Content).Value
                            }

                        if ($value -eq $true -or $value -eq $false) {
                            $value = $value.ToString().ToLower()
                        }

                        if ($address -match '^/(?<R>[^/]+)/(?<N>\D[^/]+)/(?<M>.+)$') {
                            $matchInfo = @{} + $matches
                            $getResourceSplat = @{
                                "$($matches.R)" = $true
                                Name = $matches.N, $camelCaseSpace.Replace($matches.N, '?')
                            }
                            Get-HueBridge @getResourceSplat -WhatIf:$false |
                                ForEach-Object {
                                    [PSCustomObject][Ordered]@{
                                        address = "/$($matchInfo.R)/$($_.Id)/$($matchInfo.M)"
                                        operator = $tokens[$i].Content.TrimStart('-')
                                        value = $value
                                    }    
                                }
                        } else {
                            [PSCustomObject][Ordered]@{
                                address =$address
                                operator = $tokens[$i].Content.TrimStart('-')
                                value = $value
                            }
                        }
                    }
                }
            } else {
                foreach ($_ in $c) {
                    $ht = [Ordered]@{
                        address = $_.Address
                        operator =  $_.operator
                    }
                    if ($_.Value) {
                        $ht.value = $_.value
                    }
                    [PSCustomObject]$ht
                }
            }
        })

        $ConditionWasTerminated = $realCondition | Where-Object { $_.Operator -like 'dx' }
        if (-not $ConditionWasTerminated) {
            $realCondition += New-Object PSObject -Property @{
                address = $realCondition[-1].address
                operator = 'dx'
            }
        }
        #endregion Translate Condition


        #region Translate Action
        $realAction = @(foreach ($a in $Action) {
            if ($a -is [ScriptBlock]) {
                foreach ($func in $myCmd.Module.ExportedFunctions.Values) {
                    if ($func.Parameters.OutputInput) {
                        $global:PSDefaultParameterValues["${func}:OutputInput"] = $true
                    }
                    if ($func.Parameters.WhatIf) {
                        $global:PSDefaultParameterValues["${func}:WhatIf"] = $false
                    }
                }
                & $a | Select-Object @{
                    Name='address';Expression={
                        $parts = @($_.Address -split '/' -ne '')
                        '/' + ($parts[2..$parts.Count] -join '/')}
                    },
                    @{Name='method';Expression={$_.Method}},
                    @{Name='body';Expression={$_.Body}}
                foreach ($func in $myCmd.Module.ExportedFunctions.Values) {
                    if ($func.Parameters.OutputInput) {
                        $global:PSDefaultParameterValues.Remove("${func}:OutputInput")
                    }
                    if ($func.Parameters.WhatIf) {
                        $global:PSDefaultParameterValues.Remove("${func}:WhatIf")
                    }
                }
            } else {
                $a | Select-Object @{Name='address';Expression={$_.Address}},
                    @{Name='method';Expression={$_.Method}},
                    @{Name='body';Expression={$_.Body}}
            }
        })
        #endregion Translate Action

        #region Create or Update Rule
        $restIn = @{name=$Name;conditions=$realCondition;actions=$realAction;status = if ($Disable) { "disabled" } else { "enabled" }}


        if (-not $RuleExists) {
            Get-HueBridge |
                Where-Object {
                    if ($DeviceID -and $_.DeviceID -ne $DeviceID) { return $false }
                    if ($IPAddress -and $_.IPAddress -ne $IPAddress) { return $false }
                    $true
                } |
                Send-HueBridge -Command rules -Method POST -Data $restIn
        } else {
            Get-HueBridge |
                Where-Object {
                    if ($DeviceID -and $_.DeviceID -ne $DeviceID) { return $false }
                    if ($IPAddress -and $_.IPAddress -ne $IPAddress) { return $false }
                    $true
                } |
                Send-HueBridge -Command "rules/$($RuleExists.ID)" -Method PUT -Data $restIn
            Get-HueRule -ID $RuleExists.ID
        }
        #endregion Create or Update Rule
    }
}