Public/Device/Set-LifxDeviceColor.ps1

<#
    .SYNOPSIS
        Controls the color of a Lifx device
    .DESCRIPTION
        This cmdlet allows you to set the brightness, saturation, hue, and kelvin of a Lifx device based on different parameters. It also
        supports SecondsToTransition to control how fast the change occurs.
    .EXAMPLE
        $devices = Get-LifxDevice | Initialize-LifxDevice
        $devices | Where-Object {$_.Group -eq "Living Room"} | Set-LifxDeviceColor -Red 200 -Blue 13 -Brightness 75 -Saturation 100
    .EXAMPLE
        $devices = Get-LifxDevice | Initialize-LifxDevice
        $devices | Where-Object {$_.Group -eq "Living Room"} | Set-LifxDeviceColor -Brightness 100 -White "Sunset" -SecondsToTransition 1.5
    .EXAMPLE
        $devices = Get-LifxDevice | Initialize-LifxDevice
        $devices | Where-Object {$_.Group -eq "Living Room"} | Set-LifxDeviceColor -Brightness 100 -White "Noon Daylight" -SecondsToTransition 4
    .EXAMPLE
        $devices = Get-LifxDevice | Initialize-LifxDevice
        $devices | Where-Object {$_.Group -eq "Living Room"} | Set-LifxDeviceColor -Kelvin 12000 -Brightness 10000
    .EXAMPLE
        $devices = Get-LifxDevice | Initialize-LifxDevice
        $devices | Where-Object {$_.Group -eq "Living Room"} | Set-LifxDeviceColor -White 'Cloudy Daylight' -Brightness 90
    .EXAMPLE
        $zoneColor1 = [PSCustomObject]@{
            "Red" = 216
            "Green" = 45
            "Blue" = 231
            "Saturation" = 80
            "Brightness" = 90
        }
        $devices = Get-LifxDevice | Initialize-LifxDevice | Get-LifxDeviceSetting
        $devices | Where-Object {$_.Product.Name -like "LIFX Z*"} | Set-LifxDeviceColor -Zone $zoneColor01 -SecondsToTransition 3 -StartingZone 0
    .EXAMPLE
        $zoneColor1 = [PSCustomObject]@{
            "Red" = 216
            "Green" = 45
            "Blue" = 231
            "Saturation" = 80
            "Brightness" = 90
        }
        $zoneColor2 = [PSCustomObject]@{
            "Hue" = 109
            "Saturation" = 85
            "Brightness" = 90
        }
        $zoneColor3 = [PSCustomObject]@{
            "Kelvin" = 7200
            "Brightness" = 65
        }
        $devices = Get-LifxDevice | Initialize-LifxDevice | Get-LifxDeviceSetting
        $devices | Where-Object {$_.Product.Name -like "LIFX Z*"} | Set-LifxDeviceColor -zones $zoneColor01, $zoneColor02 -SecondsToTransition 2 -StartingZone 0
#>


