# $Id$ ############################################## # # 31_MilightDevice.pm (Based on 32_WifiLight.pm by hermannj) # FHEM module for MILIGHT lightbulbs. Supports RGB (untested), RGBW and White models. # Author: Matthew Wire (mattwire) # # This file is part of fhem. # # Fhem is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## package main; use strict; use warnings; use IO::Handle; use IO::Socket; use IO::Select; use Time::HiRes; #use Math::Round (); use Color; use SetExtensions; my %dim_values = ( 0 => "dim_00", 1 => "dim_10", 2 => "dim_20", 3 => "dim_30", 4 => "dim_40", 5 => "dim_50", 6 => "dim_60", 7 => "dim_70", 8 => "dim_80", 9 => "dim_90", 10 => "dim_100", ); # RGBW 3 byte commands. 3rd byte not required Bridge V3+ my @RGBWCmdsOn = ("\x45", "\x47", "\x49", "\x4B", "\x42"); # Byte 1 for setting On my @RGBWCmdsOff = ("\x46", "\x48", "\x4A", "\x4C", "\x41"); # Byte 1 for setting Off my @RGBWCmdsWT = ("\xC5", "\xC7", "\xC9", "\xCB", "\xC2"); # Byte 1 for setting WhiteMode my @RGBWCmdsNt = ("\xC6", "\xC8", "\xCA", "\xCC", "\xC1"); # Byte 1 for setting NightMode my $RGBWCmdBri = "\x4E"; # Byte 1 for setting brightness (Byte 2 specifies level (0x02-0x1B 25 steps) my $RGBWCmdCol = "\x40"; # Byte 1 for setting color (Byte 2 specifies color value (0x00-0xFF (255 steps)) my $RGBWCmdDiscoUp = "\x4D"; # Byte 1 for setting discoMode Up my $RGBWCmdDiscoInc = "\x44"; # Byte 1 for setting discoMode speed + my $RGBWCmdDiscoDec = "\x43"; # Byte 1 for setting discoMode speed - my $RGBWCmdEnd = "\x55"; # Byte 3 # White 3 byte commands. my @WhiteCmdsOn = ("\x38", "\x3D", "\x37", "\x32", "\x35"); # Byte 1 for setting On my @WhiteCmdsOff = ("\x3B", "\x33", "\x3A", "\x36", "\x39"); # Byte 1 for setting Off my @WhiteCmdsOnFull = ("\xB8", "\xBD", "\xB7", "\xB2", "\xB5"); # Byte 1 for setting full brightness my @WhiteCmdsNt = ("\xBB", "\xB3", "\xBA", "\xB6", "\xB9"); # Byte 1 for setting NightMode my @WhiteCmdBriDn = ("\x34", "\x34", "\x34", "\x34", "\xB4"); # Byte 1 for setting Brightness down (11 steps, no direct setting) my @WhiteCmdBriUp = ("\x3C", "\x3C", "\x3C", "\x3C", "\xBC"); # Byte 1 for setting Brightness up (11 steps, no direct setting) my @WhiteCmdColDn = ("\x3F", "\x3F", "\x3F", "\x3F", "\xBF"); # Byte 1 for setting colour temp down my @WhiteCmdColUp = ("\x3E", "\x3E", "\x3E", "\x3E", "\xBE"); # Byte 1 for setting colour temp up my $WhiteCmdEnd = "\x55"; # Byte 3 sub MilightDevice_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "MilightDevice_Define"; $hash->{UndefFn} = "MilightDevice_Undef"; $hash->{ShutdownFn} = "MilightDevice_Undef"; $hash->{SetFn} = "MilightDevice_Set"; $hash->{GetFn} = "MilightDevice_Get"; $hash->{AttrFn} = "MilightDevice_Attr"; $hash->{NotifyFn} = "MilightDevice_Notify"; $hash->{AttrList} = "IODev dimStep defaultBrightness defaultRampOn " . "defaultRampOff presets dimOffWhite:1,0 updateGroupDevices:1,0 " . "restoreAtStart:1,0 colorCast gamma lightSceneParamsToSave " . $readingFnAttributes; FHEM_colorpickerInit(); } ##################################### # Device State Icon for FHEMWEB: Shows a colour changing icon with dim level sub MilightDevice_devStateIcon($) { my($hash) = @_; $hash = $defs{$hash} if(ref($hash) ne 'HASH'); return undef if(!$hash); return undef if($hash->{helper}->{group}); my $name = $hash->{NAME}; my $percent = ReadingsVal($name,"brightness","100"); my $s = $dim_values{MilightDevice_roundfunc($percent/10)}; # Return SVG coloured icon with toggle as default action return ".*:light_light_$s@#".ReadingsVal($name, "rgb", "FFFFFF").":toggle" if (($hash->{LEDTYPE} eq 'RGBW') || ($hash->{LEDTYPE} eq 'RGB')); # Return SVG icon with toggle as default action (for White bulbs) return ".*:light_light_$s:toggle"; } ##################################### # Define Milight device sub MilightDevice_Define($$) { my ($hash, $def) = @_; my @args = split("[ \t][ \t]*", $def); my ($name, $type, $ledtype, $iodev, $slot) = @args; $hash->{INIT} = 0; # Set to 1 when lamp initialised (MilightDevice_Restore) $hash->{LEDTYPE} = $ledtype; $hash->{SLOT} = $slot; $hash->{SLOTID} = $slot; if($slot eq 'A') { $hash->{SLOTID} = 9 if ($hash->{LEDTYPE} eq 'RGBW'); $hash->{SLOTID} = 5 if ($hash->{LEDTYPE} eq 'White'); $hash->{SLOTID} = 0 if ($hash->{LEDTYPE} eq 'RGB'); } # Validate parameters return "wrong syntax: define MilightDevice " if(@args < 5); return "unknown LED type ($hash->{LEDTYPE}): choose one of RGB, RGBW, White" if ($hash->{LEDTYPE} !~ /RGBW|White|RGB/); return "Invalid slot: Select one of 1..4 / A for White" if (($hash->{SLOTID} !~ /^\d*$/) || (($hash->{SLOT} ne 'A') && (($hash->{SLOT} < 1) || ($hash->{SLOT} > 4))) && ($hash->{LEDTYPE} eq 'White')); return "Invalid slot: Select one of 5..8 / A for RGBW" if (($hash->{SLOTID} !~ /^\d*$/) || (($hash->{SLOT} ne 'A') && (($hash->{SLOT} < 5) || ($hash->{SLOT} > 8))) && ($hash->{LEDTYPE} eq 'RGBW')); return "Invalid slot: Select 0 for RGB" if (($hash->{SLOTID} !~ /^\d*$/) || ($hash->{SLOTID} != 0 && $hash->{LEDTYPE} eq 'RGB')); Log3 ($hash, 4, $name."_Define: $name $type $hash->{LEDTYPE} $iodev $hash->{SLOT}"); # Verify IODev is valid AssignIoPort($hash, $iodev); if(defined($hash->{IODev}->{NAME})) { Log3 $name, 4, $name."_Define: I/O device is " . $hash->{IODev}->{NAME}; } else { Log3 $name, 1, $name."_Define: no I/O device"; } # Look for already defined device on IODev if ($hash->{SLOT} ne 'A' && defined($hash->{IODev}->{$hash->{SLOT}}->{NAME})) { # If defined slot does not match current device name don't allow new definition. Redefining the same device is ok though. #return "Slot $hash->{SLOT} already defined as $hash->{IODev}->{$hash->{SLOT}}->{NAME}" if ($hash->{IODev}->{$hash->{SLOT}}->{NAME} ne $name); } # Define device on IODev if ($hash->{SLOT} ne 'A') { $hash->{IODev}->{$hash->{SLOT}}->{NAME} = $name; #$hash->{IODev}->{$hash->{SLOT}}->{DEVNAME} = $name; } # Define Command Queue my @cmdQueue = []; $hash->{helper}->{cmdQueue} = \@cmdQueue; my $baseCmds = "on off toggle dimup dimdown"; my $sharedCmds = "pair unpair restorePreviousState:noArg saveState:noArg restoreState:noArg"; my $rgbCmds = "hsv rgb:colorpicker,RGB hue:colorpicker,HUE,0,1,360 saturation:slider,0,100,100 preset"; $hash->{helper}->{COMMANDSET} = "$baseCmds discoModeUp:noArg discoSpeedUp:noArg discoSpeedDown:noArg night:noArg white:noArg toggleWhite:noArg $sharedCmds $rgbCmds" if ($hash->{LEDTYPE} eq 'RGBW'); $hash->{helper}->{COMMANDSET} = "$baseCmds discoModeUp:noArg discoModeDown:noArg discoSpeedUp:noArg discoSpeedDown:noArg $sharedCmds $rgbCmds" if ($hash->{LEDTYPE} eq 'RGB'); $hash->{helper}->{COMMANDSET} = "$baseCmds hsv ct:colorpicker,CT,3000,350,6500 night:noArg $sharedCmds" if ($hash->{LEDTYPE} eq 'White'); my $defaultcommandset = $hash->{helper}->{COMMANDSET}; $hash->{helper}->{COMMANDSET} .= " dim:slider,0,".MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)).",100 brightness:slider,0,".MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)).",100"; # webCmds if (!defined($attr{$name}{webCmd})) { $attr{$name}{webCmd} = 'on:off:dim:hue:night:rgb ffffff:rgb ff0000:rgb 00ff00:rgb 0000ff:rgb ffff00' if ($hash->{LEDTYPE} eq 'RGBW'); $attr{$name}{webCmd} = 'on:off:dim:hue:rgb ffffff:rgb ff0000:rgb 00ff00:rgb 0000ff:rgb ffff00' if ($hash->{LEDTYPE} eq 'RGB'); $attr{$name}{webCmd} = 'on:off:dim:ct:night' if ($hash->{LEDTYPE} eq 'White'); } $hash->{helper}->{GAMMAMAP} = MilightDevice_CreateGammaMapping($hash, 1.0); $hash->{helper}->{COLORMAP} = MilightDevice_ColorConverter($hash, split(',', "0,0,0,0,0,0")); # Define devStateIcon $attr{$name}{devStateIcon} = '{(MilightDevice_devStateIcon($name),"toggle")}' if(!defined($attr{$name}{devStateIcon})); # Event on change reading $attr{$name}{"event-on-change-reading"} = "state,transitionInProgress" if (!defined($attr{$name}{"event-on-change-reading"})); # lightScene if(!defined($attr{$name}{"lightSceneParamsToSave"})) { $attr{$name}{"lightSceneParamsToSave"} = "hsv" if (($hash->{LEDTYPE} eq 'RGBW')|| ($hash->{LEDTYPE} eq 'RGB')); $attr{$name}{"lightSceneParamsToSave"} = "brightness" if ($hash->{LEDTYPE} eq 'White'); } # IODev $attr{$name}{IODev} = $hash->{IODev} if (!defined($attr{$name}{IODev})); # restoreAtStart if($slot eq 'A') { $attr{$name}{"restoreAtStart"} = 0 if (!defined($attr{$name}{"restoreAtStart"})); } else { $attr{$name}{"restoreAtStart"} = 1 if (!defined($attr{$name}{"restoreAtStart"})); } return undef; } sub MilightDevice_Init($) { my ($hash) = @_; my $name = $hash->{NAME}; if( AttrVal($hash->{NAME}, "gamma", "1.0") eq "1.0") { Log3 ($name, 5, $name." dimstep ".MilightDevice_roundfunc(100 / MilightDevice_DimSteps($hash))." / gamma 1.0"); } else { $hash->{helper}->{COMMANDSET} =~ s/dim:slider,0,.*,100/dim:slider,0,1,100/g; $hash->{helper}->{COMMANDSET} =~ s/brightness:slider,0,.*,100/brightness:slider,0,1,100/g; Log3 $name, 5, $name." dimstep 1 / gamma ".AttrVal($hash->{NAME}, "gamma", "1.0"); $hash->{helper}->{GAMMAMAP} = MilightDevice_CreateGammaMapping($hash, AttrVal($hash->{NAME}, "gamma", "1.0")); } # Colormap / Commandsets if (($hash->{LEDTYPE} eq 'RGBW') || ($hash->{LEDTYPE} eq 'RGB')) { my @a = split(',', "0,0,0,0,0,0"); if ( defined( $attr{$name}{colorCast} ) ) { @a = split(',', AttrVal($hash->{NAME}, "colorCast", "0,0,0,0,0,0")); @a = split(',', "0,0,0,0,0,0") unless (@a == 6); foreach my $tc (@a) { @a = split(',', "0,0,0,0,0,0") unless ($tc =~ m/^\s*[\-]{0,1}[0-9]+[\.]{0,1}[0-9]*\s*$/g); @a = split(',', "0,0,0,0,0,0") if (abs($tc) >= 30); } } $hash->{helper}->{COLORMAP} = MilightDevice_ColorConverter($hash, @a); } return undef; } ##################################### # Undefine device sub MilightDevice_Undef(@) { my ($hash,$args) = @_; RemoveInternalTimer($hash); # Remove slot on bridge delete ($hash->{IODev}->{$hash->{SLOT}}->{NAME}) if ($hash->{SLOT} ne 'A'); return undef; } ##################################### # Set functions sub MilightDevice_Set(@) { my ($hash, $name, $cmd, @args) = @_; my $cnt = @args; my $ramp = 0; my $flags = ""; my $event = undef; my $usage = "set $name ..."; if ($hash->{IODev}->{STATE} ne "ok" && $hash->{IODev}->{STATE} ne "Initialized") { readingsSingleUpdate($hash, "state", "error", 1); $flags = "q"; $args[2] = "" if(!defined($args[2])); $args[2] .= "q" if ($args[2] !~ m/.*[qQ].*/); # return SetExtensions($hash, $hash->{helper}->{COMMANDSET}, $name, $cmd, @args); # IO error, we need to keep our current state settings! } # Commands that map to other commands if ($cmd eq "toggle") { $cmd = ReadingsVal($name,"state","on") ne "off" ? "off" :"on"; } elsif ($cmd eq "white") { $cmd = "saturation"; $args[0] = 0; } elsif ($cmd eq "toggleWhite") { $cmd = "saturation"; $args[0] = (ReadingsVal($name,"saturation",100) > 0) ? 0 : 100; } # Commands if ($cmd eq 'on') { if (defined($args[0])) { return "Usage: set $name on [seconds(0..X)]" if ($args[0] !~ /^\d+$/); $ramp = $args[0]; } elsif (defined($attr{$name}{defaultRampOn})) { $ramp = $attr{$name}{defaultRampOn}; } return MilightDevice_RGBW_On($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW'); return MilightDevice_White_DimOn($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White' && AttrVal($hash->{NAME}, "dimOffWhite", 0) == 1); return MilightDevice_White_On($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White'); return MilightDevice_RGB_On($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB'); } elsif ($cmd eq 'off') { if (defined($args[0])) { return "Usage: set $name off [seconds(0..X)]" if ($args[0] !~ /^\d+$/); $ramp = $args[0]; } elsif (defined($attr{$name}{defaultRampOff})) { $ramp = $attr{$name}{defaultRampOff}; } return MilightDevice_RGBW_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW'); return MilightDevice_White_DimOff($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White' && AttrVal($hash->{NAME}, "dimOffWhite", 0) == 1); return MilightDevice_White_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White'); return MilightDevice_RGB_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB'); } # Set HSV value elsif ($cmd eq 'hsv') { $usage = "Usage: set $name hsv ,, [seconds(0..x)] [flags(l=long path|q=don't clear queue)]"; $usage = "Usage: set $name hsv ,, [seconds(0..x)] [flags(q=don't clear queue)]" if ($hash->{LEDTYPE} eq 'White'); return $usage if ($args[0] !~ /^(\d{1,4}),(\d{1,3}),(\d{1,3})$/); my ($h, $s, $v) = ($1, $2, $3); return "Invalid hue ($h): valid range 0..360" if (!(($h >= 0) && ($h <= 360)) && ($hash->{LEDTYPE} ne 'White')); return "Invalid color temperature ($h): valid range 3000..6500" if (!(($h >= 3000) && ($h <= 6500)) && ($hash->{LEDTYPE} eq 'White')); return "Invalid saturation ($s): valid range 0..100" if (!(($s >= 0) && ($s <= 100)) && ($hash->{LEDTYPE} ne 'White')); return "Invalid brightness ($v): valid range 0..100" if !(($v >= 0) && ($v <= 100)); if (defined($args[1])) { return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0 $ramp = $args[1]; } if (defined($args[2])) { return $usage if ($args[2] !~ m/.*[lLqQ].*/); # Flags l=Long way round for transition, q=don't clear queue (add to end) $flags = $args[2]; } return MilightDevice_White_Transition($hash, $h, 0, $v, $ramp, $flags) if($hash->{LEDTYPE} eq 'White'); return MilightDevice_HSV_Transition($hash, $h, $s, $v, $ramp, $flags); } # Dim to a fixed percentage with transition if requested elsif ($cmd eq 'dim' || $cmd eq 'brightness') { $usage = "Usage: set $name dim [seconds(0..x)] [flags(l=long path|q=don't clear queue)]"; return $usage if (($args[0] !~ /^\d+$/) || ($args[0] < 0) || ($args[0] > 100)); # Decimal value for percent between 0..100 if (defined($args[1])) { return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0 $ramp = $args[1]; } if (defined($args[2])) { return $usage if ($args[2] !~ m/.*[lLqQ].*/); # Flags l=Long way round for transition, q=don't clear queue (add to end) $flags = $args[2]; } return MilightDevice_RGBW_Dim($hash, $args[0], $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW'); return MilightDevice_White_Dim($hash, $args[0], $ramp, $flags) if ($hash->{LEDTYPE} eq 'White'); return MilightDevice_RGB_Dim($hash, $args[0], $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB'); } # Set night mode elsif ($cmd eq 'night') { if (defined($args[0])) { return "Usage: set $name night"; } return MilightDevice_RGBW_Night($hash) if ($hash->{LEDTYPE} eq 'RGBW'); return MilightDevice_White_Night($hash) if ($hash->{LEDTYPE} eq 'White'); } # Set hue elsif ($cmd eq 'hue') { $usage = "Usage: set $name hue [seconds(0..x)] [flags(l=long path|q=don't clear queue)]"; return $usage if (($args[0] !~ /^(\d+)$/) || ($args[0] < 0) || ($args[0] > 360)); if (defined($args[1])) { return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0 $ramp = $args[1]; } if (defined($args[2])) { return $usage if ($args[2] !~ m/.*[lLqQ].*/); # Flags l=Long way round for transition, q=don't clear queue (add to end) $flags = $args[2]; } my $sat = ReadingsVal($hash->{NAME}, "saturation", 100); $sat = 100 if(ReadingsVal($hash->{NAME}, "saturation", 0) == 0); return MilightDevice_HSV_Transition($hash, $args[0], $sat, ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), $ramp, $flags); } # Set color temperature elsif ($cmd eq 'ct') { if (defined($args[0])) { return "Usage: set $name ct <3000=Warm..6500=Cool>" if (($args[0] !~ /^\d+$/) || ($args[0] < 2500 || $args[0] > 7000)); } if (defined($args[1])) { return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0 $ramp = $args[1]; } if (defined($args[2])) { return $usage if ($args[2] !~ m/.*[lLqQ].*/); # Flags l=Long way round for transition, q=don't clear queue (add to end) $flags = $args[2]; } return MilightDevice_White_SetColourTemp($hash, $args[0], $ramp, $flags); } # Set RGB value elsif( $cmd eq "rgb") { $usage = "Usage: set $name rgb RRGGBB [seconds(0..x)] [flags(l=long path|q=don't clear queue)]"; return $usage if ($args[0] !~ /^([0-9A-Fa-f]{1,2})([0-9A-Fa-f]{1,2})([0-9A-Fa-f]{1,2})$/); my( $r, $g, $b ) = (hex($1), hex($2), hex($3)); #change to color.pm? my( $h, $s, $v ) = Color::rgb2hsv($r/255.0,$g/255.0,$b/255.0); $h = MilightDevice_roundfunc($h * 360); $s = MilightDevice_roundfunc($s * 100); $v = MilightDevice_roundfunc($v * 100); if (defined($args[1])) { return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0 $ramp = $args[1]; } if (defined($args[2])) { return $usage if ($args[2] !~ m/.*[lLqQ].*/); # Flags l=Long way round for transition, q=don't clear queue (add to end) $flags = $args[2]; } return MilightDevice_HSV_Transition($hash, $h, $s, $v, $ramp, $flags); } # Dim up by 1 "dimStep" or by a percentage with transition if requested elsif ($cmd eq 'dimup') { $usage = "Usage: set $name dimup [percent change(0..100)] [seconds(0..x)]"; my $percentChange = MilightDevice_roundfunc(100 / MilightDevice_DimSteps($hash)); # Default one dimStep if (defined($args[0])) { # Percent change (0..100%) return $usage if (($args[0] !~ /^\d+$/) || ($args[0] < 0) || ($args[0] > 100)); # Decimal value for percent between 0..100 $percentChange = $args[0]; # Percentage to change, will be converted in dev specific function } if (defined($args[1])) { # Seconds for transition (0..x) return $usage if (($args[1] !~ /^\d+$/) && ($args[1] >= 0)); # Decimal value for ramp > 0 $ramp = $args[1]; # Special case, if percent=100 adjust the ramp so it matches the actual amount required. # Eg. start: 80%. ramp 5seconds. Amount change: 100-80=20. Ramp time req: 20/100*5 = 1second. if ($percentChange == 100) { my $difference = $percentChange - ReadingsVal($hash->{NAME}, "brightness", 0); $ramp = ($difference/100) * $ramp; Log3 ($hash, 5, "$hash->{NAME}_Set: dimdown. Adjusted ramp to $ramp"); } } my $newBrightness = ReadingsVal($hash->{NAME}, "brightness", 0) + $percentChange; $newBrightness = 100 if $newBrightness > 100; return MilightDevice_RGBW_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW'); return MilightDevice_White_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White'); return MilightDevice_RGB_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB'); } # Dim down by 1 "dimStep" or by a percentage with transition if requested elsif ($cmd eq 'dimdown') { $usage = "Usage: set $name dimdown [percent change(0..100)] [seconds(0..x)]"; my $percentChange = MilightDevice_roundfunc(100 / MilightDevice_DimSteps($hash)); # Default one dimStep if (defined($args[0])) { # Percent change (0..100%) return $usage if (($args[0] !~ /^\d+$/) || ($args[0] < 0) || ($args[0] > 100)); # Decimal value for percent between 0..100 $percentChange = $args[0]; # Percentage to change, will be converted in dev specific function } if (defined($args[1])) { # Seconds for transition (0..x) return $usage if (($args[1] !~ /^\d+$/) && ($args[1] >= 0)); # Decimal value for ramp > 0 $ramp = $args[1]; # Special case, if percent=100 adjust the ramp so it matches the actual amount required. # Eg. start: 80%. ramp 5seconds. Amount change: 80. Ramp time req: 80/100*5 = 4second. if ($percentChange == 100) { my $difference = ReadingsVal($hash->{NAME}, "brightness", 0); $ramp = ($difference/100) * $ramp; Log3 ($hash, 5, "$hash->{NAME}_Set: dimdown. Adjusted ramp to $ramp"); } } my $newBrightness = ReadingsVal($hash->{NAME}, "brightness", 0) - $percentChange; $newBrightness = 0 if $newBrightness < 0; return MilightDevice_RGBW_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW'); return MilightDevice_White_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White'); return MilightDevice_RGB_Dim($hash, $newBrightness, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB'); } elsif ($cmd eq 'saturation') { $usage = "Usage: set $name saturation [seconds(0..x)] [flags(q=don't clear queue)]"; return $usage if (($args[0] !~ /^\d+$/) || ($args[0] < 0) || ($args[0] > 100)); if (defined($args[1])) { return $usage if (($args[1] !~ /^\d+$/) && ($args[1] > 0)); # Decimal value for ramp > 0 $ramp = $args[1]; } if (defined($args[2])) { return $usage if ($args[2] !~ m/.*[qQ].*/); # Flags q=don't clear queue (add to end) $flags = $args[2]; } return MilightDevice_HSV_Transition($hash, ReadingsVal($hash->{NAME}, "hue", 0), $args[0], ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), $ramp, $flags); } elsif ($cmd eq 'discoModeUp') { return MilightDevice_RGBW_DiscoModeStep($hash, 1); } elsif ($cmd eq 'discoModeDown') { return MilightDevice_RGBW_DiscoModeStep($hash, 0); } elsif ($cmd eq 'discoSpeedUp') { return MilightDevice_RGBW_DiscoModeSpeed($hash, 1); } elsif ($cmd eq 'discoSpeedDown') { return MilightDevice_RGBW_DiscoModeSpeed($hash, 0); } elsif ($cmd eq 'restorePreviousState') { # Restore the previous state (as store in previous* readings) my ($h, $s, $v) = MilightDevice_HSVFromStr($hash, ReadingsVal($hash->{NAME}, "previousState", MilightDevice_HSVToStr($hash, 0, 0, 0))); if($v eq 0) { if (defined($attr{$name}{defaultRampOff})) { $ramp = $attr{$name}{defaultRampOff}; } return MilightDevice_RGBW_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGBW'); return MilightDevice_White_DimOff($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White' && AttrVal($hash->{NAME}, "dimOffWhite", 0) == 1); return MilightDevice_White_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'White'); return MilightDevice_RGB_Off($hash, $ramp, $flags) if ($hash->{LEDTYPE} eq 'RGB'); } MilightDevice_HSV_Transition($hash, $h, $s, $v, 0, ''); return undef; } elsif ($cmd eq 'saveState') { # Save the hsv state as a string readingsSingleUpdate($hash, "savedState", MilightDevice_HSVToStr($hash, ReadingsVal($hash->{NAME}, "hue", 0), ReadingsVal($hash->{NAME}, "saturation", 0), ReadingsVal($hash->{NAME}, "brightness", 0)), 1); return undef; } elsif ($cmd eq 'restoreState') { my ($h, $s, $v) = MilightDevice_HSVFromStr($hash, ReadingsVal($hash->{NAME}, "savedState", MilightDevice_HSVToStr($hash, 0, 0, 0))); return MilightDevice_HSV_Transition($hash, $h, $s, $v, 0, ''); } elsif ($cmd eq 'preset') { my $preset = "+"; # Default to "preset +" if no args defined if (defined($args[0])) { return "Usage: set $name preset <0..X|+>" if ($args[0] !~ /^(\d+|\+)$/); $preset = $args[0]; } # Get presets, if not defined default to 1 preset 0,0,100. my @presets = split(/ /, AttrVal($hash->{NAME}, "presets", MilightDevice_HSVToStr($hash, 0, 0, 100))); # Load the next preset (and loop back to the first) if "+" specified. if ("$preset" eq "+") { $preset = (ReadingsVal($hash->{NAME}, "lastPreset", -1) + 1); if ($#presets < $preset) { $preset = 0; } } return "No preset defined at index $preset" if ($#presets < $preset); # Update reading and load preset readingsSingleUpdate($hash, "lastPreset", $preset, 1); my ($h, $s, $v) = MilightDevice_HSVFromStr($hash, $presets[$preset]); return MilightDevice_HSV_Transition($hash, $h, $s, $v, 0, ''); } elsif ($cmd eq 'pair') { if (defined($args[0])) { return "Usage: set $name pair [seconds(0..X)(default 3)]" if ($args[0] !~ /^\d+$/); $ramp = $args[0]; } else { $ramp = 3; } # Default pair for 3 seconds MilightDevice_CmdQueue_Clear($hash); return MilightDevice_RGBW_Pair($hash, $ramp) if ($hash->{LEDTYPE} eq 'RGBW'); return MilightDevice_White_Pair($hash, $ramp) if ($hash->{LEDTYPE} eq 'White'); return MilightDevice_RGB_Pair($hash, $ramp) if ($hash->{LEDTYPE} eq 'RGB'); } elsif ($cmd eq 'unpair') { if (defined($args[0])) { return "Usage: set $name unpair [seconds(0..X)(default 3)]" if ($args[0] !~ /^\d+$/); $ramp = $args[0]; } else { $ramp = 3; } # Default unpair for 3 seconds MilightDevice_CmdQueue_Clear($hash); return MilightDevice_RGBW_UnPair($hash, $ramp) if ($hash->{LEDTYPE} eq 'RGBW'); return MilightDevice_White_UnPair($hash, $ramp) if ($hash->{LEDTYPE} eq 'White'); return MilightDevice_RGB_UnPair($hash, $ramp) if ($hash->{LEDTYPE} eq 'RGB'); } return SetExtensions($hash, $hash->{helper}->{COMMANDSET}, $name, $cmd, @args); } ##################################### # Get functions sub MilightDevice_Get(@) { my ($hash, @args) = @_; my $name = $args[0]; return "$name: get needs at least one parameter" if(@args < 2); my $cmd= $args[1]; if($cmd eq "rgb" || $cmd eq "RGB") { return ReadingsVal($name, "rgb", "FFFFFF"); } elsif($cmd eq "hsv") { return MilightDevice_HSVToStr($hash, ReadingsVal($hash->{NAME}, "ct", 3000), 0, ReadingsVal($hash->{NAME}, "brightness", 0)) if ($hash->{LEDTYPE} eq 'White'); return MilightDevice_HSVToStr($hash, ReadingsVal($hash->{NAME}, "hue", 0), ReadingsVal($hash->{NAME}, "saturation", 0), ReadingsVal($hash->{NAME}, "brightness", 0)); } return "Unknown argument $cmd, choose one of rgb:noArg hsv:noArg"; } ##################################### # Attribute functions sub MilightDevice_Attr(@) { my ($cmd, $device, $attribName, $attribVal) = @_; my $hash = $defs{$device}; $attribVal = "" if (!defined($attribVal)); Log3 ($hash, 4, "$hash->{NAME}_Attr: Cmd: $cmd; Attribute: $attribName; Value: $attribVal"); if ($cmd eq 'set' && $attribName eq 'gamma') { return "gamma is required as numerical value with one decimal (eg. 0.5 or 2.2)" if ($attribVal !~ /^\d*\.\d*$/); $hash->{helper}->{GAMMAMAP} = MilightDevice_CreateGammaMapping($hash, $attribVal); if($attribVal ne "1.0") { $hash->{helper}->{COMMANDSET} =~ s/dim:slider,0,.*,100/dim:slider,0,1,100/g; $hash->{helper}->{COMMANDSET} =~ s/brightness:slider,0,.*,100/brightness:slider,0,1,100/g; } } # Allows you to modify the default number of dimSteps for a device elsif ($cmd eq 'set' && $attribName eq 'dimStep') { return "dimStep is required as numerical value [1..100]" if ($attribVal !~ /^\d*$/) || (($attribVal < 1) || ($attribVal > 100)); } # Allows you to set a default transition time for on/off elsif ($cmd eq 'set' && (($attribName eq 'defaultRampOn') || ($attribName eq 'defaultRampOff'))) { return "defaultRampOn/Off is required as numerical value [0..100]" if ($attribVal !~ /^[0-9]*\.?[0-9]*$/) || (($attribVal < 0) || ($attribVal > 100)); } # List of presets in hsv separated by space. Loaded by set command preset X elsif ($cmd eq 'set' && ($attribName eq 'presets')) { return "presets is required as space separated list of hsv(h,s,v) (eg. 0,0,100, 0,100,50)" if ($attribVal !~ /^[(\d{1,3}),(\d{1,3}),(\d{1,3})(?:$|\s)]*$/); } elsif ($cmd eq 'set' && $attribName eq 'colorCast') { return "colorCast: only works with RGB(W) devices" if ($hash->{LEDTYPE} eq 'White'); my @a = split(',', $attribVal); my $msg = "colorCast: correction requires red, yellow, green ,cyan, blue, magenta (each in a range of -29 .. 29)"; return $msg unless (@a == 6); foreach my $tc (@a) { return $msg unless ($tc =~ m/^\s*[\-]{0,1}[0-9]+[\.]{0,1}[0-9]*\s*$/g); return $msg if (abs($tc) >= 30); } $hash->{helper}->{COLORMAP} = MilightDevice_ColorConverter($hash, @a); #MilightDevice_RGB_ColorConverter($hash, @a); if ($init_done && !(@{$hash->{helper}->{cmdQueue}} > 0)) { my $hue = $hash->{READINGS}->{hue}->{VAL}; my $sat = $hash->{READINGS}->{saturation}->{VAL}; my $val = $hash->{READINGS}->{brightness}->{VAL}; return MilightDevice_RGBW_SetHSV($hash, $hue, $sat, $val, 1) if ($hash->{LEDTYPE} eq 'RGBW'); return MilightDevice_RGB_SetHSV($hash, $hue, $sat, $val, 1) if ($hash->{LEDTYPE} eq 'RGB'); } } elsif ($cmd eq 'set' && $attribName eq 'defaultBrightness') { return "defaultBrighness: has to be between ".MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash))." and 100" if ($attribVal < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)) || $attribVal > 100); } return undef; } ##################################### # Notify functions sub MilightDevice_Notify(@) { my ($hash,$dev) = @_; return MilightDevice_Restore($hash); } ##################################### # Restore HSV settings from readings. # Called after initialization to synchronise lamp state with fhem. sub MilightDevice_Restore(@) { my ($hash) = @_; return if ($hash->{INIT}); if ($init_done) { return if (AttrVal($hash->{NAME}, "restoreAtStart", 0) == 0); Log3 ($hash, 4, "$hash->{NAME}_Restore: Restoring saved HSV values"); $hash->{INIT} = 1; # Initialize device MilightDevice_Init($hash); # Clear inProgress flag: MJW Do we still need to do this? readingsSingleUpdate($hash, "transitionInProgress", 0, 1); # Default to OFF if not defined my ($hue, $sat, $val); $hue = ReadingsVal($hash->{NAME}, "hue", 0); $sat = ReadingsVal($hash->{NAME}, "saturation", 0); $val = ReadingsVal($hash->{NAME}, "brightness", 0); # Restore state return MilightDevice_RGBW_SetHSV($hash, $hue, $sat, $val, 1) if ($hash->{LEDTYPE} eq 'RGBW'); return MilightDevice_White_SetHSV($hash, $hue, $sat, $val, 1) if ($hash->{LEDTYPE} eq 'White'); return MilightDevice_RGB_SetHSV($hash, $hue, $sat, $val, 1) if ($hash->{LEDTYPE} eq 'RGB'); } } ############################################################################### # device specific controller functions RGB # LED Strip or bulb, no white, controller V2+. No longer manufactured Jan2014 ############################################################################### sub MilightDevice_RGB_Pair(@) { my ($hash, $numSeconds) = @_; $numSeconds = 3 if (($numSeconds || 0) == 0); Log3 ($hash, 4, "$hash->{NAME}_RGB_Pair: RGB LED slot $hash->{SLOT} pair $numSeconds s"); # DISCO SPEED FASTER 0x25 (SYNC/PAIR RGB Bulb within 2 seconds of Wall Switch Power being turned ON) my $ctrl = "\x25\x00\x55"; for (my $i = 0; $i < $numSeconds; $i++) { MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 1000, undef); } return undef; } ##################################### sub MilightDevice_RGB_UnPair(@) { my ($hash) = @_; my $numSeconds = 8; Log3 ($hash, 4, "$hash->{NAME}_RGB_UnPair: RGB LED slot $hash->{SLOT} unpair $numSeconds s"); # DISCO SPEED FASTER 0x25 (SYNC/PAIR RGB Bulb within 2 seconds of Wall Switch Power being turned ON) my $ctrl = "\x25\x00\x55"; for (my $i = 0; $i < $numSeconds; $i++) { MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); } return undef; } ##################################### sub MilightDevice_RGB_On(@) { my ($hash, $ramp, $flags) = @_; my $name = $hash->{NAME}; my $v = AttrVal($hash->{NAME}, "defaultBrightness", 36); Log3 ($hash, 4, "$hash->{NAME}_RGB_On: RGB slot $hash->{SLOT} set on $ramp"); # Switch on with same brightness it was switched off with, or max if undefined. if (ReadingsVal($hash->{NAME}, "state", "off") eq "off") { $v = ReadingsVal($hash->{NAME}, "brightness_on", AttrVal($hash->{NAME}, "defaultBrightness", 36)); } else { $v = ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)); } # When turning on, make sure we request at least minimum dim step. if ($v < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash))) { $v = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)); } return MilightDevice_RGB_Dim($hash, $v, $ramp, $flags); } ##################################### sub MilightDevice_RGB_Off(@) { my ($hash, $ramp, $flags) = @_; my $name = $hash->{NAME}; Log3 ($hash, 4, "$hash->{NAME}_RGB_Off: RGB slot $hash->{SLOT} set off $ramp"); # Store value of brightness before turning off # "on" will be of the form "on 50" where 50 is current dimlevel if (ReadingsVal($hash->{NAME}, "state", "off") ne "off") { readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), 1); MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1); # Dim down to min brightness then send off command (avoid flicker on turn on) MilightDevice_RGB_Dim($hash, MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)), $ramp, $flags); return MilightDevice_RGB_Dim($hash, 0, 0, 'qP'); } else { # If we are already off just send the off command again return MilightDevice_RGB_Dim($hash, 0, 0, 'P'); } } ##################################### sub MilightDevice_RGB_Dim(@) { my ($hash, $level, $ramp, $flags) = @_; my $h = ReadingsVal($hash->{NAME}, "hue", 0); my $s = ReadingsVal($hash->{NAME}, "saturation", 0); Log3 ($hash, 4, "$hash->{NAME}_RGB_Dim: RGB slot $hash->{SLOT} dim $level $ramp $flags"); return MilightDevice_HSV_Transition($hash, $h, $s, $level, $ramp, $flags); } ##################################### sub MilightDevice_RGB_SetHSV(@) { my ($hash, $hue, $sat, $val, $repeat) = @_; Log3 ($hash, 4, "$hash->{NAME}_RGB_setHSV: RGB slot $hash->{SLOT} set h:$hue, s:$sat, v:$val"); $sat = 100; MilightDevice_SetHSV_Readings($hash, $hue, $sat, $val); # apply gamma correction my $gammaVal = $hash->{helper}->{GAMMAMAP}[$val]; # convert to device specs my ($cv, $cl, $wl) = MilightDevice_RGB_ColorConverter($hash, $hue, $sat, $gammaVal); Log3 ($hash, 4, "$hash->{NAME}_RGB_setHSV: RGB slot $hash->{SLOT} set levels: $cv, $cl, $wl"); $repeat = 1 if (!defined($repeat)); # On first load, colorLevel won't be defined, define it. $hash->{helper}->{colorLevel} = $cl if (!defined($hash->{helper}->{colorLevel})); # NOTE: All commands sent twice for reliability (it's udp with no feedback) if (($wl < 1) && ($cl < 1)) # off { # if no white or colour switch off IOWrite($hash, "\x21\x00\x55"); # switch off $hash->{helper}->{colorLevel} = 0; } else # on { if (($wl > 0) || ($cl > 0)) # Colour/White on { IOWrite($hash, "\x22\x00\x55"); # switch on IOWrite($hash, "\x20".chr($cv)."\x55"); # set color if ($repeat eq 1) { IOWrite($hash, "\x22\x00\x55"); # switch on IOWrite($hash, "\x20".chr($cv)."\x55"); # set color } # cl decrease if ($hash->{helper}->{colorLevel} > $cl) { for (my $i=$hash->{helper}->{colorLevel}; $i > $cl; $i--) { IOWrite($hash, "\x24\x00\x55"); # brightness down $hash->{helper}->{colorLevel} = $i - 1; } } # cl increase if ($hash->{helper}->{colorLevel} < $cl) { for (my $i=$hash->{helper}->{colorLevel}; $i < $cl; $i++) { IOWrite($hash, "\x23\x00\x55"); # brightness up $hash->{helper}->{colorLevel} = $i + 1; } } } } return undef; } ##################################### sub MilightDevice_RGB_ColorConverter(@) { my ($hash, $h, $s, $v) = @_; my $color = $hash->{helper}->{COLORMAP}[$h % 360]; # there are 0..9 dim level, setup correction my $valueSpread = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)); my $totalVal = MilightDevice_roundfunc($v / $valueSpread); # saturation 100..50: color full, white increase. 50..0 white full, color decrease my $colorVal = ($s >= 50) ? $totalVal : int(($s / 50 * $totalVal) +0.5); my $whiteVal = ($s >= 50) ? int(((100-$s) / 50 * $totalVal) +0.5) : $totalVal; return ($color, $colorVal, $whiteVal); } ############################################################################### # RGBW device specific: Bridge V3+ only. # Available as GU10, E14, E27, B22, led strip controller... ############################################################################### sub MilightDevice_RGBW_Pair(@) { my ($hash, $numSeconds) = @_; $numSeconds = 3 if (($numSeconds || 0) == 0); Log3 ($hash, 4, "$hash->{NAME}_RGBW_Pair: $hash->{LEDTYPE} at $hash->{CONNECTION}, slot $hash->{SLOT}: pair $numSeconds"); # find my slot and get my group-all-on cmd my $ctrl = @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd; # Send on command once a second for (my $i = 0; $i < $numSeconds; $i++) { MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 1000, undef); } return undef; } ##################################### sub MilightDevice_RGBW_UnPair(@) { my ($hash, $numSeconds, $releaseFromSlot) = @_; $numSeconds = 3 if (($numSeconds || 0) == 0); Log3 ($hash, 4, "$hash->{NAME}_RGBW_UnPair: $hash->{LEDTYPE} at $hash->{CONNECTION}, slot $hash->{SLOT}: unpair $numSeconds"); # find my slot and get my group-all-on cmd my $ctrl = @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd; # Send on command every 200ms for (my $i = 0; $i < $numSeconds; $i++) { MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); } return undef; } ##################################### sub MilightDevice_RGBW_On(@) { my ($hash, $ramp, $flags) = @_; my $name = $hash->{NAME}; my $v = AttrVal($hash->{NAME}, "defaultBrightness", 36); Log3 ($hash, 4, "$hash->{NAME}_RGBW_On: Set ON; Ramp: $ramp"); # Switch on with same brightness it was switched off with, or max if undefined. if (ReadingsVal($hash->{NAME}, "state", "off") eq "off" || ReadingsVal($hash->{NAME}, "state", "off") eq "night") { $v = ReadingsVal($hash->{NAME}, "brightness_on", AttrVal($hash->{NAME}, "defaultBrightness", 36)); } else { $v = ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)); } # When turning on, make sure we request at least minimum dim step. if ($v < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash))) { $v = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)); } return MilightDevice_RGBW_Dim($hash, $v, $ramp, $flags); } ##################################### sub MilightDevice_RGBW_Off(@) { my ($hash, $ramp, $flags) = @_; my $name = $hash->{NAME}; Log3 ($hash, 4, "$hash->{NAME}_RGBW_Off: Set OFF; Ramp: $ramp"); # Store value of brightness before turning off # "on" will be of the form "on 50" where 50 is current dimlevel if (ReadingsVal($hash->{NAME}, "state", "off") ne "off" && ReadingsVal($hash->{NAME}, "state", "off") ne "night") { readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", 0), 1); MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1); # Dim down to min brightness then send off command (avoid flicker on turn on) MilightDevice_RGBW_Dim($hash, MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)), $ramp, $flags); return MilightDevice_RGBW_Dim($hash, 0, 0, 'qP'); } else { # If we are already off just send the off command again return MilightDevice_RGBW_Dim($hash, 0, 0, 'P'); } } ##################################### sub MilightDevice_RGBW_Night(@) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 ($hash, 4, "$hash->{NAME}_RGBW_Night: Set NIGHTMODE"); if(ReadingsVal($hash->{NAME}, "state", "off") ne "night") { if (ReadingsVal($hash->{NAME}, "brightness", 0) > 0) { readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", 4), 1); MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1); } IOWrite($hash, @RGBWCmdsOff[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd); # off } IOWrite($hash, @RGBWCmdsNt[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd); # night readingsSingleUpdate($hash, "state", "night", 1); MilightDevice_BridgeDevices_Update($hash, "state") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1); return undef; } ##################################### sub MilightDevice_RGBW_Dim(@) { my ($hash, $v, $ramp, $flags) = @_; my $h = ReadingsVal($hash->{NAME}, "hue", 0); my $s = ReadingsVal($hash->{NAME}, "saturation", 0); Log3 ($hash, 4, "$hash->{NAME}_RGBW_Dim: Brightness: $v; Ramp: $ramp; Flags: ". $flags || ''); return MilightDevice_HSV_Transition($hash, $h, $s, $v, $ramp, $flags); } ##################################### sub MilightDevice_RGBW_SetHSV(@) { my ($hash, $hue, $sat, $val, $repeat) = @_; my ($cl, $wl); $repeat = 1 if (!defined($repeat)); my $cv = $hash->{helper}->{COLORMAP}[$hue % 360]; #check dim levels to decide wether to change color or brightness first my $dimup = 0; $dimup = 1 if($val > ReadingsVal($hash->{NAME}, "brightness", 100)); # apply gamma correction my $gammaVal = $hash->{helper}->{GAMMAMAP}[$val]; # brightness 2..27 (x02..x1b) | 25 dim levels my $cf = MilightDevice_roundfunc((($gammaVal / 100) * MilightDevice_DimSteps($hash)) + 1); if ($sat < 20) { $wl = $cf; $cl = 0; $sat = 0; } else { $cl = $cf; $wl = 0; $sat = 100; } Log3 ($hash, 5, "MilightDevice_RGBW_SetHSV: h:$hue s:$sat v:$val / cv:$cv wl:$wl cl:$cl "); # Set readings in FHEM MilightDevice_SetHSV_Readings($hash, $hue, $sat, $val); # NOTE: All commands sent twice for reliability (it's udp with no feedback) # Off is shifted to "2" above so check for < 2 if (($wl < 2) && ($cl < 2)) # off { IOWrite($hash, @RGBWCmdsOff[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd); # group off IOWrite($hash, @RGBWCmdsOff[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if ($repeat eq 1); # group off $hash->{helper}->{whiteLevel} = 0; $hash->{helper}->{colorLevel} = 0; } else # on { if ($wl > 0) # white { IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($wl > 0) || ($cl > 0)); # group on IOWrite($hash, @RGBWCmdsWT[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd); # white IOWrite($hash, $RGBWCmdBri.chr($wl).$RGBWCmdEnd); # brightness if ($repeat eq 1) { IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($wl > 0) || ($cl > 0)); # group on IOWrite($hash, @RGBWCmdsWT[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd); # white IOWrite($hash, $RGBWCmdBri.chr($wl).$RGBWCmdEnd); # brightness } } elsif ($cl > 0) # color { IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($wl > 0) || ($cl > 0)); # group on if($dimup) { IOWrite($hash, $RGBWCmdCol.chr($cv).$RGBWCmdEnd); # color IOWrite($hash, $RGBWCmdBri.chr($cl).$RGBWCmdEnd); # brightness } else { IOWrite($hash, $RGBWCmdBri.chr($cl).$RGBWCmdEnd); # brightness IOWrite($hash, $RGBWCmdCol.chr($cv).$RGBWCmdEnd); # color } if ($repeat eq 1) { IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($wl > 0) || ($cl > 0)); # group on if($dimup) { IOWrite($hash, $RGBWCmdCol.chr($cv).$RGBWCmdEnd); # color IOWrite($hash, $RGBWCmdBri.chr($cl).$RGBWCmdEnd); # brightness } else { IOWrite($hash, $RGBWCmdBri.chr($cl).$RGBWCmdEnd); # brightness IOWrite($hash, $RGBWCmdCol.chr($cv).$RGBWCmdEnd); # color } } } $hash->{helper}->{colorValue} = $cv; $hash->{helper}->{colorLevel} = $cl; $hash->{helper}->{whiteLevel} = $wl; } return undef; } #################################### # RGB and RGBW types sub MilightDevice_RGBW_DiscoModeStep(@) { my ($hash, $step) = @_; MilightDevice_CmdQueue_Clear($hash); $step = 0 if ($step < 0); $step = 1 if ($step > 1); # Set readings in FHEM MilightDevice_SetDisco_Readings($hash, $step, ReadingsVal($hash->{NAME}, 'discoSpeed', 5)); # NOTE: Only sending commands once, because it makes changes on each successive command IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($hash->{LEDTYPE} eq 'RGBW')); # group on IOWrite($hash, "\x22\x00\x55") if (($hash->{LEDTYPE} eq 'RGB')); # switch on if ($step == 1) { IOWrite($hash, $RGBWCmdDiscoUp."\x00".$RGBWCmdEnd) if (($hash->{LEDTYPE} eq 'RGBW')); # discoMode step up IOWrite($hash, "\x27\x00\x55") if (($hash->{LEDTYPE} eq 'RGB')); # discoMode step up } elsif ($step == 0) { IOWrite($hash, "\x28\x00\x55") if (($hash->{LEDTYPE} eq 'RGB')); # discoMode step down # There is no discoMode step down for RGBW } return undef; } ##################################### # RGB and RGBW types sub MilightDevice_RGBW_DiscoModeSpeed(@) { my ($hash, $speed) = @_; MilightDevice_CmdQueue_Clear($hash); $speed = 0 if ($speed < 0); $speed = 1 if ($speed > 1); # Set readings in FHEM MilightDevice_SetDisco_Readings($hash, ReadingsVal($hash->{NAME}, 'discoMode', 1), $speed); # NOTE: Only sending commands once, because it makes changes on each successive command IOWrite($hash, @RGBWCmdsOn[$hash->{SLOTID} -5]."\x00".$RGBWCmdEnd) if (($hash->{LEDTYPE} eq 'RGBW')); # group on IOWrite($hash, "\x22\x00\x55") if (($hash->{LEDTYPE} eq 'RGB')); # switch on if ($speed == 1) { IOWrite($hash, $RGBWCmdDiscoInc."\x00".$RGBWCmdEnd) if ($hash->{LEDTYPE} eq 'RGBW'); # discoMode speed up IOWrite($hash, "\x25\x00\x55") if ($hash->{LEDTYPE} eq 'RGB'); # discoMode speed up } elsif ($speed == 0) { IOWrite($hash, $RGBWCmdDiscoDec."\x00".$RGBWCmdEnd) if ($hash->{LEDTYPE} eq 'RGBW'); # discoMode speed down IOWrite($hash, "\x26\x00\x55") if ($hash->{LEDTYPE} eq 'RGB'); # discoMode speed down } return undef; } ############################################################################### # White device specific: Warm/Cold White with Dim - Bridge V2+ ############################################################################### sub MilightDevice_White_Pair(@) { my ($hash, $numSeconds) = @_; $numSeconds = 3 if (($numSeconds || 0) == 0); Log3 ($hash, 4, "$hash->{NAME}_White_Pair: $hash->{LEDTYPE} at $hash->{CONNECTION}, slot $hash->{SLOT}: pair $numSeconds"); # find my slot and get my group-all-on cmd my $ctrl = @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd; # Send on command once a second for (my $i = 0; $i < $numSeconds; $i++) { MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 1000, undef); } return undef; } ##################################### sub MilightDevice_White_UnPair(@) { my ($hash, $numSeconds, $releaseFromSlot) = @_; $numSeconds = 3 if (($numSeconds || 0) == 0); Log3 ($hash, 4, "$hash->{NAME}_White_UnPair: $hash->{LEDTYPE} at $hash->{CONNECTION}, slot $hash->{SLOT}: unpair $numSeconds"); # find my slot and get my group-all-on cmd my $ctrl = @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd; for (my $i = 0; $i < $numSeconds; $i++) { MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); } return undef; } ##################################### sub MilightDevice_White_On(@) { my ($hash, $ramp, $flags) = @_; my $name = $hash->{NAME}; my $v = AttrVal($hash->{NAME}, "defaultBrightness", 36); Log3 ($hash, 4, "$hash->{NAME}_White_On: Set ON: Ramp: $ramp"); # Switch on with same brightness it was switched off with, or max if undefined. if (ReadingsVal($hash->{NAME}, "state", "off") eq "off" || ReadingsVal($hash->{NAME}, "state", "off") eq "night") { $v = ReadingsVal($hash->{NAME}, "brightness_on", AttrVal($hash->{NAME}, "defaultBrightness", 36)); } else { $v = ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)); } # When turning on, make sure we request at least minimum dim step. if ($v < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash))) { $v = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)); } return MilightDevice_White_Dim($hash, $v, $ramp, $flags); } ##################################### sub MilightDevice_White_Off(@) { my ($hash, $ramp, $flags) = @_; my $name = $hash->{NAME}; Log3 ($hash, 4, "$hash->{NAME}_White_Off: Set OFF; Ramp: $ramp"); # Store value of brightness before turning off # "on" will be of the form "on 50" where 50 is current dimlevel if (ReadingsVal($hash->{NAME}, "state", "off") ne "off" && ReadingsVal($hash->{NAME}, "state", "off") ne "night") { if (ReadingsVal($hash->{NAME}, "brightness", 0) > 0) { readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), 1); MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1); } # Dim down to min brightness then send off command (avoid flicker on turn on) MilightDevice_White_Dim($hash, MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)), $ramp, $flags); return MilightDevice_White_Dim($hash, 0, 0, 'q'); } else { # If we are already off just send the off command again return MilightDevice_White_Dim($hash, 0, 0, 'P'); } } ##################################### sub MilightDevice_White_DimOff(@) { my ($hash, $ramp, $flags) = @_; my $name = $hash->{NAME}; Log3 ($hash, 4, "$hash->{NAME}_White_DimOff: Set OFF; Ramp: $ramp"); if (ReadingsVal($hash->{NAME}, "brightness", 0) > 0) { readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), 1); MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1); } for (my $i = 0; $i < 12; $i++) { IOWrite($hash, @WhiteCmdBriDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); } IOWrite($hash, @WhiteCmdsOff[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); return MilightDevice_White_Dim($hash, 0, 0, 'q'); } ##################################### sub MilightDevice_White_DimOn(@) { my ($hash, $ramp, $flags) = @_; my $name = $hash->{NAME}; Log3 ($hash, 4, "$hash->{NAME}_White_DimOn: Set ON; Ramp: $ramp"); my $v = AttrVal($hash->{NAME}, "defaultBrightness", 36); if (ReadingsVal($hash->{NAME}, "state", "off") eq "off" || ReadingsVal($hash->{NAME}, "state", "off") eq "night") { $v = ReadingsVal($hash->{NAME}, "brightness_on", AttrVal($hash->{NAME}, "defaultBrightness", 36)); } else { $v = ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)); } # When turning on, make sure we request at least minimum dim step. if ($v < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash))) { $v = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)); } MilightDevice_White_Dim($hash, $v, $ramp, $flags); for (my $i = 0; $i < ($v/(100/MilightDevice_DimSteps($hash))); $i++) { #IOWrite($hash, @WhiteCmdBriUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); } #$ctrl = @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd; #MilightDevice_CmdQueue_Add($hash, undef, undef, undef, $ctrl, 200, undef); #return MilightDevice_White_Dim($hash, 0, 0, 'q'); } ##################################### sub MilightDevice_White_Night(@) { my ($hash) = @_; my $name = $hash->{NAME}; Log3 ($hash, 4, "$hash->{NAME}_White_NIGHT: Set NIGHTMODE"); if(ReadingsVal($hash->{NAME}, "state", "off") ne "night") { if (ReadingsVal($hash->{NAME}, "brightness", 0) > 0) { readingsSingleUpdate($hash, "brightness_on", ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36)), 1); MilightDevice_BridgeDevices_Update($hash, "brightness_on") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1); } IOWrite($hash, @WhiteCmdsOff[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # off } IOWrite($hash, @WhiteCmdsNt[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # night readingsSingleUpdate($hash, "state", "night", 1); MilightDevice_BridgeDevices_Update($hash, "state") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1); return undef; } ##################################### sub MilightDevice_White_Dim(@) { my ($hash, $level, $ramp, $flags) = @_; Log3 ($hash, 4, "$hash->{NAME}_White_Dim: Brightness: $level; Ramp: $ramp; Flags: $flags"); return MilightDevice_HSV_Transition($hash, ReadingsVal($hash->{NAME}, "ct", 3000), 0, $level, $ramp, $flags); } ##################################### # $hue is colourTemperature, $val is brightness sub MilightDevice_White_SetHSV(@) { my ($hash, $hue, $sat, $val, $repeat) = @_; my $name = $hash->{NAME}; $repeat = 1 if (!defined($repeat)); # Validate brightness $val = 100 if ($val > 100); $val = 0 if ($val < 0); # Validate colour temperature $hue = 6500 if ($hue > 6500); $hue = 3000 if ($hue < 3000); my $oldHueStep = MilightDevice_White_ct_hwValue($hash, ReadingsVal($hash->{NAME}, "ct", 6500)); my $newHueStep = MilightDevice_White_ct_hwValue($hash, $hue); $hue = MilightDevice_White_ct_hwValue($hash, $newHueStep); # Set colour temperature if ($oldHueStep != $newHueStep) { IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on if ($oldHueStep > $newHueStep) { Log3 ($hash, 4, "$hash->{NAME}_setColourTemp: Decrease from $oldHueStep to $newHueStep"); for (my $i=$oldHueStep; $i > $newHueStep; $i--) { IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # Cooler (colourtemp up) } } elsif ($oldHueStep < $newHueStep) { Log3 ($hash, 4, "$hash->{NAME}_setColourTemp: Increase from $oldHueStep to $newHueStep"); for (my $i=$oldHueStep; $i < $newHueStep; $i++) { IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # Warmer (colourtemp down) } } if(AttrVal($hash->{NAME}, "dimOffWhite", 0) == 1) { if($newHueStep == 1) { IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); } elsif($newHueStep == 11) { IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); } } } # apply gamma correction my $gammaVal = $hash->{helper}->{GAMMAMAP}[$val]; # Calculate brightness hardware value (10 steps / 11 positions for white) my $maxWl = (100 / MilightDevice_DimSteps($hash)); my $wl = MilightDevice_roundfunc($gammaVal / $maxWl); # On first load, whiteLevel won't be defined, define it. $hash->{helper}->{whiteLevel} = $wl if (!defined($hash->{helper}->{whiteLevel})); if (ReadingsVal($hash, "brightness", 0) > 0) { # We are transitioning from on to off so store new value of wl and stop brightness up/down being triggered below $hash->{helper}->{whiteLevel} = $wl; } # Store new values for colourTemperature and Brightness MilightDevice_SetHSV_Readings($hash, $hue, 0, $val); # Make sure we actually send off command if we should be off if ($wl == 0) { IOWrite($hash, @WhiteCmdsOff[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group off IOWrite($hash, @WhiteCmdsOff[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd) if ($repeat eq 1); # group off Log3 ($hash, 4, "$hash->{NAME}_White_setHSV: OFF"); } elsif ($wl == $maxWl) { IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on IOWrite($hash, @WhiteCmdsOnFull[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on full if ($repeat eq 1) { IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on IOWrite($hash, @WhiteCmdsOnFull[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on full } Log3 ($hash, 4, "$hash->{NAME}_White_setHSV: Full Brightness"); } else { # Not off or MAX brightness, so make sure we are on IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd) if ($repeat eq 1); # group on if ($hash->{helper}->{whiteLevel} > $wl) { # Brightness level should be decreased Log3 ($hash, 4, "$hash->{NAME}_White_setHSV: Brightness decrease from $hash->{helper}->{whiteLevel} to $wl"); for (my $i=$hash->{helper}->{whiteLevel}; $i > $wl; $i--) { IOWrite($hash, @WhiteCmdBriDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # brightness down $hash->{helper}->{whiteLevel} = $i - 1; } } elsif ($hash->{helper}->{whiteLevel} < $wl) { # Brightness level should be increased $hash->{helper}->{whiteLevel} = 1 if ($hash->{helper}->{whiteLevel} == 0); Log3 ($hash, 4, "$hash->{NAME}_White_setHSV: Brightness increase from $hash->{helper}->{whiteLevel} to $wl"); for (my $i=$hash->{helper}->{whiteLevel}; $i < $wl; $i++) { IOWrite($hash, @WhiteCmdBriUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # brightness up $hash->{helper}->{whiteLevel} = $i + 1; } } else { Log3 ($hash, 4, "$hash->{NAME}_White_setHSV: ON"); } } $hash->{helper}->{whiteLevel} = $wl; return undef; } ##################################### sub MilightDevice_White_SetColourTemp(@) { # $hue is colourTemperature (1-11), $val is brightness (0-100%) my ($hash, $hue) = @_; my $name = $hash->{NAME}; MilightDevice_CmdQueue_Clear($hash); # Save old value of ct my $oldHue = MilightDevice_White_ct_hwValue($hash, ReadingsVal($hash->{NAME}, "ct", 6500)); # Store new values for colourTemperature and Brightness MilightDevice_SetHSV_Readings($hash, $hue, 0, ReadingsVal($hash->{NAME}, "brightness", AttrVal($hash->{NAME}, "defaultBrightness", 36) ) ); # Validate colourTemperature (11 steps) # 3000-6500 (350 per step) Warm-White to Cool White # Maps backwards 1=6500 11=3000 $hue = MilightDevice_White_ct_hwValue($hash, $hue); Log3 ($hash, 4, "$hash->{NAME}_setColourTemp: $oldHue to $hue"); # Set colour temperature if ($oldHue != $hue) { IOWrite($hash, @WhiteCmdsOn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # group on if ($oldHue > $hue) { Log3 ($hash, 4, "$hash->{NAME}_setColourTemp: Decrease from $oldHue to $hue"); for (my $i=$oldHue; $i > $hue; $i--) { IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # Cooler (colourtemp up) } } elsif ($oldHue < $hue) { Log3 ($hash, 4, "$hash->{NAME}_setColourTemp: Increase from $oldHue to $hue"); for (my $i=$oldHue; $i < $hue; $i++) { IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); # Warmer (colourtemp down) } } } if(AttrVal($hash->{NAME}, "dimOffWhite", 0) == 1) { if($hue == 1) { IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColDn[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); } elsif($hue == 11) { IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); IOWrite($hash, @WhiteCmdColUp[$hash->{SLOTID} -1]."\x00".$WhiteCmdEnd); } } return undef; } # Convert from 3000-6500 colourtemperature to hardware value sub MilightDevice_White_ct_hwValue(@) { my ($hash, $ct) = @_; # Couldn't get switch statement to work so using if if ($ct == 11) { return 3000; } elsif ($ct == 10) { return 3350; } elsif ($ct == 9) { return 3700; } elsif ($ct == 8) { return 4050; } elsif ($ct == 7) { return 4400; } elsif ($ct == 6) { return 4750; } elsif ($ct == 5) { return 5100; } elsif ($ct == 4) { return 5450; } elsif ($ct == 3) { return 5800; } elsif ($ct == 2) { return 6150; } elsif ($ct == 1) { return 6500; } elsif ($ct < 3350) { return 11; } elsif ($ct < 3700) { return 10; } elsif ($ct < 4050) { return 9; } elsif ($ct < 4400) { return 8; } elsif ($ct < 4750) { return 7; } elsif ($ct < 5100) { return 6; } elsif ($ct < 5450) { return 5; } elsif ($ct < 5800) { return 4; } elsif ($ct < 6150) { return 3; } elsif ($ct < 6500) { return 2; } return 1; } ############################################################################### # Device independent routines ############################################################################### sub MilightDevice_HSVFromStr(@) { # Convert HSV values from string in format "h,s,v" my ($hash, @args) = @_; if ((!defined($args[0])) || ($args[0] !~ /^(\d{1,4}),(\d{1,3}),(\d{1,3})$/)) { Log3 ($hash, 3, "MilightDevice_HSVFromStr: Could not parse h,s,v values from $args[0]"); return (0, 0, 0); } Log3 ($hash, 5, "MilightDevice_HSVFromStr: Parsed hsv string: h:$1,s:$2,v:$3"); return ($1, $2, $3); } ##################################### sub MilightDevice_HSVToStr(@) { # Convert HSV values to string in format "h,s,v" my ($hash, $h, $s, $v) = @_; $h=0 if (!defined($h)); $s=0 if (!defined($s)); $v=0 if (!defined($v)); Log3 ($hash, 5, "MilightDevice_HSVToStr: h:$h,s:$s,v:$v"); return "$h,$s,$v"; } ##################################### sub MilightDevice_ValidateHSV(@) { # Validate and return valid values for HSV my ($hash, $h, $s, $v) = @_; $h = 0 if ($h < 0); $h = 360 if ($h > 360 && $hash->{LEDTYPE} ne 'White'); $h = 3000 if ($h < 3000 && $hash->{LEDTYPE} eq 'White'); $h = 6500 if ($h > 6500); $s = 0 if ($s < 0); $s = 100 if ($s > 100); $v = 0 if ($v < 0); $v = 100 if ($v > 100); return ($h, $s, $v); } ##################################### # Return number of steps for each type of bulb # White: 11 steps (step = 9.1) # RGB: 9 steps (step = 11) # RGBW: 25 steps (step = 4) sub MilightDevice_DimSteps(@) { my ($hash) = @_; return AttrVal($hash->{NAME}, "dimStep", 25) if ($hash->{LEDTYPE} eq 'RGBW'); return AttrVal($hash->{NAME}, "dimStep", 11) if ($hash->{LEDTYPE} eq 'White'); return AttrVal($hash->{NAME}, "dimStep", 9) if ($hash->{LEDTYPE} eq 'RGB'); } ##################################### # Return number of colour steps for each type of bulb # White: 11 steps (this is colour temperature) # RGB: 255 steps (not mentioned in API?) # RGBW: 255 steps sub MilightDevice_ColourSteps(@) { my ($hash) = @_; return 255 if ($hash->{LEDTYPE} eq 'RGBW'); return 11 if ($hash->{LEDTYPE} eq 'White'); return 255 if ($hash->{LEDTYPE} eq 'RGB'); } ##################################### # dispatcher sub MilightDevice_SetHSV(@) { my ($hash, $hue, $sat, $val, $repeat) = @_; MilightDevice_RGBW_SetHSV($hash, $hue, $sat, $val, $repeat) if ($hash->{LEDTYPE} eq 'RGBW'); MilightDevice_White_SetHSV($hash, $hue, $sat, $val, $repeat) if ($hash->{LEDTYPE} eq 'White'); MilightDevice_RGB_SetHSV($hash, $hue, $sat, $val, $repeat) if ($hash->{LEDTYPE} eq 'RGB'); return undef; } ##################################### sub MilightDevice_HSV_Transition(@) { my ($hash, $hue, $sat, $val, $ramp, $flags) = @_; my ($hueFrom, $satFrom, $valFrom, $timeFrom)=0; # Clear command queue if flag "q" not specified MilightDevice_CmdQueue_Clear($hash) if ($flags !~ m/.*[qQ].*/); # if queue in progress set start vals to last cached hsv target, else set start to actual hsv if (@{$hash->{helper}->{cmdQueue}} > 0) { $hueFrom = $hash->{helper}->{targetHue}; $satFrom = $hash->{helper}->{targetSat}; $valFrom = $hash->{helper}->{targetVal}; $timeFrom = $hash->{helper}->{targetTime}; $hueFrom = 0 if(!defined($hueFrom)); $satFrom = 100 if(!defined($satFrom)); $valFrom = 0 if(!defined($valFrom)); $timeFrom = 0 if(!defined($timeFrom)); Log3 ($hash, 5, "$hash->{NAME}_HSV_Transition: Prepare Start (cached): $hueFrom,$satFrom,$valFrom@".$timeFrom); } else { $hueFrom = ReadingsVal($hash->{NAME}, "hue", 0); $satFrom = ReadingsVal($hash->{NAME}, "saturation", 0); $valFrom = ReadingsVal($hash->{NAME}, "brightness", 0); $timeFrom = gettimeofday(); Log3 ($hash, 5, "$hash->{NAME}_HSV_Transition: Prepare Start (actual): $hueFrom,$satFrom,$valFrom@".$timeFrom); if ($flags !~ m/.*[pP].*/ and ($hash->{LEDTYPE} eq 'RGB') || ($hash->{LEDTYPE} eq 'RGBW')) { # Store previous state if different to requested state if (($hueFrom != $hue) || ($satFrom != $sat) || ($valFrom != $val)) { readingsSingleUpdate($hash, "previousState", MilightDevice_HSVToStr($hash, $hueFrom, $satFrom, $valFrom),1); } } } Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Current: $hueFrom,$satFrom,$valFrom"); Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Set: $hue,$sat,$val; Ramp: $ramp; Flags: ". $flags); # Store target vales $hash->{helper}->{targetHue} = $hue; $hash->{helper}->{targetSat} = $sat; $hash->{helper}->{targetVal} = $val; # if there is no ramp we don't need transition if (($ramp || 0) == 0) { Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Set: $hue,$sat,$val; No Ramp"); $hash->{helper}->{targetTime} = $timeFrom; return MilightDevice_CmdQueue_Add($hash, $hue, $sat, $val, undef, 0, undef); } # calculate the left and right turn length based # startAngle +360 -endAngle % 360 = counter clock # endAngle +360 -startAngle % 360 = clockwise my $hueTo = ($hue == 0) ? 1 : ($hue == 360) ? 359 : $hue; my $fadeLeft = ($hueFrom + 360 - $hue) % 360; my $fadeRight = ($hue + 360 - $hueFrom) % 360; my $direction = ($fadeLeft <=> $fadeRight); # -1 = counterclock, +1 = clockwise $direction = ($direction == 0)?1:$direction; # in dupt cw Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Colour rotation: cc(-1): $fadeLeft, cw(+1): $fadeRight; Shortest: $direction;"); $direction *= -1 if ($flags =~ m/.*[lL].*/); # reverse if long path desired (flag l or L is set) my $rotation = ($direction == 1)?$fadeRight:$fadeLeft; # angle of hue rotation in based on flags my $sFade = abs($sat - $satFrom); my $vFade = abs($val - $valFrom); # No transition, so set immediately and ignore ramp setting if ($rotation == 0 && $sFade == 0 && $vFade == 0) { Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Unchanged. Set: $hue,$sat,$val; Ignoring Ramp"); $hash->{helper}->{targetTime} = $timeFrom; return MilightDevice_CmdQueue_Add($hash, $hue, $sat, $val, undef, 0, undef); } my ($stepWidth, $steps, $maxSteps, $hueToSet, $hueStep, $satToSet, $satStep, $valToSet, $valStep); # Calculate stepWidth if ($rotation >= ($sFade || $vFade)) { # Transition based on Hue, so max steps = colourSteps $stepWidth = ($ramp * 1000 / $rotation); # how long is one step (set hsv) in ms based on hue $maxSteps = MilightDevice_ColourSteps($hash); } elsif ($sFade >= ($rotation || $vFade)) { # Transition based on Saturation, so max steps = 2 (devices don't support sat, so set to 0 or 100 mostly) $stepWidth = ($ramp * 1000 / $sFade); # how long is one step (set hsv) in ms based on sat $maxSteps = 2; } else { # Transition based on Brightness, so max steps = dimSteps $stepWidth = ($ramp * 1000 / $vFade); # how long is one step (set hsv) in ms based on val $maxSteps = MilightDevice_DimSteps($hash); } # Calculate number of steps, limit to max number (no point running more if they are the same) $steps = int($ramp * 1000 / $stepWidth); if ($steps > $maxSteps) { $stepWidth *= ($steps/$maxSteps); $steps = $maxSteps; } # Calculate number of steps, limit to max number (no point running more if they are the same) $steps = int($ramp * 1000 / $stepWidth); if ($steps > $maxSteps) { $stepWidth *= ($steps/$maxSteps); $steps = $maxSteps; } # Calculate minimum stepWidth # Min bridge delay as specified by Bridge * 3 (eg. 100*3=300ms). # On average min 3 commands need to be sent per step (eg. Group On; Mode; Brightness;) so this gets it approximately right my $minStepWidth = $hash->{IODev}->{INTERVAL} * 3; $stepWidth = $minStepWidth if ($stepWidth < $minStepWidth); # Make sure we have min stepWidth Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Steps: $steps; Step Interval(ms): $stepWidth"); # Calculate hue step $hueToSet = $hueFrom; # Start at current hue $hueStep = $rotation / $steps * $direction; # Calculate saturation step $satToSet = $satFrom; # Start at current saturation $satStep = ($sat - $satFrom) / $steps; # Calculate brightness step $valToSet = $valFrom; # Start at current brightness $valStep = ($val - $valFrom) / $steps; for (my $i=1; $i <= $steps; $i++) { $hueToSet += $hueStep; # Increment new hue by step (negative step decrements) $hueToSet -= 360 if ($hueToSet > 360); #handle turn over zero $hueToSet += 360 if ($hueToSet < 0); $satToSet += $satStep; # Increment new saturation by step (negative step decrements) $valToSet += $valStep; # Increment new brightness by step (negative step decrements) Log3 ($hash, 4, "$hash->{NAME}_HSV_Transition: Add to Queue: h:".($hueToSet).", s:".($satToSet).", v:".($valToSet)." ($i/$steps)"); MilightDevice_CmdQueue_Add($hash, MilightDevice_roundfunc($hueToSet), MilightDevice_roundfunc($satToSet), MilightDevice_roundfunc($valToSet), undef, $stepWidth, $timeFrom + (($i-1) * $stepWidth / 1000) ); } # Set target time for completion of sequence. # This may be slightly higher than what was requested since $stepWidth > minDelay (($steps * $stepWidth) > $ramp) $hash->{helper}->{targetTime} = $timeFrom + ($steps * $stepWidth / 1000); Log3 ($hash, 5, "$hash->{NAME}_HSV_Transition: TargetTime: $hash->{helper}->{targetTime}"); return undef; } ##################################### sub MilightDevice_White_Transition(@) { my ($hash, $ct, $sat, $val, $ramp, $flags) = @_; my ($ctFrom, $valFrom, $timeFrom)=0; # Clear command queue if flag "q" not specified MilightDevice_CmdQueue_Clear($hash) if ($flags !~ m/.*[qQ].*/); # if queue in progress set start vals to last cached hsv target, else set start to actual hsv if (@{$hash->{helper}->{cmdQueue}} > 0) { $ctFrom = $hash->{helper}->{targetCt}; $valFrom = $hash->{helper}->{targetVal}; $timeFrom = $hash->{helper}->{targetTime}; $ctFrom = 3000 if(!defined($ctFrom)); $valFrom = 0 if(!defined($valFrom)); $timeFrom = 0 if(!defined($timeFrom)); Log3 ($hash, 5, "$hash->{NAME}_White_Transition: Prepare Start (cached): $ctFrom,$valFrom@".$timeFrom); } else { $ctFrom = ReadingsVal($hash->{NAME}, "ct", 3000); $valFrom = ReadingsVal($hash->{NAME}, "brightness", 0); $timeFrom = gettimeofday(); Log3 ($hash, 5, "$hash->{NAME}_White_Transition: Prepare Start (actual): $ctFrom,$valFrom@".$timeFrom); if ($flags !~ m/.*[pP].*/) { # Store previous state if different to requested state if (($ctFrom != $ct) || ($valFrom != $val)) { readingsSingleUpdate($hash, "previousState", MilightDevice_HSVToStr($hash, $ctFrom, 0, $valFrom),1); } } } Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Current: $ctFrom,$valFrom"); Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Set: $ct,$val; Ramp: $ramp; Flags: ". $flags); # Store target vales $hash->{helper}->{targetCt} = $ct; $hash->{helper}->{targetVal} = $val; # if there is no ramp we don't need transition if (($ramp || 0) == 0) { Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Set: $ct,$val; No Ramp"); $hash->{helper}->{targetTime} = $timeFrom; return MilightDevice_CmdQueue_Add($hash, $ct, 0, $val, undef, 0, undef); } my $vFade = abs($val - $valFrom); my $ctFade = abs($ct - $ctFrom); Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Colour temp: $ctFade, Brightness: $vFade;"); # No transition, so set immediately and ignore ramp setting if ($ctFade == 0 && $vFade == 0) { Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Unchanged. Set: $ct,0,$val; Ignoring Ramp"); $hash->{helper}->{targetTime} = $timeFrom; return MilightDevice_CmdQueue_Add($hash, $ct, 0, $val, undef, 0, undef); } my ($stepWidth, $steps, $maxSteps, $ctToSet, $ctStep, $valToSet, $valStep); # Calculate stepWidth if ($ctFade >= $vFade) { # Transition based on ct, so max steps = colourSteps $stepWidth = ($ramp * 1000 / $ctFade /100); # how long is one step (set hsv) in ms based on ct $maxSteps = MilightDevice_ColourSteps($hash); } else { # Transition based on Brightness, so max steps = dimSteps $stepWidth = ($ramp * 1000 / $vFade); # how long is one step (set hsv) in ms based on val $maxSteps = MilightDevice_DimSteps($hash); } # Calculate number of steps, limit to max number (no point running more if they are the same) $steps = int($ramp * 1000 / $stepWidth); if ($steps > $maxSteps) { $stepWidth *= ($steps/$maxSteps); $steps = $maxSteps; } # Calculate number of steps, limit to max number (no point running more if they are the same) $steps = int($ramp * 1000 / $stepWidth); if ($steps > $maxSteps) { $stepWidth *= ($steps/$maxSteps); $steps = $maxSteps; } # Calculate minimum stepWidth # Min bridge delay as specified by Bridge * 3 (eg. 100*3=300ms). # On average min 3 commands need to be sent per step (eg. Group On; Mode; Brightness;) so this gets it approximately right my $minStepWidth = $hash->{IODev}->{INTERVAL} * 3; $stepWidth = $minStepWidth if ($stepWidth < $minStepWidth); # Make sure we have min stepWidth Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Steps: $steps; Step Interval(ms): $stepWidth"); # Calculate hue step $ctToSet = $ctFrom; # Start at current hue $ctStep = ($ct - $ctFrom) / $steps; # Calculate brightness step $valToSet = $valFrom; # Start at current brightness $valStep = ($val - $valFrom) / $steps; for (my $i=1; $i <= $steps; $i++) { $ctToSet += $ctStep; # Increment new hue by step (negative step decrements) $valToSet += $valStep; # Increment new brightness by step (negative step decrements) Log3 ($hash, 4, "$hash->{NAME}_White_Transition: Add to Queue: ct:".(int($ctToSet)).", s:0, v:".(int($valToSet))." ($i/$steps)"); MilightDevice_CmdQueue_Add($hash, MilightDevice_roundfunc($ctToSet), 0, MilightDevice_roundfunc($valToSet), undef, $stepWidth, $timeFrom + (($i-1) * $stepWidth / 1000) ); } # Set target time for completion of sequence. # This may be slightly higher than what was requested since $stepWidth > minDelay (($steps * $stepWidth) > $ramp) $hash->{helper}->{targetTime} = $timeFrom + ($steps * $stepWidth / 1000); Log3 ($hash, 5, "$hash->{NAME}_White_Transition: TargetTime: $hash->{helper}->{targetTime}"); return undef; } ##################################### sub MilightDevice_SetHSV_Readings(@) { my ($hash, $hue, $sat, $val, $val_on) = @_; my $name = $hash->{NAME}; readingsBeginUpdate($hash); # Start update readings # Store requested values readingsBulkUpdate($hash, "brightness", $val); # Store on brightness so we can turn on at a set brightness readingsBulkUpdate($hash, "brightness_on", $val_on); if (($hash->{LEDTYPE} eq 'RGB') || ($hash->{LEDTYPE} eq 'RGBW')) { # Store previous state if different to requested state my $prevHue = ReadingsVal($hash->{NAME}, "hue", 0); my $prevSat = ReadingsVal($hash->{NAME}, "saturation", 0); my $prevVal = ReadingsVal($hash->{NAME}, "brightness", 0); if (($prevHue != $hue) || ($prevSat != $sat) || ($prevVal != $val)) { readingsBulkUpdate($hash, "previousState", MilightDevice_HSVToStr($hash, $prevHue, $prevSat, $prevVal)) if ReadingsVal($hash->{NAME}, "transitionInProgress", 1) eq 0; } readingsBulkUpdate($hash, "saturation", $sat); readingsBulkUpdate($hash, "hue", $hue); readingsBulkUpdate($hash, "hsv", MilightDevice_HSVToStr($hash, $hue,$sat,$val)); # Calc RGB values from HSV my ($r,$g,$b) = Color::hsv2rgb($hue/360.0,$sat/100.0,$val/100.0); $r *=255; $g *=255; $b*=255; # Store values readingsBulkUpdate($hash, "rgb", sprintf("%02X%02X%02X",$r,$g,$b)); # Int to Hex convert readingsBulkUpdate($hash, "discoMode", 0); readingsBulkUpdate($hash, "discoSpeed", 0); } elsif ($hash->{LEDTYPE} eq 'White') { readingsBulkUpdate($hash, "ct", $hue); readingsBulkUpdate($hash, "hsv", MilightDevice_HSVToStr($hash, $hue,0,$val)); } readingsBulkUpdate($hash, "state", "on $val") if ($val > 1); readingsBulkUpdate($hash, "state", "off") if ($val < 2); readingsEndUpdate($hash, 1); MilightDevice_BridgeDevices_Update($hash, "bulk") if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1); } ##################################### sub MilightDevice_SetDisco_Readings(@) { # Step/Speed can be "1" or "0" when active my ($hash, $step, $speed) = @_; my $name = $hash->{NAME}; if (($hash->{LEDTYPE} eq 'RGBW') || ($hash->{LEDTYPE} eq 'RGB')) { my $discoMode = ReadingsVal($hash->{NAME}, "discoMode", 0); $discoMode = "on"; my $discoSpeed = ReadingsVal($hash->{NAME}, "discoSpeed", 5); $discoSpeed = "-" if ($speed == 0); $discoSpeed = "+" if ($speed == 1); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "discoMode", $step); readingsBulkUpdate($hash, "discoSpeed", $speed); readingsEndUpdate($hash, 1); if ($hash->{SLOT} eq 'A' && AttrVal($hash->{NAME}, "updateGroupDevices", 0) == 1) { MilightDevice_BridgeDevices_Update($hash, "discoMode"); MilightDevice_BridgeDevices_Update($hash, "discoSpeed"); } } } ##################################### sub MilightDevice_ColorConverter(@) { my ($hash, $cr, $cy, $cg, $cc, $cb, $cm) = @_; my @colorMap; my $adjRed = 0 + $cr; my $adjYellow = 60 + $cy; my $adjGreen = 120 + $cg; my $adjCyan = 180 + $cc; my $adjBlue = 240 + $cb; my $adjLilac = 300 + $cm; my $devRed = 176; # (0xB0) #my $devYellow = 128; # (0x80) my $devYellow = 144; my $devGreen = 96; # (0x60) #my $devCyan = 48; # (0x30) my $devCyan = 56; my $devBlue = 16; # (0x10) my $devLilac = 224; # (0xE0) my $i= 360; # red to yellow $adjRed += 360 if ($adjRed < 0); # in case of negative adjustment $devRed += 256 if ($devRed < $devYellow); $adjYellow += 360 if ($adjYellow < $adjRed); for ($i = $adjRed; $i <= $adjYellow; $i++) { $colorMap[$i % 360] = ($devRed - int((($devRed - $devYellow) / ($adjYellow - $adjRed) * ($i - $adjRed)) +0.5)) % 255; Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]); } #yellow to green $devYellow += 256 if ($devYellow < $devGreen); $adjGreen += 360 if ($adjGreen < $adjYellow); for ($i = $adjYellow; $i <= $adjGreen; $i++) { $colorMap[$i % 360] = ($devYellow - int((($devYellow - $devGreen) / ($adjGreen - $adjYellow) * ($i - $adjYellow)) +0.5)) % 255; Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]); } #green to cyan $devGreen += 256 if ($devGreen < $devCyan); $adjCyan += 360 if ($adjCyan < $adjGreen); for ($i = $adjGreen; $i <= $adjCyan; $i++) { $colorMap[$i % 360] = ($devGreen - int((($devGreen - $devCyan) / ($adjCyan - $adjGreen) * ($i - $adjGreen)) +0.5)) % 255; Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]); } #cyan to blue $devCyan += 256 if ($devCyan < $devCyan); $adjBlue += 360 if ($adjBlue < $adjCyan); for ($i = $adjCyan; $i <= $adjBlue; $i++) { $colorMap[$i % 360] = ($devCyan - int((($devCyan - $devBlue) / ($adjBlue - $adjCyan) * ($i - $adjCyan)) +0.5)) % 255; Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]); } #blue to lilac $devBlue += 256 if ($devBlue < $devLilac); $adjLilac += 360 if ($adjLilac < $adjBlue); for ($i = $adjBlue; $i <= $adjLilac; $i++) { $colorMap[$i % 360] = ($devBlue - int((($devBlue - $devLilac) / ($adjLilac - $adjBlue) * ($i- $adjBlue)) +0.5)) % 255; Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]); } #lilac to red $devLilac += 256 if ($devLilac < $devRed); $adjRed += 360 if ($adjRed < $adjLilac); for ($i = $adjLilac; $i <= $adjRed; $i++) { $colorMap[$i % 360] = ($devLilac - int((($devLilac - $devRed) / ($adjRed - $adjLilac) * ($i - $adjLilac)) +0.5)) % 255; Log3 ($hash, 5, "$hash->{NAME}_ColorConverter: create colormap h: ".($i % 360)." d: ".$colorMap[$i % 360]); } return \@colorMap; } ##################################### sub MilightDevice_CreateGammaMapping(@) { my ($hash, $gamma) = @_; #original wifilight gamma was inverted $gamma = 1/$gamma; my @gammaMap; $gammaMap[0] = 0; for (my $i = 1; $i <= 100; $i += 1) { my $correction = ($i / 100) ** (1 / $gamma); $gammaMap[$i] = $correction * 100; $gammaMap[$i] = MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash)) if($gammaMap[$i] < MilightDevice_roundfunc(100/MilightDevice_DimSteps($hash))); Log3 ($hash, 5, "$hash->{NAME} create gammamap v-in: ".$i.", v-out: $gammaMap[$i]"); } return \@gammaMap; } ############################################################################### # Device Command Queue # Triggers commands for long running transitions for a device ############################################################################### sub MilightDevice_CmdQueue_Add(@) { my ($hash, $hue, $sat, $val, $ctrl, $delay, $targetTime) = @_; my $cmd; # Validate input ($hue, $sat, $val) = MilightDevice_ValidateHSV($hash, $hue, $sat, $val); $cmd->{hue} = $hue; $cmd->{sat} = $sat; $cmd->{val} = $val; $cmd->{ctrl} = $ctrl; $cmd->{delay} = $delay; $cmd->{targetTime} = $targetTime; $cmd->{inProgess} = 0; push @{$hash->{helper}->{cmdQueue}}, $cmd; my $hexStr = defined($cmd->{ctrl})? unpack("H*", $cmd->{ctrl} || '') : ""; Log3 ($hash, 4, "$hash->{NAME}_CmdQueue_Add: h: ".(defined($cmd->{hue})? $cmd->{hue}: "")."; s: ".(defined($cmd->{sat})? $cmd->{sat}: "")."; v: ".(defined($cmd->{val})? $cmd->{val}: "")."; Ctrl $hexStr; TargetTime: ".(defined($cmd->{targetTime})? $cmd->{targetTime}: "")."; QLen: ".@{$hash->{helper}->{cmdQueue}}); my $actualCmd = @{$hash->{helper}->{cmdQueue}}[0]; # sender busy ? if(defined($actualCmd)) { return undef if (ref($actualCmd) ne 'HASH'); return undef if (!defined($actualCmd->{inProgess})); return undef if (($actualCmd->{inProgess} || 0) == 1); } return MilightDevice_CmdQueue_Exec($hash); } ##################################### sub MilightDevice_CmdQueue_Exec(@) { my ($hash) = @_; RemoveInternalTimer($hash); #if ($hash->{IODev}->{STATE} ne "ok" && $hash->{IODev}->{STATE} ne "Initialized") { # InternalTimer(gettimeofday() + 60, "MilightDevice_CmdQueue_Exec", $hash, 0); # return undef; #} my $actualCmd = @{$hash->{helper}->{cmdQueue}}[0]; # transmission complete, remove shift @{$hash->{helper}->{cmdQueue}} if ($actualCmd->{inProgess}); # next in queue $actualCmd = @{$hash->{helper}->{cmdQueue}}[0]; my $nextCmd = @{$hash->{helper}->{cmdQueue}}[1]; # return if no more elements in queue if (!defined($actualCmd->{inProgess})) { readingsSingleUpdate($hash, "transitionInProgress", 0, 1); # Clear transitionInProgress flag return undef; } readingsSingleUpdate($hash, "transitionInProgress", 1, 1); # Set transitionInProgress flag # drop frames if next frame is already scheduled for given time. do not drop if it is the last frame or if it is a control command while (defined($nextCmd->{targetTime}) && ($nextCmd->{targetTime} < gettimeofday()) && !$actualCmd->{ctrl}) { shift @{$hash->{helper}->{cmdQueue}}; $actualCmd = @{$hash->{helper}->{cmdQueue}}[0]; $nextCmd = @{$hash->{helper}->{cmdQueue}}[1]; Log3 ($hash, 4, "$hash->{NAME}_CmdQueue_Exec: Drop Frame. Queue Length: ".@{$hash->{helper}->{cmdQueue}}); } Log3 ($hash, 5, "$hash->{NAME}_CmdQueue_Exec: Dropper Delay: ".($actualCmd->{targetTime} - gettimeofday())) if (defined($actualCmd->{targetTime})); # set hsv or if a device ctrl command is scheduled: send it and ignore hsv if ($actualCmd->{ctrl}) { my $dbgStr = unpack("H*", $actualCmd->{ctrl}); Log3 ($hash, 4, "$hash->{NAME}_CmdQueue_Exec: Send ctrl: $dbgStr; Queue Length: ".@{$hash->{helper}->{cmdQueue}}); IOWrite($hash, $actualCmd->{ctrl}); } else { # Send an HSV Command. my $repeat = 0; # If queue length < 2 (ie. 1) we are last command so repeat sending (takes twice as long...) $repeat = 1 if (@{$hash->{helper}->{cmdQueue}} < 2); MilightDevice_SetHSV($hash, $actualCmd->{hue}, $actualCmd->{sat}, $actualCmd->{val}, $repeat); } $actualCmd->{inProgess} = 1; my $next = defined($nextCmd->{targetTime})?$nextCmd->{targetTime}:gettimeofday() + ($actualCmd->{delay} / 1000); Log3 ($hash, 5, "$hash->{NAME}_CmdQueue_Exec: Next Exec: $next"); InternalTimer($next, "MilightDevice_CmdQueue_Exec", $hash, 0); return undef; } ##################################### sub MilightDevice_CmdQueue_Clear(@) { my ($hash) = @_; Log3 ($hash, 4, "$hash->{NAME}_CmdQueue_Clear"); RemoveInternalTimer($hash); #if ($hash->{IODev}->{STATE} ne "ok" && $hash->{IODev}->{STATE} ne "Initialized") { # InternalTimer(gettimeofday() + 60, "MilightDevice_CmdQueue_Exec", $hash, 0); # return undef; #} readingsSingleUpdate($hash, "transitionInProgress", 0, 1); # Clear inProgress flag #foreach my $args (keys %intAt) #{ # if (($intAt{$args}{ARG} eq $hash) && ($intAt{$args}{FN} eq 'MilightDevice_CmdQueue_Exec')) # { # Log3 ($hash, 5, "$hash->{NAME}_CmdQueue_Clear: Remove timer at: ".$intAt{$args}{TRIGGERTIME}); # delete($intAt{$args}); # } #} $hash->{helper}->{cmdQueue} = []; return undef; } ##################################### sub MilightDevice_BridgeDevices_Update(@) { my ($hash, $attr) = @_; my @rdlist = ($attr); if($attr eq 'bulk') { @rdlist = ("state","brightness","brightness_on","hue", "saturation", "hsv", "rgb", "discoMode", "discoSpeed")if ($hash->{LEDTYPE} eq 'RGBW'); @rdlist = ("state","brightness","brightness_on","ct")if ($hash->{LEDTYPE} eq 'White'); } my $sl = 5; $sl = 1 if ($hash->{LEDTYPE} eq 'White'); for (my $i = 0; $i < 4; $i++) { my $devname = $hash->{IODev}->{$sl+$i}->{NAME}; next if (!defined($defs{$devname})); my $device = $defs{$devname}; $devname = "?" if(!defined($devname)); readingsSingleUpdate($device, "transitionInProgress", 1, 1); readingsBeginUpdate($device); foreach my $rdname (@rdlist) { if (exists ($device->{READINGS}{$rdname})) { readingsBulkUpdate($device, $rdname, $hash->{READINGS}{$rdname}{VAL}, 1); Log3 ($hash, 4, $rdname.": ".$device->{READINGS}{$rdname}{VAL}." for ".$devname); } } readingsEndUpdate($device, 1); readingsSingleUpdate($device, "transitionInProgress", 0, 1); } return undef; } sub MilightDevice_roundfunc($) { my ($number) = @_; return sprintf("%.0f", $number); #return Math::Round::round($number); } 1; =pod =item device =item summary This module represents a Milight LED Bulb or LED strip controller =begin html

