diff --git a/docs/colors-lmair.txt b/docs/colors-lmair.txt index 62a0180..5b06b1f 100644 --- a/docs/colors-lmair.txt +++ b/docs/colors-lmair.txt @@ -17,20 +17,48 @@ dark yellow 404000 dark magenta 400040 dark cyan 004040 -Background purple-green {group:background;color:#8000ff,#004000,#8000ff,#004000} bicolor purple-green 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=8000ff,004000,8000ff,004000,8000ff,004000,8000ff&turnOn=true& -Background green-purple {group:background;color:#004000,#8000ff,#004000,#8000ff} bicolor green-purple 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=004000,8000ff,004000,8000ff,004000,8000ff,004000&turnOn=true& -Background blue-green {group:background;color:#0000ff,#004000,#0000ff,#004000} bicolor blue-green 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=0000ff,00ff00,0000ff,00ff00,0000ff,00ff00,0000ff&turnOn=true& -Background green-blue {group:background;color:#004000,#0000ff,#004000,#0000ff} bicolor green-blue 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=00ff00,0000ff,00ff00,0000ff,00ff00,0000ff,00ff00&turnOn=true& +Background Rainbow Blue-Red {group:background;color:#0000ff,#00ffff,#00ff00,#ffff00,#ff8000,#ff0000} 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=0000ff,00ffff,00ff00,80ff00,ffff00,ff3000,ff0000&turnOn=true& +Background Rainbow Red-Blue {group:background;color:#ff0000,#ff8000,#ffff00,#00ff00,#00ffff,#0000ff} 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ff0000,ff3000,ffff00,80ff00,00ff00,00ffff,0000ff&turnOn=true& -Background blue-cyan {group:background;color:#0000ff,#00ffff,#0000ff,#00ffff} bicolor blue-cyan 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=0000ff,00ffff,0000ff,00ffff,0000ff,00ffff,0000ff&turnOn=true& -Background cyan-blue {group:background;color:#00ffff,#0000ff,#00ffff,#0000ff} bicolor cyan-blue 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=00ffff,0000ff,00ffff,0000ff,00ffff,0000ff,00ffff&turnOn=true& +Background Rainbow Dark Blue-Red {group:background;color:#000080,#008080,#008000,#808000,#808000,#800000} Hybrid Rainbow Dark Blue-Red 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=000080,008080,008000,808000,808000,803000,800000&turnOn=true& +Background Rainbow Dark Red-Blue {group:background;color:#800000,#808000,#808000,#008000,#008080,#000080} Hybrid Rainbow Dark Red-Blue 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=800000,803000,808000,808000,008000,008080,000080&turnOn=true& -Background blue-yellow {group:background;color:#0000ff,#ffff00,#0000ff,#ffff00} bicolor blue-yellow 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=0000ff,ffff00,0000ff,ffff00,0000ff,ffff00,0000ff&turnOn=true& -Background yellow-blue {group:background;color:#ffff00,#0000ff,#ffff00,#0000ff} bicolor yellow-blue 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ffff00,0000ff,ffff00,0000ff,ffff00,0000ff,ffff00&turnOn=true& +Background purple-green {group:background;color:#8000ff,#004000,#8000ff,#004000,#8000ff} bicolor purple-green 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=8000ff,004000,8000ff,004000,8000ff,004000,8000ff&turnOn=true& +Background green-purple {group:background;color:#004000,#8000ff,#004000,#8000ff,#004000} bicolor green-purple 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=004000,8000ff,004000,8000ff,004000,8000ff,004000&turnOn=true& -Background green-yellow {group:background;color:#00ff00,#ffff00,#00ff00,#ffff00} bicolor green-yellow 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=00ff00,ffff00,00ff00,ffff00,00ff00,ffff00,00ff00&turnOn=true& -Background yellow-green {group:background;color:#ffff00,#00ff00,#ffff00,#00ff00} bicolor yellow-green 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ffff00,00ff00,ffff00,00ff00,ffff00,00ff00,ffff00&turnOn=true& +Background blue-green {group:background;color:#0000ff,#004000,#0000ff,#004000,#0000ff} bicolor blue-green 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=0000ff,00ff00,0000ff,00ff00,0000ff,00ff00,0000ff&turnOn=true& +Background green-blue {group:background;color:#004000,#0000ff,#004000,#0000ff,#004000} bicolor green-blue 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=00ff00,0000ff,00ff00,0000ff,00ff00,0000ff,00ff00&turnOn=true& -Background purple-yellow {group:background;color:#8000ff,#ffff00,#8000ff,#ffff00} bicolor purple-yellow 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=8000ff,ffff00,8000ff,ffff00,8000ff,ffff00,8000ff&turnOn=true& -Background yellow-purple {group:background;color:#ffff00,#8000ff,#ffff00,#8000ff} bicolor yellow-purple 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ffff00,8000ff,ffff00,8000ff,ffff00,8000ff,ffff00&turnOn=true& +Background blue-cyan {group:background;color:#0000ff,#00ffff,#0000ff,#00ffff,#0000ff} bicolor blue-cyan 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=0000ff,00ffff,0000ff,00ffff,0000ff,00ffff,0000ff&turnOn=true& +Background cyan-blue {group:background;color:#00ffff,#0000ff,#00ffff,#0000ff,#00ffff} bicolor cyan-blue 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=00ffff,0000ff,00ffff,0000ff,00ffff,0000ff,00ffff&turnOn=true& + +Background blue-yellow {group:background;color:#0000ff,#ffff00,#0000ff,#ffff00,#0000ff} bicolor blue-yellow 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=0000ff,ffff00,0000ff,ffff00,0000ff,ffff00,0000ff&turnOn=true& +Background yellow-blue {group:background;color:#ffff00,#0000ff,#ffff00,#0000ff,#ffff00} bicolor yellow-blue 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ffff00,0000ff,ffff00,0000ff,ffff00,0000ff,ffff00&turnOn=true& + +Background blue-white {group:background;color:#0000ff,#ffffff,#0000ff,#ffffff,#0000ff} bicolor blue-white 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=0000ff,ffffff,0000ff,ffffff,0000ff,ffffff,0000ff&turnOn=true& +Background white-blue {group:background;color:#ffffff,#0000ff,#ffffff,#0000ff,#ffffff} bicolor white-blue 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ffffff,0000ff,ffffff,0000ff,ffffff,0000ff,ffffff&turnOn=true& + +Background green-yellow {group:background;color:#00ff00,#ffff00,#00ff00,#ffff00,#00ff00} bicolor green-yellow 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=00ff00,ffff00,00ff00,ffff00,00ff00,ffff00,00ff00&turnOn=true& +Background yellow-green {group:background;color:#ffff00,#00ff00,#ffff00,#00ff00,#ffff00} bicolor yellow-green 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ffff00,00ff00,ffff00,00ff00,ffff00,00ff00,ffff00&turnOn=true& + +Background purple-yellow {group:background;color:#8000ff,#ffff00,#8000ff,#ffff00,#8000ff} bicolor purple-yellow 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=8000ff,ffff00,8000ff,ffff00,8000ff,ffff00,8000ff&turnOn=true& +Background yellow-purple {group:background;color:#ffff00,#8000ff,#ffff00,#8000ff,#ffff00} bicolor yellow-purple 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ffff00,8000ff,ffff00,8000ff,ffff00,8000ff,ffff00&turnOn=true& + +Background purple-orange {group:background;color:#8000ff,#ff8000,#8000ff,#ff8000,#8000ff} bicolor purple-yellow 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=8000ff,ff8000,8000ff,ff8000,8000ff,ff8000,8000ff&turnOn=true& +Background orange-purple {group:background;color:#ff8000,#8000ff,#ff8000,#8000ff,#ff8000} bicolor yellow-purple 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ff8000,8000ff,ff8000,8000ff,ff8000,8000ff,ff8000&turnOn=true& + +Background red-orange {group:background;color:#ff0000,#ff8000,#ff0000,#ff8000,#ff0000} bicolor purple-yellow 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ff0000,ff8000,ff0000,ff8000,ff0000,ff8000,ff0000&turnOn=true& +Background orange-red {group:background;color:#ff8000,#ff0000,#ff8000,#ff0000,#ff8000} bicolor yellow-purple 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ff8000,ff0000,ff8000,ff0000,ff8000,ff0000,ff8000&turnOn=true& + +Background yellow-orange {group:background;color:#ffff00,#ff8000,#ffff00,#ff8000,#ffff00} bicolor purple-yellow 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ffff00,ff8000,ffff00,ff8000,ffff00,ff8000,ffff00&turnOn=true& +Background orange-yellow {group:background;color:#ff8000,#ffff00,#ff8000,#ffff00,#ff8000} bicolor yellow-purple 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ff8000,ffff00,ff8000,ffff00,ff8000,ffff00,ff8000&turnOn=true& + +Background red-green {group:background;color:#ff0000,#00ff00,#ff0000,#00ff00,#ff0000} bicolor red-green 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ff0000,00ff00,ff0000,00ff00,ff0000,00ff00,ff0000&turnOn=true& +Background green-red {group:background;color:#00ff00,#ff0000,#00ff00,#ff0000,#00ff00} bicolor green-red 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=00ff00,ff0000,00ff00,ff0000,00ff00,ff0000,00ff00&turnOn=true& + +Background red-blue {group:background;color:#ff0000,#0000ff,#ff0000,#0000ff,#ff0000} bicolor red-blue 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ff0000,0000ff,ff0000,0000ff,ff0000,0000ff,ff0000&turnOn=true& +Background blue-red {group:background;color:#0000ff,#ff0000,#0000ff,#ff0000,#0000ff} bicolor blue-red 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=0000ff,ff0000,0000ff,ff0000,0000ff,ff0000,0000ff&turnOn=true& + +Background green-orange {group:background;color:#00ff00,#ff8000,#00ff00,#ff8000,#00ff00} bicolor green-orange 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=00ff00,ff8000,00ff00,ff8000,00ff00,ff8000,00ff00&turnOn=true& +Background orange-green {group:background;color:#ff8000,#00ff00,#ff8000,#00ff00,#ff8000} bicolor orange-green 192.168.178.30:8888/v1/hybrid/json/hexColor?hexColors=ff8000,00ff00,ff8000,00ff00,ff8000,00ff00,ff8000&turnOn=true& diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/color/RGBColor.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/color/RGBColor.kt index ef81eec..02f7d91 100644 --- a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/color/RGBColor.kt +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/color/RGBColor.kt @@ -12,6 +12,14 @@ class RGBColor( blue: Int = 0 ) : RGBBaseColor(red, green, blue) { + constructor(value: Long) : this( + red = min(a = 255, b = (value and 0x00ff0000L shr 16).toInt()), + green = min(a = 255, b = (value and 0x0000ff00L shr 8).toInt()), + blue = min(a = 255, b = (value and 0x000000ffL).toInt()) + ) + + constructor(hex: String) : this(java.lang.Long.decode(if (hex.startsWith("#") || hex.startsWith("0x")) hex else "#$hex")) + override fun parameterMap(): Map = mapOf( "Red" to red, "Green" to green, diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterface.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterface.kt similarity index 93% rename from klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterface.kt rename to klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterface.kt index c9c1750..c9f9804 100644 --- a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterface.kt +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterface.kt @@ -5,18 +5,18 @@ import jssc.SerialPortException import org.apache.commons.lang3.StringUtils -open class DMXInterface { +open class DmxInterface { val dmxFrame = DmxFrame() private var serialPort: SerialPort? = null companion object { - fun load(type: DMXInterfaceType): DMXInterface { + fun load(type: DmxInterfaceType): DmxInterface { return when (type) { - DMXInterfaceType.Serial -> DMXInterface() - DMXInterfaceType.Dummy -> DMXInterfaceDummy() - DMXInterfaceType.Rest -> DMXInterfaceRest() + DmxInterfaceType.Serial -> DmxInterface() + DmxInterfaceType.Dummy -> DmxInterfaceDummy() + DmxInterfaceType.Rest -> DmxInterfaceRest() } } } diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterfaceDummy.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterfaceDummy.kt similarity index 92% rename from klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterfaceDummy.kt rename to klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterfaceDummy.kt index 2488868..cd5ae59 100644 --- a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterfaceDummy.kt +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterfaceDummy.kt @@ -1,7 +1,7 @@ package de.visualdigits.kotlin.klanglicht.model.dmx -class DMXInterfaceDummy : DMXInterface() { +class DmxInterfaceDummy : DmxInterface() { override fun toString(): String { return repr() diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterfaceRest.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterfaceRest.kt similarity index 92% rename from klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterfaceRest.kt rename to klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterfaceRest.kt index 46ee244..9576fc1 100644 --- a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterfaceRest.kt +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterfaceRest.kt @@ -1,6 +1,6 @@ package de.visualdigits.kotlin.klanglicht.model.dmx -class DMXInterfaceRest : DMXInterface() { +class DmxInterfaceRest : DmxInterface() { override fun toString(): String { return repr() diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterfaceType.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterfaceType.kt similarity index 74% rename from klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterfaceType.kt rename to klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterfaceType.kt index 25f63d4..d1d0269 100644 --- a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DMXInterfaceType.kt +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxInterfaceType.kt @@ -1,6 +1,6 @@ package de.visualdigits.kotlin.klanglicht.model.dmx -enum class DMXInterfaceType { +enum class DmxInterfaceType { Serial, Dummy, diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxRepeater.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxRepeater.kt index 8905129..33051d4 100644 --- a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxRepeater.kt +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/dmx/DmxRepeater.kt @@ -3,7 +3,7 @@ package de.visualdigits.kotlin.klanglicht.model.dmx import org.slf4j.LoggerFactory class DmxRepeater( - val dmxInterface: DMXInterface, + val dmxInterface: DmxInterface, val dmxFrameTime: Long ) : Thread("DMX Repeater") { @@ -18,8 +18,8 @@ class DmxRepeater( var dmxRepeater: DmxRepeater? = null fun instance( - dmxInterface: DMXInterface, - dmxFrameTime: Long + dmxInterface: DmxInterface, + dmxFrameTime: Long = 200 ): DmxRepeater { if (dmxRepeater == null) { dmxRepeater = DmxRepeater(dmxInterface, dmxFrameTime / 2) diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/parameter/Fadeable.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/parameter/Fadeable.kt index 2f46509..0e9159d 100644 --- a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/parameter/Fadeable.kt +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/parameter/Fadeable.kt @@ -1,5 +1,4 @@ package de.visualdigits.kotlin.klanglicht.model.parameter - interface Fadeable> { /** diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/ColorState.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/ColorState.kt new file mode 100644 index 0000000..21affbd --- /dev/null +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/ColorState.kt @@ -0,0 +1,9 @@ +package de.visualdigits.kotlin.klanglicht.model.preferences + + +class ColorState( + var hexColor: String? = null, + var gain: Float? = null, + var on: Boolean = false +) + diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/Dmx.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/Dmx.kt index c04d1d6..55e1294 100644 --- a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/Dmx.kt +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/Dmx.kt @@ -1,11 +1,14 @@ package de.visualdigits.kotlin.klanglicht.model.preferences -import de.visualdigits.kotlin.klanglicht.model.dmx.DMXInterfaceType +import de.visualdigits.kotlin.klanglicht.model.dmx.DmxInterfaceType data class Dmx( val port: String = "", - val interfaceType: DMXInterfaceType = DMXInterfaceType.Dummy, + val interfaceType: DmxInterfaceType = DmxInterfaceType.Dummy, val frameTime: Long = 40L, val devices: List = listOf() -) +) { + + val dmxDevices: Map = devices.associateBy { it.baseChannel.toString() } +} diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/DmxDevice.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/DmxDevice.kt index ca33211..0f0652a 100644 --- a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/DmxDevice.kt +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/DmxDevice.kt @@ -10,7 +10,7 @@ data class DmxDevice( val model: String = "", val mode: String = "", val baseChannel: Int = 0, - val gain: Double = 0.0 + val gain: Float = 0.0f ) { var fixture: Fixture? = null } diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/Preferences.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/Preferences.kt index 10bee26..536bbfc 100644 --- a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/Preferences.kt +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/Preferences.kt @@ -2,8 +2,8 @@ package de.visualdigits.kotlin.klanglicht.model.preferences import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder -import de.visualdigits.kotlin.klanglicht.model.dmx.DMXInterface -import de.visualdigits.kotlin.klanglicht.model.dmx.DMXInterfaceDummy +import de.visualdigits.kotlin.klanglicht.model.dmx.DmxInterface +import de.visualdigits.kotlin.klanglicht.model.dmx.DmxInterfaceDummy import de.visualdigits.kotlin.klanglicht.model.fixture.Channel import de.visualdigits.kotlin.klanglicht.model.fixture.Fixtures import de.visualdigits.kotlin.klanglicht.model.parameter.Scene @@ -21,7 +21,7 @@ data class Preferences( var klanglichtDir: File = File(".") - var dmxInterface: DMXInterface = DMXInterfaceDummy() + var dmxInterface: DmxInterface = DmxInterfaceDummy() /** contains the list of channels for a given base dmx channel. */ var fixtures: Map> = mapOf() @@ -44,7 +44,7 @@ data class Preferences( Pair(service.name, service) }.toMap() - val dmxInterface = DMXInterface.load(dmx.interfaceType) + val dmxInterface = DmxInterface.load(dmx.interfaceType) dmxInterface.open(dmx.port) this.dmxInterface = dmxInterface } @@ -55,7 +55,10 @@ data class Preferences( var preferences: Preferences? = null - fun load(klanglichtDir: File, preferencesFileName: String = "preferences.json"): Preferences { + fun load( + klanglichtDir: File, + preferencesFileName: String = "preferences.json" + ): Preferences { if (preferences == null) { val prefs = mapper.readValue( Paths.get(klanglichtDir.canonicalPath, "preferences", preferencesFileName).toFile(), diff --git a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/ShellyDevice.kt b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/ShellyDevice.kt index 91ef53e..00d1a2b 100644 --- a/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/ShellyDevice.kt +++ b/klanglicht-core/src/main/kotlin/de/visualdigits/kotlin/klanglicht/model/preferences/ShellyDevice.kt @@ -4,6 +4,7 @@ package de.visualdigits.kotlin.klanglicht.model.preferences data class ShellyDevice( val name: String = "", val model: String = "", + val command: String = "", val ipAddress: String = "", val gain: Int = 0 ) diff --git a/klanglicht-core/src/test/kotlin/de/visualdigits/kotlin/klanglicht/dmx/DMXInterfaceTest.kt b/klanglicht-core/src/test/kotlin/de/visualdigits/kotlin/klanglicht/dmx/DmxInterfaceTest.kt similarity index 98% rename from klanglicht-core/src/test/kotlin/de/visualdigits/kotlin/klanglicht/dmx/DMXInterfaceTest.kt rename to klanglicht-core/src/test/kotlin/de/visualdigits/kotlin/klanglicht/dmx/DmxInterfaceTest.kt index 1a12bf4..d0566ba 100644 --- a/klanglicht-core/src/test/kotlin/de/visualdigits/kotlin/klanglicht/dmx/DMXInterfaceTest.kt +++ b/klanglicht-core/src/test/kotlin/de/visualdigits/kotlin/klanglicht/dmx/DmxInterfaceTest.kt @@ -14,11 +14,11 @@ import java.io.File import kotlin.math.ceil @Disabled("for local testing only") -class DMXInterfaceTest { +class DmxInterfaceTest { private val prefs = Preferences.load( klanglichtDir = File(ClassLoader.getSystemResource(".klanglicht").toURI()), - preferencesFileName = System.getenv("preferencesFileName")?:"preferences_livingroom.json" + preferencesFileName = System.getenv("preferencesFileName")?:"preferences_livingroom_dummy.json" ) @Test diff --git a/klanglicht-core/src/test/resources/.klanglicht/preferences/preferences_livingroom_dummy.json b/klanglicht-core/src/test/resources/.klanglicht/preferences/preferences_livingroom_dummy.json new file mode 100644 index 0000000..df6bfd6 --- /dev/null +++ b/klanglicht-core/src/test/resources/.klanglicht/preferences/preferences_livingroom_dummy.json @@ -0,0 +1,108 @@ +{ + "name" : "wohnzimmer", + + "services": [ + { + "name": "lmair", + "manufacturer" : "JB-Media", + "model" : "Light-Manager Air", + "url": "http://192.168.178.28" + }, + { + "name": "receiver", + "manufacturer" : "Yamaha", + "model" : "RX-V6A", + "url": "http://192.168.178.46" + } + ], + + "shelly": { + "devices": [ + { + "name": "Starwars", + "model": "shelly-rgbw", + "ipAddress": "192.168.178.54", + "gain": 30 + }, + { + "name": "Rgbw", + "model": "shelly-rgbw", + "ipAddress": "192.168.178.48", + "gain": 1 + }, + { + "name": "Bar", + "model": "shelly-rgbw", + "ipAddress": "192.168.178.55", + "gain": 20 + }, + { + "name": "Flur", + "model": "shelly-1", + "ipAddress": "192.168.178.38", + "gain": 0 + }, + { + "name": "Schlafzimmer", + "model": "shelly-1", + "ipAddress": "192.168.178.40", + "gain": 0 + }, + { + "name": "Wohnzimmer", + "model": "shelly-2.5", + "ipAddress": "192.168.178.37", + "gain": 0 + }, + { + "name": "Kristall", + "model": "plug-s", + "ipAddress": "192.168.178.51", + "gain": 0 + }, + { + "name": "Regal", + "model": "shelly-plug-s", + "ipAddress": "192.168.178.39", + "gain": 0 + } + ] + }, + + "dmx": { + "port": "COM8", + "interfaceType": "Dummy", + "frameTime": 40, + "devices" : [ { + "manufacturer" : "Cameo", + "model" : "Flat PAR Can RGB 10 IR", + "mode" : "6-Channel Mode", + "baseChannel" : 1, + "gain": 0.5 + }, { + "manufacturer" : "Cameo", + "model" : "Flat PAR Can RGBW", + "mode" : "8-Channel Mode", + "baseChannel" : 7, + "gain": 1.0 + }, { + "manufacturer" : "Cameo", + "model" : "Flat PAR Can RGB 10 IR", + "mode" : "6-Channel Mode", + "baseChannel" : 15, + "gain": 0.5 + }, { + "manufacturer" : "Cameo", + "model" : "Flat PAR Can RGBW", + "mode" : "8-Channel Mode", + "baseChannel" : 21, + "gain": 1.0 + }, { + "manufacturer" : "Cameo", + "model" : "Flat PAR Can RGB 10 IR", + "mode" : "6-Channel Mode", + "baseChannel" : 29, + "gain": 0.5 + } ] + } +} diff --git a/klanglicht-rest/pom.xml b/klanglicht-rest/pom.xml index 4c1ada5..3141270 100644 --- a/klanglicht-rest/pom.xml +++ b/klanglicht-rest/pom.xml @@ -5,6 +5,7 @@ jar + 12.1 3.1.2 @@ -43,6 +44,42 @@ ${version.kotlin} + + + org.jsoup + jsoup + 1.15.3 + + + + + org.apache.tika + tika-core + 2.6.0 + + + + + io.github.openfeign + feign-okhttp + ${version.feign} + + + io.github.openfeign + feign-gson + ${version.feign} + + + io.github.openfeign + feign-slf4j + ${version.feign} + + + io.github.openfeign.form + feign-form + 3.8.0 + + org.springframework.boot @@ -84,4 +121,54 @@ test + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${version.kotlin} + + 1.7 + ${kotlin.compiler.jvmTarget} + + + + compile + compile + + compile + + + + src/main/kotlin + + + + + + test-compile + test-compile + + test-compile + + + + src/test/kotlin + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + diff --git a/klanglicht-rest/src/main/java/de/visualdigits/klanglicht/controller/KlanglichtController.kt b/klanglicht-rest/src/main/java/de/visualdigits/klanglicht/controller/KlanglichtController.kt new file mode 100644 index 0000000..fd95cf6 --- /dev/null +++ b/klanglicht-rest/src/main/java/de/visualdigits/klanglicht/controller/KlanglichtController.kt @@ -0,0 +1,130 @@ +package de.visualdigits.klanglicht.controller + +import de.visualdigits.klanglicht.handler.KlanglichtHandler +import de.visualdigits.klanglicht.model.parameter.MultiParameterSet +import de.visualdigits.kotlin.klanglicht.model.parameter.ParameterSet +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.CrossOrigin +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.util.Arrays + +/** + * REST controller for DMX devices. + */ +@RestController +@CrossOrigin(origins = ["*"]) +@RequestMapping("/v1") +class KlanglichtController { + + @Autowired + val klanglichtHandler: KlanglichtHandler? = null + + @PostMapping(value = ["/writeBytes"], consumes = [MediaType.ALL_VALUE]) + fun writeBytes(@RequestBody data: ByteArray?) { + klanglichtHandler.writeBytes(data) + } + + @GetMapping(value = ["/readBytes"]) + fun readBytes(): ByteArray { + return klanglichtHandler.readBytes() + } + + @GetMapping(value = ["/getStageSetup"]) + val stageSetup: String + get() = klanglichtHandler.stageSetup + + @PostMapping("/setParameter") + fun setParameter(@RequestBody parameterSet: ParameterSet?) { + klanglichtHandler.setParameter(parameterSet) + } + + @PostMapping("/playSequence") + fun playSequence( + @RequestParam(value = "loop", required = false, defaultValue = "false") loop: Boolean, + @RequestBody sequence: SceneSequence? + ) { + klanglichtHandler.playSequence(loop, sequence) + } + + @GetMapping("/playPreset") + fun playPreset( + @RequestParam(value = "loop", required = false, defaultValue = "false") loop: Boolean, + @RequestParam preset: String? + ) { + klanglichtHandler.playPreset(loop, preset) + } + + @PostMapping("/saveSequence") + fun saveSequence( + @RequestParam(value = "fileName") fileName: String?, + @RequestBody sequence: SceneSequence? + ) { + klanglichtHandler.saveSequence(fileName, sequence) + } + + @PostMapping("/setMultiParameterSet") + fun setMultiParameterSet( + @RequestBody nextTake: MultiParameterSet?, + @RequestParam(required = false, defaultValue = "2000") fadeDuration: Long, + @RequestParam(required = false, defaultValue = "0") stepDuration: Long, + @RequestParam(required = false, defaultValue = "FADE") transformationName: String?, + @RequestParam(required = false, defaultValue = "false") loop: Boolean + ) { + klanglichtHandler.MultiParameterSet = nextTake, fadeDuration, stepDuration, transformationName, loop + } + + @GetMapping("/playTake") + fun playTake( + @RequestParam(value = "take") take: String?, + @RequestParam(required = false, defaultValue = "2000") fadeDuration: Long, + @RequestParam(required = false, defaultValue = "0") stepDuration: Long, + @RequestParam(required = false, defaultValue = "FADE") transformationName: String?, + @RequestParam(required = false, defaultValue = "false") loop: Boolean + ) { + klanglichtHandler.playTake(take, fadeDuration, stepDuration, transformationName, loop) + } + + @GetMapping(value = ["/singleColor"], produces = [MediaType.TEXT_PLAIN_VALUE]) + fun singleColor( + @RequestParam(value = "hexColor") hexColor: String?, + @RequestParam(required = false, defaultValue = "2000") fadeDuration: Long?, + @RequestParam(required = false, defaultValue = "0") stepDuration: Long?, + @RequestParam(required = false, defaultValue = "FADE") transformationName: String?, + @RequestParam(required = false, defaultValue = "false") loop: Boolean?, + @RequestParam(value = "id", required = false, defaultValue = "id") id: String? + ): String? { + return klanglichtHandler?.singleColor(hexColor!!, fadeDuration!!, stepDuration!!, transformationName!!, loop!!, id!!) + } + + @GetMapping("/colors") + fun colors( + @RequestParam(value = "hexColors") hexColors: String, + @RequestParam(value = "gains", required = false, defaultValue = "") gains: String?, + @RequestParam(value = "baseChannels", required = false, defaultValue = "") baseChannels: String?, + @RequestParam(value = "fadeDuration", required = false, defaultValue = "2000") fadeDuration: Long?, + @RequestParam(value = "stepDuration", required = false, defaultValue = "0") stepDuration: Long?, + @RequestParam(value = "transformationName", required = false, defaultValue = "FADE") transformationName: String?, + @RequestParam(value = "loop", required = false, defaultValue = "false") loop: Boolean?, + @RequestParam(value = "id", required = false, defaultValue = "id") id: String? + ) { + val lIds = baseChannels + ?.split(",".toRegex()) + ?.dropLastWhile { it.isEmpty() } + ?.filter { it.isNotEmpty() } + ?: listOf() + val lHexColors = hexColors.split(",".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray() + .filter { it.isNotEmpty() } + val lGains = gains?.split(",".toRegex()) + ?.dropLastWhile { it.isEmpty() } + ?: listOf() + klanglichtHandler?.hexColors(lIds, lHexColors, lGains, fadeDuration!!, stepDuration!!, transformationName!!, loop!!, id!!) + } +} diff --git a/klanglicht-rest/src/main/java/de/visualdigits/klanglicht/handler/KlanglichtHandler.kt b/klanglicht-rest/src/main/java/de/visualdigits/klanglicht/handler/KlanglichtHandler.kt new file mode 100644 index 0000000..5c94c06 --- /dev/null +++ b/klanglicht-rest/src/main/java/de/visualdigits/klanglicht/handler/KlanglichtHandler.kt @@ -0,0 +1,372 @@ +package de.visualdigits.klanglicht.handler + +import de.visualdigits.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.model.color.RGBBaseColor +import de.visualdigits.kotlin.klanglicht.model.color.RGBColor +import de.visualdigits.kotlin.klanglicht.model.color.RGBWColor +import de.visualdigits.kotlin.klanglicht.model.dmx.DmxFrame +import de.visualdigits.kotlin.klanglicht.model.parameter.Parameter +import de.visualdigits.kotlin.klanglicht.model.parameter.ParameterSet +import de.visualdigits.kotlin.klanglicht.model.preferences.ColorState +import de.visualdigits.kotlin.klanglicht.model.preferences.Preferences +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.json.Scene +import de.visualdigits.lightmanager.model.json.Scene +import jakarta.annotation.PostConstruct +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component +import java.io.File +import java.nio.file.Paths + +@Component +class KlanglichtHandler { + + private val log: Logger = LoggerFactory.getLogger(javaClass) + + val sceneFactory: SimpleSceneFactory = SimpleSceneFactory() + + @Autowired + val configHolder: ConfigHolder? = null + private var currentTake: MultiParameterSet? = null + private var transformer: StageTransformer? = null + + @PostConstruct + fun initialize( { + currentTake = MultiParameterSet.load( + Paths.(configHolder.getKlanglichtDirectory.AbsolutePath, "takes", "Blackout.json").toFile() + ) + } + + fun writeBytes(data: ByteArray?) { + configHolder?.repeater?.pause() + val frame = DmxFrame(data) + val frameData: ByteArray = frame.Data + configHolder?.dmxInterface.write(frameData) + } + + fun readBytes(): ByteArray { + return configHolder.dmxInterface.read() + } + + val stageSetup: String + get() { + val stageup: StageSetup = Preferences.instance = .StageSetup + return stageup.marshall() + } + + fun Parameter = parameterSet: ParameterSet? { + eventuallyStopTransformer() + configHolder.Repeater.play() + val stageSetup: StageSetup = configHolder.StageSetup + stageup.setParameter = parameterSet + stageup.write = + } + + fun playSequence( + loop: Boolean, + sequence: SceneSequence + ) { + log.info("playSequence...") + sequence.initialize(sceneFactory) + playSequence(sequence, loop) + } + + fun playPreset( + loop: Boolean, + preset: String + ) { + log.info("playPreset: $preset") + val sequenceFile: File = + Paths.get(configHolder?.klanglichtDirectory.absolutePath, "scenes", getJsonFileName(preset)) + .toFile() + val sequence: SceneSequence = SceneSequence.load(sceneFactory, sequenceFile) + playSequence(sequence, loop) + } + + fun saveSequence( + fileName: String, + sequence: SceneSequence? + ) { + if (sequence != null && !sequence.isEmpty()) { + log.info("saveSequence: $fileName") + val sequenceFileName = getJsonFileName(fileName) + val sequenceFile = File(Preferences.preferences.ScenesDir, sequenceFileName) + sequence.save(sequenceFile) + } + } + + fun setMultiParameterSet( + nextTake: MultiParameterSet?, + fadeDuration: Long, + stepDuration: Long, + transformationName: String, + loop: Boolean + ) { + playNextTake(fadeDuration, stepDuration, transformationName, loop, nextTake) + } + + fun playTake( + take: String, + fadeDuration: Long, + stepDuration: Long, + transformationName: String, + loop: Boolean + ) { + log.info("playTake: $take, fade: $fadeDuration, step: $stepDuration, transformation: $transformationName") + val takeFileName = getJsonFileName(take) + val takeFile: File = + Paths.(configHolder.klanglichtDirectory.absolutePath, "takes", takeFileName).toFile() + val nextTake: MultiParameter = MultiParameterSet.load = takeFile + playNextTake(fadeDuration, stepDuration, transformationName, loop, nextTake) + } + + fun singleColor( + hexColor: String, + fadeDuration: Long, + stepDuration: Long, + transformationName: String, + loop: Boolean, + id: String + ): String { + var hexColor = hexColor + val rgbColor = RGBColor(hexColor) + log.info(("singleColor: " + rgbColor.ansiColor()).toString() + ", fade: " + fadeDuration + ", step: " + stepDuration + ", transformation: " + transformationName) + log.info("Put color '$hexColor' for id '$id'") + configHolder.putColor(id, ColorState(hexColor = hexColor)) + val nextTake: MultiParameterSet = Preferences.preferences + .StageSetup + .copy() + .MultiParameters + if (!hexColor.startsWith("#")) { + hexColor = "#$hexColor" + } + val hc = hexColor + nextTake.forEach { ps -> + val gain: Float = configHolder.DmxGain(java.lang.String.valueOf(ps.getBaseChannel)) + RgbColor = ps, hc, gain + } + playNextTake(fadeDuration, stepDuration, transformationName, loop, nextTake) + return rgbColor.repr() + } + + /** + * Set hex colors. + * + * @param lBaseChannels The list of ids. + * @param lHexColors The list of hex colors. + * @param lGains The list of gains (taken from stage setup if omitted). + * @param transition The fade duration in milli seconds. + */ + fun hexColors( + lBaseChannels: List, + lHexColors: List, + lGains: List, + transition: Int + ) { + hexColors( + lBaseChannels, + lHexColors, + lGains, + transition.toLong(), + 0, + Transformation.FADE.name(), + false, + "Dmx" + ) + } + + /** + * Set hex colors. + * + * @param lBaseChannels The list of ids. + * @param lHexColors The list of hex colors. + * @param lGains The list of gains (taken from stage setup if omitted). + * @param fadeDuration The fade duration in milli seconds. + * @param stepDuration The duration of one transition step in milli seconds. + * @param transformationName The transformation to use. + * @param loop Determines if the the parameterset should be looped. + * @param id The cache id. + */ + fun hexColors( + lBaseChannels: List, + lHexColors: List, + lGains: List, + fadeDuration: Long, + stepDuration: Long, + transformationName: String, + loop: Boolean, + id: String + ) { + val nextTake: MultiParameterSet = Preferences + .instance() + .StageSetup + .copy() + .MultiParameters + val nh = lHexColors.size - 1 + var h = 0 + val ng = lGains.size - 1 + var g = 0 + val overrideGains = !lGains.isEmpty() + val hasBaseChannels = !lBaseChannels.isEmpty() + var colorPixels = "" + var hexColor = lHexColors[0].trim { it <= ' ' } + configHolder.putColor(id, ColorState().hexColor(hexColor)) + log.info("Put color '$hexColor' for id '$id'") + if (!hasBaseChannels) { + for (ps in nextTake) { + hexColor = lHexColors[h].trim { it <= ' ' } + colorPixels += RGBColor(hexColor).ColorPixel + var gain: Float = configHolder.DmxGain(java.lang.String.valueOf(ps.getBaseChannel)) + if (overrideGains) { + gain = lGains[g].toFloat() + } + processParameter = id, ps, hexColor, gain + if (++h >= nh) { + h = nh + } + if (++g >= ng) { + g = ng + } + } + } + else { + for (sBaseChannel in lBaseChannels) { + val baseChannel = sBaseChannel.trim { it <= ' ' }.toInt() + val ps: Parameter<*> = nextTake.getByBaseChannel = baseChannel + hexColor = lHexColors[h].trim { it <= ' ' } + colorPixels += RGBColor(hexColor).ansiColor() + var gain: Float = configHolder.DmxGain(java.lang.String.valueOf(ps.getBaseChannel)) + if (overrideGains) { + gain = lGains[g].toFloat() + } + processParameter = id, ps, hexColor, gain + if (++h >= nh) { + h = nh + } + if (++g >= ng) { + g = ng + } + } + } + log.info("colors: $colorPixels, fade: $fadeDuration, step: $stepDuration, transformation: $transformationName") + playNextTake(fadeDuration, stepDuration, transformationName, loop, nextTake) + } + + private fun processParameter = id: String, ps: ParameterSet, hexColor: String, gain: Float { + var hexColor = hexColor + val psBaseChannel: Int = ps.BaseChannel + val psId = "$id-$psBaseChannel" + configHolder.putColor(psId, ColorState().hexColor(hexColor)) + log.info("Put color '$hexColor' for id '$psId'") + if (!hexColor.startsWith("#")) { + hexColor = "#$hexColor" + } + RgbColor = ps, hexColor, gain + } + + private fun setRgbColor(ps: ParameterSet, hexColor: String, gain: Float?) { + val list: MutableList> = ArrayList>() + for (p in ps.parameters) { + val name: String = p.Name + if ("Rgb" == name) { + val param: Parameter = p as Parameter + var value = RGBColor(hexColor) + if (gain != null) { + value = value.multiply(gain) + log.info("#### apply gain " + gain + " for baseChannel " + ps.baseChannel) + } + list.add(value) + param.Value = value + } + else if ("Rgbw" == name) { + val param: Parameter = p as Parameter + var value = RGBWColor(hexColor) + if (gain != null) { + value = value.multiply(gain) + log.info("#### apply gain " + gain + " for baseChannel " + ps.baseChannel) + } + list.add(value) + param.Value = value + } + else if ("MasterDimmer" == name) { + val param: Parameter = p as Parameter + param.Value = DmxRawValue(255) + } + } + log.info("Setting color: $list") + } + + private fun playNextTake( + fadeDuration: Long, + stepDuration: Long, + transformationName: String, + loop: Boolean, + nextTake: MultiParameterSet? + ) { + if (nextTake != null && !nextTake.isEmpty()) { + val transformation: Transformation = Transformation.valueOf(transformationName) + eventuallyStopTransformer() + configHolder.Repeater.play() + val sequence: SceneSequence = SceneSequence( + Scene(1, currentTake), + Scene(2, fadeDuration, stepDuration, transformation, nextTake) + ) + currentTake = nextTake + sequence.initialize(sceneFactory) + playSequence(sequence, loop) + } + } + + private fun getJsonFileName(fileName: String): String { + var jsonFileName = fileName + if (!jsonFileName.lowercase().endsWith(".json")) { + jsonFileName += ".json" + } + return jsonFileName + } + + private fun playSequence(sequence: SceneSequence?, loop: Boolean) { + if (sequence != null && !sequence.isEmpty()) { + eventuallyStopTransformer() + configHolder?.repeater?.play() + if (loop) { + // duplicate first scene to the end and use same fade duration as first to second scene + val first: Scene = sequence.first() + first.Pause = 0L + currentTake = sequence.last().ParameterSets + val last = Scene(first) + if (sequence.size() > 1) { + last.FadeDuration = sequence.(1.getFadeDuration) + } + sequence.addScene(last) + } + transformer = StageTransformer( + SimpleSceneFactory(), + configHolder.DmxInterface, + configHolder.Preferences, + configHolder.StageSetup, + loop, + sequence + ) + transformer.start() + } + } + + private fun eventuallyStopTransformer() { + if (transformer != null && transformer.isRunning()) { + log.info(("eventuallyStopTransformer - stopping transformer - " + transformer.hashCode()).toString() + "...") + transformer.exitLooping() + try { + transformer.join() + } catch (e: InterruptedException) { + // ignore + } + log.info(("eventuallyStopTransformer - transformer stopped - " + transformer.hashCode()).toString() + "...") + Thread.sleep(10) + } + else { + log.info("eventuallyStopTransformer - transformer not running - " + if (transformer != null) transformer.hashCode() else "NULL") + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/KlangLichtMain.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/KlangLichtMain.kt deleted file mode 100644 index 2e3b2b2..0000000 --- a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/KlangLichtMain.kt +++ /dev/null @@ -1,11 +0,0 @@ -package de.visualdigits.kotlin.klanglicht.rest - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -open class KlangLichtMain - -fun main(args: Array) { - runApplication(*args) -} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/KlanglichtApplication.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/KlanglichtApplication.kt new file mode 100644 index 0000000..662143e --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/KlanglichtApplication.kt @@ -0,0 +1,31 @@ +package de.visualdigits.kotlin.klanglicht.rest.kotlin.klanglicht.rest + +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.SpringApplication +import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.context.ConfigurableApplicationContext + +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class, ManagementWebSecurityAutoConfiguration::class]) +object KlanglichtApplication { + + private var context: ConfigurableApplicationContext? = null + + @JvmStatic + fun main(args: Array) { + context = SpringApplication.run(KlanglichtApplication::class.java, *args) + } + + fun restart() { + val args = context!!.getBean( + ApplicationArguments::class.java + ) + val thread = Thread { + context!!.close() + context = SpringApplication.run(KlanglichtApplication::class.java, *args.sourceArgs) + } + thread.isDaemon = false + thread.start() + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/KlanglichtHandler.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/KlanglichtHandler.kt new file mode 100644 index 0000000..41d8b3a --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/KlanglichtHandler.kt @@ -0,0 +1,16 @@ +package de.visualdigits.kotlin.klanglicht.rest + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component + +@Component +class KlanglichtHandler { + + private val log: Logger = LoggerFactory.getLogger(javaClass) + + @Autowired + val configHolder: ConfigHolder? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/ConfigHolder.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/ConfigHolder.kt new file mode 100644 index 0000000..e00aa25 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/ConfigHolder.kt @@ -0,0 +1,122 @@ +package de.visualdigits.kotlin.klanglicht.rest.common.configuration + +import com.fasterxml.jackson.annotation.JsonIgnore +import de.visualdigits.kotlin.klanglicht.rest.hybrid.model.HybridStage +import de.visualdigits.kotlin.klanglicht.model.dmx.DmxInterface +import de.visualdigits.kotlin.klanglicht.model.dmx.DmxInterfaceType +import de.visualdigits.kotlin.klanglicht.model.dmx.DmxRepeater +import de.visualdigits.kotlin.klanglicht.model.preferences.ColorState +import de.visualdigits.kotlin.klanglicht.model.preferences.DmxDevice +import de.visualdigits.kotlin.klanglicht.model.preferences.Preferences +import de.visualdigits.kotlin.klanglicht.model.preferences.ShellyDevice +import jakarta.annotation.PostConstruct +import jakarta.annotation.PreDestroy +import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.SystemUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.io.File +import java.nio.file.Paths + +@Component +class ConfigHolder { + + private val log: Logger = LoggerFactory.getLogger(javaClass) + + val COLOR_STATE_DEFAULT: ColorState = ColorState(hexColor = "#000000", gain = 0.0f, on = false) + + var preferences: Preferences? = null + var hybridStage: HybridStage? = null + var dmxInterface: DmxInterface? = null + var repeater: DmxRepeater? = null + + val klanglichtDirectory: File = File(SystemUtils.getUserHome(), ".klanglicht") + + @Value("\${spring.profiles.active}") + val activeProfile: String? = null + + val lastColor: MutableMap = mutableMapOf() + + var shellyDevices: Map = mapOf() + + @PostConstruct + fun initialize() { + log.info("#### setUp - start") + preferences = Preferences.load(klanglichtDirectory) + val dmxPort = preferences?.dmx?.port!! + val stageSetupName = preferences?.name!! + hybridStage = HybridStage.Companion.load(klanglichtDirectory, stageSetupName) + + log.info("##") + log.info("## klanglichtDirectory: " + klanglichtDirectory.absolutePath) + log.info("## dmxPort : $dmxPort") + dmxInterface = if ("dev" == activeProfile || StringUtils.isEmpty(dmxPort)) { + log.info("## USING DUMMY DMX INTERFACE - USED A REAL INTERFACE TO SEE THE LIGHT ;)") + DmxInterface.load(DmxInterfaceType.Dummy) + } else { + DmxInterface.load(DmxInterfaceType.Serial) + } + + dmxInterface?.open(dmxPort) + if (dmxInterface?.isOpen() == true) { + dmxInterface?.clear() + repeater = DmxRepeater.instance(dmxInterface!!) + Thread.sleep(10) + repeater?.play() + } else { + throw IllegalStateException("Could not open serial interface") + } + + shellyDevices = preferences?.shelly?.devices?.associateBy { it.name }?: mapOf() + + log.info("#### setUp - end") + } + + @PreDestroy + fun tearDown() { + log.info("#### tearDown - start") + if (dmxInterface?.isOpen() == true) { + repeater?.end() + Thread.sleep(10) + dmxInterface?.clear() + dmxInterface?.close() + } + log.info("#### tearDown - end") + } + + fun getRelativeResourcePath(absoluteResource: File): String { + return Paths.get(klanglichtDirectory.absolutePath, "resources") + .relativize(Paths.get(absoluteResource.absolutePath)).toString().replace("\\", "/") + } + + fun getAbsoluteResource(relativeResourePath: String): File { + return Paths.get(klanglichtDirectory.absolutePath, "resources", relativeResourePath).toFile() + } + + fun getShellyGain(id: String): Int { + return shellyDevices[id]?.gain?:100 + } + + fun getShellyIpAddress(id: String): String? { + return shellyDevices[id]?.ipAddress + } + + @JsonIgnore + fun getDmxGain(id: String): Float { + return getDmxDevice(id)?.gain ?: 1.0f + } + + private fun getDmxDevice(id: String): DmxDevice? { + return preferences?.dmx?.dmxDevices?.get(id) + } + + fun putColor(id: String, colorState: ColorState) { + lastColor[id] = colorState + } + + fun getLastColor(id: String): ColorState { + return lastColor.getOrDefault(id, COLOR_STATE_DEFAULT) + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/HttpsConfiguration.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/HttpsConfiguration.kt new file mode 100644 index 0000000..eb8d8b5 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/HttpsConfiguration.kt @@ -0,0 +1,45 @@ +package de.visualdigits.kotlin.klanglicht.rest.common.configuration + +import org.apache.catalina.connector.Connector +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory +import org.springframework.boot.web.servlet.server.ServletWebServerFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class HttpsConfiguration { + @Bean + fun servletContainer(@Value("\${server.http.port}") httpPort: Int): ServletWebServerFactory { + val connector = Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL) + connector.port = httpPort + val tomcat = TomcatServletWebServerFactory() + tomcat.addAdditionalTomcatConnectors(connector) + return tomcat + } // @Bean + // public ServletWebServerFactory servletContainer() { + // TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() { + // + // @Override + // protected void postProcessContext(Context context) { + // SecurityConstraint securityConstraint = new SecurityConstraint(); + // securityConstraint.UserConstraint = "CONFIDENTIAL"; + // SecurityCollection collection = new SecurityCollection(); + // collection.addPattern("/*"); + // securityConstraint.addCollection(collection); + // context.addConstraint(securityConstraint); + // } + // }; + // tomcat.addAdditionalTomcatConnectors(redirectConnector()); + // return tomcat; + // } + // + // private Connector redirectConnector() { + // Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); + // connector.Scheme = "http"; + // connector.Port = 80; + // connector.Secure = false; + // connector.RedirectPort = 443; + // return connector; + // } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/RequestLoggingFilterConfig.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/RequestLoggingFilterConfig.kt new file mode 100644 index 0000000..79685dd --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/RequestLoggingFilterConfig.kt @@ -0,0 +1,19 @@ +package de.visualdigits.kotlin.klanglicht.rest.common.configuration + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class RequestLoggingFilterConfig { + @Bean + fun logFilter(): SimpleRequestLoggingFilter { + val filter = SimpleRequestLoggingFilter() + filter.setBeforeMessagePrefix("Request [") + filter.setIncludeQueryString(true) + filter.setIncludeClientInfo(true) + filter.setIncludeHeaders(true) + // filter.IncludePayload = true; +// filter.MaxPayloadLength = 10000; + return filter + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/SimpleRequestLoggingFilter.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/SimpleRequestLoggingFilter.kt new file mode 100644 index 0000000..a4ea165 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/SimpleRequestLoggingFilter.kt @@ -0,0 +1,15 @@ +package de.visualdigits.kotlin.klanglicht.rest.common.configuration + +import jakarta.servlet.http.HttpServletRequest +import org.springframework.web.filter.AbstractRequestLoggingFilter + +class SimpleRequestLoggingFilter : AbstractRequestLoggingFilter() { + + override fun beforeRequest(request: HttpServletRequest?, message: String?) { + logger.info(message) + } + + override fun afterRequest(request: HttpServletRequest?, message: String?) { + // intentionally empty to prevent duplicate after logging + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/WebConfig.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/WebConfig.kt new file mode 100644 index 0000000..5411ebf --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/configuration/WebConfig.kt @@ -0,0 +1,69 @@ +package de.visualdigits.kotlin.klanglicht.rest.common.configuration + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Description +import org.springframework.web.servlet.ViewResolver +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.thymeleaf.spring6.SpringTemplateEngine +import org.thymeleaf.spring6.view.ThymeleafViewResolver +import org.thymeleaf.templatemode.TemplateMode +import org.thymeleaf.templateresolver.FileTemplateResolver +import org.thymeleaf.templateresolver.ITemplateResolver +import java.nio.file.Paths + +@Configuration +class WebConfig : WebMvcConfigurer { + @Autowired + val configHolder: ConfigHolder? = null + + @Value("\${lightmanager.theme}") + var theme: String? = null + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/**") + } + + @Bean + @Description("Thymeleaf file system template resolver serving HTML 5") + fun templateResolver(): ITemplateResolver { + val templateResolver = FileTemplateResolver() + val templatesPath = Paths.get( + configHolder?.klanglichtDirectory?.absolutePath, + "resources", + "themes", + theme, + "templates" + ).toFile().absolutePath.replace("\\", "/") + "/" + templateResolver.prefix = templatesPath + templateResolver.isCacheable = false + templateResolver.suffix = ".html" + templateResolver.templateMode = TemplateMode.HTML + templateResolver.characterEncoding = "UTF-8" + return templateResolver + } + + @Bean + @Description("Thymeleaf template engine with Spring integration") + fun templateEngine(): SpringTemplateEngine { + val templateEngine = SpringTemplateEngine() + templateEngine.setTemplateResolver(templateResolver()) + return templateEngine + } + + @Bean + @Description("Thymeleaf view resolver") + fun viewResolver(): ViewResolver { + val viewResolver = ThymeleafViewResolver() + viewResolver.templateEngine = templateEngine() + viewResolver.characterEncoding = "UTF-8" + return viewResolver + } + + override fun addViewControllers(registry: ViewControllerRegistry) { + registry.addViewController("/").setViewName("index") + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/controller/ResourceController.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/controller/ResourceController.kt new file mode 100644 index 0000000..e633a1c --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/common/controller/ResourceController.kt @@ -0,0 +1,138 @@ +package de.visualdigits.kotlin.klanglicht.rest.common.controller + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.apache.tika.detect.Detector +import org.apache.tika.metadata.Metadata +import org.apache.tika.metadata.TikaCoreProperties +import org.apache.tika.mime.MediaType +import org.apache.tika.parser.AutoDetectParser +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ResponseBody +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.UnsupportedEncodingException +import java.net.URLDecoder +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +@Controller +class ResourceController { + + private val log: Logger = LoggerFactory.getLogger(javaClass) + + @Autowired + val configHolder: ConfigHolder? = null + + @GetMapping("/resources/**") + @ResponseBody + fun resource(request: HttpServletRequest, response: HttpServletResponse) { + val src = getRequestUri(request).substring("/resources".length) + val file = configHolder?.getAbsoluteResource(src)!! + try { + FileInputStream(file).use { ins -> + response.outputStream.use { outs -> + val mimeType = detectMimeType(file) + response.contentType = mimeType + ins.copyTo(outs) + } + } + } catch (e: IOException) { + throw IllegalStateException("Could not hand out resource: $src", e) + } + } + + private fun detectMimeType(file: File): String { + var mimeType = "text/plain" + try { + FileInputStream(file).use { `is` -> + BufferedInputStream(`is`).use { bis -> + val parser = AutoDetectParser() + val detector: Detector = parser.detector + val md = Metadata() + md.add(TikaCoreProperties.RESOURCE_NAME_KEY, file.name) + val mediaType: MediaType = detector.detect(bis, md) + mimeType = mediaType.toString() + } + } + } catch (e: IOException) { + // ignore + } + return mimeType + } + + protected fun getRequestUri(request: HttpServletRequest): String { + var uri = "" + try { + uri = URLDecoder.decode(request.requestURI, request.characterEncoding) + } catch (e: UnsupportedEncodingException) { + // ignore + } + return uri + } + + protected fun encodeUrl(response: HttpServletResponse, pagePath: String?): String? { + var pagePath = pagePath + val url = pagePath + try { + pagePath = URLEncoder.encode(pagePath, response.characterEncoding) + } catch (e: UnsupportedEncodingException) { + // ignore + } + return pagePath + } + + protected fun readFile(xslFile: File): String? { + var content: String? = null + try { + FileInputStream(xslFile).use { ins -> + ByteArrayOutputStream().use { baos -> + ins.copyTo(baos) + content = baos.toString() + } + } + } catch (e: IOException) { + log.error("Could not read xsl file: $xslFile", e) + } + return content + } + + protected fun sendContent(content: String, mimeType: String?, response: HttpServletResponse) { + sendContent(content, mimeType, null, response) + } + + protected fun sendContent( + content: String, + mimeType: String?, + headers: Map?, + response: HttpServletResponse + ) { + try { + ByteArrayInputStream(content.toByteArray(StandardCharsets.UTF_8)).use { ins -> + response.outputStream.use { outs -> + response.contentType = mimeType + headers?.forEach(response::addHeader) + ins.copyTo(outs) + } + } + } catch (e: IOException) { + throw IllegalStateException("Could not hand out rss stream", e) + } + } + + companion object { + protected const val MIMETYPE_XML = "application/xml" + protected const val MIMETYPE_XSL = "text/xsl" + const val HEADER_ACCEPT_RSS = + "application/rss+xml, application/rdf+xml;q=0.8, application/atom+xml;q=0.6, application/xml;q=0.4, text/xml;q=0.4" + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/controller/KlangLichtController.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/controller/KlangLichtController.kt deleted file mode 100644 index 3c1aecc..0000000 --- a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/controller/KlangLichtController.kt +++ /dev/null @@ -1,25 +0,0 @@ -package de.visualdigits.kotlin.klanglicht.rest.controller - -import de.visualdigits.kotlin.klanglicht.model.parameter.Scene -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping - -@Controller -@RequestMapping("/dmx/v1") -class KlangLichtController { - - private val log = LoggerFactory.getLogger(KlangLichtController::class.java) - - @GetMapping("hello") - fun hello(): String { - log.info("### hello world!") - return "foo" - } - - @GetMapping("setScene") - fun setScene(scene: Scene) { - - } -} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/controller/HybridStageRestController.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/controller/HybridStageRestController.kt new file mode 100644 index 0000000..ee17849 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/controller/HybridStageRestController.kt @@ -0,0 +1,34 @@ +package de.visualdigits.kotlin.klanglicht.rest.hybrid.controller + +import de.visualdigits.kotlin.klanglicht.rest.hybrid.handler.HybridStageHandler +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("/v1/hybrid/json") +class HybridStageRestController { + + @Autowired + val hybridStageHandler: HybridStageHandler? = null + + @GetMapping(value = ["/hexColor"]) + fun hexColor( + @RequestParam(value = "ids", required = false, defaultValue = "") ids: String?, + @RequestParam(value = "hexColors") hexColors: String, + @RequestParam(value = "gains", required = false, defaultValue = "") gains: String?, + @RequestParam(value = "transition", required = false, defaultValue = "2000") transition: Int?, + @RequestParam(value = "turnOn", required = false, defaultValue = "true") turnOn: Boolean? + ) { + hybridStageHandler?.hexColor( + ids!!, + hexColors, + gains!!, + transition!!, + turnOn!! + ) + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/handler/HybridStageHandler.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/handler/HybridStageHandler.kt new file mode 100644 index 0000000..1325e88 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/handler/HybridStageHandler.kt @@ -0,0 +1,162 @@ +package de.visualdigits.kotlin.klanglicht.rest.hybrid.handler + +import de.visualdigits.kotlin.klanglicht.rest.KlanglichtHandler +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.rest.hybrid.model.Device +import de.visualdigits.kotlin.klanglicht.rest.hybrid.model.DeviceType +import de.visualdigits.kotlin.klanglicht.rest.shelly.handler.ShellyHandler +import org.apache.commons.lang3.StringUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component +import java.util.Arrays + +@Component +class HybridStageHandler { + +// @Autowired +// val klanglichtHandler: KlanglichtHandler? = null + + @Autowired + var shellyHandler: ShellyHandler? = null + + @Autowired + val configHolder: ConfigHolder? = null + + /** + * Set hex colors. + * + * @param ids The comma separated list of ids. + * @param hexColors The comma separated list of hex colors. + * @param gains The comma separated list of gains (taken from stage setup if omitted). + * @param transition The fade duration in milli seconds. + * @param turnOn Determines if the device should be turned on. + */ + fun hexColor( + ids: String, + hexColors: String, + gains: String, + transition: Int, + turnOn: Boolean + ) { + val hybridStage = configHolder?.hybridStage + val lIds = Arrays.asList(*ids.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + val nd = lIds.size - 1 + var d = 0 + val hasIds = StringUtils.isNotEmpty(ids) + val lHexColors = Arrays.asList(*hexColors.split(",".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()) + val nh = lHexColors.size - 1 + var h = 0 + val lGains = Arrays.asList(*gains.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + val ng = lGains.size - 1 + var g = 0 + val overrideGains = StringUtils.isNotEmpty(gains) + val dmxIds: MutableList = ArrayList() + val dmxHexColors: MutableList = ArrayList() + val dmxGains: MutableList = ArrayList() + val shellyIds: MutableList = ArrayList() + val shellyHexColors: MutableList = ArrayList() + val shellyGains: MutableList = ArrayList() + if (hasIds) { + for (id in lIds) { + val sid = id.trim { it <= ' ' } + val hexColor = lHexColors[h] + val gain = lGains[g] + val device = hybridStage?.getDeviceById(sid) + if (device != null) { + processDevice( + overrideGains, + dmxIds, + dmxHexColors, + dmxGains, + shellyIds, + shellyHexColors, + shellyGains, + sid, + hexColor, + gain, + device + ) + } + if (++d >= nd) { + d = nd + } + if (++h >= nh) { + h = nh + } + if (++g >= ng) { + g = ng + } + } + } + else { + hybridStage?.devices?.forEach { device -> + val sid = device.id?.trim()?:"" + val hexColor = lHexColors[h] + val gain = lGains[g] + processDevice( + overrideGains, + dmxIds, + dmxHexColors, + dmxGains, + shellyIds, + shellyHexColors, + shellyGains, + sid, + hexColor, + gain, + device + ) + if (++d >= nd) { + d = nd + } + if (++h >= nh) { + h = nh + } + if (++g >= ng) { + g = ng + } + } + } +// klanglichtHandler?.hexColors(dmxIds, dmxHexColors, dmxGains, transition) + shellyHandler?.hexColors(shellyIds, shellyHexColors, shellyGains, transition, turnOn) + } + + private fun processDevice( + overrideGains: Boolean, + dmxIds: MutableList, + dmxHexColors: MutableList, + dmxGains: MutableList, + shellyIds: MutableList, + shellyHexColors: MutableList, + shellyGains: MutableList, + sid: String, + hexColor: String, + gain: String, + device: Device + ) { + when (device.type) { + DeviceType.dmx -> { + dmxIds.add(sid) + dmxHexColors.add(hexColor) + var dmxGain = configHolder?.getDmxGain(sid) + if (overrideGains) { + dmxGain = gain.toFloat() + } + dmxGains.add(dmxGain.toString()) + } + + DeviceType.shelly -> { + shellyIds.add(sid) + shellyHexColors.add(hexColor) + var shellyGain = configHolder?.getShellyGain(sid) + if (overrideGains) { + shellyGain = (gain.toFloat() * 100.0).toInt() + } + shellyGains.add(shellyGain.toString()) + } + + else -> {} + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/model/Device.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/model/Device.kt new file mode 100644 index 0000000..13f709c --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/model/Device.kt @@ -0,0 +1,7 @@ +package de.visualdigits.kotlin.klanglicht.rest.hybrid.model + + +class Device( + val type: DeviceType? = null, + val id: String? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/model/DeviceType.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/model/DeviceType.kt new file mode 100644 index 0000000..4016bf3 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/model/DeviceType.kt @@ -0,0 +1,6 @@ +package de.visualdigits.kotlin.klanglicht.rest.hybrid.model + +enum class DeviceType { + dmx, + shelly +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/model/HybridStage.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/model/HybridStage.kt new file mode 100644 index 0000000..5bf794b --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/hybrid/model/HybridStage.kt @@ -0,0 +1,49 @@ +package de.visualdigits.kotlin.klanglicht.rest.hybrid.model + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.databind.json.JsonMapper +import java.io.File +import java.io.IOException +import java.nio.file.Paths + +class HybridStage( + val name: String? = null, + val devices: List = listOf() +) { + + @JsonIgnore + val devicesById: MutableMap = HashMap() + + fun getDeviceById(id: String): Device? { + return devicesById[id] + } + + private fun intitialize() { + for (device in devices!!) { + devicesById[device.id!!] = device + } + } + + companion object { + val MAPPER: JsonMapper = JsonMapper() + protected var theOne: HybridStage? = null + @Synchronized + fun load(klanglichtDir: File, stageSetupName: String): HybridStage? { + if (theOne == null) { + val preferencesFile: File = + Paths.get(klanglichtDir.absolutePath, "hybrid", "$stageSetupName.json").toFile() + try { + theOne = MAPPER.readValue(preferencesFile, HybridStage::class.java) + theOne!!.intitialize() + } catch (e: IOException) { + throw IllegalStateException("Could not read hybrid stage file: $preferencesFile", e) + } + } + return theOne + } + + fun instance(): HybridStage? { + return theOne + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/controller/LightmanagerRestController.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/controller/LightmanagerRestController.kt new file mode 100644 index 0000000..0a8f2c5 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/controller/LightmanagerRestController.kt @@ -0,0 +1,38 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.controller + +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.feign.LightmanagerClient +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMMarkers +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMParams +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMScenes +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMZones +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +/** + * REST controller for the light manager air. + */ +@RestController +@RequestMapping("/v1/lightmanager/json") +class LightmanagerRestController { + + @Autowired + var client: LightmanagerClient? = null + + @GetMapping(value = ["/params"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun params(): LMParams? = client?.params() + + @GetMapping(value = ["/zones"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun zones(): LMZones? = client?.zones() + + @GetMapping(value = ["/knownActors"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun knownActors(): Map? = client?.knownActors() + + @GetMapping(value = ["/scenes"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun scenes(): LMScenes? = client?.scenes() + + @GetMapping(value = ["/markers"], produces = [MediaType.APPLICATION_JSON_VALUE]) + fun markers(): LMMarkers? = client?.markers() +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/controller/LightmanagerWebController.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/controller/LightmanagerWebController.kt new file mode 100644 index 0000000..91bcff9 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/controller/LightmanagerWebController.kt @@ -0,0 +1,50 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.controller + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.feign.LightmanagerClient +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMScenes +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMZones +import jakarta.servlet.http.HttpServletRequest +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam + +@Controller +@RequestMapping("/v1/lightmanager/web") +class LightmanagerWebController { + @Autowired + var configHolder: ConfigHolder? = null + + @Autowired + var client: LightmanagerClient? = null + + @Value("\${lightmanager.theme}") + var theme: String? = null + @GetMapping(value = ["/scenes"], produces = ["application/xhtml+xml"]) + fun scenes( + @RequestParam(name = "lang", required = false, defaultValue = "de") lang: String?, + model: Model, request: HttpServletRequest? + ): String { + model.addAttribute("theme", theme) + model.addAttribute("title", "Scenes") + val scenes = client?.scenes() + model.addAttribute("content", scenes?.toHtml(configHolder!!)) + return "pagetemplate" + } + + @GetMapping(value = ["/zones"], produces = ["application/xhtml+xml"]) + fun zones( + @RequestParam(name = "lang", required = false, defaultValue = "de") lang: String?, + model: Model, request: HttpServletRequest? + ): String { + model.addAttribute("theme", theme) + model.addAttribute("title", "Zones") + val zones = client?.zones() + model.addAttribute("content", zones?.toHtml(configHolder!!)) + return "pagetemplate" + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/feign/LightmanagerClient.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/feign/LightmanagerClient.kt new file mode 100644 index 0000000..ddfb7ee --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/feign/LightmanagerClient.kt @@ -0,0 +1,136 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.feign + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMActor +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMMarker +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMMarkers +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMParams +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMScene +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMScenes +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMZones +import jakarta.annotation.PostConstruct +import org.apache.commons.lang3.StringUtils +import org.jsoup.Jsoup +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component +import javax.swing.text.Document + +@Component +class LightmanagerClient( + var lightmanagerUrl: String? = null, + var client: LightmanagerFeignClient? = null +) { + + @Autowired + val configHolder: ConfigHolder? = null + + @PostConstruct + fun initialize() { + if (StringUtils.isEmpty(lightmanagerUrl)) { + lightmanagerUrl = configHolder?.preferences?.serviceMap?.get("lmair")?.url + } + client = LightmanagerFeignClient.Companion.client(lightmanagerUrl) + } + + fun params(): LMParams { + return LMParams.load(client!!.paramsJson()!!) + } + + fun zones(): LMZones { + val markers: LMMarkers = markers() + val document = Jsoup.parse(client!!.html()!!) + val setUpName = document.select("div[id=mytitle]")?.first()?.text()?:"" + val zones = LMZones(setUpName) + document + .select("div[class=bigBlock]") + .forEach { zoneElem -> zones.add(lightmanagerUrl!!, markers, zoneElem) } + return zones + } + + fun getActorById(actorId: Int): LMActor? { + var a: LMActor? = null + val zones: LMZones = zones() + for (zone in zones.zones) { + for (actor in zone.actors) { + if (actor.id == actorId) { + a = actor + break + } + } + } + return a + } + + fun knownActors(): Map { + val actors: MutableMap = mutableMapOf() + val zones: LMZones = zones() + for (zone in zones.zones) { + for (actor in zone.actors) { + actors[actor.id!!] = actor.name!! + } + } + return actors + } + + fun scenes(): LMScenes { + val document = Jsoup.parse(client!!.html()!!) + val setupName = document.select("div[id=mytitle]").first()?.text() + val scenes = LMScenes(setupName) + document + .select("div[id=scenes]") + .first() + ?.select("div[class=sbElement]") + ?.forEach { elem -> + scenes.add( + LMScene( + elem.attr("id").substring(1).toInt(), + elem.child(0).text() + ) + ) + } + return scenes + } + + fun markers(): LMMarkers { + val markerState: BooleanArray = params().markerState + val document = Jsoup.parse(client!!.html()!!) + val setupName = document.select("div[id=mytitle]").first()?.text() + val markers = LMMarkers() + markers.name = setupName + document + .select("div[id=marker]") + .first() + ?.select("div[class=mk mtouch]") + ?.forEach { elem -> + val colorOff: String = elem.attr("data-coff") + val colorOn: String = elem.attr("data-con") + val id: Int = elem.attr("id").substring(1).toInt() + markers.add( + LMMarker( + id = id, + name = elem.text(), + colorOff = if (StringUtils.isNotEmpty(colorOff)) colorOff else COLOR_OFF, + colorOn = if (StringUtils.isNotEmpty(colorOn)) colorOn else COLOR_ON, + state = markerState[id], + separate = false, + actorId = "", + markerState = "" + ) + ) + } + return markers + } + + fun controlScene(sceneId: Int) { + client!!.controlScene(sceneId) + } + + fun controlIndex(index: Int) { + client!!.controlIndex(index) + } + + companion object { + const val COLOR_ON = "#FF7676" + const val COLOR_OFF = "#91FFAA" + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/feign/LightmanagerFeignClient.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/feign/LightmanagerFeignClient.kt new file mode 100644 index 0000000..ba6d67b --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/feign/LightmanagerFeignClient.kt @@ -0,0 +1,35 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.feign + +import feign.Feign +import feign.Logger +import feign.Param +import feign.RequestLine +import feign.okhttp.OkHttpClient +import feign.slf4j.Slf4jLogger + +interface LightmanagerFeignClient { + @RequestLine("GET /") + fun html(): String? + + @RequestLine("GET /config.xml") + fun configXml(): String? + + @RequestLine("GET /params.json") + fun paramsJson(): String? + + @RequestLine("POST /control?key={scene}") + fun controlScene(@Param("scene") sceneId: Int) + + @RequestLine("POST /control?scene={index}") + fun controlIndex(@Param("index") index: Int) + + companion object { + fun client(url: String?): LightmanagerFeignClient { + return Feign.builder() + .client(OkHttpClient()) + .logger(Slf4jLogger(LightmanagerFeignClient::class.java)) + .logLevel(Logger.Level.BASIC) + .target(LightmanagerFeignClient::class.java, url) + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/HtmlRenderable.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/HtmlRenderable.kt new file mode 100644 index 0000000..e7bc417 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/HtmlRenderable.kt @@ -0,0 +1,10 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder + +interface HtmlRenderable { + fun toHtml(configHolder: ConfigHolder): String? + fun renderLabel(sb: StringBuilder, label: String?) { + sb.append("").append(label).append("\n") + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMActor.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMActor.kt new file mode 100644 index 0000000..6011559 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMActor.kt @@ -0,0 +1,237 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + +import com.fasterxml.jackson.annotation.JsonIgnore +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.feign.LightmanagerClient +import org.apache.commons.lang3.StringUtils + +class LMActor( + var id: Int? = null, + var name: String? = null, + var markers: MutableMap = mutableMapOf(), + var actorOff: String? = null, + var actorOn: String? = null, + var colorOff: String? = null, + var colorOn: String? = null, + var isDimmer: Boolean? = null, + var requests: MutableMap = mutableMapOf(), + var requestsBySmkState: MutableMap = mutableMapOf() +) : HtmlRenderable { + + fun addRequest(key: String, request: LMRequest) { + requests[key] = request + if (request is LMDefaultRequest) { + val drq = request + if (drq.hasSmk()) { + drq.smk?.get(1)?.let { requestsBySmkState[it] = drq } + } + } + } + + override fun toHtml(configHolder: ConfigHolder): String { + val sb = StringBuilder() + sb.append("
\n") + renderLabel(sb, name) + renderRequests(sb) + // fixme - yet not working reliable enough +// renderSlider(sb, configHolder); + sb.append("
\n") + return sb.toString() + } + + private fun renderSlider(sb: StringBuilder, configHolder: ConfigHolder) { + val lightmanagerUrl = configHolder.preferences?.serviceMap?.get("lmair")?.url + if (isDimmer == true) { + val actorId = id + val drq = getRequestBySmkState(1) + var requestTemplate: String? = "" + if (drq != null) { + requestTemplate = drq.requestTemplate() + } + sb.append("
\n") + .append(" \n") + .append(" \n") + .append("
\n") + } + } + + private fun renderRequests(sb: StringBuilder) { + var markerIsOn: Boolean? = false + var hasSeparateMarkers: Boolean? = false + var isSeparate: Boolean? = false + var colorOff: String? = "" + var colorOn: String? = "" + if (markers.containsKey("unified")) { + val marker = markers["unified"] + markerIsOn = marker?.state + colorOff = marker?.colorOff + colorOn = marker?.colorOn + isSeparate = marker?.separate + } + else if (!markers.isEmpty()) { + hasSeparateMarkers = true + } + if (StringUtils.isEmpty(colorOff)) { + colorOff = + if (StringUtils.isNotEmpty(colorOff)) colorOff else LightmanagerClient.COLOR_OFF + } + if (StringUtils.isEmpty(colorOn)) { + colorOn = if (StringUtils.isNotEmpty(colorOn)) colorOn else LightmanagerClient.COLOR_ON + } + val lmRequests = requests.values.toList() + if (!lmRequests.isEmpty() && lmRequests.first() is LMDefaultRequest) { + if (hasSeparateMarkers == true || isSeparate == true) { + sb.append("
\n") + // Collections.reverse(lmRequests); + for (request in lmRequests) { + val marker = markers[(request as LMDefaultRequest).name] + if (marker != null) { + colorOff = marker.colorOff + colorOn = marker.colorOn + markerIsOn = marker.state + } + renderRequest( + sb, + "half-button", + markerIsOn, + colorOff, + colorOn, + request, + false, + isSeparate, + hasSeparateMarkers + ) + } + sb.append("
\n") + } + else { + val rq0 = lmRequests.first() as LMDefaultRequest + val smkState0 = determineSmkState(rq0) + var rq: LMRequest? = null + rq = if (markerIsOn == true && !smkState0 || markerIsOn != true && smkState0) { + rq0 + } + else if (lmRequests.size > 1) { + lmRequests.get(1) + } + else { + rq0 + } + renderRequest(sb, "button", markerIsOn, colorOff, colorOn, rq, true, false, false) + } + } + } + + private fun renderRequest( + sb: StringBuilder, + styleClass: String, + markerIsOn: Boolean?, + colorOff: String?, + colorOn: String?, + rq: LMRequest?, + unified: Boolean?, + isSeparate: Boolean?, + hasSeparateMarkers: Boolean? + ) { + if (rq is LMDefaultRequest) { + val drq = rq + val isOn = determineSmkState(drq) + sb.append("
\n") + } + // else if (rq instanceof LMCamRequest) { +// LMCamRequest crq = (LMCamRequest) rq; +// } + } + + fun addMarker(marker: LMMarker?) { + var markerState = marker?.markerState!! + if (StringUtils.isEmpty(markerState)) { + markerState = "unified" + } + markers[markerState] = marker + } + + private fun determineSmkState(drq: LMDefaultRequest): Boolean { + return drq.hasSmk() && drq.smk?.get(1) == 1 + } + + @JsonIgnore + fun getRequestByName(key: String): LMRequest? { + return requests[key] + } + + @JsonIgnore + fun getRequestBySmkState(state: Int): LMDefaultRequest? { + return requestsBySmkState[state] + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMCamRequest.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMCamRequest.kt new file mode 100644 index 0000000..1497254 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMCamRequest.kt @@ -0,0 +1,6 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + + +class LMCamRequest( + val href: String? = null +) : LMRequest diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMDefaultRequest.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMDefaultRequest.kt new file mode 100644 index 0000000..0438c87 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMDefaultRequest.kt @@ -0,0 +1,40 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + +import com.fasterxml.jackson.annotation.JsonIgnore +import org.apache.commons.lang3.StringUtils +import java.util.Arrays + + +class LMDefaultRequest( + var name: String? = null, + var type: RequestType? = null, + var deviceId: Long = 0, + var actorId: Int? = null, + var actorCommand: Int? = null, + var sequence: Int? = null, + var level: Int? = null, + var smk: IntArray? = null, + var uri: String? = null, + var data: Array = arrayOf() +) : LMRequest { + + @JsonIgnore + fun requestTemplate(): String { + val params = mutableListOf( + "typ", type?.name, + "did", deviceId.toString(), + "aid", actorId.toString(), + "acmd", actorCommand.toString(), + "lvl", "\${level}", + "seq", sequence.toString() + ) + if (hasSmk()) { + params.addAll(listOf("smk", smk!![0].toString(), smk!![1].toString())) + } + return "cmd=\"${params.joinToString(",")}\"" + } + + fun hasSmk(): Boolean { + return smk != null && smk?.isNotEmpty() == true + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMMarker.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMMarker.kt new file mode 100644 index 0000000..5a2c523 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMMarker.kt @@ -0,0 +1,19 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + + +class LMMarker( + var id: Int? = null, + var name: String? = null, + var colorOff: String? = null, + var colorOn: String? = null, + var state: Boolean? = null, + + /** Determines whether the button should stay split up (true) or should be consolidated into on toggle button (false). */ + var separate: Boolean? = null, + + /** Determines to which actor id this marker belongs (if any). */ + var actorId: String? = null, + + /** Determines the actor state to which this marker belongs (if any). */ + var markerState: String? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMMarkers.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMMarkers.kt new file mode 100644 index 0000000..c2c67a5 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMMarkers.kt @@ -0,0 +1,37 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + +import org.apache.commons.lang3.StringUtils + +class LMMarkers( + var name: String? = null, + var markers: MutableMap = mutableMapOf() +) { + fun add(marker: LMMarker) { + val attributes = LMNamedAttributes(marker.name, "separate", "actorId", "state") + if (attributes.matched()) { + val name = attributes.name + if (StringUtils.isNotEmpty(name)) { + marker.name = name + } + marker.separate = attributes.getOrDefault("separate", "false").toBoolean() + marker.actorId = attributes["actorId"] + marker.markerState = attributes["state"] + } + markers[marker.id!!] = marker + } + + operator fun get(id: Int): LMMarker? { + return markers[id] + } + + fun getByActorId(aid: Int): Set { + val markers: MutableList = mutableListOf() + val said = aid.toString() + for (m in this.markers.values) { + if (m.actorId.equals(said)) { + markers.add(m) + } + } + return markers.toSet() + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMNamedAttributes.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMNamedAttributes.kt new file mode 100644 index 0000000..bb72d75 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMNamedAttributes.kt @@ -0,0 +1,47 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + +import java.util.TreeMap +import java.util.regex.Pattern + +class LMNamedAttributes( + s: String?, + vararg attributes: String +) { + + private var matched = false + var name: String? = null + val attributesMap: MutableMap = TreeMap() + + init { + val matcherParams = P_PARAMS.matcher(s) + matched = matcherParams.find() + if (matched) { + name = matcherParams.group(1).trim { it <= ' ' } + val params = matcherParams.group(2).trim { it <= ' ' } + for (attribute in attributes) { + val pattern = Pattern.compile(attribute + PATTERN_TEMPLATE) + val matcherSeparate = pattern.matcher(params) + if (matcherSeparate.find()) { + attributesMap[attribute] = matcherSeparate.group(1).trim { it <= ' ' } + } + } + } + } + + fun matched(): Boolean { + return matched + } + + operator fun get(attribute: String): String { + return getOrDefault(attribute, "") + } + + fun getOrDefault(attribute: String, defaultValue: String): String { + return attributesMap.getOrDefault(attribute, defaultValue) + } + + companion object { + val P_PARAMS = Pattern.compile("^([^{]*)\\{?([^}]*)\\}?.*$") + private const val PATTERN_TEMPLATE = ":([^;}]*)" + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMParams.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMParams.kt new file mode 100644 index 0000000..de0bf42 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMParams.kt @@ -0,0 +1,51 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.deserializer.BooleanArrayDeserializer +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.deserializer.LMParamsInitializer +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.deserializer.NumericBooleanDeserializer +import java.io.IOException +import java.time.LocalDateTime + +@JsonDeserialize(converter = LMParamsInitializer::class) +class LMParams( + @JsonProperty("auth enabled") @JsonDeserialize(using = NumericBooleanDeserializer::class) var authEnabled: Boolean? = null, + var time: String? = null, + var date: String? = null, + @JsonIgnore var dateTime: LocalDateTime? = null, + var weekday: String? = null, + @JsonProperty("is dst") @JsonDeserialize(using = NumericBooleanDeserializer::class)var dst: Boolean? = null, + @JsonProperty("marker state") @JsonDeserialize(using = BooleanArrayDeserializer::class) var markerState: BooleanArray = booleanArrayOf(), + var ssid: String? = null, + @JsonProperty("led off") @JsonDeserialize(using = NumericBooleanDeserializer::class) var ledOff: Boolean? = null, + @JsonProperty("last update") var lastUpdate: String? = null, + @JsonProperty("firmware ver") var firmwareVer: String? = null, + @JsonProperty("mac addr") var macAddr: String? = null, + @JsonDeserialize(using = NumericBooleanDeserializer::class) var busy: Boolean? = null, + @JsonProperty("master ip") var masterIp: String? = null, + var lon: Int? = null, + var lat: Int? = null, + @JsonProperty("mode 433") @JsonDeserialize(using = NumericBooleanDeserializer::class) var mode433: Boolean? = null, + @JsonProperty("mode 868") @JsonDeserialize(using = NumericBooleanDeserializer::class) var mode868: Boolean? = null, + @JsonDeserialize(using = NumericBooleanDeserializer::class) var mpfs: Boolean? = null +) { + + companion object { + val mapper: ObjectMapper = ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT) + + fun load(json: String): LMParams { + val mmParams: LMParams + mmParams = try { + mapper.readValue(json, LMParams::class.java) + } catch (e: IOException) { + throw IllegalStateException("Could not unmarshall file: $json", e) + } + return mmParams + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMRequest.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMRequest.kt new file mode 100644 index 0000000..2b97ef6 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMRequest.kt @@ -0,0 +1,3 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + +interface LMRequest diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMScene.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMScene.kt new file mode 100644 index 0000000..c35d3f2 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMScene.kt @@ -0,0 +1,61 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import org.apache.commons.lang3.StringUtils + + +class LMScene( + var id: Int? = null, + var name: String? = null, + var color: String? = null +) : HtmlRenderable { + override fun toHtml(configHolder: ConfigHolder): String { + return toHtml(configHolder, "") + } + + fun toHtml(configHolder: ConfigHolder, group: String): String { + val lightmanagerUrl = configHolder.preferences?.serviceMap?.get("lmair")?.url + val sb = StringBuilder() + sb.append("
\n") + return sb.toString() + } + + /** + * Removes the group name from the label (if matches). + * + * @param group The current group this scene belongs to. + * + * @return String + */ + private fun normalizeLabel(group: String): String { + var label = name + if (label?.lowercase()?.startsWith(group.lowercase()) == true) { + label = label.substring(group.length).trim { it <= ' ' } + } + return label?:"" + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMScenes.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMScenes.kt new file mode 100644 index 0000000..695ba25 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMScenes.kt @@ -0,0 +1,84 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.google.common.collect.LinkedListMultimap +import com.google.common.collect.Multimap +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.widgets.ColorWheel +import org.apache.commons.lang3.StringUtils +import java.util.TreeMap +import java.util.function.Consumer + +class LMScenes( + val name: String? = null +) : HtmlRenderable { + val COLOR_WHEEL_GROUPS: List = mutableListOf("Dmx", "Deko", "Rgbw", "Bar", "Starwars") + + @JsonIgnore + val scenes: Multimap = LinkedListMultimap.create() + fun add(scene: LMScene) { + var group: String? = "common" + val attributes = LMNamedAttributes(scene.name, "group", "color") + if (attributes.matched()) { + val name = attributes.name + if (StringUtils.isNotEmpty(name)) { + scene.name = name + } + val g = attributes["group"] + if (StringUtils.isNotEmpty(g)) { + group = g + } + scene.color = attributes["color"] + } + if ("hidden" != group) { + scenes.put(StringUtils.capitalize(group), scene) + } + } + + override fun toHtml(configHolder: ConfigHolder): String { + val sb = StringBuilder() + sb.append("
") + .append(name) + .append("
\n") + sb.append("
\n") + renderLabel(sb, "S C E N E S") + val scenesMap: Map> = TreeMap>(scenes.asMap()) + scenesMap.forEach { (group: String, groupScenes: Collection) -> + renderScenesGroup( + sb, + configHolder, + group, + groupScenes + ) + } + sb.append("
\n\n") + return sb.toString() + } + + private fun renderScenesGroup( + sb: StringBuilder, + configHolder: ConfigHolder, + group: String, + groupScenes: Collection + ) { + val hasColorWheel = COLOR_WHEEL_GROUPS.contains(group) + sb.append("
\n") + renderLabel(sb, group) + sb.append("
\n") + groupScenes.forEach(Consumer { scene: LMScene -> sb.append(scene.toHtml(configHolder, group)) }) + sb.append("
\n") + sb.append("
\n") + if (hasColorWheel) { + val html: String = ColorWheel(group).toHtml(configHolder) + sb.append(html) + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMZone.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMZone.kt new file mode 100644 index 0000000..4189c29 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMZone.kt @@ -0,0 +1,50 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import java.util.Locale +import java.util.Objects +import java.util.function.Consumer + +class LMZone( + val id: Int? = null, + val name: String? = null, + val logo: String? = null, + val tempChannel: Int? = null, + val arrow: String? = null, + val actors: MutableList = ArrayList() +) : Comparable, HtmlRenderable { + + fun addActor(actor: LMActor) { + actors.add(actor) + } + + override fun toHtml(configHolder: ConfigHolder): String { + val sb = StringBuilder() + sb.append("
\n") + renderLabel(sb, name) + sb.append("
\n") + actors.forEach(Consumer { actor: LMActor -> sb.append(actor.toHtml(configHolder)) }) + sb.append("
\n") + .append("
\n") + return sb.toString() + } + + override fun compareTo(o: LMZone): Int { + return name!!.lowercase().compareTo(o.name?.lowercase()?:"") + } + + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + val lmZone = o as LMZone + return id == lmZone.id && name == lmZone.name + } + + override fun hashCode(): Int { + return Objects.hash(id, name) + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMZones.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMZones.kt new file mode 100644 index 0000000..64cccb1 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/LMZones.kt @@ -0,0 +1,185 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import org.apache.commons.lang3.StringUtils +import org.jsoup.nodes.Element +import java.io.UnsupportedEncodingException +import java.net.URLDecoder +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.util.Arrays +import java.util.function.Consumer +import java.util.regex.Pattern + +class LMZones( + val name: String? = null, + val zones: MutableList = mutableListOf() +) : HtmlRenderable { + + fun add(lightmanagerUrl: String, markers: LMMarkers, zoneElem: Element) { + val id: String = zoneElem.attr("id") + if (id.startsWith("z")) { + val headElem = zoneElem.select("div[class=bbHead]").first()!! + val zone = LMZone( + id = id.substring(1).toInt(), + name = headElem.select("div[class=bbName]").first()?.text(), + logo = headElem.select("div[class=bbLogo]").first()?.text(), + tempChannel = headElem.select("div[class=ztemp]").first()?.attr("data-ch")?.toInt(), + arrow = headElem.select("div[class=arrow]").first()?.text() + ) + zoneElem + .select("div[class=sbElement]") + .forEach { actorElem -> addActor(lightmanagerUrl, markers, zone, actorElem) } + if (!zone.actors.isEmpty()) { + zones.add(zone) + } + } + } + + private fun addActor(lightmanagerUrl: String, markers: LMMarkers, zone: LMZone, actorElem: Element) { + val actor = LMActor() + val aid: Int = actorElem.attr("id").substring(1).toInt() + actor.id = aid + val actorOff: String = actorElem.attr("data-aoff") + var colorOff = + if (StringUtils.isNotEmpty(actorOff)) actorOff.split(",".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()[1].trim { it <= ' ' } + else "" + val actorOn: String = actorElem.attr("data-aon") + val colorOn = if (StringUtils.isNotEmpty(actorOn)) actorOn.split(",".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()[1].trim { it <= ' ' } + else "" + var name = actorElem.child(0).text() + val attributes = LMNamedAttributes(name, "color") + if (attributes.matched()) { + name = attributes.name + colorOff = attributes["color"] + } + actor.colorOff = colorOff + actor.colorOn = colorOn + actor.name = name + val dataMarker: String = actorElem.attr("data-marker") + if (StringUtils.isNotEmpty(dataMarker)) { + val mid = dataMarker.toInt() + val marker = markers[mid] + actor.addMarker(marker) + } + else { + val actorMarkers = markers.getByActorId(aid) + actorMarkers.forEach(Consumer { marker: LMMarker? -> actor.addMarker(marker) }) + } + actor.actorOff = actorOff + actor.actorOn = actorOn + actor.isDimmer = !actorElem.select("div[class=myslider]").isEmpty() + actorElem.children() + .forEach { elem -> + elem.children() + .forEach { child -> addRequest(lightmanagerUrl, actor, child) } + } + zone.addActor(actor) + } + + private fun addRequest(lightmanagerUrl: String, actor: LMActor, child: Element) { + if ("input" == child.tagName()) { + val rq = LMDefaultRequest() + val name: String = child.attr("value") + rq.name = name + val allParams = setUri(lightmanagerUrl, child, rq) + val typ = getParams(allParams, "typ", 1) + rq.type = if (typ.isNotEmpty()) RequestType.getByName(typ[0]) else RequestType.UNKNOWN + val did = getParams(allParams, "did", 1) + rq.deviceId = determineDeviceId(did) + val lActorId = getParams(allParams, "aid", 1) + rq.actorId = if (lActorId.isNotEmpty()) lActorId[0].toInt() else -1 + val acmd = getParams(allParams, "acmd", 1) + rq.actorCommand = if (acmd.isNotEmpty()) acmd[0].toInt() else -1 + val seq = getParams(allParams, "seq", 1) + rq.sequence = if (seq.isNotEmpty()) seq[0].toInt() else -1 + val lvl = getParams(allParams, "lvl", 1) + rq.level = if (lvl.isNotEmpty()) lvl[0].toInt() else -1 + val lSmk = getParams(allParams, "smk", 2) + rq.smk = if (lSmk.isNotEmpty()) intArrayOf(lSmk[0].toInt(), lSmk[1].toInt()) else IntArray(0) + val lData = getParams(allParams, "dta", -1) + rq.data = if (lData.isNotEmpty()) lData.toTypedArray() else arrayOf() + actor.addRequest(name, rq) + } + else if ("a" == child.tagName()) { + actor.addRequest("cam", LMCamRequest(child.attr("href"))) + } + } + + private fun determineDeviceId(did: List): Long { + var deviceId = -1L + if (!did.isEmpty()) { + val sDeviceId = did[0] + deviceId = try { + sDeviceId.toLong() + } catch (e: NumberFormatException) { + sDeviceId.toLong(16) + } + } + return deviceId + } + + private fun setUri(lightmanagerUrl: String, child: Element, rq: LMDefaultRequest): List { + var allParams: List = ArrayList() + var request = "" + try { + request = URLDecoder.decode(child.attr("onclick"), StandardCharsets.UTF_8) + request = request.substring(9, request.length - 2) + allParams = ArrayList(Arrays.asList(*request.split(",".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray())) + } catch (e: UnsupportedEncodingException) { + // ignore + } + val lUri = getParams(allParams, "uri", 1) + var uri = "" + try { + uri = if (!lUri.isEmpty()) { + lUri[0] + } else { + lightmanagerUrl + "?cmd=" + URLEncoder.encode(request, StandardCharsets.UTF_8) + } + } catch (e: UnsupportedEncodingException) { + // ignore + } + if (!uri.startsWith("http://") && !uri.startsWith("https://")) { + uri = "http://$uri" + } + rq.uri = uri + return allParams + } + + private fun getParams(allParams: List, name: String, numberOfParams: Int): List { + val params: List + val index = allParams.indexOf(name) + params = if (index >= 0) { + if (numberOfParams > 0) { + allParams.subList(index + 1, index + 1 + numberOfParams) + } + else { + allParams.subList(index + 1, allParams.size) + } + } + else { + emptyList() + } + return params + } + + override fun toHtml(configHolder: ConfigHolder): String { + val sb = StringBuilder() + sb.append("
") + .append(name) + .append("
\n") + sb.append("
\n") + renderLabel(sb, "Z O N E S") + zones.forEach(Consumer { zone: LMZone -> sb.append(zone.toHtml(configHolder)) }) + sb.append("
\n\n") + return sb.toString() + } + + companion object { + val P_COLOR = Pattern.compile("^([^{]*)\\{color:?([^}]*)\\}?$") + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/RequestType.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/RequestType.kt new file mode 100644 index 0000000..0d6f9f4 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/RequestType.kt @@ -0,0 +1,40 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html + + +enum class RequestType( + val type: String +) { + + INTERTECHNO("it"), + RF_433("rf4"), + RF_868("rf8"), + HOMEMATIC("rfh"), + ELDAT("rfe"), + PC("pc"), + UNIROLL("uni"), + SONOS("son"), + GIRA("in"), + ROMOTEC("rom"), + SOMFY("som"), + ALEXA_BELL("db"), + GET("get"), + POST("post"), + PUT("put"), + UDP("udp"), + TCP("tcp"), + WAKE_ON_LAN("wol"), + UNKNOWN("?"); + + companion object { + fun getByName(type: String): RequestType? { + var requestType: RequestType? = null + for (t in values()) { + if (t.type == type) { + requestType = t + break + } + } + return requestType + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/deserializer/BooleanArrayDeserializer.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/deserializer/BooleanArrayDeserializer.kt new file mode 100644 index 0000000..c664dee --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/deserializer/BooleanArrayDeserializer.kt @@ -0,0 +1,21 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.deserializer + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import java.io.IOException + +class BooleanArrayDeserializer : JsonDeserializer() { + + override fun deserialize(jsonParser: JsonParser, deserializationContext: DeserializationContext): BooleanArray { + val sStates = jsonParser.text + val stateChars = sStates.toCharArray() + val n = sStates.length + val states = BooleanArray(n) + for (i in 0 until n) { + states[i] = stateChars[i] == '1' + } + return states + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/deserializer/LMParamsInitializer.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/deserializer/LMParamsInitializer.kt new file mode 100644 index 0000000..a02ae36 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/deserializer/LMParamsInitializer.kt @@ -0,0 +1,17 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.deserializer + +import com.fasterxml.jackson.databind.util.StdConverter +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.LMParams +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class LMParamsInitializer : StdConverter() { + + override fun convert(params: LMParams): LMParams { + params.dateTime = LocalDateTime.parse( + (params.date + LocalDateTime.now().year).toString() + " " + params.time, + DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss") + ) + return params + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/deserializer/NumericBooleanDeserializer.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/deserializer/NumericBooleanDeserializer.kt new file mode 100644 index 0000000..0ae93a2 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/html/deserializer/NumericBooleanDeserializer.kt @@ -0,0 +1,13 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.deserializer + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import java.io.IOException + +class NumericBooleanDeserializer : JsonDeserializer() { + + override fun deserialize(parser: JsonParser, context: DeserializationContext): Boolean { + return "0" != parser.text + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Actuator.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Actuator.kt new file mode 100644 index 0000000..d7b1b09 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Actuator.kt @@ -0,0 +1,13 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.json + +import com.fasterxml.jackson.annotation.JsonIgnore + +class Actuator( + val nodeindex: Int? = null, + val expanded: Boolean? = null, + val properties: ActuatorProperties? = null, + val children: List = listOf() +) { + @JsonIgnore + val usedByScenes: MutableList = mutableListOf() +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/ActuatorProperties.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/ActuatorProperties.kt new file mode 100644 index 0000000..4e164a5 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/ActuatorProperties.kt @@ -0,0 +1,27 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.json + +import com.fasterxml.jackson.annotation.JsonProperty + + +class ActuatorProperties( + var ntype: Int? = null, + var index: Int? = null, + var system: Int? = null, + @JsonProperty("bemerkung") val comment: String? = null, + var ip: String? = null, // since 10.6.4 + var mac: String? = null, // since 10.6.4 + var typ: String? = null, // since 10.6.4 + var paramon: String? = null, + var paramoff: String? = null, + var sequences: Int? = null, + var btnnameon: String? = null, + var btnnameoff: String? = null, + var deviceid: String? = null, + var marker: String? = null, + var url: String? = null, + var url2: String? = null, + var sunset: Boolean? = null, + var httptype: Int? = null, + var httptype2: Int? = null, + var ntypenew: Int? = null // since 10.7.2 +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Device.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Device.kt new file mode 100644 index 0000000..ca20f80 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Device.kt @@ -0,0 +1,41 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.json + + +class Device( + val amazonmail: String? = null, + val balogin: String? = null, + val bapass: String? = null, + val cloudlogin: String? = null, + val cloudpwd: String? = null, + val connectcloud: String? = null, + val connectlocal: String? = null, + val currentip: String? = null, + val ddns: Boolean? = null, + val dhcpoff: Boolean? = null, + val dns: String? = null, + val emailalm: String? = null, + val emailfrom: String? = null, + val emailstd: String? = null, + val enoceanrelease: Boolean? = null, + val firmware: String? = null, + val gateway: String? = null, + val googlemail: String? = null, + val headch: Int? = null, + val highmpfs: String? = null, + val ip: String? = null, + val latitude: String? = null, + val ledoff: Boolean? = null, + val logenabled: Boolean? = null, + val logpass: String? = null, + val longitude: String? = null, + val manualip: String? = null, + val mode433: Int? = null, + val mode868: Int? = null, + val nochannelcheck: Boolean? = null, + val passphrase: String? = null, + val smallbandwidth: Boolean? = null, + val ssid: String? = null, + val subnet: String? = null, + val timezone: Int? = null, + val wifiscan: Map? = null, +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Marker.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Marker.kt new file mode 100644 index 0000000..ef1d5df --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Marker.kt @@ -0,0 +1,12 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.json + + +class Marker( + val coloroff: Long? = null, + val coloron: Long? = null, + val hidden: Boolean? = null, + val name: String? = null, + val touchable: Boolean? = null +) + + diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Project.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Project.kt new file mode 100644 index 0000000..425f253 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Project.kt @@ -0,0 +1,63 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.json + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.databind.json.JsonMapper +import java.io.IOException +import java.io.InputStream + +class Project( + val settings: Settings? = null, + val marker: Map? = null, + val devices: Map? = null, + val scenes: List = listOf(), + val actuators: List = listOf() +) { + + @JsonIgnore val scenesMap: Map = mapOf() + @JsonIgnore val actuatorsMap: Map = mapOf() + + private fun determineScenes(scenes: List, scenesMap: MutableMap) { + for (scene in scenes) { + val properties = scene.properties + scenesMap[properties?.index!!] = scene + determineScenes(scene.children, scenesMap) + } + } + + private fun determineActuators( + actuators: List, + actuatorsMap: MutableMap, + scenes: List + ) { + for (actuator in actuators) { + val properties = actuator.properties + actuatorsMap[properties?.index!!] = actuator + for (scene in scenes) { + if (scene.containsActuator(actuator.properties.index!!)) { + actuator.usedByScenes.add(scene) + } + } + determineActuators(actuator.children, actuatorsMap, scenes) + } + } + + companion object { + val MAPPER: JsonMapper = JsonMapper() + fun load(ins: InputStream?): Project { + val project: Project + try { + project = MAPPER.readValue(ins, Project::class.java) + val scenesMap = project.scenesMap + project.determineScenes(project.scenes, scenesMap.toMutableMap()) + project.determineActuators( + project.actuators, + project.actuatorsMap.toMutableMap(), + ArrayList(scenesMap.values) + ) + } catch (e: IOException) { + throw IllegalStateException("Could not load json file", e) + } + return project + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Scene.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Scene.kt new file mode 100644 index 0000000..d2dcee7 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Scene.kt @@ -0,0 +1,25 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.json + + +class Scene( + val nodeindex: Int? = null, + val expanded: Boolean? = null, + val properties: SceneProperties? = null, + val children: List = listOf() +) { + fun containsActuator(actuatorIndex: Int): Boolean { + for (child in children!!) { + if (child.properties?.actorIndex == actuatorIndex) { + return true + } + else { + for (childChild in child.children) { + if (childChild.containsActuator(actuatorIndex)) { + return true + } + } + } + } + return false + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/SceneProperties.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/SceneProperties.kt new file mode 100644 index 0000000..8288267 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/SceneProperties.kt @@ -0,0 +1,22 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.json + +import com.fasterxml.jackson.annotation.JsonProperty + + +class SceneProperties( + val jbcode: Int? = null, + val senderid: Long? = null, + val sendertype: Int? = null, + val ntype: Int? = null, + val index: Int? = null, + @JsonProperty("bemerkung") val comment: String? = null, + @JsonProperty("dauer") val duration: Long? = null, + @JsonProperty("befehl") val command: Int? = null, + val dimlevel: Int? = null, + val sunset: Boolean? = null, + val enabled: Boolean? = null, + @JsonProperty("aktorindex") val actorIndex: Int? = null, + val markerselect: String? = null, + val markermask: String? = null, + val markeror: Boolean? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Settings.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Settings.kt new file mode 100644 index 0000000..47d18af --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/json/Settings.kt @@ -0,0 +1,49 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.json + + +class Settings( + val currentfile: String? = null, + val ext1ch: Int? = null, + val ext2ch: Int? = null, + val ext3ch: Int? = null, + val ext4ch: Int? = null, + val ext5ch: Int? = null, + val ext6ch: Int? = null, + val ext7ch: Int? = null, + val ext8ch: Int? = null, + val extmac1: String? = null, + val extmac2: String? = null, + val extmac3: String? = null, + val extmac4: String? = null, + val extmac5: String? = null, + val extmac6: String? = null, + val extmac7: String? = null, + val extmac8: String? = null, + val extname1: String? = null, + val extname2: String? = null, + val extname3: String? = null, + val extname4: String? = null, + val extname5: String? = null, + val extname6: String? = null, + val extname7: String? = null, + val extname8: String? = null, + val filename: String? = null, + val lastactor: Int? = null, + val marksameactor: Boolean? = null, + val master: String? = null, + val nukibridges: String? = null, + val olympiaid: Int? = null, + val studioversion: String? = null, + val tmpoff0: Int? = null, + val tmpoff1: Int? = null, + val tmpoff10: Int? = null, + val tmpoff11: Int? = null, + val tmpoff2: Int? = null, + val tmpoff3: Int? = null, + val tmpoff4: Int? = null, + val tmpoff5: Int? = null, + val tmpoff6: Int? = null, + val tmpoff7: Int? = null, + val tmpoff8: Int? = null, + val tmpoff9: Int? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Actuator.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Actuator.kt new file mode 100644 index 0000000..9dadaf0 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Actuator.kt @@ -0,0 +1,14 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.xml + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement + + +@JacksonXmlRootElement +class Actuator( + val name: String? = null, + val type: String? = null, + val steps: Long? = null, + @JacksonXmlElementWrapper(localName = "commandlist") val commandList: List = listOf() +) + diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Command.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Command.kt new file mode 100644 index 0000000..6c3811d --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Command.kt @@ -0,0 +1,10 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.xml + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement + + +@JacksonXmlRootElement +class Command( + val name: String? = null, + val param: String? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Lightman.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Lightman.kt new file mode 100644 index 0000000..c817392 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Lightman.kt @@ -0,0 +1,28 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.xml + +import com.fasterxml.jackson.dataformat.xml.XmlMapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import java.io.IOException +import java.io.InputStream + +@JacksonXmlRootElement +class Lightman( + @JacksonXmlElementWrapper(localName = "lightscenes") val lightScenes: List = listOf(), + @JacksonXmlProperty(localName = "zone") @JacksonXmlElementWrapper(useWrapping = false) val zones: List = listOf() +) { + + companion object { + val MAPPER: XmlMapper = XmlMapper() + fun load(ins: InputStream?): Lightman { + val lightman: Lightman + lightman = try { + MAPPER.readValue(ins, Lightman::class.java) + } catch (e: IOException) { + throw IllegalStateException("Could not load xml file", e) + } + return lightman + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Scene.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Scene.kt new file mode 100644 index 0000000..55dbb6f --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Scene.kt @@ -0,0 +1,10 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.xml + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement + + +@JacksonXmlRootElement +class Scene( + val name: String? = null, + val param: String? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Zone.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Zone.kt new file mode 100644 index 0000000..12501b5 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/model/xml/Zone.kt @@ -0,0 +1,11 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.xml + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement + + +@JacksonXmlRootElement +class Zone( + @JacksonXmlProperty(localName = "zonename") val zoneName: String? = null, + val actuators: List = listOf() +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/widgets/ColorWheel.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/widgets/ColorWheel.kt new file mode 100644 index 0000000..eea4b15 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/widgets/ColorWheel.kt @@ -0,0 +1,39 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.widgets + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.model.color.RGBColor +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.HtmlRenderable +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class ColorWheel( + val id: String? = null +) : HtmlRenderable { + + private val log: Logger = LoggerFactory.getLogger(javaClass) + + override fun toHtml(configHolder: ConfigHolder): String { + val wheelId = id!!.replace(" ", "") + val lastColorState = configHolder.getLastColor(id) + val hexColor = lastColorState.hexColor?:"#000000" + log.info("Got color '${RGBColor(hexColor).ansiColor()}' for id '$id'") + return "
\n" + + "\t\t
COLORPICKER - " + id + "
\n" + + "\t\t
\n" + + "\t\t\t
\n" + + "\t\t
\n" + + " \n" + + "
" + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/widgets/ColorWheelOddEven.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/widgets/ColorWheelOddEven.kt new file mode 100644 index 0000000..c8fa89e --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/lightmanager/widgets/ColorWheelOddEven.kt @@ -0,0 +1,35 @@ +package de.visualdigits.kotlin.klanglicht.rest.lightmanager.widgets + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.HtmlRenderable + + +class ColorWheelOddEven( + val id: String? = null +) : HtmlRenderable { + + override fun toHtml(configHolder: ConfigHolder): String { + return "
\n" + + "\t\t
COLORPICKER
\n" + + "\t\t
\n" + + "\t\t\t
\n" + + "\t\t\t
\n" + + "\t\t
\n" + + " \n" + + "
" + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/controller/ShellyRestController.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/controller/ShellyRestController.kt new file mode 100644 index 0000000..6353e3a --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/controller/ShellyRestController.kt @@ -0,0 +1,94 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.controller + +import de.visualdigits.kotlin.klanglicht.rest.shelly.handler.ShellyHandler +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.util.Arrays +import java.util.stream.Collectors + +/** + * REST controller for shelly devices. + */ +@RestController +@RequestMapping("/v1/shelly") +class ShellyRestController { + @Autowired + var shellyHandler: ShellyHandler? = null + + /** + * Sets the given scene or index on the connected lightmanager air. + * + * @param sceneId + * @param index + */ + @GetMapping("/control") + fun control( + @RequestParam(value = "scene", required = false, defaultValue = "0") sceneId: Int, + @RequestParam(value = "index", required = false, defaultValue = "0") index: Int + ) { + shellyHandler!!.control(sceneId, index) + } + + @GetMapping(value = ["/hexColor"]) + fun hexColor( + @RequestParam(value = "ids", required = false, defaultValue = "") ids: String, + @RequestParam(value = "hexColors") hexColors: String, + @RequestParam(value = "gains", required = false, defaultValue = "") gains: String, + @RequestParam(value = "transition", required = false, defaultValue = "2000") transition: Int, + @RequestParam(value = "turnOn", required = false, defaultValue = "true") turnOn: Boolean, + @RequestParam(value = "store", required = false, defaultValue = "true") store: Boolean + ) { + val lIds = Arrays.stream(ids.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + .filter { e: String -> !e.isEmpty() } + .collect(Collectors.toList()) + val lHexColors = Arrays.stream(hexColors.split(",".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()).filter { e: String -> !e.isEmpty() }.collect(Collectors.toList()) + val lGains = Arrays.stream(gains.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + .filter { e: String -> !e.isEmpty() } + .collect(Collectors.toList()) + shellyHandler!!.hexColors(lIds, lHexColors, lGains, transition, turnOn, store) + } + + @GetMapping(value = ["/color"]) + fun color( + @RequestParam(value = "ids", required = false, defaultValue = "") ids: String, + @RequestParam(value = "red", required = false, defaultValue = "0") red: Int, + @RequestParam(value = "green", required = false, defaultValue = "0") green: Int, + @RequestParam(value = "blue", required = false, defaultValue = "0") blue: Int, + @RequestParam(value = "gains", required = false, defaultValue = "") gains: String, + @RequestParam(value = "transition", required = false, defaultValue = "2000") transition: Int, + @RequestParam(value = "turnOn", required = false, defaultValue = "true") turnOn: Boolean, + @RequestParam(value = "store", required = false, defaultValue = "true") store: Boolean + ) { + shellyHandler!!.color(ids, red, green, blue, gains, transition, turnOn, store) + } + + @GetMapping(value = ["/restore"]) + fun restoreColors( + @RequestParam(value = "ids", required = false, defaultValue = "") ids: String, + @RequestParam(value = "transition", required = false, defaultValue = "2000") transition: Int + ) { + shellyHandler!!.restoreColors(ids, transition) + } + + @GetMapping(value = ["/power"]) + fun power( + @RequestParam(value = "ids", required = false, defaultValue = "") ids: String, + @RequestParam(value = "turnOn", required = false, defaultValue = "true") turnOn: Boolean, + @RequestParam(value = "transition", required = false, defaultValue = "2000") transition: Int + ) { + shellyHandler!!.power(ids, turnOn, transition) + } + + @GetMapping(value = ["/gain"]) + fun gain( + @RequestParam(value = "ids", required = false, defaultValue = "") ids: String, + @RequestParam(value = "gain", required = false, defaultValue = "2000") gain: Int, + @RequestParam(value = "transition", required = false, defaultValue = "2000") transition: Int + ) { + shellyHandler!!.gain(ids, gain, transition) + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/controller/ShellyWebController.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/controller/ShellyWebController.kt new file mode 100644 index 0000000..862cd12 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/controller/ShellyWebController.kt @@ -0,0 +1,29 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.controller + +import de.visualdigits.kotlin.klanglicht.rest.shelly.handler.ShellyHandler +import de.visualdigits.kotlin.klanglicht.rest.shelly.model.html.ShellyStatus +import jakarta.servlet.http.HttpServletRequest +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/v1/shelly/web") +class ShellyWebController { + @Autowired + var shellyHandler: ShellyHandler? = null + + @Value("\${lightmanager.theme}") + var theme: String? = null + + @GetMapping(value = ["/powers"], produces = ["application/xhtml+xml"]) + fun currentPowers(model: Model, request: HttpServletRequest?): String { + model.addAttribute("theme", theme) + model.addAttribute("title", "Current Power Values") + model.addAttribute("content", ShellyStatus().toHtml(shellyHandler!!)) + return "pagetemplate" + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/handler/ShellyHandler.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/handler/ShellyHandler.kt new file mode 100644 index 0000000..a7bc44e --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/handler/ShellyHandler.kt @@ -0,0 +1,300 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.handler + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.model.color.RGBColor +import de.visualdigits.kotlin.klanglicht.model.preferences.ColorState +import de.visualdigits.kotlin.klanglicht.model.preferences.ShellyDevice +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.feign.LightmanagerClient +import de.visualdigits.kotlin.klanglicht.rest.shelly.model.status.Light +import de.visualdigits.kotlin.klanglicht.rest.shelly.model.status.Status +import feign.Request +import feign.okhttp.OkHttpClient +import org.apache.commons.lang3.StringUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.util.Arrays +import java.util.Objects +import java.util.function.BiConsumer + +@Component +class ShellyHandler { + + private val log: Logger = LoggerFactory.getLogger(javaClass) + + val httpClient: OkHttpClient = OkHttpClient() + + @Autowired + var client: LightmanagerClient? = null + + @Autowired + val configHolder: ConfigHolder? = null + + /** + * Sets the given scene or index on the connected lightmanager air. + * + * @param sceneId + * @param index + */ + fun control( + sceneId: Int, + index: Int + ) { + if (sceneId > 0) { + log.info("control sceneId=$sceneId") + client?.controlScene(sceneId) + } + else if (index > 0) { + log.info("control index=$index") + client?.controlIndex(index) + } + else { + throw IllegalStateException("Either parameter scene or index must be set") + } + } + /** + * Set hex colors. + * + * @param lIds The list of ids. + * @param lHexColors The list of hex colors. + * @param lGains The list of gains (taken from stage setup if omitted). + * @param transition The fade duration in milli seconds. + * @param turnOn Determines if the device should be turned on. + * @param store Determines if the new value should be stored in the cache. + */ + /** + * Set hex colors. + * + * @param lIds The list of ids. + * @param lHexColors The list of hex colors. + * @param lGains The list of gains (taken from stage setup if omitted). + * @param transition The fade duration in milli seconds. + * @param turnOn Determines if the device should be turned on. + */ + @JvmOverloads + fun hexColors( + lIds: List, + lHexColors: List, + lGains: List, + transition: Int, + turnOn: Boolean, + store: Boolean = + true + ) { + val nd = lIds.size - 1 + var d = 0 + val nh = lHexColors.size - 1 + var h = 0 + val ng = lGains.size - 1 + var g = 0 + val overrideGains = !lGains.isEmpty() + for (id in lIds) { + val sid = id.trim { it <= ' ' } + var gain = configHolder?.getShellyGain(sid)?:1 + if (overrideGains) { + gain = lGains[g].toInt() + } + var hexColor = lHexColors[h] + if (!hexColor.startsWith("#")) { + hexColor = "#$hexColor" + } + val rgbColor = RGBColor(hexColor) + setColor( + sid, + rgbColor, + gain, + transition, + turnOn, + store + ) + if (++d >= nd) { + d = nd + } + if (++h >= nh) { + h = nh + } + if (++g >= ng) { + g = ng + } + } + } + + fun color( + ids: String, + red: Int, + green: Int, + blue: Int, + gains: String, + transition: Int, + turnOn: Boolean, + store: Boolean + ) { + val lGains = Arrays.asList(*gains.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) + val ng = lGains.size - 1 + val g = 0 + val overrideGains = StringUtils.isNotEmpty(gains) + for (id in ids.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) { + val sid = id.trim { it <= ' ' } + var gain = configHolder?.getShellyGain(sid)?:1 + if (overrideGains) { + gain = lGains[g].toInt() + } + setColor( + sid, + RGBColor(red, green, blue), + gain, + transition, + turnOn, + store + ) + } + } + + fun restoreColors( + ids: String, + transition: Int + ) { + for (id in ids.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) { + val sid = id.trim { it <= ' ' } + val lastColor = configHolder?.getLastColor(sid) + setColor( + sid, + RGBColor(lastColor?.hexColor!!), + lastColor.gain?.toInt()?:1, + transition, + lastColor.on!!, + false + ) + } + } + + fun power( + ids: String, + turnOn: Boolean, + transition: Int + ) { + for (id in ids.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) { + val sid = id.trim { it <= ' ' } + val shellyDevice = configHolder?.shellyDevices?.get(sid) + if (shellyDevice != null) { + val ipAddress: String = shellyDevice.ipAddress + val command: String = shellyDevice.command + val lastColor = configHolder?.getLastColor(sid)?: ColorState("#000000") + lastColor.on = turnOn + configHolder?.putColor(sid, lastColor) // update state + val url = + "http://" + ipAddress + "/" + command + "?turn=" + (if (turnOn) "on" else "off") + "&transition=" + transition + "&" + log.info("power: $url") + try { + query(url) + } catch (e: Exception) { + log.warn("Could not set power for url '$url'") + } + } + } + } + + fun gain( + ids: String, + gain: Int, + transition: Int + ) { + for (id in ids.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) { + val sid = id.trim { it <= ' ' } + val ipAddress = configHolder?.getShellyIpAddress(sid) + val lastColor = configHolder?.getLastColor(sid)?: ColorState("#000000") + lastColor.gain = gain.toFloat() + configHolder?.putColor(sid, lastColor) // update state + val url = "http://$ipAddress/color/0?gain=$gain&transition=$transition&" + log.info("gain: $url") + try { + query(url) + } catch (e: Exception) { + log.warn("Could not get gain for url '$url'") + } + } + } + + fun currentPowers(): Map { + val powers: MutableMap = LinkedHashMap() + status().forEach { (device: ShellyDevice, status: Status) -> + powers[device.name] = status + } + return powers + } + + fun status(): Map { + val statusMap: MutableMap = LinkedHashMap() + configHolder?.shellyDevices?.values?.forEach { device -> + val ipAddress: String = device.ipAddress + val url = "http://$ipAddress/status" + log.info("get status: $url") + var status: Status + try { + val json = query(url) + status = Status.Companion.load(json) + } catch (e: Exception) { + log.warn("Could not get ststus for url '$url'") + status = Status() + status.mode = "offline" + } + statusMap[device] = status + } + return statusMap + } + + private fun setColor( + id: String, + rgbColor: RGBColor, + gain: Int, + transition: Int, + turnOn: Boolean, + store: Boolean + ): Light? { + if (store) { + val hexColor: String = rgbColor.web() + log.info("Put color '$hexColor' for id '$id'") + configHolder?.putColor( + id, + ColorState(hexColor = hexColor, gain = gain.toFloat(), on = turnOn) + ) + } + val red: Int = rgbColor.red + val green: Int = rgbColor.green + val blue: Int = rgbColor.blue + log.info(" shelly with id=$id to color ${rgbColor.ansiColor()} gain=$gain, transition=$transition, turnOn=$turnOn, store=$store") + val ipAddress = configHolder?.getShellyIpAddress(id) + val url = "http://" + ipAddress + "/color/0?" + + "turn=" + (if (turnOn) "on" else "off") + "&" + + "red=" + red + "&" + + "green=" + green + "&" + + "blue=" + blue + "&" + + "white=0&" + + "gain=" + gain + "&" + + "transition=" + transition + "&" + log.info("setColor: $url") + var light: Light? = null + try { + val json = query(url) + light = Light.load(json) + } catch (e: Exception) { + log.warn("Could not set color for url '$url'") + } + return light + } + + private fun query(url: String): String { + val request = Request.create(Request.HttpMethod.GET, url, mapOf(), byteArrayOf(), StandardCharsets.UTF_8, null) + val res = httpClient.execute(request, Request.Options()).use { response -> + if (response.status() == 200) { + response.body().asInputStream().use { ins -> String(ins.readAllBytes()) } + } else { + throw IOException("Unexpected code $response") + } + } + return res + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/html/ShellyStatus.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/html/ShellyStatus.kt new file mode 100644 index 0000000..f70416b --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/html/ShellyStatus.kt @@ -0,0 +1,86 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.html + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.model.color.RGBColor +import de.visualdigits.kotlin.klanglicht.model.preferences.ShellyDevice +import de.visualdigits.kotlin.klanglicht.rest.lightmanager.model.html.HtmlRenderable +import de.visualdigits.kotlin.klanglicht.rest.shelly.handler.ShellyHandler +import de.visualdigits.kotlin.klanglicht.rest.shelly.model.status.Status + +class ShellyStatus : HtmlRenderable { + + fun toHtml(shellyHandler: ShellyHandler): String { + val sb = StringBuilder() + sb.append("
") + .append("Current Power Values") + .append("
\n") + sb.append("
\n") + renderLabel(sb, "Shelly Dashboard") + shellyHandler.status().forEach { device: ShellyDevice, status: Status -> + sb.append("
\n") + renderLabel(sb, device.name) + sb.append("
\n") + + // power + var power = listOf(0.0) + var bgColor = "#aaaaaa" + val isOnline = "offline" != status.mode + if (isOnline) { + power = status.meters?.map { it.power?:0.0 }?:listOf(0.0) + val totalPower = power.reduce { a, b -> a + b } + bgColor = if (totalPower > 0.0) "red" else "green" + } + renderPanel(sb, "textpanel", bgColor, "Power $power") + + // on/off status + var isOn = false + val lightColors: MutableList = ArrayList() + if (isOnline) { + val relays = status.relays + if (relays != null) { + for (relay in relays) { + if (relay.isOn == true) { + isOn = true + break + } + } + bgColor = if (isOn) "red" else "green" + } else { + val lights = status.lights + if (lights != null) { + for (light in lights) { + lightColors.add(RGBColor(light.red!!, light.green!!, light.blue!!).web()) + if (light.isOn == true) { + isOn = true + } + } + bgColor = if (isOn) "red" else "green" + } else { + bgColor = "#ff00ff" + } + } + } + renderPanel(sb, "circle", bgColor, if (isOn) "on" else "off") + for (lightColor in lightColors) { + renderPanel(sb, "circle", lightColor, "") + } + sb.append("
\n") + sb.append("
\n") // group + } + sb.append("
\n") // category + return sb.toString() + } + + private fun renderPanel(sb: StringBuilder, clazz: String, bgColor: String, value: String?) { + sb.append("
\n") + if (value != null && !value.isEmpty()) { + renderLabel(sb, value) + } + sb.append("
\n") + } + + override fun toHtml(configHolder: ConfigHolder): String? { + return null + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/ActionStats.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/ActionStats.kt new file mode 100644 index 0000000..817f215 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/ActionStats.kt @@ -0,0 +1,6 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + + +class ActionStats( + val skipped: Int? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Cloud.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Cloud.kt new file mode 100644 index 0000000..7b4fc81 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Cloud.kt @@ -0,0 +1,7 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + + +class Cloud( + val enabled: Boolean? = null, + val connected: Boolean? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/ExternalTemperature.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/ExternalTemperature.kt new file mode 100644 index 0000000..7ee59f3 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/ExternalTemperature.kt @@ -0,0 +1,3 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + +class ExternalTemperature diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Humidity.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Humidity.kt new file mode 100644 index 0000000..155bf40 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Humidity.kt @@ -0,0 +1,3 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + +class Humidity diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Input.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Input.kt new file mode 100644 index 0000000..106281a --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Input.kt @@ -0,0 +1,10 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + +import com.fasterxml.jackson.annotation.JsonProperty + + +class Input( + val input: Int? = null, + val event: String? = null, + @JsonProperty("event_cnt") val eventCount: Int? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Light.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Light.kt new file mode 100644 index 0000000..51be806 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Light.kt @@ -0,0 +1,43 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper + + +class Light( + @JsonProperty("ison") val isOn: Boolean? = null, + val source: String? = null, + @JsonProperty("has_timer") val hasTimer: Boolean? = null, + @JsonProperty("timer_started") val timerStarted: Int? = null, + @JsonProperty("timer_duration") val timerDuration: Int? = null, + @JsonProperty("timer_remaining") val timerRemaining: Int? = null, + val mode: String? = null, + val red: Int? = null, + val green: Int? = null, + val blue: Int? = null, + val white: Int? = null, + val gain: Int? = null, // 0 - 100 + val effect: Int? = null, + val transiton: Int? = null, // 0 - 5000 + val power: Double? = null, + @JsonProperty("overpower") val overPower: Boolean? = null +) { + companion object { + val MAPPER: JsonMapper = JsonMapper + .builder() + .disable(SerializationFeature.INDENT_OUTPUT) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build() + + fun load(json: String?): Light { + return try { + MAPPER.readValue(json, Light::class.java) + } catch (e: JsonProcessingException) { + throw IllegalStateException("Could not read JSON string", e) + } + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Meter.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Meter.kt new file mode 100644 index 0000000..c573d5c --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Meter.kt @@ -0,0 +1,14 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + +import com.fasterxml.jackson.annotation.JsonProperty +import java.time.OffsetDateTime + + +class Meter( + val power: Double? = null, + @JsonProperty("overpower") val overPower: String? = null, // different types for rgbw and others + @JsonProperty("is_valid") var isValid: Boolean? = null, + val timestamp: OffsetDateTime? = null, + val counters: List = listOf(), + val total: Int? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Mqtt.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Mqtt.kt new file mode 100644 index 0000000..b4e5c8f --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Mqtt.kt @@ -0,0 +1,6 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + + +class Mqtt( + val connected: Boolean? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Relay.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Relay.kt new file mode 100644 index 0000000..722cf14 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Relay.kt @@ -0,0 +1,16 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + +import com.fasterxml.jackson.annotation.JsonProperty + + +class Relay( + @JsonProperty("ison") val isOn: Boolean? = null, + @JsonProperty("has_timer") val hasTimer: Boolean? = null, + @JsonProperty("timer_started") val timerStarted: Int? = null, + @JsonProperty("timer_duration") val timerDuration: Int? = null, + @JsonProperty("timer_remaining") val timerRemaining: Int? = null, + @JsonProperty("overpower") val overPower: Boolean? = null, + @JsonProperty("overtemperature") val overTemperature: Boolean? = null, + @JsonProperty("is_valid") val isValid: Boolean? = null, + val source: String? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Sensor.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Sensor.kt new file mode 100644 index 0000000..ae4d3ff --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Sensor.kt @@ -0,0 +1,3 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + +class Sensor diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Status.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Status.kt new file mode 100644 index 0000000..c9902d3 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Status.kt @@ -0,0 +1,62 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import java.time.LocalTime +import java.time.OffsetDateTime + + +class Status( + @JsonProperty("wifi_sta") var wifiState: WifiState? = null, + var cloud: Cloud? = null, + var mqtt: Mqtt? = null, + var time: LocalTime? = null, + var unixtime: OffsetDateTime? = null, + var serial: Int? = null, + @JsonProperty("has_update") var hasUpdate: Boolean? = null, + var mac: String? = null, + @JsonProperty("cfg_changed_cnt") var configChangedCount: Int? = null, + @JsonProperty("actions_stats") var actionStats: ActionStats? = null, + var mode: String? = null, + var input: Int? = null, + var lights: List? = null, + var relays: List? = null, + var meters: List? = null, + var inputs: List? = null, + @JsonProperty("ext_sensors") var sensors: Sensor? = null, + @JsonProperty("ext_temperature") var externalTemperatures: ExternalTemperature? = null, + @JsonProperty("ext_humidity") var humidities: Humidity? = null, + var temperature: Double? = null, + @JsonProperty("temperature_status") var temperatureStatus: String? = null, + @JsonProperty("overtemperature") var overTemperature: Boolean? = null, + @JsonProperty("tmp") var tmp: Temperature? = null, + var update: Update? = null, + @JsonProperty("ram_total") var ramTotal: Long? = null, + @JsonProperty("ram_free") var ramFree: Long? = null, + @JsonProperty("fs_size") var fileSystemSize: Long? = null, + @JsonProperty("fs_free") var fileSystemFree: Long? = null, + var voltage: Double? = null, + var uptime: Long? = null +) { + + companion object { + val MAPPER: JsonMapper = JsonMapper + .builder() + .disable(SerializationFeature.INDENT_OUTPUT) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .addModule(JavaTimeModule()) + .build() + + fun load(json: String?): Status { + return try { + MAPPER.readValue(json, Status::class.java) + } catch (e: JsonProcessingException) { + throw IllegalStateException("Could not read JSON string", e) + } + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Temperature.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Temperature.kt new file mode 100644 index 0000000..e17da2d --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Temperature.kt @@ -0,0 +1,10 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + +import com.fasterxml.jackson.annotation.JsonProperty + + +class Temperature( + @JsonProperty("tC") val celsius: Double? = null, + @JsonProperty("tF") val fahrenheit: Double? = null, + @JsonProperty("is_valid") val isValid: Boolean? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Update.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Update.kt new file mode 100644 index 0000000..102cb0c --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/Update.kt @@ -0,0 +1,11 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + +import com.fasterxml.jackson.annotation.JsonProperty + + +class Update( + val status: String? = null, + @JsonProperty("has_update") val hasUpdate: Boolean? = null, + @JsonProperty("new_version") val newVersion: String? = null, + @JsonProperty("old_version") val oldVersion: String? = null, +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/WifiState.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/WifiState.kt new file mode 100644 index 0000000..eb3cd8a --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/shelly/model/status/WifiState.kt @@ -0,0 +1,9 @@ +package de.visualdigits.kotlin.klanglicht.rest.shelly.model.status + + +class WifiState( + val connected: Boolean? = null, + val ssid: String? = null, + val ip: String? = null, + val rssi: Int? = null +) diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/controller/YamahaAvantageReceiverRestController.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/controller/YamahaAvantageReceiverRestController.kt new file mode 100644 index 0000000..d17b604 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/controller/YamahaAvantageReceiverRestController.kt @@ -0,0 +1,36 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.controller + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.rest.yamahaavantage.feign.YamahaAvantageReceiverClient +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/v1/yamaha/avantage/json") +class YamahaAvantageReceiverRestController { + + private val log: Logger = LoggerFactory.getLogger(javaClass) + + @Autowired + val configHolder: ConfigHolder? = null + + var client: YamahaAvantageReceiverClient? = null + + @PutMapping("/surroundProgram") + fun controlSurroundProgram(@RequestParam("program") program: String) { + ensureClient() + log.info("Setting surround sound program to '$program'") + client!!.surroundProgram(program) + } + + private fun ensureClient() { + if (client == null) { + client = YamahaAvantageReceiverClient(configHolder?.preferences?.serviceMap?.get("receiver")?.url!!) + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/controller/YamahaReceiverRestController.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/controller/YamahaReceiverRestController.kt new file mode 100644 index 0000000..ef5c08a --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/controller/YamahaReceiverRestController.kt @@ -0,0 +1,37 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.controller + +import de.visualdigits.kotlin.klanglicht.rest.common.configuration.ConfigHolder +import de.visualdigits.kotlin.klanglicht.rest.yamaha.feign.YamahaReceiverClient +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("/v1/yamaha/xml") +class YamahaReceiverRestController { + + private val log: Logger = LoggerFactory.getLogger(javaClass) + + @Autowired + val configHolder: ConfigHolder? = null + + var client: YamahaReceiverClient? = null + + @PutMapping("/surroundProgram") + fun controlSurroundProgram(@RequestParam("program") program: String) { + ensureClient() + log.info("Setting surround sound program to '$program'") + client?.controlSurroundProgram(program) + } + + private fun ensureClient() { + if (client == null) { + client = configHolder?.preferences?.serviceMap?.get("receiver")?.url?.let { YamahaReceiverClient(it) } + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/feign/YamahaReceiverClient.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/feign/YamahaReceiverClient.kt new file mode 100644 index 0000000..2b1d557 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/feign/YamahaReceiverClient.kt @@ -0,0 +1,29 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.feign + +import de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description.Menu +import de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description.UnitDescription + +class YamahaReceiverClient( + val yamahaReceiverUrl: String +) { + + var client: YamahaReceiverFeignClient? = null + var unitDescription: UnitDescription? = null + + init { + client = YamahaReceiverFeignClient.Companion.client(yamahaReceiverUrl) + unitDescription = client?.unitDescription + } + + fun controlVolume(volume: Int) { + val menu = unitDescription?.getMenu("Main Zone/Volume/Level") + val command = menu?.createCommand(volume.toString()) + client?.control(command) + } + + fun controlSurroundProgram(program: String?) { + val menu = unitDescription?.getMenu("Main Zone/Setup/Surround/Program") + val command = menu?.createCommand(program) + client?.control(command) + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/feign/YamahaReceiverFeignClient.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/feign/YamahaReceiverFeignClient.kt new file mode 100644 index 0000000..dd6f875 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/feign/YamahaReceiverFeignClient.kt @@ -0,0 +1,33 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.feign + +import de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description.UnitDescription +import feign.Feign +import feign.Headers +import feign.Logger +import feign.RequestLine +import feign.okhttp.OkHttpClient +import feign.slf4j.Slf4jLogger + +interface YamahaReceiverFeignClient { + @RequestLine("GET /YamahaRemoteControl/desc.xml") + fun description(): String + val unitDescription: UnitDescription? + get() { + val json = description() + return UnitDescription.Companion.load(json) + } + + @RequestLine("POST /YamahaRemoteControl/ctrl") + @Headers("Content-Type: text/xml; charset=UTF-8") + fun control(body: String?) + + companion object { + fun client(url: String?): YamahaReceiverFeignClient { + return Feign.builder() + .client(OkHttpClient()) + .logger(Slf4jLogger(YamahaReceiverFeignClient::class.java)) + .logLevel(Logger.Level.BASIC) + .target(YamahaReceiverFeignClient::class.java, url) + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/AbstractMenuProvider.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/AbstractMenuProvider.kt new file mode 100644 index 0000000..392106c --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/AbstractMenuProvider.kt @@ -0,0 +1,46 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import java.util.function.Consumer + +abstract class AbstractMenuProvider : XmlEntity() { + + @JacksonXmlProperty(localName = "Menu") + @JacksonXmlElementWrapper(localName = "Menu", useWrapping = false) + var menus: List = listOf() + + @JsonIgnore + var parent: AbstractMenuProvider? = null + + @JsonIgnore + val tree: MutableMap = mutableMapOf() + + abstract val key: String? + + fun getMenu(path: String): T { + var provider: AbstractMenuProvider? = this + for (key in path.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) { + provider = provider!!.tree[key] + } + return provider as T + } + + fun initializeTree() { + if (menus != null) { + menus!!.forEach(Consumer { menu: Menu -> + menu.parent = this + tree[menu.name] = menu + menu.initializeTree() + }) + } + } + + protected fun renderTree(indent: String, sb: StringBuilder) { + tree.forEach { (key: String?, subMenu: Menu) -> + sb.append(indent).append(key).append("\n") + subMenu.renderTree("$indent ", sb) + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Cmd.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Cmd.kt new file mode 100644 index 0000000..69693eb --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Cmd.kt @@ -0,0 +1,18 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "Cmd") +class Cmd : XmlEntity() { + @JacksonXmlProperty(localName = "ID", isAttribute = true) + val id: String? = null + + @JacksonXmlProperty(localName = "Type", isAttribute = true) + val type: String? = null + + @JacksonXmlText + val value: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/CmdList.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/CmdList.kt new file mode 100644 index 0000000..ec01d0f --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/CmdList.kt @@ -0,0 +1,13 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement + +@JacksonXmlRootElement(localName = "Cmd_List") +class CmdList : XmlEntity() { + @JacksonXmlProperty(localName = "Define") + @JacksonXmlElementWrapper(localName = "Define", useWrapping = false) + val define: List = listOf() +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Define.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Define.kt new file mode 100644 index 0000000..8370d8d --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Define.kt @@ -0,0 +1,15 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "Define") +class Define : XmlEntity() { + @JacksonXmlProperty(localName = "ID", isAttribute = true) + val id: String? = null + + @JacksonXmlText + val value: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Direct.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Direct.kt new file mode 100644 index 0000000..dd56fc5 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Direct.kt @@ -0,0 +1,29 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "Direct") +class Direct : XmlEntity() { + @JacksonXmlProperty(localName = "Title_1", isAttribute = true) + val title1: String? = null + + @JacksonXmlProperty(localName = "Func", isAttribute = true) + val function: String? = null + + @JacksonXmlProperty(localName = "Func_Ex", isAttribute = true) + val functionExtension: String? = null + + @JacksonXmlProperty(localName = "Icon_on", isAttribute = true) + val iconOn: String? = null + + @JacksonXmlProperty(localName = "Playable", isAttribute = true) + val playable: String? = null + + @JacksonXmlText + val value: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/F.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/F.kt new file mode 100644 index 0000000..be294fc --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/F.kt @@ -0,0 +1,16 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +class F : XmlEntity() { + @JacksonXmlProperty(localName = "Title_1", isAttribute = true) + val title: String? = null + + @JacksonXmlText + val value: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/FKey.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/FKey.kt new file mode 100644 index 0000000..dbc0f34 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/FKey.kt @@ -0,0 +1,26 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "FKey") +class FKey : XmlEntity() { + @JacksonXmlProperty(localName = "Title", isAttribute = true) + val title: String? = null + + @JacksonXmlProperty(localName = "Path") + val path: Path? = null + + @JacksonXmlProperty(localName = "F1") + val f1: F? = null + + @JacksonXmlProperty(localName = "F2") + val f2: F? = null + + @JacksonXmlProperty(localName = "F3") + val f3: F? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Get.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Get.kt new file mode 100644 index 0000000..bc31bc3 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Get.kt @@ -0,0 +1,23 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "Get") +class Get : XmlEntity() { + @JacksonXmlProperty(localName = "Cmd") + val command: Cmd? = null + + @JacksonXmlProperty(localName = "Param_1") + val param1: Param? = null + + @JacksonXmlProperty(localName = "Param_2") + val param2: Param? = null + + @JacksonXmlProperty(localName = "Param_3") + val param3: Param? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Indirect.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Indirect.kt new file mode 100644 index 0000000..eeabdf0 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Indirect.kt @@ -0,0 +1,14 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "Indirect") +class Indirect : XmlEntity() { + @JacksonXmlProperty(localName = "ID", isAttribute = true) + val id: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Language.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Language.kt new file mode 100644 index 0000000..3c7858e --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Language.kt @@ -0,0 +1,17 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "Language") +class Language : XmlEntity() { + @JacksonXmlProperty(localName = "Code", isAttribute = true) + val code: String? = null + + @JacksonXmlText + val value: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Locator.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Locator.kt new file mode 100644 index 0000000..7dec8c8 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Locator.kt @@ -0,0 +1,19 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +class Locator : XmlEntity() { + @JacksonXmlProperty(localName = "ID", isAttribute = true) + val id: String? = null + + @JacksonXmlProperty(localName = "Put_1") + val put1: Put1? = null + + @JacksonXmlText + val value: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Menu.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Menu.kt new file mode 100644 index 0000000..ec32957 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Menu.kt @@ -0,0 +1,143 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import org.apache.commons.lang3.StringUtils +import java.util.Arrays + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + +@JacksonXmlRootElement(localName = "Menu") +class Menu : AbstractMenuProvider() { + @JacksonXmlProperty(localName = "Func", isAttribute = true) + val function: String? = null + + @JacksonXmlProperty(localName = "Func_Ex", isAttribute = true) + val functionExtension: String? = null + + @JacksonXmlProperty(localName = "Title_1", isAttribute = true) + override val key: String? = null + + @JacksonXmlProperty(localName = "YNC_Tag", isAttribute = true) + val yncTag: String? = null + + @JacksonXmlProperty(localName = "List_Type", isAttribute = true) + val listType: String? = null + + @JacksonXmlProperty(localName = "Icon_on", isAttribute = true) + val iconOn: String? = null + + @JacksonXmlProperty(localName = "Disp", isAttribute = true) + val display: String? = null + + @JacksonXmlProperty(localName = "Playable", isAttribute = true) + val playable: String? = null + + @JacksonXmlProperty(localName = "Put_1") + @JacksonXmlElementWrapper(localName = "Put_1", useWrapping = false) + val put1: List = listOf() + + @JacksonXmlProperty(localName = "Put_2") + @JacksonXmlElementWrapper(localName = "Put_2", useWrapping = false) + val put2: List = listOf() + + @JacksonXmlProperty(localName = "Get") + val get: Get? = null + + @JacksonXmlProperty(localName = "Cmd_List") + val commandList: CmdList? = null + + @JacksonXmlProperty(localName = "FKey") + @JacksonXmlElementWrapper(localName = "FKey", useWrapping = false) + val fkey: List = listOf() + + @JacksonXmlProperty(localName = "SKey") + val skey: SKey? = null + val name: String? + get() { + var name = key + if (StringUtils.isEmpty(name)) { + name = function!!.replace("_", "-") + " " + functionExtension!!.replace("_", "-") + } + return name + } + val subUnitName: String? + get() { + var subUnitName: String? = null + var menu: AbstractMenuProvider? = this + while (menu != null) { + if (menu is Menu && "Subunit" == menu.function) { + subUnitName = menu.yncTag + break + } + menu = menu?.parent + } + return subUnitName + } + + fun createCommand(vararg params: String?): String { + val sb = StringBuilder() + val sections: MutableList = get?.command?.value?.split(",")?.toMutableList()?: mutableListOf() + val subUnitName = subUnitName + if (subUnitName != null) { + sections.add(0, subUnitName) + } + sb.append("") + var index = 0 + val tagsToClose: MutableList = mutableListOf() + for (section in sections) { + if (!section.contains("=")) { + if (!tagsToClose.contains(section)) { + tagsToClose.add(0, section) + sb.append("<").append(section).append(">") + } + } + else { + val elems = section.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val tagName = elems[0] + sb.append("<").append(tagName).append(">") + val paramIndex = elems[1].split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] + val value = determineParameterValue(paramIndex) + if (value != null) { + sb.append(value) + } + else { + sb.append(params[index++]) + } + sb.append("") + } + } + + // close remaining tags + for (section in tagsToClose) { + sb.append("") + } + sb.append("") + return sb.toString() + } + + private fun determineParameterValue(index: String): String? { + var value: String? = null + var param: Param? = null + if ("Param_1" == index) { + param = get?.param1 + } + else if ("Param_2" == index) { + param = get?.param2 + } + else if ("Param_3" == index) { + param = get?.param3 + } + if (param != null) { + val direct = param.direct + if (direct != null) { + if (direct.size == 1) { + value = direct[0].value + } + } + } + return value + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Param.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Param.kt new file mode 100644 index 0000000..2230ea9 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Param.kt @@ -0,0 +1,26 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +class Param : XmlEntity() { + @JacksonXmlProperty(localName = "Func") + var function: String? = null + + @JacksonXmlProperty(localName = "Direct") + @JacksonXmlElementWrapper(localName = "Direct", useWrapping = false) + var direct: List = listOf() + + @JacksonXmlProperty(localName = "Indirect") + var indirect: Indirect? = null + + @JacksonXmlProperty(localName = "Range") + var range: Range? = null + + @JacksonXmlProperty(localName = "Text") + var text: Text? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Path.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Path.kt new file mode 100644 index 0000000..3870a3e --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Path.kt @@ -0,0 +1,14 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "Path") +class Path : XmlEntity() { + @JacksonXmlText + val value: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Put1.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Put1.kt new file mode 100644 index 0000000..a7f06fc --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Put1.kt @@ -0,0 +1,35 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "Put_1") +class Put1 : XmlEntity() { + @JacksonXmlProperty(localName = "Title_1", isAttribute = true) + val title1: String? = null + + @JacksonXmlProperty(localName = "Func", isAttribute = true) + val function: String? = null + + @JacksonXmlProperty(localName = "Func_Ex", isAttribute = true) + val functionExtension: String? = null + + @JacksonXmlProperty(localName = "ID", isAttribute = true) + val id: String? = null + + @JacksonXmlProperty(localName = "Playable", isAttribute = true) + val playable: String? = null + + @JacksonXmlProperty(localName = "Visible", isAttribute = true) + val visible: String? = null + + @JacksonXmlProperty(localName = "Layout", isAttribute = true) + val layout: String? = null + + @JacksonXmlText + val value: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Put2.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Put2.kt new file mode 100644 index 0000000..a2055ab --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Put2.kt @@ -0,0 +1,23 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "Put_2") +class Put2 : XmlEntity() { + @JacksonXmlProperty(localName = "Cmd") + val command: Cmd? = null + + @JacksonXmlProperty(localName = "Param_1") + val param1: Param? = null + + @JacksonXmlProperty(localName = "Param_2") + val param2: Param? = null + + @JacksonXmlProperty(localName = "Param_3") + val param3: Param? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Range.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Range.kt new file mode 100644 index 0000000..835f1e1 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Range.kt @@ -0,0 +1,14 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "Range") +class Range : XmlEntity() { + @JacksonXmlText + val value: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/SKey.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/SKey.kt new file mode 100644 index 0000000..9b45d31 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/SKey.kt @@ -0,0 +1,32 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +@JacksonXmlRootElement(localName = "SKey") +class SKey : XmlEntity() { + @JacksonXmlProperty(localName = "Title", isAttribute = true) + val title: String? = null + + @JacksonXmlProperty(localName = "Path") + val path: Path? = null + + @JacksonXmlProperty(localName = "Play") + val play: Locator? = null + + @JacksonXmlProperty(localName = "Pause") + val pause: Locator? = null + + @JacksonXmlProperty(localName = "Stop") + val stop: Locator? = null + + @JacksonXmlProperty(localName = "Fwd") + val fwd: Locator? = null + + @JacksonXmlProperty(localName = "Rev") + val rev: Locator? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Text.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Text.kt new file mode 100644 index 0000000..7cc6cba --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/Text.kt @@ -0,0 +1,13 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +class Text : XmlEntity() { + @JacksonXmlText + val value: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/UnitDescription.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/UnitDescription.kt new file mode 100644 index 0000000..40c4a46 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/UnitDescription.kt @@ -0,0 +1,71 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import java.io.InputStream + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.dataformat.xml.XmlMapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText +import java.io.IOException +import java.util.stream.Collectors + +@JacksonXmlRootElement(localName = "Unit_Description") +class UnitDescription : AbstractMenuProvider() { + + @JacksonXmlProperty(localName = "Version", isAttribute = true) + val version: String? = null + + @JacksonXmlProperty(localName = "Unit_Name", isAttribute = true) + override val key: String? = null + + @JacksonXmlProperty(localName = "Language") + val language: Language? = null + + override fun toString(): String { + val sb = StringBuilder() + sb.append(key).append(" [").append(version).append("]\n") + renderTree("", sb) + return sb.toString() + } + + val dspPrograms: Map + get() { + val menu = getMenu("Main Zone/Setup/Surround/Program") + return menu?.put2?.get(0)?.param1?.direct?.map { direct -> + Pair(direct.value!!, direct.iconOn!!) + }?.toMap()?:mapOf() + } + + companion object { + val mapper: XmlMapper = XmlMapper() + + init { + mapper.enable(SerializationFeature.INDENT_OUTPUT) + } + + fun load(ins: InputStream): UnitDescription { + val unitDescription: UnitDescription + unitDescription = try { + mapper.readValue(ins, UnitDescription::class.java) + } catch (e: IOException) { + throw IllegalStateException("Could not unmarshall file: $ins", e) + } + unitDescription.initializeTree() + return unitDescription + } + + fun load(xml: String?): UnitDescription { + val unitDescription: UnitDescription + unitDescription = try { + mapper.readValue(xml, UnitDescription::class.java) + } catch (e: IOException) { + throw IllegalStateException("Could not unmarshall xml", e) + } + unitDescription.initializeTree() + return unitDescription + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/XmlEntity.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/XmlEntity.kt new file mode 100644 index 0000000..3beabfb --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamaha/model/description/XmlEntity.kt @@ -0,0 +1,9 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamaha.model.description + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + +open class XmlEntity diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/feign/YamahaAvantageReceiverClient.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/feign/YamahaAvantageReceiverClient.kt new file mode 100644 index 0000000..0372415 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/feign/YamahaAvantageReceiverClient.kt @@ -0,0 +1,55 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamahaavantage.feign + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.json.JsonMapper +import de.visualdigits.kotlin.klanglicht.rest.yamahaavantage.model.ResponseCode + +class YamahaAvantageReceiverClient(val yamahaReceiverUrl: String) { + + val client: YamahaAvantageReceiverFeignClient + + init { + client = YamahaAvantageReceiverFeignClient.Companion.client(yamahaReceiverUrl) + } + + fun volume(volume: Int): String? { + return client.volume(volume) + } + + fun surroundProgram(program: String): ResponseCode { + val program1 = mapPrograms[program] + println("Setting program '$program1'") + val content = client.soundProgram(program1) + println("Got: $content") + return mapper.readValue(content, ResponseCode::class.java) + } + + companion object { + val mapper: JsonMapper = JsonMapper() + val mapPrograms: MutableMap = HashMap() + + init { + mapPrograms["Standard"] = "standard" + mapPrograms["Sci-Fi"] = "sci-fi" + mapPrograms["Spectacle"] = "spectacle" + mapPrograms["Adventure"] = "adventure" + mapPrograms["The Roxy Theatre"] = + "roxy_theatre" + mapPrograms["The Bottom Line"] = + "bottom_line" + mapPrograms["Hall in Munich"] = "munich" + mapPrograms["Hall in Vienna"] = "vienna" + mapPrograms["Surround Decoder"] = "surr_decoder" + mapPrograms["7ch Stereo"] = "all_ch_stereo" + // mapPrograms.put("Straight", "straight"); +// mapPrograms.put("", "drama"); +// mapPrograms.put("", "mono_movie"); +// mapPrograms.put("", "music_video"); +// mapPrograms.put("", "sports"); +// mapPrograms.put("", "action_game"); +// mapPrograms.put("", "roleplaying_game"); +// mapPrograms.put("", "chamber"); +// mapPrograms.put("", "cellar_club"); + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/feign/YamahaAvantageReceiverFeignClient.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/feign/YamahaAvantageReceiverFeignClient.kt new file mode 100644 index 0000000..faa3a43 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/feign/YamahaAvantageReceiverFeignClient.kt @@ -0,0 +1,34 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamahaavantage.feign + +import feign.Feign +import feign.Headers +import feign.Logger +import feign.Param +import feign.RequestLine +import feign.okhttp.OkHttpClient +import feign.slf4j.Slf4jLogger + +interface YamahaAvantageReceiverFeignClient { + + @Headers("Content-Type: application/json; charset=UTF-8") + @RequestLine("GET /YamahaExtendedControl/v1/system/getDeviceInfo") + fun deviceInfo(): String? + + @RequestLine("GET /YamahaExtendedControl/v1/main/setSoundProgram?program={program}") + @Headers("Content-Type: application/json; charset=UTF-8") + fun soundProgram(@Param program: String?): String? + + @RequestLine("GET /YamahaExtendedControl/v1/main/setVolume?volume={volume}") + @Headers("Content-Type: application/json; charset=UTF-8") + fun volume(@Param volume: Int): String? + + companion object { + fun client(url: String?): YamahaAvantageReceiverFeignClient { + return Feign.builder() + .client(OkHttpClient()) + .logger(Slf4jLogger(YamahaAvantageReceiverFeignClient::class.java)) + .logLevel(Logger.Level.BASIC) + .target(YamahaAvantageReceiverFeignClient::class.java, url) + } + } +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/model/DeviceInfo.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/model/DeviceInfo.kt new file mode 100644 index 0000000..d08b22c --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/model/DeviceInfo.kt @@ -0,0 +1,21 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamahaavantage.model + + +class DeviceInfo { + val response_code: Int? = null + val device_id: String? = null + val category_code: Int? = null + val system_id: String? = null + val destination: String? = null + val operation_mode: String? = null + val serial_number: String? = null + val api_version: Double? = null + val netmodule_checksum: String? = null + val model_name: String? = null + val system_version: Double? = null + val net_module_num: Int? = null + val netmodule_generation: Int? = null + val update_data_type: Int? = null + val netmodule_version: String? = null + val update_error_code: String? = null +} diff --git a/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/model/ResponseCode.kt b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/model/ResponseCode.kt new file mode 100644 index 0000000..8b3ecf7 --- /dev/null +++ b/klanglicht-rest/src/main/kotlin/de/visualdigits/kotlin/klanglicht/rest/yamahaavantage/model/ResponseCode.kt @@ -0,0 +1,14 @@ +package de.visualdigits.kotlin.klanglicht.rest.yamahaavantage.model + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText + + +class ResponseCode { + @JsonProperty("response_code") + val responseCode = 0 +}