mirror of
				https://github.com/hyperion-project/hyperion.ng.git
				synced 2025-03-01 10:33:28 +00:00 
			
		
		
		
	AtmoOrb Fix (#988)
* AtmoOrb UdpSocket-Bind Fix * Cleanup and update defaults (to work via PowerLan) * Cleanup and update defaults (to work via PowerLan) * AtmoOrb identification support, small updates * AtmoOrb discovery & identification support, fixes and stability updates * Small clean-ups * Type fix * Add missing include * Adalight - Update default config and levels * Update Atmoorb sketch * Yeelight - Update default value
This commit is contained in:
		
							
								
								
									
										354
									
								
								assets/firmware/esp8266/AtmoOrb/AtmoOrb.ino
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										354
									
								
								assets/firmware/esp8266/AtmoOrb/AtmoOrb.ino
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,354 @@
 | 
			
		||||
// AtmoOrb by Lightning303 & Rick164, Additions by Lord-Grey
 | 
			
		||||
//
 | 
			
		||||
// ESP8266 Standalone Version
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
// You may change the settings that are commented
 | 
			
		||||
 | 
			
		||||
#define FASTLED_ALLOW_INTERRUPTS 0
 | 
			
		||||
// To make sure that all leds get changed 100% of the time, we need to allow FastLED to disabled interrupts for a short while.
 | 
			
		||||
// If you experience problems, please set this value to 1.
 | 
			
		||||
// This is only needed for 3 wire (1 data line + Vcc and GND) chips (e.g. WS2812B). If you are using WS2801, APA102 or similar chipsets, you can set the value back to 1.
 | 
			
		||||
 | 
			
		||||
#include <ESP8266WiFi.h>
 | 
			
		||||
#include <WiFiUdp.h>
 | 
			
		||||
#include <FastLED.h>
 | 
			
		||||
 | 
			
		||||
#define NUM_LEDS 24 // Number of leds
 | 
			
		||||
#define DATA_PIN 7 // Data pin for leds (the default pin 7 might correspond to pin 13 on some boards)
 | 
			
		||||
#define SERIAL_DEBUG 0 // Serial debugging (0=Off, 1=On)
 | 
			
		||||
 | 
			
		||||
#define ID 1 // Id of this lamp
 | 
			
		||||
 | 
			
		||||
// Smoothing
 | 
			
		||||
#define SMOOTH_STEPS 20 // Steps to take for smoothing colors
 | 
			
		||||
#define SMOOTH_DELAY 10 // Delay between smoothing steps
 | 
			
		||||
#define SMOOTH_BLOCK 0 // Block incoming colors while smoothing
 | 
			
		||||
 | 
			
		||||
// Startup color
 | 
			
		||||
#define STARTUP_RED 255 // Color shown directly after power on
 | 
			
		||||
#define STARTUP_GREEN 175 // Color shown directly after power on
 | 
			
		||||
#define STARTUP_BLUE 100 // Color shown directly after power on
 | 
			
		||||
 | 
			
		||||
// White adjustment
 | 
			
		||||
#define RED_CORRECTION 220 // Color Correction
 | 
			
		||||
#define GREEN_CORRECTION 255 // Color Correction
 | 
			
		||||
#define BLUE_CORRECTION 180 // Color Correction
 | 
			
		||||
 | 
			
		||||
// RC Switch
 | 
			
		||||
#define RC_SWITCH 0 // RF transmitter to swtich remote controlled power sockets (0=Off, 1=On)
 | 
			
		||||
#if RC_SWITCH == 1
 | 
			
		||||
  #include <RCSwitch.h>
 | 
			
		||||
  #define RC_PIN 2 // Data pin for RF transmitter
 | 
			
		||||
  #define RC_SLEEP_DELAY 900000 // Delay until RF transmitter send signals
 | 
			
		||||
  char* rcCode0 = "10001"; // First part of the transmission code
 | 
			
		||||
  char* rcCode1 = "00010"; // Second part of the transmission code
 | 
			
		||||
  RCSwitch mySwitch = RCSwitch();
 | 
			
		||||
  boolean remoteControlled = false;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
// Network settings
 | 
			
		||||
const char* ssid = "***"; // WiFi SSID
 | 
			
		||||
const char* password = "***"; // WiFi password
 | 
			
		||||
 | 
			
		||||
const IPAddress multicastIP(239,255,255,250); // Multicast IP address
 | 
			
		||||
const int multicastPort = 49692; // Multicast port number
 | 
			
		||||
IPAddress    ip_null(0,0,0,0);
 | 
			
		||||
IPAddress    local_IP(0,0,0,0);
 | 
			
		||||
WiFiUDP Udp;
 | 
			
		||||
 | 
			
		||||
int          timeout    = 20000;      // wait 20 sec for successfull login
 | 
			
		||||
boolean      is_connect = false;      // ... not yet connected
 | 
			
		||||
 | 
			
		||||
CRGB leds[NUM_LEDS];
 | 
			
		||||
 | 
			
		||||
byte nextColor[3];
 | 
			
		||||
byte prevColor[3];
 | 
			
		||||
byte currentColor[3];
 | 
			
		||||
byte smoothStep = SMOOTH_STEPS;
 | 
			
		||||
unsigned long smoothMillis;
 | 
			
		||||
 | 
			
		||||
void setColor(byte red, byte green, byte blue);
 | 
			
		||||
void setSmoothColor(byte red, byte green, byte blue);
 | 
			
		||||
void smoothColor();
 | 
			
		||||
void clearSmoothColors();
 | 
			
		||||
 | 
			
		||||
void setup()
 | 
			
		||||
{
 | 
			
		||||
  FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);
 | 
			
		||||
  //FastLED.setCorrection(TypicalSMD5050);
 | 
			
		||||
  FastLED.setCorrection(CRGB(RED_CORRECTION, GREEN_CORRECTION, BLUE_CORRECTION));
 | 
			
		||||
  FastLED.showColor(CRGB(STARTUP_RED, STARTUP_GREEN, STARTUP_BLUE));
 | 
			
		||||
 | 
			
		||||
  #if RC_SWITCH == 1
 | 
			
		||||
    mySwitch.enableTransmit(RC_PIN);
 | 
			
		||||
  #endif
 | 
			
		||||
 | 
			
		||||
  #if SERIAL_DEBUG == 1
 | 
			
		||||
    Serial.begin(115200);
 | 
			
		||||
  #endif
 | 
			
		||||
 | 
			
		||||
  #if SERIAL_DEBUG == 1
 | 
			
		||||
    Serial.printf("Connecting to %s ", ssid);
 | 
			
		||||
  #endif
 | 
			
		||||
 | 
			
		||||
  // .... wait for WiFi gets valid !!!
 | 
			
		||||
  unsigned long tick = millis();      // get start-time for login
 | 
			
		||||
  WiFi.begin(ssid, password);
 | 
			
		||||
  while ( (!is_connect) &&  ((millis() - tick) < timeout) )
 | 
			
		||||
  {
 | 
			
		||||
    yield();                          // ... for safety
 | 
			
		||||
    is_connect = WiFi.status();       // connected ?
 | 
			
		||||
    if (!is_connect)                  // only if not yet connected !
 | 
			
		||||
    {
 | 
			
		||||
      #if SERIAL_DEBUG == 1
 | 
			
		||||
        Serial.print(".");              // print a dot while waiting
 | 
			
		||||
      #endif
 | 
			
		||||
      delay(50);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (is_connect)
 | 
			
		||||
  {
 | 
			
		||||
    #if SERIAL_DEBUG == 1
 | 
			
		||||
      Serial.print("after ");
 | 
			
		||||
      Serial.print(millis() - tick);
 | 
			
		||||
      Serial.println(" ms");
 | 
			
		||||
    #endif
 | 
			
		||||
    // .... wait for local_IP becomes valid !!!
 | 
			
		||||
    is_connect = false;
 | 
			
		||||
    tick = millis();      // get start-time for login
 | 
			
		||||
    while ( (!is_connect) &&  ((millis() - tick) < timeout) )
 | 
			
		||||
    {
 | 
			
		||||
      yield();                          // ... for safety
 | 
			
		||||
      local_IP = WiFi.localIP(); 
 | 
			
		||||
      is_connect = local_IP != ip_null;       // connected ?
 | 
			
		||||
      if (!is_connect)                  // only if not yet connected !
 | 
			
		||||
      {
 | 
			
		||||
        #if SERIAL_DEBUG == 1
 | 
			
		||||
          Serial.print(".");              // print a dot while waiting
 | 
			
		||||
        #endif
 | 
			
		||||
        delay(50);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (is_connect)
 | 
			
		||||
    {
 | 
			
		||||
      #if SERIAL_DEBUG == 1
 | 
			
		||||
        Serial.print("local_IP valid after ");
 | 
			
		||||
        Serial.print(millis() - tick);
 | 
			
		||||
        Serial.println(" ms");
 | 
			
		||||
        Serial.println("");
 | 
			
		||||
        Serial.print(F("Connected to "));
 | 
			
		||||
        Serial.println(ssid);
 | 
			
		||||
      #endif
 | 
			
		||||
 | 
			
		||||
       // ... now start UDP and check the result:
 | 
			
		||||
      is_connect = Udp.beginMulticast(local_IP, multicastIP, multicastPort);
 | 
			
		||||
      if (is_connect)
 | 
			
		||||
      {
 | 
			
		||||
        #if SERIAL_DEBUG == 1      
 | 
			
		||||
          Serial.print("Listening to Multicast at ");
 | 
			
		||||
          Serial.print(multicastIP);
 | 
			
		||||
          Serial.println(":" + String(multicastPort));
 | 
			
		||||
        #endif
 | 
			
		||||
      }
 | 
			
		||||
      else
 | 
			
		||||
      {
 | 
			
		||||
        #if SERIAL_DEBUG == 1      
 | 
			
		||||
          Serial.println(" - ERROR beginMulticast !");
 | 
			
		||||
        #endif
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
    {
 | 
			
		||||
      #if SERIAL_DEBUG == 1     
 | 
			
		||||
        Serial.println("local_IP invalid after timeout !");
 | 
			
		||||
      #endif
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  else
 | 
			
		||||
  {
 | 
			
		||||
    #if SERIAL_DEBUG == 1     
 | 
			
		||||
      Serial.println("- invalid after timeout !");      
 | 
			
		||||
    #endif
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void loop()
 | 
			
		||||
{
 | 
			
		||||
  #if SERIAL_DEBUG == 1
 | 
			
		||||
    if (WiFi.status() != WL_CONNECTED)
 | 
			
		||||
    {
 | 
			
		||||
      Serial.print(F("Lost connection to "));
 | 
			
		||||
      Serial.print(ssid);
 | 
			
		||||
      Serial.println(F("."));
 | 
			
		||||
      Serial.println(F("Trying to reconnect."));
 | 
			
		||||
      while (WiFi.status() != WL_CONNECTED)
 | 
			
		||||
      {
 | 
			
		||||
        delay(500);
 | 
			
		||||
        Serial.print(F("."));
 | 
			
		||||
      }
 | 
			
		||||
      Serial.println("");
 | 
			
		||||
      Serial.println(F("Reconnected."));
 | 
			
		||||
    }
 | 
			
		||||
  #endif
 | 
			
		||||
  if (Udp.parsePacket())
 | 
			
		||||
  {
 | 
			
		||||
    byte len = Udp.available();
 | 
			
		||||
    byte rcvd[len];
 | 
			
		||||
    Udp.read(rcvd, len);
 | 
			
		||||
 | 
			
		||||
    #if SERIAL_DEBUG == 1
 | 
			
		||||
      Serial.print(F("UDP Packet from "));
 | 
			
		||||
      Serial.print(Udp.remoteIP());
 | 
			
		||||
      Serial.print(F(":"));
 | 
			
		||||
      Serial.print(Udp.remotePort());
 | 
			
		||||
      Serial.print(F(" to "));
 | 
			
		||||
      Serial.println(Udp.destinationIP());
 | 
			
		||||
      for (byte i = 0; i < len; i++)
 | 
			
		||||
      {
 | 
			
		||||
        Serial.print(rcvd[i]);
 | 
			
		||||
        Serial.print(F(" "));
 | 
			
		||||
      }
 | 
			
		||||
      Serial.println("");
 | 
			
		||||
    #endif
 | 
			
		||||
    if (len >= 8 && rcvd[0] == 0xC0 && rcvd[1] == 0xFF && rcvd[2] == 0xEE && (rcvd[4] == ID || rcvd[4] == 0))
 | 
			
		||||
    {
 | 
			
		||||
      switch (rcvd[3])
 | 
			
		||||
      {
 | 
			
		||||
        case 1:
 | 
			
		||||
          smoothStep = SMOOTH_STEPS;
 | 
			
		||||
          forceLedsOFF();
 | 
			
		||||
          break;
 | 
			
		||||
        case 2:
 | 
			
		||||
        default:
 | 
			
		||||
          setSmoothColor(rcvd[5], rcvd[6], rcvd[7]);
 | 
			
		||||
          break;
 | 
			
		||||
        case 4:
 | 
			
		||||
          setColor(rcvd[5], rcvd[6], rcvd[7]);
 | 
			
		||||
          smoothStep = SMOOTH_STEPS;
 | 
			
		||||
          break;
 | 
			
		||||
        case 8:
 | 
			
		||||
          #if SERIAL_DEBUG == 1
 | 
			
		||||
            Serial.print(F("Announce myself. OrbID: "));
 | 
			
		||||
            Serial.println(ID);
 | 
			
		||||
          #endif
 | 
			
		||||
          Udp.beginPacket(Udp.remoteIP(), Udp.remotePort());
 | 
			
		||||
          Udp.write(ID);
 | 
			
		||||
          Udp.endPacket();
 | 
			
		||||
          break;
 | 
			
		||||
        case 9:
 | 
			
		||||
          #if SERIAL_DEBUG == 1
 | 
			
		||||
            Serial.print(F("Identify myself. OrbID: "));
 | 
			
		||||
            Serial.println(ID);
 | 
			
		||||
          #endif
 | 
			
		||||
          identify();
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (smoothStep < SMOOTH_STEPS && millis() >= (smoothMillis + (SMOOTH_DELAY * (smoothStep + 1))))
 | 
			
		||||
  {
 | 
			
		||||
    smoothColor();
 | 
			
		||||
  }
 | 
			
		||||
  #if RC_SWITCH == 1
 | 
			
		||||
    if (remoteControlled && currentColor[0] == 0 && currentColor[1] == 0 && currentColor[2] == 0 && millis() >= smoothMillis + RC_SLEEP_DELAY)
 | 
			
		||||
    {
 | 
			
		||||
      // Send this signal only once every seconds
 | 
			
		||||
      smoothMillis += 1000;
 | 
			
		||||
      mySwitch.switchOff(rcCode0, rcCode1);
 | 
			
		||||
    }
 | 
			
		||||
  #endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Display color on leds
 | 
			
		||||
void setColor(byte red, byte green, byte blue)
 | 
			
		||||
{
 | 
			
		||||
  // Is the new color already active?
 | 
			
		||||
  if (currentColor[0] == red && currentColor[1] == green && currentColor[2] == blue)
 | 
			
		||||
  {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  currentColor[0] = red;
 | 
			
		||||
  currentColor[1] = green;
 | 
			
		||||
  currentColor[2] = blue;
 | 
			
		||||
 | 
			
		||||
  FastLED.showColor(CRGB(red, green, blue));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set a new color to smooth to
 | 
			
		||||
void setSmoothColor(byte red, byte green, byte blue)
 | 
			
		||||
{
 | 
			
		||||
  if (smoothStep == SMOOTH_STEPS || SMOOTH_BLOCK == 0)
 | 
			
		||||
  {
 | 
			
		||||
    // Is the new color the same as the one we already are smoothing towards?
 | 
			
		||||
    // If so dont do anything.
 | 
			
		||||
    if (nextColor[0] == red && nextColor[1] == green && nextColor[2] == blue)
 | 
			
		||||
    {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // Is the new color the same as we have right now?
 | 
			
		||||
    // If so stop smoothing and keep the current color.
 | 
			
		||||
    else if (currentColor[0] == red && currentColor[1] == green && currentColor[2] == blue)
 | 
			
		||||
    {
 | 
			
		||||
      smoothStep = SMOOTH_STEPS;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    prevColor[0] = currentColor[0];
 | 
			
		||||
    prevColor[1] = currentColor[1];
 | 
			
		||||
    prevColor[2] = currentColor[2];
 | 
			
		||||
 | 
			
		||||
    nextColor[0] = red;
 | 
			
		||||
    nextColor[1] = green;
 | 
			
		||||
    nextColor[2] = blue;
 | 
			
		||||
 | 
			
		||||
    smoothMillis = millis();
 | 
			
		||||
    smoothStep = 0;
 | 
			
		||||
 | 
			
		||||
    #if RC_SWITCH == 1
 | 
			
		||||
      if (!remoteControlled)
 | 
			
		||||
      {
 | 
			
		||||
        remoteControlled = true;
 | 
			
		||||
      }
 | 
			
		||||
    #endif
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Display one step to the next color
 | 
			
		||||
void smoothColor()
 | 
			
		||||
{
 | 
			
		||||
  smoothStep++;
 | 
			
		||||
 | 
			
		||||
  byte red = prevColor[0] + (((nextColor[0] - prevColor[0]) * smoothStep) / SMOOTH_STEPS);
 | 
			
		||||
  byte green = prevColor[1] + (((nextColor[1] - prevColor[1]) * smoothStep) / SMOOTH_STEPS);
 | 
			
		||||
  byte blue = prevColor[2] + (((nextColor[2] - prevColor[2]) * smoothStep) / SMOOTH_STEPS);   
 | 
			
		||||
 | 
			
		||||
  setColor(red, green, blue);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Force all leds OFF
 | 
			
		||||
void forceLedsOFF()
 | 
			
		||||
{
 | 
			
		||||
    setColor(0,0,0);
 | 
			
		||||
    clearSmoothColors();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
// Clear smooth color byte arrays
 | 
			
		||||
void clearSmoothColors()
 | 
			
		||||
{
 | 
			
		||||
    memset(prevColor, 0, sizeof(prevColor));
 | 
			
		||||
    memset(currentColor, 0, sizeof(nextColor));
 | 
			
		||||
    memset(nextColor, 0, sizeof(nextColor));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void identify()
 | 
			
		||||
{
 | 
			
		||||
  for (byte i = 0; i < 3; i++)
 | 
			
		||||
  {
 | 
			
		||||
    FastLED.showColor(CRGB::LemonChiffon);
 | 
			
		||||
    delay(500);
 | 
			
		||||
    FastLED.showColor(CRGB::Black);   
 | 
			
		||||
    delay(500);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -397,6 +397,10 @@
 | 
			
		||||
	"wiz_yeelight_desc2" : "Now choose which lamps should be added. The position assigns the lamp to a specific position on your \"picture\". Disabled lamps won't be added. To identify single lamps press the button on the right.",
 | 
			
		||||
	"wiz_yeelight_noLights": "No Yeelights found! Please get the lights connected to the network or configure them mannually.",
 | 
			
		||||
	"wiz_yeelight_unsupported" : "Unsupported",
 | 
			
		||||
	"wiz_atmoorb_title" : "AtmoOrb Wizard",
 | 
			
		||||
	"wiz_atmoorb_intro1" : "This wizards configures Hyperion for AtmoOrbs. Features are the AtmoOrb auto detection, setting each light to a specific position on your picture or disable it and tune the Hyperion settings automatically! So in short: All you need are some clicks and you are done!",
 | 
			
		||||
	"wiz_atmoorb_desc2" : "Now choose which Orbs should be added. The position assigns the lamp to a specific position on your \"picture\". Disabled lamps won't be added. To identify single lamps press the button on the right.",
 | 
			
		||||
	"wiz_atmoorb_noLights": "No AtmoOrbs found! Please get the lights connected to the network or configure them mannually.",
 | 
			
		||||
	"wiz_pos": "Position/State",
 | 
			
		||||
	"wiz_ids_disabled" : "Deactivated",
 | 
			
		||||
	"wiz_ids_entire" : "Whole picture",	
 | 
			
		||||
 
 | 
			
		||||
@@ -543,6 +543,12 @@ $(document).ready(function() {
 | 
			
		||||
    	    changeWizard(data, wled_title, startWizardWLED);
 | 
			
		||||
	}
 | 
			
		||||
*/
 | 
			
		||||
    else if(ledType == "atmoorb") {
 | 
			
		||||
    	    var ledWizardType = (this.checked) ? "atmoorb" : ledType;
 | 
			
		||||
    	    var data = { type: ledWizardType };
 | 
			
		||||
    	    var atmoorb_title = 'wiz_atmoorb_title';
 | 
			
		||||
    	    changeWizard(data, atmoorb_title, startWizardAtmoOrb);
 | 
			
		||||
	}
 | 
			
		||||
	else if(ledType == "yeelight") {
 | 
			
		||||
		var ledWizardType = (this.checked) ? "yeelight" : ledType;
 | 
			
		||||
		var data = { type: ledWizardType };
 | 
			
		||||
 
 | 
			
		||||
@@ -1259,16 +1259,16 @@ function startWizardWLED(e)
 | 
			
		||||
 | 
			
		||||
// For testing only
 | 
			
		||||
  discover_wled();
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
  var hostAddress = conf_editor.getEditor("root.specificOptions.host").getValue();
 | 
			
		||||
  if(hostAddress != "")
 | 
			
		||||
  {
 | 
			
		||||
    getProperties_wled(hostAddress,"info");
 | 
			
		||||
    identify_wled(hostAddress)  
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
// For testing only
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1552,13 +1552,14 @@ function assign_yeelight_lights(){
 | 
			
		||||
				options+= '>'+$.i18n(txt+val)+'</option>';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var enabled = 'enabled'
 | 
			
		||||
			if (! models.includes (lights[lightid].model) )
 | 
			
		||||
			{
 | 
			
		||||
				var enabled = 'disabled';
 | 
			
		||||
				options = '<option value=disabled>'+$.i18n('wiz_yeelight_unsupported')+'</option>';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			$('.lidsb').append(createTableRow([(parseInt(lightid, 10) + 1)+'. '+lightName+' ('+lightHostname+')', '<select id="yee_'+lightid+'" '+enabled+' class="yee_sel_watch form-control">'
 | 
			
		||||
			$('.lidsb').append(createTableRow([(parseInt(lightid, 10) + 1)+'. '+lightName+'<br>('+lightHostname+')', '<select id="yee_'+lightid+'" '+enabled+' class="yee_sel_watch form-control">'
 | 
			
		||||
											   + options
 | 
			
		||||
											   + '</select>','<button class="btn btn-sm btn-primary" onClick=identify_yeelight_device("'+lightHostname+'",'+lightPort+')>'
 | 
			
		||||
											   + $.i18n('wiz_identify_light',lightName)+'</button>']));
 | 
			
		||||
@@ -1615,6 +1616,276 @@ function identify_yeelight_device(hostname, port){
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//****************************
 | 
			
		||||
// Wizard AtmoOrb
 | 
			
		||||
//****************************
 | 
			
		||||
var lights = null;
 | 
			
		||||
function startWizardAtmoOrb(e)
 | 
			
		||||
{
 | 
			
		||||
	//create html
 | 
			
		||||
 | 
			
		||||
	var atmoorb_title = 'wiz_atmoorb_title';
 | 
			
		||||
	var atmoorb_intro1 = 'wiz_atmoorb_intro1';
 | 
			
		||||
 | 
			
		||||
	$('#wiz_header').html('<i class="fa fa-magic fa-fw"></i>'+$.i18n(atmoorb_title));
 | 
			
		||||
	$('#wizp1_body').html('<h4 style="font-weight:bold;text-transform:uppercase;">'+$.i18n(atmoorb_title)+'</h4><p>'+$.i18n(atmoorb_intro1)+'</p>');
 | 
			
		||||
 | 
			
		||||
	$('#wizp1_footer').html('<button type="button" class="btn btn-primary" id="btn_wiz_cont"><i class="fa fa-fw fa-check"></i>'
 | 
			
		||||
	+$.i18n('general_btn_continue')+'</button><button type="button" class="btn btn-danger" data-dismiss="modal"><i class="fa fa-fw fa-close"></i>'
 | 
			
		||||
	+$.i18n('general_btn_cancel')+'</button>');
 | 
			
		||||
 | 
			
		||||
	$('#wizp2_body').html('<div id="wh_topcontainer"></div>');
 | 
			
		||||
 | 
			
		||||
	$('#wh_topcontainer').append('<div class="form-group" id="usrcont" style="display:none"></div>');
 | 
			
		||||
 | 
			
		||||
	$('#wizp2_body').append('<div id="orb_ids_t" style="display:none"><p style="font-weight:bold" id="orb_id_headline">'+$.i18n('wiz_atmoorb_desc2')+'</p></div>');
 | 
			
		||||
 | 
			
		||||
	createTable("lidsh", "lidsb", "orb_ids_t");
 | 
			
		||||
	$('.lidsh').append(createTableRow([$.i18n('edt_dev_spec_lights_title'),$.i18n('wiz_pos'),$.i18n('wiz_identify')], true));
 | 
			
		||||
	$('#wizp2_footer').html('<button type="button" class="btn btn-primary" id="btn_wiz_save" style="display:none"><i class="fa fa-fw fa-save"></i>'
 | 
			
		||||
	+$.i18n('general_btn_save')+'</button><buttowindow.serverConfig.device = d;n type="button" class="btn btn-danger" id="btn_wiz_abort"><i class="fa fa-fw fa-close"></i>'
 | 
			
		||||
	+$.i18n('general_btn_cancel')+'</button>');
 | 
			
		||||
 | 
			
		||||
	//open modal
 | 
			
		||||
	$("#wizard_modal").modal({backdrop : "static", keyboard: false, show: true });
 | 
			
		||||
 | 
			
		||||
	//listen for continue
 | 
			
		||||
	$('#btn_wiz_cont').off().on('click',function() {
 | 
			
		||||
		beginWizardAtmoOrb();
 | 
			
		||||
		$('#wizp1').toggle(false);
 | 
			
		||||
		$('#wizp2').toggle(true);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function beginWizardAtmoOrb()
 | 
			
		||||
{
 | 
			
		||||
	lights = [];
 | 
			
		||||
	configuredLights = [];
 | 
			
		||||
 | 
			
		||||
	configruedOrbIds =  conf_editor.getEditor("root.specificOptions.orbIds").getValue().trim();
 | 
			
		||||
	if ( configruedOrbIds.length !== 0 )
 | 
			
		||||
	{
 | 
			
		||||
	    configuredLights =  configruedOrbIds.split(",").map( Number );
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
    var multiCastGroup = conf_editor.getEditor("root.specificOptions.output").getValue();
 | 
			
		||||
    var multiCastPort = parseInt(conf_editor.getEditor("root.specificOptions.port").getValue());
 | 
			
		||||
 | 
			
		||||
	discover_atmoorb_lights(multiCastGroup, multiCastPort);
 | 
			
		||||
 | 
			
		||||
	$('#btn_wiz_save').off().on("click", function(){
 | 
			
		||||
		var atmoorbLedConfig = [];
 | 
			
		||||
		var finalLights = [];
 | 
			
		||||
 | 
			
		||||
		//create atmoorb led config
 | 
			
		||||
		for(var key in lights)
 | 
			
		||||
		{
 | 
			
		||||
			if($('#orb_'+key).val() !== "disabled")
 | 
			
		||||
			{
 | 
			
		||||
				// Set Name to layout-position, if empty
 | 
			
		||||
				if ( lights[key].name === "" )
 | 
			
		||||
				{
 | 
			
		||||
					lights[key].name = $.i18n( 'conf_leds_layout_cl_'+$('#orb_'+key).val() );
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				finalLights.push( lights[key].id);
 | 
			
		||||
 | 
			
		||||
				var name = lights[key].id;
 | 
			
		||||
				if ( lights[key].host !== "")
 | 
			
		||||
					name += ':' + lights[key].host;
 | 
			
		||||
 | 
			
		||||
				var idx_content = assignLightPos(key, $('#orb_'+key).val(), name);
 | 
			
		||||
				atmoorbLedConfig.push(JSON.parse(JSON.stringify(idx_content)));
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//LED layout
 | 
			
		||||
		window.serverConfig.leds = atmoorbLedConfig;
 | 
			
		||||
 | 
			
		||||
		//LED device config
 | 
			
		||||
		//Start with a clean configuration
 | 
			
		||||
		var d = {};
 | 
			
		||||
 | 
			
		||||
		d.type = 'atmoorb';
 | 
			
		||||
		d.hardwareLedCount = finalLights.length;
 | 
			
		||||
		d.colorOrder = conf_editor.getEditor("root.generalOptions.colorOrder").getValue();
 | 
			
		||||
 | 
			
		||||
		d.orbIds = finalLights.toString();
 | 
			
		||||
		d.useOrbSmoothing  = (eV("useOrbSmoothing") == true);
 | 
			
		||||
 | 
			
		||||
		d.output = conf_editor.getEditor("root.specificOptions.output").getValue();
 | 
			
		||||
		d.port = parseInt(conf_editor.getEditor("root.specificOptions.port").getValue());
 | 
			
		||||
		d.latchTime = parseInt(conf_editor.getEditor("root.specificOptions.latchTime").getValue());;
 | 
			
		||||
 | 
			
		||||
		window.serverConfig.device = d;
 | 
			
		||||
 | 
			
		||||
		requestWriteConfig(window.serverConfig, true);
 | 
			
		||||
		resetWizard();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	$('#btn_wiz_abort').off().on('click', resetWizard);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getIdInLights(id) {
 | 
			
		||||
	return lights.filter(
 | 
			
		||||
				function(lights) {
 | 
			
		||||
					return lights.id === id
 | 
			
		||||
				}
 | 
			
		||||
				);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function discover_atmoorb_lights(multiCastGroup, multiCastPort){
 | 
			
		||||
 | 
			
		||||
  var light = {};
 | 
			
		||||
 | 
			
		||||
  if ( multiCastGroup === "" )
 | 
			
		||||
  	multiCastGroup = "239.255.255.250";
 | 
			
		||||
 | 
			
		||||
  if ( multiCastPort === "")
 | 
			
		||||
    multiCastPort = 49692;
 | 
			
		||||
 | 
			
		||||
  let params = { multiCastGroup : multiCastGroup, multiCastPort : multiCastPort};
 | 
			
		||||
 | 
			
		||||
  // Get discovered lights
 | 
			
		||||
  const res = await requestLedDeviceDiscovery ('atmoorb', params);
 | 
			
		||||
 | 
			
		||||
  // TODO: error case unhandled
 | 
			
		||||
  // res can be: false (timeout) or res.error (not found)
 | 
			
		||||
  if(res && !res.error){
 | 
			
		||||
    const r = res.info
 | 
			
		||||
 | 
			
		||||
	// Process devices returned by discovery
 | 
			
		||||
	for(const device of r.devices)
 | 
			
		||||
	{
 | 
			
		||||
	    if( device.id !== "")
 | 
			
		||||
	    {
 | 
			
		||||
		    if ( getIdInLights ( device.id ).length === 0 )
 | 
			
		||||
		    {
 | 
			
		||||
		      light = {};
 | 
			
		||||
		      light.id = device.id;
 | 
			
		||||
		      light.ip = device.ip;
 | 
			
		||||
		      light.host = device.hostname;
 | 
			
		||||
		      lights.push(light);
 | 
			
		||||
		    }
 | 
			
		||||
	    }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
    // Add additional items from configuration
 | 
			
		||||
    for(const keyConfig in configuredLights)
 | 
			
		||||
    {
 | 
			
		||||
      if ( configuredLights[keyConfig] !== "" && !isNaN(configuredLights[keyConfig]) ) 
 | 
			
		||||
      {
 | 
			
		||||
		  if ( getIdInLights ( configuredLights[keyConfig] ).length === 0 )
 | 
			
		||||
		  {
 | 
			
		||||
		  	light = {};
 | 
			
		||||
			light.id = configuredLights[keyConfig];
 | 
			
		||||
		    light.ip = "";
 | 
			
		||||
		    light.host = "";	    
 | 
			
		||||
		    lights.push(light);
 | 
			
		||||
		  }
 | 
			
		||||
	  }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
	lights.sort((a, b) => (a.id > b.id) ? 1 : -1);
 | 
			
		||||
 | 
			
		||||
    assign_atmoorb_lights();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function assign_atmoorb_lights(){
 | 
			
		||||
 | 
			
		||||
	// If records are left for configuration
 | 
			
		||||
	if(Object.keys(lights).length > 0)
 | 
			
		||||
	{
 | 
			
		||||
		$('#wh_topcontainer').toggle(false);
 | 
			
		||||
		$('#orb_ids_t, #btn_wiz_save').toggle(true);
 | 
			
		||||
 | 
			
		||||
		var lightOptions = [
 | 
			
		||||
					"top", "topleft", "topright",
 | 
			
		||||
					"bottom", "bottomleft", "bottomright",
 | 
			
		||||
					"left", "lefttop", "leftmiddle", "leftbottom",
 | 
			
		||||
					"right", "righttop", "rightmiddle", "rightbottom",
 | 
			
		||||
					"entire"
 | 
			
		||||
				];
 | 
			
		||||
 | 
			
		||||
		lightOptions.unshift("disabled");
 | 
			
		||||
 | 
			
		||||
		$('.lidsb').html("");
 | 
			
		||||
		var pos = "";
 | 
			
		||||
 | 
			
		||||
		for(var lightid in lights)
 | 
			
		||||
		{
 | 
			
		||||
			var orbId = lights[lightid].id;
 | 
			
		||||
			var orbIp = lights[lightid].ip;
 | 
			
		||||
			var orbHostname = lights[lightid].host;
 | 
			
		||||
 | 
			
		||||
			if ( orbHostname === "" )
 | 
			
		||||
				orbHostname = $.i18n('edt_dev_spec_lights_itemtitle');
 | 
			
		||||
 | 
			
		||||
			var options = "";
 | 
			
		||||
			for(var opt in lightOptions)
 | 
			
		||||
			{
 | 
			
		||||
				var val = lightOptions[opt];
 | 
			
		||||
				var txt = (val !== 'entire' && val !== 'disabled') ? 'conf_leds_layout_cl_' : 'wiz_ids_';
 | 
			
		||||
				options+= '<option value="'+val+'"';
 | 
			
		||||
				if(pos === val) options+=' selected="selected"';
 | 
			
		||||
				options+= '>'+$.i18n(txt+val)+'</option>';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var enabled = 'enabled'
 | 
			
		||||
			if ( orbId < 1 || orbId > 255 )
 | 
			
		||||
			{
 | 
			
		||||
				enabled = 'disabled'
 | 
			
		||||
				options = '<option value=disabled>'+$.i18n('wiz_atmoorb_unsupported')+'</option>';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var lightAnnotation ="";
 | 
			
		||||
			if ( orbIp !== "" )
 | 
			
		||||
			{
 | 
			
		||||
		      lightAnnotation = ': '+orbIp+'<br>('+orbHostname+')';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			$('.lidsb').append(createTableRow([orbId + lightAnnotation, '<select id="orb_'+lightid+'" '+enabled+' class="orb_sel_watch form-control">'
 | 
			
		||||
											   + options
 | 
			
		||||
											   + '</select>','<button class="btn btn-sm btn-primary" ' +enabled+ ' onClick=identify_atmoorb_device('+orbId+')>'
 | 
			
		||||
											   + $.i18n('wiz_identify_light',orbId)+'</button>']));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		$('.orb_sel_watch').bind("change", function(){
 | 
			
		||||
			var cC = 0;
 | 
			
		||||
			for(var key in lights)
 | 
			
		||||
			{
 | 
			
		||||
				if($('#orb_'+key).val() !== "disabled")
 | 
			
		||||
				{
 | 
			
		||||
					cC++;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if ( cC === 0)
 | 
			
		||||
				$('#btn_wiz_save').attr("disabled",true);
 | 
			
		||||
			else
 | 
			
		||||
				$('#btn_wiz_save').attr("disabled",false);
 | 
			
		||||
		});
 | 
			
		||||
		$('.orb_sel_watch').trigger('change');
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
	{
 | 
			
		||||
		var noLightsTxt = '<p style="font-weight:bold;color:red;">'+$.i18n('wiz_atmoorb_noLights')+'</p>';
 | 
			
		||||
		$('#wizp2_body').append(noLightsTxt);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function identify_atmoorb_device(orbId){
 | 
			
		||||
 | 
			
		||||
	let params = { id : orbId };
 | 
			
		||||
 | 
			
		||||
	const res = requestLedDeviceIdentification ("atmoorb", params);
 | 
			
		||||
	// TODO: error case unhandled
 | 
			
		||||
  	// res can be: false (timeout) or res.error (not found)
 | 
			
		||||
	if(res && !res.error){
 | 
			
		||||
		const r = res.info
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//****************************
 | 
			
		||||
// Wizard/Routines Nanoleaf
 | 
			
		||||
//****************************
 | 
			
		||||
 
 | 
			
		||||
@@ -165,7 +165,10 @@ void LedDevice::startRefreshTimer()
 | 
			
		||||
 | 
			
		||||
void LedDevice::stopRefreshTimer()
 | 
			
		||||
{
 | 
			
		||||
	_refreshTimer->stop();
 | 
			
		||||
	if ( _refreshTimer != nullptr )
 | 
			
		||||
	{
 | 
			
		||||
		_refreshTimer->stop();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int LedDevice::updateLeds(const std::vector<ColorRgb>& ledValues)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,20 +4,30 @@
 | 
			
		||||
 | 
			
		||||
// qt includes
 | 
			
		||||
#include <QUdpSocket>
 | 
			
		||||
#include <QNetworkInterface>
 | 
			
		||||
#include <QUrl>
 | 
			
		||||
#include <QHostInfo>
 | 
			
		||||
 | 
			
		||||
const quint16 MULTICAST_GROUPL_DEFAULT_PORT = 49692;
 | 
			
		||||
const int LEDS_DEFAULT_NUMBER = 24;
 | 
			
		||||
#include <chrono>
 | 
			
		||||
 | 
			
		||||
// Constants
 | 
			
		||||
namespace {
 | 
			
		||||
 | 
			
		||||
const QString MULTICAST_GROUP_DEFAULT_ADDRESS = "239.255.255.250";
 | 
			
		||||
const quint16 MULTICAST_GROUP_DEFAULT_PORT = 49692;
 | 
			
		||||
 | 
			
		||||
constexpr std::chrono::milliseconds DEFAULT_DISCOVERY_TIMEOUT{2000};
 | 
			
		||||
 | 
			
		||||
} //End of constants
 | 
			
		||||
 | 
			
		||||
LedDeviceAtmoOrb::LedDeviceAtmoOrb(const QJsonObject &deviceConfig)
 | 
			
		||||
	: LedDevice(deviceConfig)
 | 
			
		||||
	  , _udpSocket (nullptr)
 | 
			
		||||
	  , _multiCastGroupPort (MULTICAST_GROUPL_DEFAULT_PORT)
 | 
			
		||||
	  , _multicastGroup(MULTICAST_GROUP_DEFAULT_ADDRESS)
 | 
			
		||||
	  , _multiCastGroupPort (MULTICAST_GROUP_DEFAULT_PORT)
 | 
			
		||||
	  , _joinedMulticastgroup (false)
 | 
			
		||||
	  , _useOrbSmoothing (false)
 | 
			
		||||
	  , _transitiontime (0)
 | 
			
		||||
	  , _skipSmoothingDiff (0)
 | 
			
		||||
	  , _numLeds (LEDS_DEFAULT_NUMBER)
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -38,14 +48,24 @@ bool LedDeviceAtmoOrb::init(const QJsonObject &deviceConfig)
 | 
			
		||||
	if ( LedDevice::init(deviceConfig) )
 | 
			
		||||
	{
 | 
			
		||||
 | 
			
		||||
		_multicastGroup     = deviceConfig["output"].toString().toStdString().c_str();
 | 
			
		||||
		_multicastGroup     = deviceConfig["output"].toString(MULTICAST_GROUP_DEFAULT_ADDRESS);
 | 
			
		||||
		_multiCastGroupPort = static_cast<quint16>(deviceConfig["port"].toInt(MULTICAST_GROUP_DEFAULT_PORT));
 | 
			
		||||
		_useOrbSmoothing    = deviceConfig["useOrbSmoothing"].toBool(false);
 | 
			
		||||
		_transitiontime     = deviceConfig["transitiontime"].toInt(0);
 | 
			
		||||
		_skipSmoothingDiff  = deviceConfig["skipSmoothingDiff"].toInt(0);
 | 
			
		||||
		_multiCastGroupPort = static_cast<quint16>(deviceConfig["port"].toInt(MULTICAST_GROUPL_DEFAULT_PORT));
 | 
			
		||||
		_numLeds            = deviceConfig["numLeds"].toInt(LEDS_DEFAULT_NUMBER);
 | 
			
		||||
 | 
			
		||||
		QStringList orbIds = QStringUtils::split(deviceConfig["orbIds"].toString().simplified().remove(" "),",", QStringUtils::SplitBehavior::SkipEmptyParts);
 | 
			
		||||
 | 
			
		||||
		Debug(_log, "DeviceType        : %s", QSTRING_CSTR( this->getActiveDeviceType() ));
 | 
			
		||||
		Debug(_log, "LedCount          : %u", this->getLedCount());
 | 
			
		||||
		Debug(_log, "ColorOrder        : %s", QSTRING_CSTR( this->getColorOrder() ));
 | 
			
		||||
		Debug(_log, "RefreshTime       : %d", _refreshTimerInterval_ms);
 | 
			
		||||
		Debug(_log, "LatchTime         : %d", this->getLatchTime());
 | 
			
		||||
 | 
			
		||||
		Debug(_log, "MulticastGroup    : %s", QSTRING_CSTR(_multicastGroup));
 | 
			
		||||
		Debug(_log, "MulticastGroupPort: %d", _multiCastGroupPort);
 | 
			
		||||
		Debug(_log, "Orb ID list       : %s", QSTRING_CSTR(deviceConfig["orbIds"].toString()));
 | 
			
		||||
		Debug(_log, "Use Orb Smoothing : %d", _useOrbSmoothing);
 | 
			
		||||
		Debug(_log, "Skip SmoothingDiff: %d", _skipSmoothingDiff);
 | 
			
		||||
 | 
			
		||||
		_orbIds.clear();
 | 
			
		||||
 | 
			
		||||
		for (auto & id_str : orbIds)
 | 
			
		||||
@@ -69,6 +89,9 @@ bool LedDeviceAtmoOrb::init(const QJsonObject &deviceConfig)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		uint numberOrbs = _orbIds.size();
 | 
			
		||||
		uint configuredLedCount = this->getLedCount();
 | 
			
		||||
 | 
			
		||||
		if ( _orbIds.empty() )
 | 
			
		||||
		{
 | 
			
		||||
			this->setInError("No valid OrbIds found!");
 | 
			
		||||
@@ -76,8 +99,23 @@ bool LedDeviceAtmoOrb::init(const QJsonObject &deviceConfig)
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			_udpSocket = new QUdpSocket(this);
 | 
			
		||||
			isInitOK = true;
 | 
			
		||||
			if ( numberOrbs < configuredLedCount )
 | 
			
		||||
			{
 | 
			
		||||
				QString errorReason = QString("Not enough Orbs [%1] for configured LEDs [%2] found!")
 | 
			
		||||
										  .arg(numberOrbs)
 | 
			
		||||
										  .arg(configuredLedCount);
 | 
			
		||||
				this->setInError(errorReason);
 | 
			
		||||
				isInitOK = false;
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				if ( numberOrbs > configuredLedCount )
 | 
			
		||||
				{
 | 
			
		||||
					Info(_log, "%s: More Orbs [%u] than configured LEDs [%u].", QSTRING_CSTR(this->getActiveDeviceType()), numberOrbs, configuredLedCount );
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				isInitOK = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return isInitOK;
 | 
			
		||||
@@ -88,30 +126,39 @@ int LedDeviceAtmoOrb::open()
 | 
			
		||||
	int retval = -1;
 | 
			
		||||
	_isDeviceReady = false;
 | 
			
		||||
 | 
			
		||||
	if ( _udpSocket == nullptr )
 | 
			
		||||
	{
 | 
			
		||||
		_udpSocket = new QUdpSocket();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Try to bind the UDP-Socket
 | 
			
		||||
	if ( _udpSocket != nullptr )
 | 
			
		||||
	{
 | 
			
		||||
		_groupAddress = QHostAddress(_multicastGroup);
 | 
			
		||||
		if ( !_udpSocket->bind(QHostAddress::AnyIPv4, _multiCastGroupPort, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint) )
 | 
			
		||||
		if ( _udpSocket->state() != QAbstractSocket::BoundState )
 | 
			
		||||
		{
 | 
			
		||||
			QString errortext = QString ("(%1) %2, MulticastGroup: (%3)").arg(_udpSocket->error()).arg(_udpSocket->errorString(), _multicastGroup);
 | 
			
		||||
			this->setInError( errortext );
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			_joinedMulticastgroup = _udpSocket->joinMulticastGroup(_groupAddress);
 | 
			
		||||
			if ( !_joinedMulticastgroup )
 | 
			
		||||
			if ( !_udpSocket->bind(QHostAddress(QHostAddress::AnyIPv4), 0 ) )
 | 
			
		||||
			{
 | 
			
		||||
				QString errortext = QString ("(%1) %2, MulticastGroup: (%3)").arg(_udpSocket->error()).arg(_udpSocket->errorString(), _multicastGroup);
 | 
			
		||||
				QString errortext = QString ("Socket bind failed: (%1) %2, MulticastGroup: (%3)").arg(_udpSocket->error()).arg(_udpSocket->errorString(), _multicastGroup);
 | 
			
		||||
				this->setInError( errortext );
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				// Everything is OK, device is ready
 | 
			
		||||
				_isDeviceReady = true;
 | 
			
		||||
				retval = 0;
 | 
			
		||||
				_groupAddress = QHostAddress(_multicastGroup);
 | 
			
		||||
				_joinedMulticastgroup = _udpSocket->joinMulticastGroup(_groupAddress);
 | 
			
		||||
				if ( !_joinedMulticastgroup )
 | 
			
		||||
				{
 | 
			
		||||
					QString errortext = QString ("Joining Multicastgroup failed: (%1) %2, MulticastGroup: (%3)").arg(_udpSocket->error()).arg(_udpSocket->errorString(), _multicastGroup);
 | 
			
		||||
					this->setInError( errortext );
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ( ! _isDeviceInError )
 | 
			
		||||
		{
 | 
			
		||||
			// Everything is OK, device is ready
 | 
			
		||||
			_isDeviceReady = true;
 | 
			
		||||
			retval = 0;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return retval;
 | 
			
		||||
}
 | 
			
		||||
@@ -123,6 +170,11 @@ int LedDeviceAtmoOrb::close()
 | 
			
		||||
 | 
			
		||||
	if ( _udpSocket != nullptr )
 | 
			
		||||
	{
 | 
			
		||||
		if ( _udpSocket->state() == QAbstractSocket::BoundState )
 | 
			
		||||
		{
 | 
			
		||||
			_udpSocket->leaveMulticastGroup(_groupAddress);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Test, if device requires closing
 | 
			
		||||
		if ( _udpSocket->isOpen() )
 | 
			
		||||
		{
 | 
			
		||||
@@ -155,12 +207,17 @@ int LedDeviceAtmoOrb::write(const std::vector <ColorRgb> &ledValues)
 | 
			
		||||
		commandType = 2;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Iterate through colors and set Orb color
 | 
			
		||||
	// Start off with idx 1 as 0 is reserved for controlling all orbs at once
 | 
			
		||||
	int idx = 1;
 | 
			
		||||
 | 
			
		||||
	for (const ColorRgb &color : ledValues)
 | 
			
		||||
	ColorRgb color;
 | 
			
		||||
	for (int idx = 0; idx < _orbIds.size(); idx++ )
 | 
			
		||||
	{
 | 
			
		||||
		if ( idx < static_cast<int>(ledValues.size()) )
 | 
			
		||||
		{
 | 
			
		||||
			color = ledValues[idx];
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			color = ColorRgb::BLACK;
 | 
			
		||||
		}
 | 
			
		||||
		// Retrieve last send colors
 | 
			
		||||
		int lastRed = lastColorRedMap[idx];
 | 
			
		||||
		int lastGreen = lastColorGreenMap[idx];
 | 
			
		||||
@@ -171,33 +228,16 @@ int LedDeviceAtmoOrb::write(const std::vector <ColorRgb> &ledValues)
 | 
			
		||||
				abs(color.green - lastGreen) >= _skipSmoothingDiff))
 | 
			
		||||
		{
 | 
			
		||||
			// Skip Orb smoothing when using  (command type 4)
 | 
			
		||||
			for (int i = 0; i < _orbIds.size(); i++)
 | 
			
		||||
			{
 | 
			
		||||
				if (_orbIds[i] == idx)
 | 
			
		||||
				{
 | 
			
		||||
					setColor(idx, color, 4);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
			// Send color
 | 
			
		||||
			for (int i = 0; i < _orbIds.size(); i++)
 | 
			
		||||
			{
 | 
			
		||||
				if (_orbIds[i] == idx)
 | 
			
		||||
				{
 | 
			
		||||
					setColor(idx, color, commandType);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			commandType = 4;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Send color
 | 
			
		||||
		setColor(_orbIds[idx], color, commandType);
 | 
			
		||||
 | 
			
		||||
		// Store last colors send for light id
 | 
			
		||||
		lastColorRedMap[idx]   = color.red;
 | 
			
		||||
		lastColorGreenMap[idx] = color.green;
 | 
			
		||||
		lastColorBlueMap[idx]  = color.blue;
 | 
			
		||||
 | 
			
		||||
		// Next light id.
 | 
			
		||||
		idx++;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return 0;
 | 
			
		||||
@@ -227,12 +267,117 @@ void LedDeviceAtmoOrb::setColor(int orbId, const ColorRgb &color, int commandTyp
 | 
			
		||||
	bytes[6] = static_cast<char>(color.green);
 | 
			
		||||
	bytes[7] = static_cast<char>(color.blue);
 | 
			
		||||
 | 
			
		||||
	//std::cout << "Orb [" << orbId << "] Cmd [" << bytes.toHex(':').toStdString() <<"]"<< std::endl;
 | 
			
		||||
 | 
			
		||||
	sendCommand(bytes);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LedDeviceAtmoOrb::sendCommand(const QByteArray &bytes)
 | 
			
		||||
{
 | 
			
		||||
	//Debug ( _log, "command: [%s] -> %s:%u", QSTRING_CSTR( QString(bytes.toHex())), QSTRING_CSTR(_groupAddress.toString()), _multiCastGroupPort );
 | 
			
		||||
	_udpSocket->writeDatagram(bytes.data(), bytes.size(), _groupAddress, _multiCastGroupPort);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QJsonObject LedDeviceAtmoOrb::discover()
 | 
			
		||||
{
 | 
			
		||||
	QJsonObject devicesDiscovered;
 | 
			
		||||
	devicesDiscovered.insert("ledDeviceType", _activeDeviceType );
 | 
			
		||||
 | 
			
		||||
	QJsonArray deviceList;
 | 
			
		||||
 | 
			
		||||
	if ( open() == 0 )
 | 
			
		||||
	{
 | 
			
		||||
		Debug ( _log, "Send discovery requests to all AtmoOrbs" );
 | 
			
		||||
		setColor(0, ColorRgb::BLACK, 8);
 | 
			
		||||
 | 
			
		||||
		if ( _udpSocket->waitForReadyRead(DEFAULT_DISCOVERY_TIMEOUT.count()) )
 | 
			
		||||
		{
 | 
			
		||||
			while (_udpSocket->waitForReadyRead(500))
 | 
			
		||||
			{
 | 
			
		||||
				QByteArray datagram;
 | 
			
		||||
 | 
			
		||||
				while (_udpSocket->hasPendingDatagrams())
 | 
			
		||||
				{
 | 
			
		||||
					datagram.resize(_udpSocket->pendingDatagramSize());
 | 
			
		||||
					QHostAddress senderIP;
 | 
			
		||||
					quint16 senderPort;
 | 
			
		||||
 | 
			
		||||
					_udpSocket->readDatagram(datagram.data(), datagram.size(), &senderIP, &senderPort);
 | 
			
		||||
 | 
			
		||||
					if ( datagram.size() == 1 )
 | 
			
		||||
					{
 | 
			
		||||
						unsigned char orbId = datagram[0];
 | 
			
		||||
						if ( orbId > 0 )
 | 
			
		||||
						{
 | 
			
		||||
							Debug(_log, "Orb ID (%d) discovered at [%s]", orbId, QSTRING_CSTR(senderIP.toString()));
 | 
			
		||||
							_services.insert(orbId, senderIP);
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		close();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	QMap<int, QHostAddress>::iterator i;
 | 
			
		||||
	for (i = _services.begin(); i != _services.end(); ++i)
 | 
			
		||||
	{
 | 
			
		||||
		QJsonObject obj;
 | 
			
		||||
 | 
			
		||||
		obj.insert("id", i.key());
 | 
			
		||||
		obj.insert("ip", i.value().toString());
 | 
			
		||||
 | 
			
		||||
		QHostInfo hostInfo = QHostInfo::fromName(i.value().toString());
 | 
			
		||||
		if (hostInfo.error() == QHostInfo::NoError )
 | 
			
		||||
		{
 | 
			
		||||
			QString hostname = hostInfo.hostName();
 | 
			
		||||
			//Seems that for Windows no local domain name is resolved
 | 
			
		||||
			if (!hostInfo.localDomainName().isEmpty() )
 | 
			
		||||
			{
 | 
			
		||||
				obj.insert("hostname", hostname.remove("."+hostInfo.localDomainName()));
 | 
			
		||||
				obj.insert("domain", hostInfo.localDomainName());
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				int domainPos = hostname.indexOf('.');
 | 
			
		||||
				obj.insert("hostname", hostname.left(domainPos));
 | 
			
		||||
				obj.insert("domain", hostname.mid(domainPos+1));
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		deviceList  << obj;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	devicesDiscovered.insert("devices", deviceList);
 | 
			
		||||
	Debug(_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() );
 | 
			
		||||
 | 
			
		||||
	return devicesDiscovered;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LedDeviceAtmoOrb::identify(const QJsonObject& params)
 | 
			
		||||
{
 | 
			
		||||
	//Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData());
 | 
			
		||||
 | 
			
		||||
	int orbId = 0;
 | 
			
		||||
	if ( params["id"].isString() )
 | 
			
		||||
	{
 | 
			
		||||
		orbId = params["id"].toString().toInt();
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
	{
 | 
			
		||||
		orbId = params["id"].toInt();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ( orbId >0 && orbId < 256 )
 | 
			
		||||
	{
 | 
			
		||||
		Debug (_log, "Orb ID [%d]", orbId);
 | 
			
		||||
		if ( open() == 0 )
 | 
			
		||||
		{
 | 
			
		||||
			setColor(orbId, ColorRgb::BLACK, 9);
 | 
			
		||||
			close();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
	{
 | 
			
		||||
		Warning(_log, "Identification of Orb with ID='%d' skipped. ID must be in range 1-255", orbId);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,13 +11,10 @@
 | 
			
		||||
 | 
			
		||||
class QUdpSocket;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Implementation for the AtmoOrb
 | 
			
		||||
 *
 | 
			
		||||
 * To use set the device to "atmoorb".
 | 
			
		||||
 *
 | 
			
		||||
 * @author RickDB (github)
 | 
			
		||||
 */
 | 
			
		||||
///
 | 
			
		||||
/// Implementation of the LedDevice interface for sending to
 | 
			
		||||
/// AtmoOrb devices via network
 | 
			
		||||
///
 | 
			
		||||
class LedDeviceAtmoOrb : public LedDevice
 | 
			
		||||
{
 | 
			
		||||
	Q_OBJECT
 | 
			
		||||
@@ -43,6 +40,27 @@ public:
 | 
			
		||||
	///
 | 
			
		||||
	static LedDevice* construct(const QJsonObject &deviceConfig);
 | 
			
		||||
 | 
			
		||||
	///
 | 
			
		||||
	/// @brief Discover AtmoOrb devices available (for configuration).
 | 
			
		||||
	///
 | 
			
		||||
	/// @return A JSON structure holding a list of devices found
 | 
			
		||||
	///
 | 
			
		||||
	virtual QJsonObject discover() override;
 | 
			
		||||
 | 
			
		||||
	///
 | 
			
		||||
	/// @brief Send an update to the AtmoOrb device to identify it.
 | 
			
		||||
	///
 | 
			
		||||
	/// Following parameters are required
 | 
			
		||||
	/// @code
 | 
			
		||||
	/// {
 | 
			
		||||
	///     "orbId"  : "orb identifier in the range of (1-255)",
 | 
			
		||||
	/// }
 | 
			
		||||
	///@endcode
 | 
			
		||||
	///
 | 
			
		||||
	/// @param[in] params Parameters to address device
 | 
			
		||||
	///
 | 
			
		||||
	virtual void identify(const QJsonObject& params) override;
 | 
			
		||||
 | 
			
		||||
protected:
 | 
			
		||||
 | 
			
		||||
	///
 | 
			
		||||
@@ -111,15 +129,9 @@ private:
 | 
			
		||||
	/// use Orbs own (external) smoothing algorithm
 | 
			
		||||
	bool _useOrbSmoothing;
 | 
			
		||||
 | 
			
		||||
	/// Transition time between colors (not implemented)
 | 
			
		||||
	int _transitiontime;
 | 
			
		||||
 | 
			
		||||
	// Maximum allowed color difference, will skip Orb (external) smoothing once reached
 | 
			
		||||
	int _skipSmoothingDiff;
 | 
			
		||||
 | 
			
		||||
	/// Number of leds in Orb, used to determine buffer size
 | 
			
		||||
	int _numLeds;
 | 
			
		||||
 | 
			
		||||
	/// Array of the orb ids.
 | 
			
		||||
	QVector<int> _orbIds;
 | 
			
		||||
 | 
			
		||||
@@ -127,6 +139,8 @@ private:
 | 
			
		||||
	QMap<int, int> lastColorRedMap;
 | 
			
		||||
	QMap<int, int> lastColorGreenMap;
 | 
			
		||||
	QMap<int, int> lastColorBlueMap;
 | 
			
		||||
 | 
			
		||||
	QMultiMap<int, QHostAddress> _services;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#endif // LEDEVICEATMOORB_H
 | 
			
		||||
 
 | 
			
		||||
@@ -319,7 +319,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration()
 | 
			
		||||
		{
 | 
			
		||||
			if ( _panelLedCount > this->getLedCount() )
 | 
			
		||||
			{
 | 
			
		||||
				Info(_log, "Nanoleaf: More panels [%u] than configured LEDs [%u].", _panelLedCount, configuredLedCount );
 | 
			
		||||
				Info(_log, "%s: More panels [%u] than configured LEDs [%u].", QSTRING_CSTR(this->getActiveDeviceType()), _panelLedCount, configuredLedCount );
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Check, if start position + number of configured LEDs is greater than number of panels available
 | 
			
		||||
@@ -449,9 +449,7 @@ QJsonObject LedDeviceNanoleaf::getProperties(const QJsonObject& params)
 | 
			
		||||
void LedDeviceNanoleaf::identify(const QJsonObject& params)
 | 
			
		||||
{
 | 
			
		||||
	Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() );
 | 
			
		||||
	QJsonObject properties;
 | 
			
		||||
 | 
			
		||||
	// Get Nanoleaf device properties
 | 
			
		||||
	QString host = params["host"].toString("");
 | 
			
		||||
	if ( !host.isEmpty() )
 | 
			
		||||
	{
 | 
			
		||||
 
 | 
			
		||||
@@ -235,9 +235,7 @@ void LedDeviceWled::identify(const QJsonObject& /*params*/)
 | 
			
		||||
{
 | 
			
		||||
#if 0
 | 
			
		||||
	Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData());
 | 
			
		||||
	QJsonObject properties;
 | 
			
		||||
 | 
			
		||||
	// Get Nanoleaf device properties
 | 
			
		||||
	QString host = params["host"].toString("");
 | 
			
		||||
	if ( !host.isEmpty() )
 | 
			
		||||
	{
 | 
			
		||||
 
 | 
			
		||||
@@ -1357,7 +1357,7 @@ QJsonObject LedDeviceYeelight::discover()
 | 
			
		||||
 | 
			
		||||
	QJsonArray deviceList;
 | 
			
		||||
 | 
			
		||||
	// Discover WLED Devices
 | 
			
		||||
	// Discover Yeelight Devices
 | 
			
		||||
	SSDPDiscover discover;
 | 
			
		||||
	discover.setPort(SSDP_PORT);
 | 
			
		||||
	discover.skipDuplicateKeys(true);
 | 
			
		||||
 
 | 
			
		||||
@@ -5,26 +5,29 @@
 | 
			
		||||
		"output": {
 | 
			
		||||
			"type": "string",
 | 
			
		||||
			"title":"edt_dev_spec_outputPath_title",
 | 
			
		||||
			"default":"ttyACM0",
 | 
			
		||||
			"default":"auto",
 | 
			
		||||
			"propertyOrder" : 1
 | 
			
		||||
		},
 | 
			
		||||
		"rate": {
 | 
			
		||||
			"type": "integer",
 | 
			
		||||
			"title":"edt_dev_spec_baudrate_title",
 | 
			
		||||
			"default": 1000000,
 | 
			
		||||
			"default": 115200,
 | 
			
		||||
			"access" : "advanced",
 | 
			
		||||
			"propertyOrder" : 2
 | 
			
		||||
		},
 | 
			
		||||
		"delayAfterConnect": {
 | 
			
		||||
			"type": "integer",
 | 
			
		||||
			"title":"edt_dev_spec_delayAfterConnect_title",
 | 
			
		||||
			"default": 1500,
 | 
			
		||||
			"default": 0,
 | 
			
		||||
			"append" : "ms",
 | 
			
		||||
			"access" : "expert",
 | 
			
		||||
			"propertyOrder" : 3
 | 
			
		||||
		},
 | 
			
		||||
		"lightberry_apa102_mode": {
 | 
			
		||||
			"type": "boolean",
 | 
			
		||||
			"title":"edt_dev_spec_LBap102Mode_title",
 | 
			
		||||
			"default": false,
 | 
			
		||||
			"access" : "advanced",
 | 
			
		||||
			"propertyOrder" : 4
 | 
			
		||||
		},
 | 
			
		||||
		"latchTime": {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,22 +2,24 @@
 | 
			
		||||
	"type":"object",
 | 
			
		||||
	"required":true,
 | 
			
		||||
	"properties":{
 | 
			
		||||
		"output": {
 | 
			
		||||
			"type": "string",
 | 
			
		||||
			"title":"edt_dev_spec_multicastGroup_title",
 | 
			
		||||
			"default" : "239.15.18.2",
 | 
			
		||||
			"propertyOrder" : 1
 | 
			
		||||
		},
 | 
			
		||||
		"orbIds": {
 | 
			
		||||
			"type": "string",
 | 
			
		||||
			"title":"edt_dev_spec_orbIds_title",
 | 
			
		||||
			"default": "1",
 | 
			
		||||
			"default": "",
 | 
			
		||||
			"propertyOrder" : 1
 | 
			
		||||
		},
 | 
			
		||||
		"useOrbSmoothing": {
 | 
			
		||||
			"type": "boolean",
 | 
			
		||||
			"title":"edt_dev_spec_useOrbSmoothing_title",
 | 
			
		||||
			"default": true,
 | 
			
		||||
			"access" : "advanced",
 | 
			
		||||
			"propertyOrder" : 2
 | 
			
		||||
		},
 | 
			
		||||
		"numLeds": {
 | 
			
		||||
			"type": "integer",
 | 
			
		||||
			"title":"edt_dev_spec_numberOfLeds_title",
 | 
			
		||||
			"default": 24,
 | 
			
		||||
		"output": {
 | 
			
		||||
			"type": "string",
 | 
			
		||||
			"title":"edt_dev_spec_multicastGroup_title",
 | 
			
		||||
			"default" : "239.255.255.250",
 | 
			
		||||
			"access" : "expert",
 | 
			
		||||
			"propertyOrder" : 3
 | 
			
		||||
		},
 | 
			
		||||
		"port": {
 | 
			
		||||
@@ -25,25 +27,20 @@
 | 
			
		||||
			"title":"edt_dev_spec_port_title",
 | 
			
		||||
			"minimum" : 0,
 | 
			
		||||
			"maximum" : 65535,
 | 
			
		||||
			"default": 49.692,
 | 
			
		||||
			"default": 49692,
 | 
			
		||||
			"access" : "expert",
 | 
			
		||||
			"propertyOrder" : 4
 | 
			
		||||
		},
 | 
			
		||||
		"useOrbSmoothing": {
 | 
			
		||||
			"type": "boolean",
 | 
			
		||||
			"title":"edt_dev_spec_useOrbSmoothing_title",
 | 
			
		||||
			"default": true,
 | 
			
		||||
			"propertyOrder" : 5
 | 
			
		||||
		},
 | 
			
		||||
		},		
 | 
			
		||||
		"latchTime": {
 | 
			
		||||
			"type": "integer",
 | 
			
		||||
			"title":"edt_dev_spec_latchtime_title",
 | 
			
		||||
			"default": 0,
 | 
			
		||||
			"default": 30,
 | 
			
		||||
			"append" : "edt_append_ms",
 | 
			
		||||
			"minimum": 0,
 | 
			
		||||
			"maximum": 1000,
 | 
			
		||||
			"access" : "expert",
 | 
			
		||||
			"propertyOrder" : 6
 | 
			
		||||
		}	
 | 
			
		||||
			"propertyOrder" : 5
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"additionalProperties": true
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -47,9 +47,9 @@
 | 
			
		||||
			"type": "integer",
 | 
			
		||||
			"title":"edt_dev_spec_transistionTimeExtra_title",
 | 
			
		||||
			"default" : 0,
 | 
			
		||||
			"step": 10,
 | 
			
		||||
			"step": 100,
 | 
			
		||||
			"minimum" : 0,
 | 
			
		||||
			"maximum" : 3000,
 | 
			
		||||
			"maximum" : 8000,
 | 
			
		||||
			"append" : "ms",
 | 
			
		||||
			"access" : "advanced",
 | 
			
		||||
			"propertyOrder" : 4
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user