Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

LightScript is a PowerShell module for controlling smart lights (Philips Hue, NanoLeaf, Twinkly, Divoom Pixoo64, Elgato KeyLight, LaMetric Time, WLED). Published to the PowerShell Gallery as `LightScript`.

## Build & Test Commands

```powershell
# Import the module locally for development
Import-Module .\LightScript.psd1 -Force -PassThru

# Run tests (Pester v4, NOT v5)
Install-Module Pester -MaximumVersion 4.99.99 -Scope CurrentUser -SkipPublisherCheck
Import-Module Pester -MaximumVersion 4.99.99
Invoke-Pester -PassThru

# Static analysis
Invoke-ScriptAnalyzer -Path .\ -Recurse

# Regenerate formatting and type definitions (requires EZOut module)
.\LightScript.ezformat.ps1

# Regenerate GitHub workflow YAML (requires PSDevOps module)
.\LightScript.GitHubWorkflow.psdevops.ps1
```

## Architecture

### Module Loading
- `LightScript.psd1` — manifest (version, metadata, exports)
- `LightScript.psm1` — root module; recursively dot-sources all `*-*.ps1` files, then auto-creates aliases mapping light/room names to `Set-HueLight`
- `LightScript.ps1.psm1` — PipeScript template alternative that uses `[include("*-*.ps1")]`

### Function Organization
One function per file under `Functions/`, grouped by device:
- `Functions/Hue/` — Philips Hue (largest: ~32 functions). `Send-HueBridge` is the central HTTP dispatch.
- `Functions/NanoLeaf/` — `Send-NanoLeaf` is the HTTP dispatch.
- `Functions/Twinkly/`
- `Functions/Pixoo/`
- `Functions/KeyLight/`
- `Functions/LaMetric/`
- `Functions/WLED/` — WLED open-source LED controllers. `Send-WLED` is the HTTP dispatch. No auth required.

Each device follows the pattern: `Find-*`, `Connect-*`, `Disconnect-*`, `Get-*`, `Set-*` with a shared `Send-*` function for REST communication.

### Type System
- `Types/` — custom PowerShell type extensions (method `.ps1` files) for types like `Hue.Sensor`, `LightScript.Color`, `LaMetric.Icon`, `LaMetric.Time.Notification`
- `LightScript.types.ps1xml` — generated by EZOut from `Types/` directory
- `LightScript.format.ps1xml` — generated by EZOut from `Formatting/` directory
- Functions assign `PSTypeName` (e.g., `Hue.Light`, `Hue.Group`) to enable custom formatting

### Code Generation Pipeline
The build uses several Start-Automating modules:
- **PipeScript** — generates functions from `.ps1.ps1` templates (e.g., `Add-HueLight.ps1.ps1` → `Add-HueLight.ps1`)
- **EZOut** — `LightScript.ezformat.ps1` generates `.format.ps1xml` and `.types.ps1xml` from `Formatting/` and `Types/`
- **HelpOut** — generates help documentation from comment-based help
- **PSDevOps** — `LightScript.GitHubWorkflow.psdevops.ps1` generates `.github/workflows/` YAML

### CI/CD (GitHub Actions — `TestAndPublish.yml`)
1. **PowerShellStaticAnalysis** — ScriptCop + PSScriptAnalyzer
2. **TestPowerShellOnLinux** — Pester (pinned to 4.99.99), with code coverage
3. **TagReleaseAndPublish** — tags version, creates GitHub release, publishes to PSGallery (only on merged PRs)
4. **BuildModule** — PipeScript + EZOut + HelpOut

## Conventions

