Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #38

Merged
merged 10 commits into from
Nov 24, 2022
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ barely tested, and some are known as inoperable.
* Remote Control N2
* TRADFRI on/off switch
* TRADFRI remote control
* LEPTITER Recessed spot light
* Sound-Controller
* SYMFONISK Sound Controller
* Blinds-Controller
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,18 @@ public class LightColorAttributes {
public Float saturation;
@JsonProperty("colorTemperature")
public Integer temperature;

/**
* The minimal color temperature in Kelvin.
* @implNote higher Kelvin indicates lower color temperature
*/
@JsonProperty("colorTemperatureMin")
public Integer temperatureMin;

/**
* The maximal color temperature in Kelvin.
* @implNote lower Kelvin indicates higher color temperature
*/
@JsonProperty("colorTemperatureMax")
public Integer temperatureMax;
@JsonProperty("colorMode")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package de.dvdgeisler.iot.dirigera.client.api.model.device.light;

import de.dvdgeisler.iot.dirigera.client.api.model.device.Device;
import de.dvdgeisler.iot.dirigera.client.api.model.device.DeviceTest;

import static org.junit.jupiter.api.Assertions.assertInstanceOf;

class LEPTITERRecessedspotlight extends DeviceTest {
final static String JSON = """
{
"id" : "7e69e364-17b6-4c15-9c18-1835d0e44d87_1",
"type" : "light",
"deviceType" : "light",
"createdAt" : "2022-11-13T12:58:16.000Z",
"isReachable" : true,
"lastSeen" : "2022-11-22T17:57:24.000Z",
"attributes" : {
"customName" : "Bathroom light",
"firmwareVersion" : "2.3.087",
"hardwareVersion" : "1",
"manufacturer" : "IKEA of Sweden",
"model" : "LEPTITER Recessed spot light",
"productCode" : "T1820",
"serialNumber" : "000D6FFFFE12EC29",
"isOn" : false,
"startupOnOff" : "startOn",
"lightLevel" : 20,
"colorMode" : "temperature",
"colorTemperature" : 2500,
"colorTemperatureMax" : 2202,
"colorTemperatureMin" : 4000,
"identifyPeriod" : 0,
"identifyStarted" : "2000-01-01T00:00:00.000Z",
"permittingJoin" : false,
"otaPolicy" : "autoUpdate",
"otaProgress" : 0,
"otaScheduleEnd" : "00:00",
"otaScheduleStart" : "00:00",
"otaState" : "readyToCheck",
"otaStatus" : "upToDate"
},
"capabilities" : {
"canSend" : [ ],
"canReceive" : [ "customName", "isOn", "lightLevel", "colorTemperature" ]
},
"room" : {
"id" : "90b00dc0-6a6a-477b-82f1-f7ac47c6cd6d",
"name" : "Bathroom",
"color" : "ikea_lilac_no_3",
"icon" : "rooms_bathtub"
},
"deviceSet" : [ ],
"remoteLinks" : [ "57e25312-9e04-4fcc-adc3-2e57c9c89be0_1" ],
"isHidden" : false
}
""";

public LEPTITERRecessedspotlight() {
super(JSON);
}

@Override
public void validateDeserialize(final Device<?,?> device) {
assertInstanceOf(LightDevice.class, device);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ public MqttBridge(
@Value("${dirigera.mqtt.reconnect:true}") final Boolean reconnect,
@Value("${dirigera.mqtt.timeout:10}") final Integer timeout,
final DirigeraApi api) throws MqttException {
super(String.format("tcp://%s:%d", host, port),
Objects.requireNonNull(api.status().map(s -> s.id).block()));
super(String.format("tcp://%s:%d", host, port), clientId(api));
final MqttConnectOptions options;
this.api = api;
this.eventHandler = new HashMap<>();
Expand Down Expand Up @@ -122,4 +121,9 @@ public <_Device extends Device> void removeDevice(final _Device device) {
.flatMap(factory -> factory.removeDevice(this, this.api, device))
.forEach(this::publishMessage);
}

private static String clientId(final DirigeraApi api) {
api.pairIfRequired().block();
return api.status().map(s -> s.id).block();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,24 @@

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.PrettyPrinter;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.dvdgeisler.iot.dirigera.client.api.DirigeraApi;
import de.dvdgeisler.iot.dirigera.client.api.model.device.light.LightDevice;
import de.dvdgeisler.iot.dirigera.client.mqtt.MqttBridge;
import de.dvdgeisler.iot.dirigera.client.mqtt.MqttBridgeMessage;
import de.dvdgeisler.iot.dirigera.client.mqtt.MqttEventHandler;
import de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.Device;
import de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.DeviceAvailability;
import de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.light.LightStatus;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.DeviceAvailabilityState.OFFLINE;
import static de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.DeviceAvailabilityState.ONLINE;
import static java.lang.Math.max;
import static java.lang.Math.min;

public abstract class HassEventHandler<_Device extends de.dvdgeisler.iot.dirigera.client.api.model.device.Device, _DeviceState> implements MqttEventHandler<_Device> {
private final static Logger log = LoggerFactory.getLogger(HassEventHandler.class);
Expand Down Expand Up @@ -56,22 +47,35 @@ protected String topic(final MqttBridge mqtt, final _Device device, final String
.collect(Collectors.joining("/"));
}

protected <T> MqttBridgeMessage<_Device> build(final MqttBridge mqtt, final _Device device, final String topic, final T payload) {
protected <T> MqttBridgeMessage<_Device> build(
final MqttBridge mqtt,
final _Device device,
final String topic,
final T payload) {
try {
return new MqttBridgeMessage<>(
device,
this,
this.topic(mqtt, device, topic),
new MqttMessage(this.objectMapper.writeValueAsBytes(payload)));
new MqttMessage(this.objectMapper.writeValueAsBytes(payload)){{
this.setQos(1);
this.setRetained(true);
}});
} catch (JsonProcessingException e) {
log.error(e.getMessage());
}
return null;
}

protected void subscribe(final MqttBridge mqtt, final DirigeraApi api, final _Device device) {
protected void subscribe(final MqttBridge mqtt, final DirigeraApi api, final _Device device, final String topicSuffix) {
final String topic;

try {
mqtt.subscribe(this.topic(mqtt, device, "set"), (topic, message) -> {
topic = this.topic(mqtt, device, topicSuffix);
log.info("Subscribe to device: topic={}, id={}, name={}, category={}, type={}",
topic, device.id, device.attributes.state.customName, device.deviceType, device.type);

mqtt.subscribe(this.topic(mqtt, device, topicSuffix), (t, message) -> {
final _DeviceState state;

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public void setDevice(final MqttBridge mqtt, final DirigeraApi api, final LightD

Optional.ofNullable(status.color_temp)
.filter(t -> canReceive(device, "colorTemperature"))
.map(t -> max(t, getMaxTemperatureMireds(device))) // todo: getMaxTemperatureMireds may return null
.map(t -> min(t, getMinTemperatureMireds(device))) // todo: getMinTemperatureMireds may return null
.flatMap(t -> Optional.ofNullable(getMaxTemperatureMireds(device)).map(maxT -> min(t, maxT)))
.flatMap(t -> Optional.ofNullable(getMinTemperatureMireds(device)).map(minT -> max(t, minT)))
.map(LightUtils::miredsToKelvin)
.map(t -> api.device.light.setTemperature(device, t))
.ifPresent(Mono::block);
Expand Down Expand Up @@ -81,7 +81,7 @@ public Stream<MqttBridgeMessage<LightDevice>> addDevice(
final List<LightColorMode> colorModes;

colorModes = colorModes(device);
this.subscribe(mqtt, api, device);
this.subscribe(mqtt, api, device, TOPIC_SET);
return Stream.of(this.build(mqtt, device, TOPIC_CONFIG,
new LightConfig(
device.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ public Stream<MqttBridgeMessage<MotionSensorDevice>> addDevice(
final DirigeraApi api,
final MotionSensorDevice device) {

this.subscribe(mqtt, api, device);
return Stream.of(this.build(mqtt, device, TOPIC_CONFIG,
new MotionSensorConfig(
device.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public Stream<MqttBridgeMessage<OutletDevice>> addDevice(
final DirigeraApi api,
final OutletDevice device) {

this.subscribe(mqtt, api, device);
this.subscribe(mqtt, api, device, TOPIC_SET);
return Stream.of(this.build(mqtt, device, TOPIC_CONFIG,
new OutletConfig(
device.id,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package de.dvdgeisler.iot.dirigera.client.mqtt.hass.model;


import de.dvdgeisler.iot.dirigera.client.api.model.device.Device;

public class DeviceAvailability {
public String topic;
public String payload_available;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.light;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class LightColor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static Integer miredsToKelvin(final Integer kelvin) {
}

public static Integer getMaxTemperatureMireds(final LightDevice device) {
return Optional.ofNullable(getMaxTemperatureKelvin(device))
return Optional.ofNullable(getMinTemperatureKelvin(device))
.map(LightUtils::kelvinToMireds)
.orElse(null);
}
Expand All @@ -34,12 +34,12 @@ public static Integer getMaxTemperatureKelvin(final LightDevice device) {
.map(d -> d.attributes)
.map(d -> d.state)
.map(d -> d.color)
.map(d -> d.temperatureMax)
.map(d -> d.temperatureMin)
.orElse(null);
}

public static Integer getMinTemperatureMireds(final LightDevice device) {
return Optional.ofNullable(getTemperatureMireds(device))
return Optional.ofNullable(getMaxTemperatureKelvin(device))
.map(LightUtils::miredsToKelvin)
.orElse(null);
}
Expand All @@ -49,7 +49,7 @@ public static Integer getMinTemperatureKelvin(final LightDevice device) {
.map(d -> d.attributes)
.map(d -> d.state)
.map(d -> d.color)
.map(d -> d.temperatureMin)
.map(d -> d.temperatureMax)
.orElse(null);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.motionsensor;

import de.dvdgeisler.iot.dirigera.client.api.model.device.motionsensor.MotionSensorDevice;
import de.dvdgeisler.iot.dirigera.client.api.model.device.outlet.OutletDevice;
import de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.DeviceState;

import java.util.Optional;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.Device;
import de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.DeviceAvailability;
import de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.DeviceState;
import de.dvdgeisler.iot.dirigera.client.mqtt.hass.model.light.LightColorMode;

import java.util.List;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class OutletConfig {
Expand Down