[Netatmo] Enhance webhook handling and dispatching (#15045)

* Enhance webhook handling and dispatching

Signed-off-by: clinique <gael@lhopital.org>

* Corrects flapping channels on Home when using single home for security and energy

Signed-off-by: clinique <gael@lhopital.org>

* Some code enhancement

Signed-off-by: clinique <gael@lhopital.org>

* Adding some missing EventType submitted on the webhook

Signed-off-by: clinique <gael@lhopital.org>

---------

Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital 2023-06-13 20:35:45 +02:00 committed by GitHub
parent c9c6e95807
commit d40c20b2dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 295 additions and 189 deletions

View File

@ -460,6 +460,7 @@ The Home thing has the following configuration elements:
| securityId | String | No | Id of a home holding security monitoring devices |
At least one of these parameter must be filled - at most two :
* id or securityId
* id or energyId
* securityId and energyId

View File

@ -18,7 +18,6 @@ import java.net.URI;
import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import javax.ws.rs.core.UriBuilder;
@ -94,7 +93,7 @@ public class SecurityApi extends RestManager {
// Remove unneeded events being before oldestKnown
return events.stream().filter(event -> freshestEventTime == null || event.getTime().isAfter(freshestEventTime))
.sorted(Comparator.comparing(HomeEvent::getTime).reversed()).collect(Collectors.toList());
.sorted(Comparator.comparing(HomeEvent::getTime).reversed()).toList();
}
public List<HomeEvent> getPersonEvents(String homeId, String personId) throws NetatmoException {

View File

@ -43,6 +43,8 @@ public enum EventSubType {
BATTERY_VERY_LOW(1, EventType.BATTERY_STATUS),
SMOKE_CLEARED(0, EventType.SMOKE),
SMOKE_DETECTED(1, EventType.SMOKE),
HUSH_ACTIVATED(0, EventType.HUSH),
HUSH_DEACTIVATED(1, EventType.HUSH),
SOUND_TEST_OK(0, EventType.SOUND_TEST),
SOUND_TEST_ERROR(1, EventType.SOUND_TEST),
DETECTOR_READY(0, EventType.TAMPERED),

View File

@ -28,6 +28,8 @@ import com.google.gson.annotations.SerializedName;
@NonNullByDefault
public enum EventType {
UNKNOWN(),
@SerializedName("webhook_activation") // Ack of a 'webhook set' Api Call
WEBHOOK_ACTIVATION(ModuleType.ACCOUNT),
@SerializedName("person") // When the Indoor Camera detects a face
PERSON(ModuleType.PERSON, ModuleType.WELCOME),
@ -71,6 +73,15 @@ public enum EventType {
@SerializedName("module_end_update") // Module's firmware update is over
MODULE_END_UPDATE(ModuleType.WELCOME),
@SerializedName("tag_big_move") // Module's firmware update is over
TAG_BIG_MOVE(ModuleType.WELCOME),
@SerializedName("tag_open") // Module's firmware update is over
TAG_OPEN(ModuleType.WELCOME),
@SerializedName("tag_small_move") // Module's firmware update is over
TAG_SMALL_MOVE(ModuleType.WELCOME),
@SerializedName("connection") // When the camera connects to Netatmo servers
CONNECTION(ModuleType.WELCOME, ModuleType.PRESENCE),

View File

@ -38,17 +38,50 @@ public class HomeData extends NAThing implements NAModule, LocationEx {
public class HomesDataResponse extends ApiResponse<ListBodyResponse<HomeData>> {
}
public class Security extends HomeData {
private NAObjectMap<HomeDataPerson> persons = new NAObjectMap<>();
public NAObjectMap<HomeDataPerson> getPersons() {
return persons;
}
public List<HomeDataPerson> getKnownPersons() {
return persons.values().stream().filter(HomeDataPerson::isKnown).toList();
}
}
public class Energy extends HomeData {
private String temperatureControlMode = "";
private SetpointMode thermMode = SetpointMode.UNKNOWN;
private int thermSetpointDefaultDuration;
private List<ThermProgram> schedules = List.of();
public int getThermSetpointDefaultDuration() {
return thermSetpointDefaultDuration;
}
public SetpointMode getThermMode() {
return thermMode;
}
public String getTemperatureControlMode() {
return temperatureControlMode;
}
public List<ThermProgram> getThermSchedules() {
return schedules;
}
public @Nullable ThermProgram getActiveProgram() {
return schedules.stream().filter(ThermProgram::isSelected).findFirst().orElse(null);
}
}
private double altitude;
private double[] coordinates = {};
private @Nullable String country;
private @Nullable String timezone;
private @Nullable String temperatureControlMode;
private SetpointMode thermMode = SetpointMode.UNKNOWN;
private int thermSetpointDefaultDuration;
private List<ThermProgram> schedules = List.of();
private NAObjectMap<HomeDataPerson> persons = new NAObjectMap<>();
private NAObjectMap<HomeDataRoom> rooms = new NAObjectMap<>();
private NAObjectMap<HomeDataModule> modules = new NAObjectMap<>();
@ -77,26 +110,6 @@ public class HomeData extends NAThing implements NAModule, LocationEx {
return Optional.ofNullable(timezone);
}
public int getThermSetpointDefaultDuration() {
return thermSetpointDefaultDuration;
}
public SetpointMode getThermMode() {
return thermMode;
}
public NAObjectMap<HomeDataPerson> getPersons() {
return persons;
}
public List<HomeDataPerson> getKnownPersons() {
return persons.values().stream().filter(HomeDataPerson::isKnown).collect(Collectors.toList());
}
public Optional<String> getTemperatureControlMode() {
return Optional.ofNullable(temperatureControlMode);
}
public NAObjectMap<HomeDataRoom> getRooms() {
return rooms;
}
@ -108,12 +121,4 @@ public class HomeData extends NAThing implements NAModule, LocationEx {
public Set<FeatureArea> getFeatures() {
return getModules().values().stream().map(m -> m.getType().feature).collect(Collectors.toSet());
}
public List<ThermProgram> getThermSchedules() {
return schedules;
}
public @Nullable ThermProgram getActiveProgram() {
return schedules.stream().filter(ThermProgram::isSelected).findFirst().orElse(null);
}
}

View File

@ -31,7 +31,7 @@ public class HomeStatusPerson extends NAThing {
return ModuleType.PERSON;
}
public boolean isOutOfSight() {
return outOfSight;
public boolean atHome() {
return !outOfSight;
}
}

View File

@ -32,22 +32,26 @@ public class NAHomeStatus {
public class HomeStatus extends NAThing {
private @Nullable NAObjectMap<HomeStatusModule> modules;
private @Nullable NAObjectMap<HomeStatusPerson> persons;
private @Nullable NAObjectMap<Room> rooms;
public NAObjectMap<HomeStatusModule> getModules() {
NAObjectMap<HomeStatusModule> localModules = modules;
return localModules != null ? localModules : new NAObjectMap<>();
}
}
public NAObjectMap<HomeStatusPerson> getPersons() {
NAObjectMap<HomeStatusPerson> localPersons = persons;
return localPersons != null ? localPersons : new NAObjectMap<>();
}
public class Energy extends HomeStatus {
private NAObjectMap<Room> rooms = new NAObjectMap<>();
public NAObjectMap<Room> getRooms() {
NAObjectMap<Room> localRooms = rooms;
return localRooms != null ? localRooms : new NAObjectMap<>();
return rooms;
}
}
public class Security extends HomeStatus {
private NAObjectMap<HomeStatusPerson> persons = new NAObjectMap<>();
public NAObjectMap<HomeStatusPerson> getPersons() {
return persons;
}
}

View File

@ -19,6 +19,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.NetatmoException;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
import org.openhab.binding.netatmo.internal.api.dto.HomeData;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus.HomeStatus;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
@ -49,22 +52,26 @@ public class NADeserializer {
.registerTypeAdapter(NAObjectMap.class, new NAObjectMapDeserializer())
.registerTypeAdapter(NAPushType.class, new NAPushTypeDeserializer())
.registerTypeAdapter(ModuleType.class, new ModuleTypeDeserializer())
.registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> {
long netatmoTS = json.getAsJsonPrimitive().getAsLong();
Instant i = Instant.ofEpochSecond(netatmoTS);
return ZonedDateTime.ofInstant(i, timeZoneProvider.getTimeZone());
})
.registerTypeAdapter(HomeStatus.class,
(JsonDeserializer<HomeStatus>) (json, type, context) -> context.deserialize(json,
json.getAsJsonObject().has("persons") ? NAHomeStatus.Security.class
: NAHomeStatus.Energy.class))
.registerTypeAdapter(HomeData.class,
(JsonDeserializer<HomeData>) (json, type, context) -> context.deserialize(json,
json.getAsJsonObject().has("therm_mode") ? HomeData.Energy.class
: HomeData.Security.class))
.registerTypeAdapter(ZonedDateTime.class, (JsonDeserializer<ZonedDateTime>) (json, type, context) -> {
long netatmoTS = json.getAsJsonPrimitive().getAsLong();
Instant i = Instant.ofEpochSecond(netatmoTS);
return ZonedDateTime.ofInstant(i, timeZoneProvider.getTimeZone());
})
.registerTypeAdapter(OnOffType.class,
(JsonDeserializer<OnOffType>) (json, type, jsonDeserializationContext) -> OnOffType
(JsonDeserializer<OnOffType>) (json, type, context) -> OnOffType
.from(json.getAsJsonPrimitive().getAsString()))
.registerTypeAdapter(OpenClosedType.class,
(JsonDeserializer<OpenClosedType>) (json, type, jsonDeserializationContext) -> {
String value = json.getAsJsonPrimitive().getAsString().toUpperCase();
return "TRUE".equals(value) || "1".equals(value) ? OpenClosedType.CLOSED
: OpenClosedType.OPEN;
})
.create();
.registerTypeAdapter(OpenClosedType.class, (JsonDeserializer<OpenClosedType>) (json, type, context) -> {
String value = json.getAsJsonPrimitive().getAsString().toUpperCase();
return "TRUE".equals(value) || "1".equals(value) ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
}).create();
}
public <T> T deserialize(Class<T> clazz, String json) throws NetatmoException {

View File

@ -32,6 +32,7 @@ import com.google.gson.JsonElement;
*/
@NonNullByDefault
class NAPushTypeDeserializer implements JsonDeserializer<NAPushType> {
private final Logger logger = LoggerFactory.getLogger(NAPushTypeDeserializer.class);
@Override
@ -40,15 +41,19 @@ class NAPushTypeDeserializer implements JsonDeserializer<NAPushType> {
final String[] elements = string.split("-");
ModuleType moduleType = ModuleType.UNKNOWN;
EventType eventType = EventType.UNKNOWN;
if (elements.length == 2) {
moduleType = fromNetatmoObject(elements[0]);
eventType = fromEvent(elements[1]);
} else {
logger.warn("Unexpected syntax received for push_type field : {}", string);
} else if (elements.length == 1) {
moduleType = ModuleType.ACCOUNT;
eventType = fromEvent(string);
}
if (moduleType.equals(ModuleType.UNKNOWN) || eventType.equals(EventType.UNKNOWN)) {
logger.warn("Unknown module or event type : {}, deserialized to '{}-{}'", string, moduleType, eventType);
}
return new NAPushType(moduleType, eventType);
}

View File

@ -61,6 +61,7 @@ import org.openhab.binding.netatmo.internal.api.WeatherApi;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.ServiceError;
import org.openhab.binding.netatmo.internal.api.dto.HomeData;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataModule;
import org.openhab.binding.netatmo.internal.api.dto.NAMain;
import org.openhab.binding.netatmo.internal.api.dto.NAModule;
@ -395,8 +396,9 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
|| h.getFeatures().contains(FeatureArea.WEATHER) && h.getFeatures().size() == 1))
.forEach(home -> {
action.apply(home, accountUID).ifPresent(homeUID -> {
home.getKnownPersons().forEach(person -> action.apply(person, homeUID));
if (home instanceof HomeData.Security securityData) {
securityData.getKnownPersons().forEach(person -> action.apply(person, homeUID));
}
Map<String, ThingUID> bridgesUids = new HashMap<>();
home.getRooms().values().stream().forEach(room -> {

View File

@ -18,7 +18,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -134,8 +133,7 @@ public interface CommonInterface {
if (thing instanceof Bridge) {
return ((Bridge) thing).getThings().stream().filter(Thing::isEnabled)
.filter(th -> th.getStatusInfo().getStatusDetail() != ThingStatusDetail.BRIDGE_OFFLINE)
.map(Thing::getHandler).filter(Objects::nonNull).map(CommonInterface.class::cast)
.collect(Collectors.toList());
.map(Thing::getHandler).filter(Objects::nonNull).map(CommonInterface.class::cast).toList();
}
return List.of();
}

View File

@ -12,13 +12,20 @@
*/
package org.openhab.binding.netatmo.internal.handler.capability;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import static org.openhab.binding.netatmo.internal.utils.ChannelTypeUtils.*;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.api.dto.WebhookEvent;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.UnDefType;
/**
* {@link AlarmEventCapability} gives the ability to handle Alarm modules events
@ -34,6 +41,22 @@ public class AlarmEventCapability extends HomeSecurityThingCapability {
super(handler, descriptionProvider, channelHelpers);
}
@Override
protected void updateWebhookEvent(WebhookEvent event) {
super.updateWebhookEvent(event);
final ThingUID thingUid = handler.getThing().getUID();
handler.updateState(new ChannelUID(thingUid, GROUP_LAST_EVENT, CHANNEL_EVENT_TYPE),
toStringType(event.getEventType()));
handler.updateState(new ChannelUID(thingUid, GROUP_LAST_EVENT, CHANNEL_EVENT_TIME),
toDateTimeType(event.getTime()));
handler.updateState(new ChannelUID(thingUid, GROUP_LAST_EVENT, CHANNEL_EVENT_SUBTYPE),
event.getSubTypeDescription().map(d -> toStringType(d)).orElse(UnDefType.NULL));
final String message = event.getName();
handler.updateState(new ChannelUID(thingUid, GROUP_LAST_EVENT, CHANNEL_EVENT_MESSAGE),
message == null || message.isBlank() ? UnDefType.NULL : toStringType(message));
}
@Override
public List<NAObject> updateReadings() {
return getSecurityCapability().map(cap -> cap.getDeviceLastEvent(handler.getId(), moduleType.apiName))

View File

@ -17,7 +17,6 @@ import static org.openhab.binding.netatmo.internal.utils.ChannelTypeUtils.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -35,8 +34,10 @@ import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
@ -53,6 +54,8 @@ public class CameraCapability extends HomeSecurityThingCapability {
protected @Nullable String localUrl;
protected @Nullable String vpnUrl;
private boolean hasSubEventGroup;
private boolean hasLastEventGroup;
public CameraCapability(CommonInterface handler, NetatmoDescriptionProvider descriptionProvider,
List<ChannelHelper> channelHelpers) {
@ -63,6 +66,13 @@ public class CameraCapability extends HomeSecurityThingCapability {
"CameraCapability must find a CameraChannelHelper, please file a bug report."));
}
@Override
public void initialize() {
Thing thing = handler.getThing();
hasSubEventGroup = !thing.getChannelsOfGroup(GROUP_SUB_EVENT).isEmpty();
hasLastEventGroup = !thing.getChannelsOfGroup(GROUP_LAST_EVENT).isEmpty();
}
@Override
public void updateHomeStatusModule(HomeStatusModule newData) {
super.updateHomeStatusModule(newData);
@ -85,23 +95,13 @@ public class CameraCapability extends HomeSecurityThingCapability {
protected void updateWebhookEvent(WebhookEvent event) {
super.updateWebhookEvent(event);
final ThingUID thingUid = handler.getThing().getUID();
handler.updateState(new ChannelUID(thingUid, GROUP_SUB_EVENT, CHANNEL_EVENT_TYPE),
toStringType(event.getEventType()));
handler.updateState(new ChannelUID(thingUid, GROUP_SUB_EVENT, CHANNEL_EVENT_TIME),
toDateTimeType(event.getTime()));
handler.updateState(new ChannelUID(thingUid, GROUP_SUB_EVENT, CHANNEL_EVENT_SNAPSHOT),
toRawType(event.getSnapshotUrl()));
handler.updateState(new ChannelUID(thingUid, GROUP_SUB_EVENT, CHANNEL_EVENT_SNAPSHOT_URL),
toStringType(event.getSnapshotUrl()));
handler.updateState(new ChannelUID(thingUid, GROUP_SUB_EVENT, CHANNEL_EVENT_VIGNETTE),
toRawType(event.getVignetteUrl()));
handler.updateState(new ChannelUID(thingUid, GROUP_SUB_EVENT, CHANNEL_EVENT_VIGNETTE_URL),
toStringType(event.getVignetteUrl()));
if (hasSubEventGroup) {
updateSubGroup(event, thing.getUID(), GROUP_SUB_EVENT);
}
final String message = event.getName();
handler.updateState(new ChannelUID(thingUid, GROUP_SUB_EVENT, CHANNEL_EVENT_MESSAGE),
message == null || message.isBlank() ? UnDefType.NULL : toStringType(message));
if (hasLastEventGroup) {
updateSubGroup(event, thing.getUID(), GROUP_LAST_EVENT);
}
// The channel should get triggered at last (after super and sub methods), because this allows rules to access
// the new updated data from the other channels.
@ -111,6 +111,25 @@ public class CameraCapability extends HomeSecurityThingCapability {
handler.triggerChannel(CHANNEL_HOME_EVENT, eventType);
}
private void updateSubGroup(WebhookEvent event, ThingUID thingUid, String group) {
handler.updateState(new ChannelUID(thingUid, group, CHANNEL_EVENT_TYPE), toStringType(event.getEventType()));
handler.updateState(new ChannelUID(thingUid, group, CHANNEL_EVENT_TIME), toDateTimeType(event.getTime()));
handler.updateState(new ChannelUID(thingUid, group, CHANNEL_EVENT_SNAPSHOT), toRawType(event.getSnapshotUrl()));
handler.updateState(new ChannelUID(thingUid, group, CHANNEL_EVENT_SNAPSHOT_URL),
toStringType(event.getSnapshotUrl()));
handler.updateState(new ChannelUID(thingUid, group, CHANNEL_EVENT_VIGNETTE), toRawType(event.getVignetteUrl()));
handler.updateState(new ChannelUID(thingUid, group, CHANNEL_EVENT_VIGNETTE_URL),
toStringType(event.getVignetteUrl()));
handler.updateState(new ChannelUID(thingUid, group, CHANNEL_EVENT_SUBTYPE),
event.getSubTypeDescription().map(d -> toStringType(d)).orElse(UnDefType.NULL));
final String message = event.getName();
handler.updateState(new ChannelUID(thingUid, group, CHANNEL_EVENT_MESSAGE),
message == null || message.isBlank() ? UnDefType.NULL : toStringType(message));
State personId = event.getPersons().isEmpty() ? UnDefType.NULL
: toStringType(event.getPersons().values().iterator().next().getId());
handler.updateState(personChannelUID, personId);
}
@Override
public void handleCommand(String channelName, Command command) {
if (command instanceof OnOffType && CHANNEL_MONITORING.equals(channelName)) {
@ -125,8 +144,8 @@ public class CameraCapability extends HomeSecurityThingCapability {
super.beforeNewData();
getSecurityCapability().ifPresent(cap -> {
NAObjectMap<HomeDataPerson> persons = cap.getPersons();
descriptionProvider.setStateOptions(personChannelUID, persons.values().stream()
.map(p -> new StateOption(p.getId(), p.getName())).collect(Collectors.toList()));
descriptionProvider.setStateOptions(personChannelUID,
persons.values().stream().map(p -> new StateOption(p.getId(), p.getName())).toList());
});
}

View File

@ -63,32 +63,32 @@ public class Capability {
public final @Nullable String setNewData(NAObject newData) {
beforeNewData();
if (newData instanceof HomeData) {
updateHomeData((HomeData) newData);
if (newData instanceof HomeData homeData) {
updateHomeData(homeData);
}
if (newData instanceof HomeStatus) {
updateHomeStatus((HomeStatus) newData);
if (newData instanceof HomeStatus homeStatus) {
updateHomeStatus(homeStatus);
}
if (newData instanceof HomeStatusModule) {
updateHomeStatusModule((HomeStatusModule) newData);
if (newData instanceof HomeStatusModule homeStatusModule) {
updateHomeStatusModule(homeStatusModule);
}
if (newData instanceof Event) {
updateEvent((Event) newData);
if (newData instanceof HomeEvent homeEvent) {
updateHomeEvent(homeEvent);
} else if (newData instanceof WebhookEvent webhookEvent && webhookEvent.getEventType().validFor(moduleType)) {
updateWebhookEvent(webhookEvent);
} else if (newData instanceof Event event) {
updateEvent(event);
}
if (newData instanceof WebhookEvent) {
updateWebhookEvent((WebhookEvent) newData);
if (newData instanceof NAThing naThing) {
updateNAThing(naThing);
}
if (newData instanceof HomeEvent) {
updateHomeEvent((HomeEvent) newData);
if (newData instanceof NAMain naMain) {
updateNAMain(naMain);
}
if (newData instanceof NAThing) {
updateNAThing((NAThing) newData);
}
if (newData instanceof NAMain) {
updateNAMain((NAMain) newData);
}
if (newData instanceof Device) {
updateNADevice((Device) newData);
if (newData instanceof Device device) {
updateNADevice(device);
}
afterNewData(newData);
return statusReason;

View File

@ -15,7 +15,6 @@ package org.openhab.binding.netatmo.internal.handler.capability;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.time.ZonedDateTime;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.EnergyApi;
@ -26,6 +25,7 @@ import org.openhab.binding.netatmo.internal.api.dto.HomeData;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataModule;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataRoom;
import org.openhab.binding.netatmo.internal.api.dto.HomeStatusModule;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus.HomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.Room;
import org.openhab.binding.netatmo.internal.config.HomeConfiguration;
@ -65,38 +65,41 @@ public class EnergyCapability extends RestCapability<EnergyApi> {
@Override
protected void updateHomeData(HomeData homeData) {
NAObjectMap<HomeDataRoom> rooms = homeData.getRooms();
NAObjectMap<HomeDataModule> modules = homeData.getModules();
handler.getActiveChildren(FeatureArea.ENERGY).forEach(childHandler -> {
String childId = childHandler.getId();
rooms.getOpt(childId)
.ifPresentOrElse(roomData -> childHandler.setNewData(roomData.ignoringForThingUpdate()), () -> {
modules.getOpt(childId)
.ifPresent(childData -> childHandler.setNewData(childData.ignoringForThingUpdate()));
modules.values().stream().filter(module -> childId.equals(module.getBridge()))
.forEach(bridgedModule -> childHandler.setNewData(bridgedModule));
});
});
descriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), GROUP_ENERGY, CHANNEL_PLANNING),
homeData.getThermSchedules().stream().map(p -> new StateOption(p.getId(), p.getName()))
.collect(Collectors.toList()));
setPointDefaultDuration = homeData.getThermSetpointDefaultDuration();
if (homeData instanceof HomeData.Energy energyData) {
NAObjectMap<HomeDataRoom> rooms = energyData.getRooms();
NAObjectMap<HomeDataModule> modules = energyData.getModules();
handler.getActiveChildren(FeatureArea.ENERGY).forEach(childHandler -> {
String childId = childHandler.getId();
rooms.getOpt(childId)
.ifPresentOrElse(roomData -> childHandler.setNewData(roomData.ignoringForThingUpdate()), () -> {
modules.getOpt(childId).ifPresent(
childData -> childHandler.setNewData(childData.ignoringForThingUpdate()));
modules.values().stream().filter(module -> childId.equals(module.getBridge()))
.forEach(bridgedModule -> childHandler.setNewData(bridgedModule));
});
});
descriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), GROUP_ENERGY, CHANNEL_PLANNING),
energyData.getThermSchedules().stream().map(p -> new StateOption(p.getId(), p.getName())).toList());
setPointDefaultDuration = energyData.getThermSetpointDefaultDuration();
}
}
@Override
protected void updateHomeStatus(HomeStatus homeStatus) {
NAObjectMap<Room> rooms = homeStatus.getRooms();
NAObjectMap<HomeStatusModule> modules = homeStatus.getModules();
handler.getActiveChildren(FeatureArea.ENERGY).forEach(childHandler -> {
String childId = childHandler.getId();
rooms.getOpt(childId).ifPresentOrElse(roomData -> childHandler.setNewData(roomData), () -> {
modules.getOpt(childId).ifPresent(moduleData -> {
childHandler.setNewData(moduleData);
modules.values().stream().filter(module -> childId.equals(module.getBridge()))
.forEach(bridgedModule -> childHandler.setNewData(bridgedModule));
if (homeStatus instanceof NAHomeStatus.Energy energyStatus) {
NAObjectMap<Room> rooms = energyStatus.getRooms();
NAObjectMap<HomeStatusModule> modules = energyStatus.getModules();
handler.getActiveChildren(FeatureArea.ENERGY).forEach(childHandler -> {
String childId = childHandler.getId();
rooms.getOpt(childId).ifPresentOrElse(roomData -> childHandler.setNewData(roomData), () -> {
modules.getOpt(childId).ifPresent(moduleData -> {
childHandler.setNewData(moduleData);
modules.values().stream().filter(module -> childId.equals(module.getBridge()))
.forEach(bridgedModule -> childHandler.setNewData(bridgedModule));
});
});
});
});
}
}
public void setThermPoint(String roomId, SetpointMode mode, long endtime, double temp) {

View File

@ -13,11 +13,11 @@
package org.openhab.binding.netatmo.internal.handler.capability;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import static org.openhab.binding.netatmo.internal.utils.ChannelTypeUtils.*;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -28,13 +28,16 @@ import org.openhab.binding.netatmo.internal.api.dto.Event;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataModule;
import org.openhab.binding.netatmo.internal.api.dto.HomeEvent;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.api.dto.WebhookEvent;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
/**
* {@link PersonCapability} gives the ability to handle Person specifics
@ -60,7 +63,7 @@ public class PersonCapability extends HomeSecurityThingCapability {
Stream<HomeDataModule> cameras = cap.getModules().values().stream()
.filter(module -> module.getType() == ModuleType.WELCOME);
descriptionProvider.setStateOptions(cameraChannelUID,
cameras.map(p -> new StateOption(p.getId(), p.getName())).collect(Collectors.toList()));
cameras.map(p -> new StateOption(p.getId(), p.getName())).toList());
});
}
@ -71,6 +74,28 @@ public class PersonCapability extends HomeSecurityThingCapability {
}
}
@Override
protected void updateWebhookEvent(WebhookEvent event) {
super.updateWebhookEvent(event);
ThingUID thingUid = thing.getUID();
handler.updateState(new ChannelUID(thingUid, GROUP_LAST_EVENT, CHANNEL_EVENT_SUBTYPE),
event.getSubTypeDescription().map(d -> toStringType(d)).orElse(UnDefType.NULL));
final String message = event.getName();
handler.updateState(new ChannelUID(thingUid, GROUP_LAST_EVENT, CHANNEL_EVENT_MESSAGE),
message == null || message.isBlank() ? UnDefType.NULL : toStringType(message));
handler.updateState(new ChannelUID(thingUid, GROUP_LAST_EVENT, CHANNEL_EVENT_TIME),
toDateTimeType(event.getTime()));
handler.updateState(new ChannelUID(thingUid, GROUP_LAST_EVENT, CHANNEL_EVENT_SNAPSHOT),
toRawType(event.getSnapshotUrl()));
handler.updateState(cameraChannelUID, toStringType(event.getCameraId()));
}
@Override
public void updateEvent(Event event) {
super.updateEvent(event);

View File

@ -31,6 +31,7 @@ import org.openhab.binding.netatmo.internal.api.dto.HomeDataPerson;
import org.openhab.binding.netatmo.internal.api.dto.HomeEvent;
import org.openhab.binding.netatmo.internal.api.dto.HomeStatusModule;
import org.openhab.binding.netatmo.internal.api.dto.HomeStatusPerson;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus.HomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.config.HomeConfiguration;
@ -69,34 +70,38 @@ class SecurityCapability extends RestCapability<SecurityApi> {
@Override
protected void updateHomeData(HomeData homeData) {
persons = homeData.getPersons();
modules = homeData.getModules();
handler.getActiveChildren(FeatureArea.SECURITY).forEach(childHandler -> {
String childId = childHandler.getId();
persons.getOpt(childId)
.ifPresentOrElse(personData -> childHandler.setNewData(personData.ignoringForThingUpdate()), () -> {
modules.getOpt(childId)
.ifPresent(childData -> childHandler.setNewData(childData.ignoringForThingUpdate()));
modules.values().stream().filter(module -> childId.equals(module.getBridge()))
.forEach(bridgedModule -> childHandler.setNewData(bridgedModule));
});
});
if (homeData instanceof HomeData.Security securityData) {
persons = securityData.getPersons();
modules = homeData.getModules();
handler.getActiveChildren(FeatureArea.SECURITY).forEach(childHandler -> {
String childId = childHandler.getId();
persons.getOpt(childId).ifPresentOrElse(
personData -> childHandler.setNewData(personData.ignoringForThingUpdate()), () -> {
modules.getOpt(childId).ifPresent(
childData -> childHandler.setNewData(childData.ignoringForThingUpdate()));
modules.values().stream().filter(module -> childId.equals(module.getBridge()))
.forEach(bridgedModule -> childHandler.setNewData(bridgedModule));
});
});
}
}
@Override
protected void updateHomeStatus(HomeStatus homeStatus) {
NAObjectMap<HomeStatusPerson> persons = homeStatus.getPersons();
NAObjectMap<HomeStatusModule> modules = homeStatus.getModules();
handler.getActiveChildren(FeatureArea.SECURITY).forEach(childHandler -> {
String childId = childHandler.getId();
persons.getOpt(childId).ifPresentOrElse(personData -> childHandler.setNewData(personData), () -> {
modules.getOpt(childId).ifPresent(childData -> {
childHandler.setNewData(childData);
modules.values().stream().filter(module -> childId.equals(module.getBridge()))
.forEach(bridgedModule -> childHandler.setNewData(bridgedModule));
if (homeStatus instanceof NAHomeStatus.Security securityStatus) {
NAObjectMap<HomeStatusPerson> persons = securityStatus.getPersons();
NAObjectMap<HomeStatusModule> modules = securityStatus.getModules();
handler.getActiveChildren(FeatureArea.SECURITY).forEach(childHandler -> {
String childId = childHandler.getId();
persons.getOpt(childId).ifPresentOrElse(personData -> childHandler.setNewData(personData), () -> {
modules.getOpt(childId).ifPresent(childData -> {
childHandler.setNewData(childData);
modules.values().stream().filter(module -> childId.equals(module.getBridge()))
.forEach(bridgedModule -> childHandler.setNewData(bridgedModule));
});
});
});
});
}
}
@Override

View File

@ -50,13 +50,12 @@ public class EnergyChannelHelper extends ChannelHelper {
@Override
protected @Nullable State internalGetProperty(String channelId, NAThing data, Configuration config) {
if (data instanceof HomeData) {
HomeData homeData = (HomeData) data;
SetpointMode thermMode = homeData.getThermMode();
ThermProgram currentProgram = homeData.getActiveProgram();
if (data instanceof HomeData.Energy energyData) {
SetpointMode thermMode = energyData.getThermMode();
ThermProgram currentProgram = energyData.getActiveProgram();
switch (channelId) {
case CHANNEL_SETPOINT_DURATION:
return toQuantityType(homeData.getThermSetpointDefaultDuration(), Units.MINUTE);
return toQuantityType(energyData.getThermSetpointDefaultDuration(), Units.MINUTE);
case CHANNEL_PLANNING:
return (currentProgram != null ? toStringType(currentProgram.getName()) : null);
case CHANNEL_SETPOINT_END_TIME:

View File

@ -54,7 +54,7 @@ public class PersonChannelHelper extends ChannelHelper {
HomeStatusPerson person = (HomeStatusPerson) naThing;
switch (channelId) {
case CHANNEL_PERSON_AT_HOME:
return OnOffType.from(!person.isOutOfSight());
return OnOffType.from(person.atHome());
case CHANNEL_LAST_SEEN:
return toDateTimeType(person.getLastSeen());
}

View File

@ -17,15 +17,14 @@ import static org.openhab.binding.netatmo.internal.utils.ChannelTypeUtils.toRawT
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.dto.HomeData;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataPerson;
import org.openhab.binding.netatmo.internal.api.dto.HomeStatusPerson;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus.HomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
@ -50,16 +49,11 @@ public class SecurityChannelHelper extends ChannelHelper {
@Override
public void setNewData(@Nullable NAObject data) {
super.setNewData(data);
if (data instanceof HomeData) {
HomeData homeData = (HomeData) data;
knownIds = homeData.getPersons().values().stream().filter(person -> person.isKnown()).map(p -> p.getId())
.collect(Collectors.toList());
}
if (data instanceof HomeStatus) {
HomeStatus status = (HomeStatus) data;
NAObjectMap<HomeStatusPerson> allPersons = status.getPersons();
List<HomeStatusPerson> present = allPersons.values().stream().filter(p -> !p.isOutOfSight())
.collect(Collectors.toList());
if (data instanceof HomeData.Security securityData) {
knownIds = securityData.getKnownPersons().stream().map(HomeDataPerson::getId).toList();
} else if (data instanceof NAHomeStatus.Security securityStatus) {
List<HomeStatusPerson> present = securityStatus.getPersons().values().stream()
.filter(HomeStatusPerson::atHome).toList();
persons = present.size();
unknowns = present.stream().filter(person -> !knownIds.contains(person.getId())).count();

View File

@ -18,7 +18,6 @@ import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -63,8 +62,7 @@ public class NetatmoThingTypeProvider implements ThingTypeProvider {
@Override
public Collection<ThingType> getThingTypes(@Nullable Locale locale) {
return ModuleType.AS_SET.stream().filter(mt -> mt != ModuleType.UNKNOWN)
.map(mt -> Optional.ofNullable(getThingType(mt.thingTypeUID, locale))).map(Optional::get)
.collect(Collectors.toList());
.map(mt -> Optional.ofNullable(getThingType(mt.thingTypeUID, locale))).map(Optional::get).toList();
}
@Override
@ -95,7 +93,7 @@ public class NetatmoThingTypeProvider implements ThingTypeProvider {
private List<ChannelGroupDefinition> getGroupDefinitions(ModuleType thingType) {
return thingType.getGroupTypes().stream().map(groupType -> new ChannelGroupDefinition(toGroupName(groupType),
new ChannelGroupTypeUID(BINDING_ID, groupType))).collect(Collectors.toList());
new ChannelGroupTypeUID(BINDING_ID, groupType))).toList();
}
public static String toGroupName(String groupeTypeName) {

View File

@ -70,8 +70,8 @@ channel-group-type.netatmo.rain.channel.sum-1.label = Rain 1h
channel-group-type.netatmo.rain.channel.sum-1.description = Quantity of water over last hour.
channel-group-type.netatmo.rain.channel.sum-24.label = Rain 24h
channel-group-type.netatmo.rain.channel.sum-24.description = Quantity of water during the current day.
channel-group-type.netatmo.security.label = Home Security
channel-group-type.netatmo.security-event.label = Home Security Event
channel-group-type.netatmo.security.label = Home Security
channel-group-type.netatmo.setpoint.label = Setpoint
channel-group-type.netatmo.setpoint.channel.end.label = Setpoint End
channel-group-type.netatmo.setpoint.channel.end.description = End time of the currently applied setpoint.
@ -81,13 +81,6 @@ channel-group-type.netatmo.signal.label = Signal
channel-group-type.netatmo.siren.label = Siren Status
channel-group-type.netatmo.status-doorbell.label = Camera Status
channel-group-type.netatmo.status.label = Camera Status
channel-group-type.netatmo.sub-event.label = Sub Event
channel-group-type.netatmo.sub-event.channel.time.label = Sub-Event Timestamp
channel-group-type.netatmo.sub-event.channel.time.description = Moment when the sub-event occurred.
channel-group-type.netatmo.sub-event.channel.vignette.label = Vignette
channel-group-type.netatmo.sub-event.channel.vignette.description = Vignette of the Snapshot.
channel-group-type.netatmo.sub-event.channel.vignette-url.label = Vignette URL
channel-group-type.netatmo.sub-event.channel.vignette-url.description = URL of the vignette.
channel-group-type.netatmo.sub-event-doorbell.label = Sub Event
channel-group-type.netatmo.sub-event-doorbell.channel.time.label = Sub-Event Timestamp
channel-group-type.netatmo.sub-event-doorbell.channel.time.description = Moment when the sub-event occurred.
@ -95,6 +88,13 @@ channel-group-type.netatmo.sub-event-doorbell.channel.vignette.label = Vignette
channel-group-type.netatmo.sub-event-doorbell.channel.vignette.description = Vignette of the Snapshot.
channel-group-type.netatmo.sub-event-doorbell.channel.vignette-url.label = Vignette URL
channel-group-type.netatmo.sub-event-doorbell.channel.vignette-url.description = URL of the vignette.
channel-group-type.netatmo.sub-event.label = Sub Event
channel-group-type.netatmo.sub-event.channel.time.label = Sub-Event Timestamp
channel-group-type.netatmo.sub-event.channel.time.description = Moment when the sub-event occurred.
channel-group-type.netatmo.sub-event.channel.vignette.label = Vignette
channel-group-type.netatmo.sub-event.channel.vignette.description = Vignette of the Snapshot.
channel-group-type.netatmo.sub-event.channel.vignette-url.label = Vignette URL
channel-group-type.netatmo.sub-event.channel.vignette-url.description = URL of the vignette.
channel-group-type.netatmo.tag.label = Door Tag
channel-group-type.netatmo.temperature-extended.label = Temperature
channel-group-type.netatmo.temperature-extended.channel.max-time.label = Today Max Timestamp
@ -189,6 +189,8 @@ channel-type.netatmo.event-subtype.state.option.BATTERY_LOW = Battery low
channel-type.netatmo.event-subtype.state.option.BATTERY_VERY_LOW = Battery very low
channel-type.netatmo.event-subtype.state.option.SMOKE_CLEARED = Smoke cleared
channel-type.netatmo.event-subtype.state.option.SMOKE_DETECTED = Smoke detected
channel-type.netatmo.event-subtype.state.option.HUSH_ACTIVATED = Smoke detection activated
channel-type.netatmo.event-subtype.state.option.HUSH_DEACTIVATED = Smoke detection deactivated
channel-type.netatmo.event-subtype.state.option.WIFI_STATUS_OK = Wi-Fi status ok
channel-type.netatmo.event-subtype.state.option.WIFI_STATUS_ERROR = Wi-Fi status error
channel-type.netatmo.event-subtype.state.option.CO_OK = Carbon Monoxide OK

View File

@ -404,6 +404,8 @@
<option value="BATTERY_VERY_LOW">Battery very low</option>
<option value="SMOKE_CLEARED">Smoke cleared</option>
<option value="SMOKE_DETECTED">Smoke detected</option>
<option value="HUSH_ACTIVATED">Smoke detection activated</option>
<option value="HUSH_DEACTIVATED">Smoke detection deactivated</option>
<option value="WIFI_STATUS_OK">Wi-Fi status ok</option>
<option value="WIFI_STATUS_ERROR">Wi-Fi status error</option>
<option value="CO_OK">Carbon Monoxide OK</option>

View File

@ -17,6 +17,7 @@ import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.Collections;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.binding.netatmo.internal.api.data.EventType;
@ -27,9 +28,10 @@ import org.openhab.core.types.State;
/**
* @author Sven Strohschein - Initial contribution
*/
@NonNullByDefault
public class EventCameraChannelHelperTest {
private EventCameraChannelHelper helper;
private @NonNullByDefault({}) EventCameraChannelHelper helper;
@BeforeEach
public void before() {