MilightDevice

    This module represents a Milight LED Bulb or LED strip controller. It is controlled by a MilightBridge.

    The Milight system is sold under various brands around the world including "LimitlessLED, EasyBulb, AppLamp"

    The API documentation is available here: http://www.limitlessled.com/dev/

    Requires perl module Math::Round

    Define

      define <name> MilightDevice <devType(RGB|RGBW|White)> <IODev> <slot>

      Specifies the Milight device.
      <devType> One of RGB, RGBW, White depending on your device.
      <IODev> The MilightBridge which the device is paired with.
      <slot> The slot on the MilightBridge that the device is paired with or 'A' to group all slots.

    Readings

    • state
      [on xxx|off|night]: Current state of the device / night mode (xxx = 0-100%).
    • brightness
      [0-100]: Current brightness level in %.
    • brightness_on
      [0-100]: The brightness level before the off command was sent. This allows the light to turn back on to the last brightness level.
    • rgb
      [FFFFFF]: HEX value for RGB.
    • previousState
      [hsv]: hsv value before last change. Can be used with restorePreviousState set command.
    • savedState
      [hsv]: hsv value that was saved using saveState set function
    • hue
      [0-360]: Current hue value.
    • saturation
      [0-100]: Current saturation value.
    • transitionInProgress
      [0|1]: Set to 1 if a transition is currently in progress for this device (eg. fade).
    • discoMode
      [0|1]: 1 if discoMode is enabled, 0 otherwise.
    • discoSpeed
      [0|1]: 1 if discoSpeed is increased, 0 if decreased. Does not mean much for RGBW
    • lastPreset
      [0..X]: Last selected preset.
    • ct
      [1-10]: Current colour temperature (3000=Warm,6500=Cold) for White devices.

    Set

    • on <ramp_time (seconds)>
    • off <ramp_time (seconds)>
    • toggle
    • night
    • dim <percent(0..100)> [seconds(0..x)] [flags(l=long path|q=don't clear queue)]
      Will be replaced by brightness at some point
    • dimup <percent change(0..100)> [seconds(0..x)]
      Special case: If percent change=100, seconds will be adjusted for actual change to go from current brightness.
    • dimdown <percent change(0..100)> [seconds(0..x)]
      Special case: If percent change=100, seconds will be adjusted for actual change to go from current brightness.
    • pair
      May not work properly. Sometimes it is necessary to use a remote to clear pairing first.
    • unpair
      May not work properly. Sometimes it is necessary to use a remote to clear pairing first.
    • restorePreviousState
      Set device to previous hsv state as stored in previousState reading.
    • saveState
      Save current hsv state to savedState reading.
    • restoreState
      Set device to saved hsv state as stored in savedState reading.
    • preset (0..X|+)
      Load preset (+ for next preset).
    • hsv <h(0..360)>,<s(0..100)>,<v(0..100)> [seconds(0..x)] [flags(l=long path|q=don't clear queue)]
      Set hsv value directly
    • rgb RRGGBB [seconds(0..x)] [flags(l=long path|q=don't clear queue)]
      Set rgb value directly or using colorpicker.
    • hue <(0..360)> [seconds(0..x)] [flags(l=long path|q=don't clear queue)]
      Set hue value.
    • saturation <s(0..100)> [seconds(0..x)] [flags(q=don't clear queue)]
      Set saturation value directly
    • discoModeUp
      Next disco Mode setting (for RGB and RGBW).
    • discoModeDown
      Previous disco Mode setting (for RGB).
    • discoSpeedUp
      Increase speed of disco mode (for RGB and RGBW).
    • discoSpeedDown
      Decrease speed of disco mode (for RGB and RGBW).
    • ct <3000-6500>
      Colour temperature 3000=Warm White,6500=Cold White (10 steps) (for White devices only).
    • set extensions are supported.

    Get

    • rgb
    • hsv

    Attributes

    • dimStep
      Allows you to modify the default dimStep if required.
    • defaultRampOn
      Set the default ramp time if not specified for on command.
    • defaultRampOff
      Set the default ramp time if not specified for off command.
    • presets
      List of hsv presets separated by spaces (eg 0,0,100 9,0,50).
    • colorCast
      Color shift values for red,yellow,green,cyan,blue,magenta (-29..29) for HSV color correction (eg 0,5,10,-5,0,0)
    • gamma
      Set gamma correction value for device (eg 0.8)
    • dimOffWhite
      Use a different switching logic for White bulbs to better handle packet loss.
    • updateGroupDevices
      Update the state of single devices switched with slot 'A'.
    • restoreAtStart
      Restore the state of devices at startup. Default 0 for slot 'A', 1 otherwise.
    • defaultBrightness
      Set the default brightness if not known. (Default: 36)
=end html =cut