- Functions use PowerShell `Verb-Noun` naming with device prefix: `Get-HueLight`, `Set-NanoLeaf`, `Connect-Pixoo`
- Heavy use of `[ComponentModel.DefaultBindingProperty]` and `[ComponentModel.AmbientValue]` attributes for pipeline binding
- Connection info (IP addresses, auth tokens) stored in `$home/LightScript/` directory
- All `Set-*` commands support `ShouldProcess` (WhatIf/Confirm)
- `LightScript.Color` type provides RGB/HSL/color-temperature conversions with daisy-chaining support
1 change: 1 addition & 0 deletions Formatting/WLED.format.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Write-FormatView -TypeName WLED -Property Name, IPAddress, On, Brightness, CurrentEffect, CurrentPalette, LEDCount, Version
96 changes: 96 additions & 0 deletions Functions/WLED/Connect-WLED.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
function Connect-WLED {
<#
.Synopsis
Connects to a WLED device
.Description
Connects to a WLED device over WiFi and saves connection information.
WLED is open-source firmware for ESP8266/ESP32 LED controllers with a JSON API.
.Example
Connect-WLED 192.168.1.100 -PassThru
.Link
Get-WLED
.Link
Disconnect-WLED
#>
[OutputType([Nullable], [PSObject])]
param(
# The IP Address for the WLED device.
[Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
[Alias('WLEDIPAddress')]
[IPAddress]
$IPAddress,

# If set, will output the connection information.
[switch]
$PassThru
)

begin {
if (-not $script:WLEDCache) {
$script:WLEDCache = @{}
}
if ($home) {
$lightScriptRoot = Join-Path $home -ChildPath LightScript
}
}

process {
#region Attempt to Contact the Device
$wledData = Invoke-RestMethod -Uri "http://$IPAddress/json"
#endregion Attempt to Contact the Device

if ($wledData) {
$macAddress =
if ($wledData.info.mac) {
$wledData.info.mac
}
elseif ($PSVersionTable.Platform -like 'Win*' -or -not $PSVersionTable.Platform) {
Get-NetNeighbor | Where-Object IPAddress -eq $IPAddress | Select-Object -ExpandProperty LinkLayerAddress
}
elseif ($ExecutionContext.SessionState.InvokeCommand.GetCommand('nmap', 'Application')) {
nmap -Pn "$IPAddress" |
Where-Object { $_ -like 'MAC Address:*' } |
ForEach-Object { @($_ -split ' ')[2] }
}

if (-not $macAddress) {
Write-Error "Unable to resolve MAC address for $IPAddress, will not save connection"
return
}

#region Save Device Information
if ($home) {
if (-not (Test-Path $lightScriptRoot)) {
$createLightScriptDir = New-Item -ItemType Directory -Path $lightScriptRoot
if (-not $createLightScriptDir) { return }
}

$mainSeg = $wledData.state.seg | Select-Object -First 1
$wledData.pstypenames.clear()
$wledData.pstypenames.add('WLED')
$wledData |
Add-Member NoteProperty IPAddress $IPAddress -Force -PassThru |
Add-Member NoteProperty MACAddress $macAddress -Force -PassThru |
Add-Member NoteProperty Name $wledData.info.name -Force -PassThru |
Add-Member NoteProperty Version $wledData.info.ver -Force -PassThru |
Add-Member NoteProperty LEDCount $wledData.info.leds.count -Force -PassThru |
Add-Member NoteProperty On $wledData.state.on -Force -PassThru |
Add-Member NoteProperty Brightness $wledData.state.bri -Force -PassThru |
Add-Member NoteProperty CurrentEffect $(
if ($wledData.effects -and $mainSeg) { $wledData.effects[$mainSeg.fx] }
) -Force -PassThru |
Add-Member NoteProperty CurrentPalette $(
if ($wledData.palettes -and $mainSeg) { $wledData.palettes[$mainSeg.pal] }
) -Force -PassThru |
Export-Clixml -Path (Join-Path $lightScriptRoot ".$macAddress.wled.clixml")

$script:WLEDCache["$IPAddress"] = $wledData
}
#endregion Save Device Information

if ($PassThru) {
$wledData
}
}
}
}
41 changes: 41 additions & 0 deletions Functions/WLED/Disconnect-WLED.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
function Disconnect-WLED {
<#
.Synopsis
Disconnects a WLED device
.Description
Disconnects a WLED device, removing stored device info
.Example
Disconnect-WLED 192.168.1.100
.Link
Connect-WLED
#>
[OutputType([Nullable], [PSObject])]
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
# The IP Address for the WLED device.
[Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
[Alias('WLEDIPAddress')]
[IPAddress]
$IPAddress
)

begin {
if ($home) {
$lightScriptRoot = Join-Path $home -ChildPath LightScript
}
}

process {
@(Get-ChildItem -Filter *.wled.clixml -Path $lightScriptRoot -ErrorAction SilentlyContinue) |
ForEach-Object {
$file = $_
$fileInfo = Import-Clixml -LiteralPath $file.FullName
if ("$($fileInfo.IPAddress)" -eq "$IPAddress" -and $PSCmdlet.ShouldProcess("Remove WLED device '$($fileInfo.Name)' ($($fileInfo.IPAddress))")) {
Remove-Item -LiteralPath $file.FullName -Force
if ($script:WLEDCache) {
$script:WLEDCache.Remove("$IPAddress")
}
}
}
}
}
133 changes: 133 additions & 0 deletions Functions/WLED/Get-WLED.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
function Get-WLED {
<#
.Synopsis
Gets WLED devices
.Description
Gets saved WLED devices and their current state.
Can also list available effects and palettes.
.Example
Get-WLED
.Example
Get-WLED -ListEffect
.Example
Get-WLED -ListPalette
.Example
Get-WLED -Force
.Link
Connect-WLED
.Link
Set-WLED
#>
[CmdletBinding(DefaultParameterSetName = 'ListDevices')]
[OutputType([PSObject])]
param(
# The IP Address for the WLED device.
[Parameter(ValueFromPipelineByPropertyName)]
[Alias('WLEDIPAddress')]
[IPAddress[]]
$IPAddress,

# If set, will list available effects.
[Parameter(ParameterSetName = 'ListEffect')]
[Alias('Effect', 'Effects')]
[switch]
$ListEffect,

# If set, will list available palettes.
[Parameter(ParameterSetName = 'ListPalette')]
[Alias('Palette', 'Palettes')]
[switch]
$ListPalette,

# If set, will refresh device state from the API.
[switch]
$Force
)

begin {
if (-not $script:WLEDCache) {
$script:WLEDCache = @{}
}
if ($home) {
$lightScriptRoot = Join-Path $home -ChildPath LightScript
}
}

process {
#region Default to All Devices
if (-not $IPAddress) {
if ($home -and (-not $script:WLEDCache.Count -or $Force)) {
Get-ChildItem -Path $lightScriptRoot -ErrorAction SilentlyContinue -Filter *.wled.clixml -Force |
Import-Clixml |
ForEach-Object {
if (-not $_) { return }
$script:WLEDCache["$($_.IPAddress)"] = $_
}
}
$IPAddress = $script:WLEDCache.Keys
if (-not $IPAddress) { return }
}
#endregion Default to All Devices

foreach ($ip in $IPAddress) {
#region Refresh from Device
if ($Force) {
$wledData = $null
$wledData = Invoke-RestMethod -Uri "http://$ip/json" -ErrorAction SilentlyContinue
if ($wledData) {
$mainSeg = $wledData.state.seg | Select-Object -First 1
$wledData.pstypenames.clear()
$wledData.pstypenames.add('WLED')
$script:WLEDCache["$ip"] = $wledData |
Add-Member NoteProperty IPAddress ([IPAddress]"$ip") -Force -PassThru |
Add-Member NoteProperty MACAddress $wledData.info.mac -Force -PassThru |
Add-Member NoteProperty Name $wledData.info.name -Force -PassThru |
Add-Member NoteProperty Version $wledData.info.ver -Force -PassThru |
Add-Member NoteProperty LEDCount $wledData.info.leds.count -Force -PassThru |
Add-Member NoteProperty On $wledData.state.on -Force -PassThru |
Add-Member NoteProperty Brightness $wledData.state.bri -Force -PassThru |
Add-Member NoteProperty CurrentEffect $(
if ($wledData.effects -and $mainSeg) { $wledData.effects[$mainSeg.fx] }
) -Force -PassThru |
Add-Member NoteProperty CurrentPalette $(
if ($wledData.palettes -and $mainSeg) { $wledData.palettes[$mainSeg.pal] }
) -Force -PassThru
} else {
Write-Warning "WLED device at $ip is not reachable"
}
}
#endregion Refresh from Device

$device = $script:WLEDCache["$ip"]
if (-not $device) { continue }

if ($ListEffect) {
if ($device.effects) {
for ($i = 0; $i -lt $device.effects.Count; $i++) {
[PSCustomObject]@{
PSTypeName = 'WLED.Effect'
Id = $i
Name = $device.effects[$i]
IPAddress = $device.IPAddress
}
}
}
}
elseif ($ListPalette) {
if ($device.palettes) {
for ($i = 0; $i -lt $device.palettes.Count; $i++) {
[PSCustomObject]@{
PSTypeName = 'WLED.Palette'
Id = $i
Name = $device.palettes[$i]
IPAddress = $device.IPAddress
}
}
}
}
else {
$device
}
}
}
}
Loading