function Set-LifxDeviceColor {
    param(
        #Discovered Lifx device. Accepts input from Get-LifxDevice or Initialize-LifxDevice
        [parameter(
            Position = 0,
            Mandatory = $true,
            ValueFromPipeline = $true)]
        [PSCustomObject[]]$Device,

        #Hue: The section of the color spectrum that represents the color of your device. This is taken as RGB values and converted to HSB
        [ValidateRange(0, 255)]
        [Parameter(ParameterSetName = "Color")]
        [decimal]$Red = 0,

        [ValidateRange(0, 255)]
        [Parameter(ParameterSetName = "Color")]
        [decimal]$Green = 0,

        [ValidateRange(0, 255)]
        [Parameter(ParameterSetName = "Color")]
        [decimal]$Blue = 0,

        #Hue, instead of RGB you can provide a value from 0-360 as seen in the LIFX app
        [ValidateRange(0, 360)]
        [Parameter(ParameterSetName = "Hue")]
        [int]$Hue = 0,

        #Brightness: How bright the light is. Zero brightness is the same as the device is off, while full brightness be just that.
        #This value is taken as a percent value i.e. 1, 27, 43, 100 and defaults to 1 if nothing is provided
        [ValidateRange(0, 100)]
        [Parameter(ParameterSetName = "Hue")]
        [Parameter(ParameterSetName = "Color")]
        [Parameter(ParameterSetName = "Zone")]
        [Parameter(ParameterSetName = "KelvinByNumber")]
        [Parameter(ParameterSetName = "KelvinByName")]
        [decimal]$Brightness = 1,

        #Saturation: How strong the color is. So a Zero saturation is completely white, whilst full saturation is the full color
        [ValidateRange(0, 100)]
        [Parameter(ParameterSetName = "Hue")]
        [Parameter(ParameterSetName = "Color")]
        [decimal]$Saturation = 0,

        #the number of seconds it will take the light(s) to change color
        [Parameter(ParameterSetName = "Hue")]
        [Parameter(ParameterSetName = "Color")]
        [Parameter(ParameterSetName = "KelvinByNumber")]
        [Parameter(ParameterSetName = "KelvinByName")]
        [Parameter(ParameterSetName = "Zone")]
        [Parameter(ParameterSetName = "Zones")]
        [decimal]$SecondsToTransition = 0,

        #Kelvin: Allowed range aka the "temperature" when the device has zero saturation. A higher value is a
        #cooler white (more blue) whereas a lower value is a warmer white (more yellow)
        [ValidateRange(1000, 12000)]
        [Parameter(ParameterSetName = "KelvinByNumber")]
        [decimal]$Kelvin,

        #kelvin values by name as defined by Lifx
        [Parameter(ParameterSetName = "KelvinByName")]
        [ValidateSet("Candlelight", "Sunset", "Ultra Warm", "Incandescent", "Warm", "Neutral", "Cool", "Cool Daylight", "Soft Daylight",
            "Daylight", "Noon Daylight", "Bright Daylight", "Cloudy Daylight", "Blue Daylight", "Blue Overcast", "Blue Ice")]
        [string]$White,

        [Parameter(ParameterSetName = "Zone")]
        [PSCustomObject]$Zone,

        #sets a multizone device's zones to different colors
        [Parameter(ParameterSetName = "Zones")]
        [array[]]$Zones,

        #defines which zone the colors will begin applying at. Colors that don't fit will be discarded
        #for example, passing 2 or more zones to a Lifx Z strip (8 zones) at position 7 will only change
        #the color of the last zone and the 8th position would be discarded as the count begins at 0
        [Parameter(ParameterSetName = "Zones")]
        [uint16]$StartingZone
    )

    #declare the counters
    [int]$Total = $Input.Count
    [int]$Count = 0

    #a device has its Product Settings defined via (Get-LifxDeviceSetting) and ExtendedMultizone=$true
    if ($Device.Product.ExtendedMultizone) {
        #multizone device, single color for all zones
        if ($Zone) {
            <#convert inputs
            https://lan.developer.lifx.com/docs/representing-color-with-hsbk
            did the user provide the direct Hue value, or RGB?
            #>

            if ($Zone.Hue) {
                $hshue = [int](([Math]::Round(0x10000 * $Zone.Hue) / 360) % 0x10000)
            }
            if ($Zone.Red -or $Zone.Green -or $Zone.Blue) {
                $hscolor = ConvertTo-HSBK -Red $Zone.Red -Green $Zone.Green -Blue $Zone.Blue
                $hshue = [int](([Math]::Round(0x10000 * $hscolor.Hue) / 360) % 0x10000)
            }
            if (!$hsHue) {
                $hsHue = 0
            }
            $Saturation = $Zone.Saturation / 100
            $hsSaturation = [int]([Math]::Round(0xFFFF * $Saturation))
            $Brightness = $Zone.Brightness / 100
            $hsbrightness = [int]([Math]::Round(0xFFFF * $Brightness))

            ####build the packet
            #119 (setWaveFormOptional) sets the same color across all zones
            $packet = [PSCustomObject]@{
                #LIFX Frame
                size               = 61;
                reserved1          = [uint64]0;
                reserved2          = [uint64]0;
                addressable        = 1;
                packet_type        = [uint16]119; #setWaveFormOptional packet number
                reserved3          = [uint32]0;
                reserved10         = [uint32]0;
                source             = [uint32]29734587;
                ackreq             = 1; #1
                resreq             = 0; #0
                sequence           = 0;
                #Payload
                transient          = [byte]0 #0 or 1
                hue                = [uint16]$hshue
                saturation         = [uint16]$hsSaturation #0 #65535
                brightness         = [uint16]$hsbrightness
                kelvin             = [uint16]$Zone.Kelvin
                period             = [uint32]300
                cycles             = [float]1
                skew               = [Int16]0
                waveform           = [byte]0 #0 saw, 1 sine, 2 half sine, 3 triangle, 4 pulse
                #set either color or kelvin
                sethue             = if (!$Zone.Kelvin) {[byte]1} else {[byte]0}
                setsaturation      = 1
                setbrightness      = 1
                setKelvin          = if ($Zone.Kelvin) {[byte]1} else {[byte]0}
                #possibly not neccesary
                protocol           = [uint16]1024;
                target_mac_address = New-Object byte[] 8
            }

            #convert the packet to a byte array
            [Byte[]]$buffer = New-Object byte[] $packet.size;
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.size), 0, $buffer, 0, 2);
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.protocol), 0, $buffer, 2, 2);
            #makes the LIFX packet portion of the protocol frame addressable, this is required for multizone
            $buffer[3] = 0x14

            #reserved should be happening on 16
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.reserved1), 0, $buffer, 16, 6);
            #[System.Array]::Copy([System.BitConverter]::GetBytes($packet.resreq), 0, $buffer, 22, 1);
            $buffer[22] = 0x06

            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.packet_type), 0, $buffer, 32, 2);
            #payload, color
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.transient), 0, $buffer, 37, 1); #transient
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.hue), 0, $buffer, 38, 2); #hue
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.saturation), 0, $buffer, 40, 2); #saturation
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.brightness), 0, $buffer, 42, 2); #brightness
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.kelvin), 0, $buffer, 44, 2); #kelvin
            #payload, remainder
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.period), 0, $buffer, 46, 4); #period/periodOpt, e.g. 3000ms
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.cycles), 0, $buffer, 50, 4); #cycles. float, e.g. 1
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.skew), 0, $buffer, 54, 2); #skewRatio
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.waveform), 0, $buffer, 56, 1); #waveform Type e.g. saw = 0
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.sethue), 0, $buffer, 57, 1); #setHue true/false, 0 or 1
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.setsaturation), 0, $buffer, 58, 1); #setSaturation true/false, 0 or 1
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.setbrightness), 0, $buffer, 59, 1); #setBrightness true/false, 0 or 1
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.setKelvin), 0, $buffer, 60, 1); #setKelvin true/false, 0 or 1

            #Device packets
            [Byte[]]$setWaveFormOptional = $buffer
        }
        #multizone device, different colors for different zones
        if ($Zones) {
            #quick validation, does the total number of zones passed in, exceed the available zones on the device?
            if ($Zones.Count -gt $Device.Zones) {
                throw "$($Zones.Count) zones defined for a device that only supports $($Device.Zones)"
            }

            ####build the packet
            #510 (SetExtendedColorZones) sets one color/zone
            $packet = [PSCustomObject]@{
                #LIFX Frame
                size               = 700;
                reserved1          = [uint64]0;
                reserved2          = [uint64]0;
                addressable        = 1;
                packet_type        = [uint16]510; #SetExtendedColorZones packet number
                reserved3          = [uint32]0;
                reserved10         = [uint32]0;
                source             = [uint32]29734587;
                ackreq             = 1; #1
                resreq             = 0; #0
                sequence           = 0;
                protocol           = [uint16]1024;
                target_mac_address = New-Object byte[] 8
                #Payload
                duration           = [uint32]($SecondsToTransition * 1000); #time to transition in ms, 4 bytes
                apply              = 1; #apply enum. 0 no_apply, 1 apply, 2 apply_only
                #a zone index of 1 with a color_count of 2 means that zones 2 and 3 would be set with color as count starts at 0
                zone_index         = [uint16]$StartingZone; #zone_index. first zone to apply COLORS to
                colors_count       = $Zones.Count; #colors_count. number of colors in COLORS to apply to the strip
            }

            #convert the packet to a byte array
            [Byte[]]$buffer = New-Object byte[] $packet.size;
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.size), 0, $buffer, 0, 2);
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.protocol), 0, $buffer, 2, 2);
            $buffer[3] = 0x14 #makes the LIFX packet portion of the protocol frame addressable, this is required for multizone

            #packet type is surrounded by reserved 2 and 3
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.reserved2), 0, $buffer, 24, 8);
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.packet_type), 0, $buffer, 32, 2);
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.reserved3), 0, $buffer, 34, 2);
            #[System.Array]::Copy([System.BitConverter]::GetBytes($packet.ackreq), 0, $buffer, 22, 1);
            $buffer[22] = 0x06 #sets ack_required True and res_required False

            #payload begins at 36
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.duration), 0, $buffer, 36, 4); #unint32
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.apply), 0, $buffer, 40, 1); #uint8
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.zone_index), 0, $buffer, 41, 2); #uint16
            [System.Array]::Copy([System.BitConverter]::GetBytes($packet.colors_count), 0, $buffer, 43, 1); #uint8

            $startingColorPosition = 44
            foreach ($zone in $Zones) {
                <#convert inputs
                https://lan.developer.lifx.com/docs/representing-color-with-hsbk
                did the user provide the direct Hue value, or RGB?
                #>


                if ($zone.Hue) {
                    $hshue = [int](([Math]::Round(0x10000 * $zone.Hue) / 360) % 0x10000)
                }
                if ($zone.Red -or $zone.Green -or $zone.Blue) {
                    $hscolor = ConvertTo-HSBK -Red $Zone.Red -Green $zone.Green -Blue $zone.Blue
                    $hshue = [int](([Math]::Round(0x10000 * $hscolor.Hue) / 360) % 0x10000)
                }
                if (!$hsHue) {
                    $hsHue = 0
                }

                $Saturation = $zone.Saturation / 100
                $hsSaturation = [int]([Math]::Round(0xFFFF * $Saturation))
                $Brightness = $zone.Brightness / 100
                $hsbrightness = [int]([Math]::Round(0xFFFF * $Brightness))
                [uint16]$kelvin = $zone.Kelvin

                #hue, 44
                [System.Array]::Copy([System.BitConverter]::GetBytes($hshue), 0, $buffer, $startingColorPosition, 2);
                #saturation, 46
                [System.Array]::Copy([System.BitConverter]::GetBytes($hsSaturation), 0, $buffer, ($startingColorPosition + 2), 2);
                #brightness, 48
                [System.Array]::Copy([System.BitConverter]::GetBytes($hsbrightness), 0, $buffer, ($startingColorPosition + 4), 2);
                #kelvin, 50
                [System.Array]::Copy([System.BitConverter]::GetBytes($kelvin), 0, $buffer, ($startingColorPosition + 6), 2);

                #increment 8 bytes, so the next pass of the loop sets the next color at the correct starting position
                $startingColorPosition += 8
            }

            #Device packets
            [Byte[]]$setExtendedColorZones = $buffer
        }
    }
    #single light source, setColor
    else {
        #convert the White value to Kelvin
        switch ($White) {
            "Candlelight" { $Kelvin = 1500 }
            "Sunset" { $Kelvin = 2000 }
            "Ultra Warm" { $Kelvin = 2500 }
            "Incandescent" { $Kelvin = 2700 }
            "Warm" { $Kelvin = 3000 }
            "Neutral" { $Kelvin = 3500 }
            "Cool" { $Kelvin = 4000 }
            "Cool Daylight" { $Kelvin = 4500 }
            "Soft Daylight" { $Kelvin = 5000 }
            "Daylight" { $Kelvin = 5600 }
            "Noon Daylight" { $Kelvin = 6000 }
            "Bright Daylight" { $Kelvin = 6500 }
            "Cloudy Daylight" { $Kelvin = 7000 }
            "Blue Daylight" { $Kelvin = 7500 }
            "Blue Overcast" { $Kelvin = 8000 }
            "Blue Ice" { $Kelvin = 9000 }
        }

        #convert inputs
        #https://lan.developer.lifx.com/docs/representing-color-with-hsbk
        $Brightness = $Brightness / 100
        $Saturation = $Saturation / 100
        $hscolor = ConvertTo-HSBK -Red $red -Green $green -Blue $blue
        $hsbrightness = [int]([Math]::Round(0xFFFF * $Brightness))
        $hshue = [int](([Math]::Round(0x10000 * $hscolor.Hue) / 360) % 0x10000)
        $hsSaturation = [int]([Math]::Round(0xFFFF * $Saturation))

        #build the packet
        $packet = [PSCustomObject]@{
            size               = 49;
            hue                = [uint16]$hshue
            saturation         = [uint16]$hsSaturation #0 #65535
            brightness         = [uint16]$hsbrightness
            kelvin             = [uint16]$Kelvin
            duration           = [uint32]($SecondsToTransition * 1000)
            packet_type        = [uint16]102;
            protocol           = [uint16]21504;
            reserved1          = [uint32]0;
            reserved2          = [uint32]0;
            reserved3          = [uint32]0;
            reserved4          = [uint32]0;
            reserved5          = [uint32]0;
            reserved6          = [uint32]0;
            site               = New-Object byte[] 8
            target_mac_address = New-Object byte[] 8
            timestamp          = [uint64]0
        }

        #convert the packet to a byte array
        [Byte[]]$buffer = New-Object byte[] $packet.size;
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.size), 0, $buffer, 0, 2);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.protocol), 0, $buffer, 2, 2);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.reserved1), 0, $buffer, 4, 4);
        [System.Array]::Copy($packet.target_mac_address, 0, $buffer, 8, 6);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.reserved2), 0, $buffer, 14, 2);
        [System.Array]::Copy($packet.site, 0, $buffer, 16, 6);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.reserved3), 0, $buffer, 22, 2);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.timestamp), 0, $buffer, 24, 8);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.packet_type), 0, $buffer, 32, 2);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.reserved4), 0, $buffer, 30, 2);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.reserved6), 0, $buffer, 34, 2);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.hue), 0, $buffer, 37, 2);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.saturation), 0, $buffer, 39, 2);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.brightness), 0, $buffer, 41, 2);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.kelvin), 0, $buffer, 43, 2);
        [System.Array]::Copy([System.BitConverter]::GetBytes($packet.duration), 0, $buffer, 45, 4);
        [Byte[]]$payload = New-Object byte[] 0 #$packet.GetPayloadBuffer();
        [System.Array]::Copy($payload, 0, $buffer, 36, $payload.Length);

        #Device packets
        [Byte[]]$changeColorPacket = $buffer
    }

    #process
    $Input | ForEach-Object {
        #constants
        $Port = "56700"
        $localIP = [System.Net.IPAddress]::Parse([System.Net.IPAddress]::Any)
        $RemoteIpEndPoint = New-Object System.Net.IPEndpoint($localIP, $Port)
        $receivingUdpClient = $null
        $receivingUdpClient = New-Object System.Net.Sockets.UDPClient($RemoteIpEndPoint)
        $receivingUdpClient.Client.Blocking = $false
        $receivingUdpClient.DontFragment = $true
        $receivingUdpClient.Client.SetSocketOption([System.Net.Sockets.SocketOptionLevel]::Socket, [System.Net.Sockets.SocketOptionName]::ReuseAddress, $true)

        if ($packet.packet_type -eq 102) {
            $send = $receivingUdpClient.SendAsync($changeColorPacket, $changeColorPacket.Length, $_.IPAddress.Address, $_.IPAddress.Port)
        }
        if ($packet.packet_type -eq 119) {
            $send = $receivingUdpClient.SendAsync($setWaveFormOptional, $setWaveFormOptional.Length, $_.IPAddress.Address, $_.IPAddress.Port)
        }
        if ($packet.packet_type -eq 510) {
            $send = $receivingUdpClient.SendAsync($setExtendedColorZones, $setExtendedColorZones.Length, $_.IPAddress.Address, $_.IPAddress.Port)
        }

        #shut the udp client down
        $receivingUdpClient.Dispose()
        $receivingUdpClient.Close()

        $Count++
        [int]$percentComplete = ($Count / $Total * 100)
        Write-Progress -Activity "Changing Lifx Device Color" -PercentComplete $percentComplete -Status ("$($_.Name) color changed - " + $percentComplete + "%")
    }
}