]> git.basschouten.com Git - openhab-addons.git/blob
54faf94b02ab550e93f1acf3d1cf381d3de12eae
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.groupepsa.internal.things;
14
15 import static org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.text.MessageFormat;
19 import java.time.ZonedDateTime;
20 import java.util.List;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.function.Function;
24
25 import javax.measure.Quantity;
26 import javax.measure.Unit;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.groupepsa.internal.bridge.GroupePSABridgeHandler;
31 import org.openhab.binding.groupepsa.internal.rest.api.dto.Air;
32 import org.openhab.binding.groupepsa.internal.rest.api.dto.AirConditioning;
33 import org.openhab.binding.groupepsa.internal.rest.api.dto.Battery;
34 import org.openhab.binding.groupepsa.internal.rest.api.dto.BatteryStatus;
35 import org.openhab.binding.groupepsa.internal.rest.api.dto.Charging;
36 import org.openhab.binding.groupepsa.internal.rest.api.dto.DoorsState;
37 import org.openhab.binding.groupepsa.internal.rest.api.dto.Energy;
38 import org.openhab.binding.groupepsa.internal.rest.api.dto.Environment;
39 import org.openhab.binding.groupepsa.internal.rest.api.dto.Health;
40 import org.openhab.binding.groupepsa.internal.rest.api.dto.Ignition;
41 import org.openhab.binding.groupepsa.internal.rest.api.dto.Kinetic;
42 import org.openhab.binding.groupepsa.internal.rest.api.dto.Luminosity;
43 import org.openhab.binding.groupepsa.internal.rest.api.dto.Odometer;
44 import org.openhab.binding.groupepsa.internal.rest.api.dto.Opening;
45 import org.openhab.binding.groupepsa.internal.rest.api.dto.Position;
46 import org.openhab.binding.groupepsa.internal.rest.api.dto.Preconditionning;
47 import org.openhab.binding.groupepsa.internal.rest.api.dto.Privacy;
48 import org.openhab.binding.groupepsa.internal.rest.api.dto.Properties;
49 import org.openhab.binding.groupepsa.internal.rest.api.dto.Safety;
50 import org.openhab.binding.groupepsa.internal.rest.api.dto.Service;
51 import org.openhab.binding.groupepsa.internal.rest.api.dto.VehicleStatus;
52 import org.openhab.binding.groupepsa.internal.rest.exceptions.GroupePSACommunicationException;
53 import org.openhab.core.i18n.TimeZoneProvider;
54 import org.openhab.core.library.types.DateTimeType;
55 import org.openhab.core.library.types.DecimalType;
56 import org.openhab.core.library.types.OpenClosedType;
57 import org.openhab.core.library.types.PointType;
58 import org.openhab.core.library.types.QuantityType;
59 import org.openhab.core.library.types.StringType;
60 import org.openhab.core.library.unit.MetricPrefix;
61 import org.openhab.core.library.unit.SIUnits;
62 import org.openhab.core.library.unit.Units;
63 import org.openhab.core.thing.Bridge;
64 import org.openhab.core.thing.Channel;
65 import org.openhab.core.thing.ChannelUID;
66 import org.openhab.core.thing.Thing;
67 import org.openhab.core.thing.ThingStatus;
68 import org.openhab.core.thing.ThingStatusDetail;
69 import org.openhab.core.thing.binding.BaseThingHandler;
70 import org.openhab.core.thing.binding.ThingHandler;
71 import org.openhab.core.thing.binding.ThingHandlerCallback;
72 import org.openhab.core.thing.binding.builder.ThingBuilder;
73 import org.openhab.core.thing.type.ChannelTypeUID;
74 import org.openhab.core.types.Command;
75 import org.openhab.core.types.RefreshType;
76 import org.openhab.core.types.UnDefType;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
79
80 import com.github.filosganga.geogson.model.Geometry;
81 import com.github.filosganga.geogson.model.positions.SinglePosition;
82
83 /**
84  * The {@link GroupePSAHandler} is responsible for handling commands, which are
85  * sent to one of the channels.
86  *
87  * @author Arjan Mels - Initial contribution
88  */
89 @NonNullByDefault
90 public class GroupePSAHandler extends BaseThingHandler {
91     private static final long DEFAULT_POLLING_INTERVAL_M = TimeUnit.MINUTES.toMinutes(1);
92     private static final long DEFAULT_ONLINE_INTERVAL_M = TimeUnit.MINUTES.toMinutes(60);
93
94     private final Logger logger = LoggerFactory.getLogger(GroupePSAHandler.class);
95
96     private final TimeZoneProvider timeZoneProvider;
97
98     private @Nullable String id = null;
99     private long lastQueryTimeNs = 0L;
100
101     private @Nullable ScheduledFuture<?> groupepsaPollingJob;
102     private long maxQueryFrequencyNanos = TimeUnit.MINUTES.toNanos(1);
103     private long onlineIntervalM;
104
105     public GroupePSAHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
106         super(thing);
107         this.timeZoneProvider = timeZoneProvider;
108     }
109
110     @Override
111     protected @Nullable Bridge getBridge() {
112         return super.getBridge();
113     }
114
115     private void pollStatus() {
116         Bridge bridge = getBridge();
117         if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
118             updateGroupePSAState();
119         } else {
120             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
121         }
122     }
123
124     @Override
125     public void handleCommand(ChannelUID channelUID, Command command) {
126         if (command instanceof RefreshType) {
127             refreshChannels(channelUID);
128         }
129     }
130
131     private void refreshChannels(ChannelUID channelUID) {
132         updateGroupePSAState();
133     }
134
135     @Override
136     public void initialize() {
137         if (getBridgeHandler() != null) {
138             GroupePSAConfiguration currentConfig = getConfigAs(GroupePSAConfiguration.class);
139             final String id = currentConfig.getId();
140             final Integer pollingIntervalM = currentConfig.getPollingInterval();
141             final Integer onlineIntervalM = currentConfig.getOnlineInterval();
142
143             if (id == null) {
144                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
145                         "@text/conf-error-no-vehicle-id");
146             } else if (pollingIntervalM != null && pollingIntervalM < 1) {
147                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
148                         "@text/conf-error-invalid-polling-interval");
149             } else if (onlineIntervalM != null && onlineIntervalM < 1) {
150                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
151                         "@text/conf-error-invalid-online-interval");
152             } else {
153                 this.id = id;
154                 this.onlineIntervalM = onlineIntervalM != null ? onlineIntervalM : DEFAULT_ONLINE_INTERVAL_M;
155                 startGroupePSAPolling(pollingIntervalM);
156             }
157
158         } else {
159             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
160         }
161     }
162
163     @Nullable
164     public GroupePSABridgeHandler getBridgeHandler() {
165         Bridge bridge = getBridge();
166         if (bridge != null) {
167             ThingHandler handler = bridge.getHandler();
168             if (handler instanceof GroupePSABridgeHandler bridgeHandler) {
169                 return bridgeHandler;
170             }
171         }
172         return null;
173     }
174
175     @Override
176     public void dispose() {
177         stopGroupePSAPolling();
178         id = null;
179     }
180
181     private void startGroupePSAPolling(@Nullable Integer pollingIntervalM) {
182         if (groupepsaPollingJob == null) {
183             final long pollingIntervalToUse = pollingIntervalM == null ? DEFAULT_POLLING_INTERVAL_M : pollingIntervalM;
184             groupepsaPollingJob = scheduler.scheduleWithFixedDelay(() -> pollStatus(), 1, pollingIntervalToUse * 60,
185                     TimeUnit.SECONDS);
186         }
187     }
188
189     private void stopGroupePSAPolling() {
190         ScheduledFuture<?> job = groupepsaPollingJob;
191         if (job != null) {
192             job.cancel(true);
193             groupepsaPollingJob = null;
194         }
195     }
196
197     private boolean isValidResult(VehicleStatus vehicle) {
198         return vehicle.getUpdatedAt() != null;
199     }
200
201     private boolean isConnected(VehicleStatus vehicle) {
202         ZonedDateTime updatedAt = vehicle.getUpdatedAt();
203         if (updatedAt == null) {
204             return false;
205         }
206
207         return updatedAt.isAfter(ZonedDateTime.now().minusMinutes(onlineIntervalM));
208     }
209
210     private synchronized void updateGroupePSAState() {
211         if (System.nanoTime() - lastQueryTimeNs <= maxQueryFrequencyNanos) {
212             return;
213         }
214
215         lastQueryTimeNs = System.nanoTime();
216
217         String id = this.id;
218         if (id == null) {
219             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-vehicle-id");
220             return;
221         }
222
223         GroupePSABridgeHandler groupepsaBridge = getBridgeHandler();
224         if (groupepsaBridge == null) {
225             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-bridge");
226             return;
227         }
228
229         try {
230             VehicleStatus vehicle = groupepsaBridge.getVehicleStatus(id);
231
232             if (vehicle != null && isValidResult(vehicle)) {
233                 logger.trace("Vehicle: {}", vehicle.toString());
234
235                 logger.debug("Update vehicle state now: {}, lastupdate: {}", ZonedDateTime.now(),
236                         vehicle.getUpdatedAt());
237
238                 updateChannelState(vehicle);
239
240                 if (isConnected(vehicle)) {
241                     updateStatus(ThingStatus.ONLINE);
242                 } else {
243                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
244                             "@text/comm-error-vehicle-not-connected-to-cloud");
245                 }
246             } else {
247                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
248                         "@text/comm-error-query-vehicle-failed");
249             }
250         } catch (GroupePSACommunicationException e) {
251             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
252                     MessageFormat.format("@text/comm-error-query-vehicle-failed", e.getMessage()));
253         }
254     }
255
256     private void updateChannelState(VehicleStatus vehicle) {
257         final DoorsState doorsState = vehicle.getDoorsState();
258         if (doorsState != null) {
259             buildDoorChannels(doorsState);
260
261             List<Opening> openings = doorsState.getOpening();
262             if (openings != null) {
263                 for (Opening opening : openings) {
264                     String id = opening.getIdentifier();
265                     if (id != null) {
266                         ChannelUID channelUID = new ChannelUID(getThing().getUID(), CHANNEL_GROUP_DOORS,
267                                 id.toLowerCase());
268                         updateState(channelUID, "open".equalsIgnoreCase(opening.getState()) ? OpenClosedType.OPEN
269                                 : OpenClosedType.CLOSED);
270                     }
271                 }
272             }
273
274             List<String> lockedState = doorsState.getLockedState();
275             updateState(CHANNEL_DOORS_LOCK, lockedState, x -> x.get(0));
276         } else {
277             updateState(CHANNEL_DOORS_LOCK, UnDefType.UNDEF);
278         }
279
280         updateState(CHANNEL_BATTERY_CURRENT, vehicle.getBattery(), Battery::getCurrent, Units.AMPERE);
281         updateState(CHANNEL_BATTERY_VOLTAGE, vehicle.getBattery(), Battery::getVoltage, Units.VOLT);
282
283         updateState(CHANNEL_ENVIRONMENT_TEMPERATURE, vehicle.getEnvironment(), Environment::getAir, Air::getTemp,
284                 SIUnits.CELSIUS);
285         updateStateBoolean(CHANNEL_ENVIRONMENT_DAYTIME, vehicle.getEnvironment(), Environment::getLuminosity,
286                 Luminosity::isDay);
287
288         updateState(CHANNEL_MOTION_IGNITION, vehicle.getIgnition(), Ignition::getType);
289
290         updateStateBoolean(CHANNEL_MOTION_MOVING, vehicle.getKinetic(), Kinetic::isMoving);
291         updateState(CHANNEL_MOTION_ACCELERATION, vehicle.getKinetic(), Kinetic::getAcceleration,
292                 Units.METRE_PER_SQUARE_SECOND);
293         updateState(CHANNEL_MOTION_SPEED, vehicle.getKinetic(), Kinetic::getSpeed, SIUnits.KILOMETRE_PER_HOUR);
294
295         updateState(CHANNEL_MOTION_MILEAGE, vehicle.getOdometer(), Odometer::getMileage,
296                 MetricPrefix.KILO(SIUnits.METRE));
297
298         Position lastPosition = vehicle.getLastPosition();
299         if (lastPosition != null) {
300             Geometry<SinglePosition> geometry = lastPosition.getGeometry();
301             if (geometry != null) {
302                 SinglePosition position = geometry.positions();
303                 if (Double.isFinite(position.alt())) {
304                     updateState(CHANNEL_POSITION_POSITION, new PointType(new DecimalType(position.lat()),
305                             new DecimalType(position.lon()), new DecimalType(position.alt())));
306                 } else {
307                     updateState(CHANNEL_POSITION_POSITION,
308                             new PointType(new DecimalType(position.lat()), new DecimalType(position.lon())));
309                 }
310             } else {
311                 updateState(CHANNEL_POSITION_POSITION, UnDefType.UNDEF);
312             }
313             updateState(CHANNEL_POSITION_HEADING, lastPosition.getProperties(), Properties::getHeading,
314                     Units.DEGREE_ANGLE);
315             updateState(CHANNEL_POSITION_TYPE, lastPosition.getProperties(), Properties::getType);
316             updateState(CHANNEL_POSITION_SIGNALSTRENGTH, lastPosition.getProperties(), Properties::getSignalQuality,
317                     Units.PERCENT);
318         }
319
320         updateState(CHANNEL_VARIOUS_LAST_UPDATED, vehicle.getUpdatedAt());
321         updateState(CHANNEL_VARIOUS_PRIVACY, vehicle.getPrivacy(), Privacy::getState);
322         updateState(CHANNEL_VARIOUS_BELT, vehicle.getSafety(), Safety::getBeltWarning);
323         updateState(CHANNEL_VARIOUS_EMERGENCY, vehicle.getSafety(), Safety::getECallTriggeringRequest);
324         updateState(CHANNEL_VARIOUS_SERVICE, vehicle.getService(), Service::getType);
325         updateState(CHANNEL_VARIOUS_PRECONDITINING, vehicle.getPreconditionning(), Preconditionning::getAirConditioning,
326                 AirConditioning::getStatus);
327         updateState(CHANNEL_VARIOUS_PRECONDITINING_FAILURE, vehicle.getPreconditionning(),
328                 Preconditionning::getAirConditioning, AirConditioning::getFailureCause);
329
330         List<Energy> energies = vehicle.getEnergy();
331         if (energies != null) {
332             for (Energy energy : energies) {
333                 if ("Fuel".equalsIgnoreCase(energy.getType())) {
334                     updateState(CHANNEL_FUEL_AUTONOMY, energy, Energy::getAutonomy, MetricPrefix.KILO(SIUnits.METRE));
335                     updateState(CHANNEL_FUEL_CONSUMPTION, energy, Energy::getConsumption,
336                             Units.LITRE.divide(MetricPrefix.KILO(SIUnits.METRE)));
337                     updateState(CHANNEL_FUEL_LEVEL, energy, Energy::getLevel, Units.PERCENT);
338                 } else if ("Electric".equalsIgnoreCase(energy.getType())) {
339                     updateState(CHANNEL_ELECTRIC_AUTONOMY, energy, Energy::getAutonomy,
340                             MetricPrefix.KILO(SIUnits.METRE));
341                     updateState(CHANNEL_ELECTRIC_RESIDUAL, energy, Energy::getResidual, Units.KILOWATT_HOUR);
342                     updateState(CHANNEL_ELECTRIC_LEVEL, energy, Energy::getLevel, Units.PERCENT);
343
344                     updateState(CHANNEL_ELECTRIC_BATTERY_CAPACITY, energy, Energy::getBattery,
345                             BatteryStatus::getCapacity, Units.KILOWATT_HOUR);
346                     updateState(CHANNEL_ELECTRIC_BATTERY_HEALTH_CAPACITY, energy, Energy::getBattery,
347                             BatteryStatus::getHealth, Health::getCapacity, Units.PERCENT);
348                     updateState(CHANNEL_ELECTRIC_BATTERY_HEALTH_RESISTANCE, energy, Energy::getBattery,
349                             BatteryStatus::getHealth, Health::getResistance, Units.PERCENT);
350
351                     updateState(CHANNEL_ELECTRIC_CHARGING_STATUS, energy, Energy::getCharging, Charging::getStatus);
352                     updateState(CHANNEL_ELECTRIC_CHARGING_MODE, energy, Energy::getCharging, Charging::getChargingMode);
353                     updateStateBoolean(CHANNEL_ELECTRIC_CHARGING_PLUGGED, energy, Energy::getCharging,
354                             Charging::isPlugged);
355                     updateState(CHANNEL_ELECTRIC_CHARGING_RATE, energy, Energy::getCharging, Charging::getChargingRate,
356                             SIUnits.KILOMETRE_PER_HOUR);
357
358                     updateState(CHANNEL_ELECTRIC_CHARGING_REMAININGTIME, energy, Energy::getCharging,
359                             Charging::getRemainingTime, x -> new BigDecimal(x.getSeconds()), Units.SECOND);
360                     updateState(CHANNEL_ELECTRIC_CHARGING_NEXTDELAYEDTIME, energy, Energy::getCharging,
361                             Charging::getNextDelayedTime, x -> new BigDecimal(x.getSeconds()), Units.SECOND);
362
363                 }
364             }
365         }
366     }
367
368     void buildDoorChannels(final DoorsState doorsState) {
369         ThingHandlerCallback callback = getCallback();
370         if (callback == null) {
371             return;
372         }
373
374         ThingBuilder thingBuilder = editThing();
375         List<Channel> channels = getThing().getChannelsOfGroup(CHANNEL_GROUP_DOORS);
376         thingBuilder.withoutChannels(channels);
377
378         ChannelUID channelUID = new ChannelUID(getThing().getUID(), CHANNEL_DOORS_LOCK);
379         ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_DOORLOCK);
380         thingBuilder.withChannel(callback.createChannelBuilder(channelUID, channelTypeUID).build());
381
382         List<Opening> openings = doorsState.getOpening();
383         if (openings != null) {
384             for (Opening opening : openings) {
385                 String id = opening.getIdentifier();
386                 if (id != null) {
387                     channelUID = new ChannelUID(getThing().getUID(), CHANNEL_GROUP_DOORS, id.toLowerCase());
388                     channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_DOOROPEN);
389                     thingBuilder.withChannel(callback.createChannelBuilder(channelUID, channelTypeUID).build());
390                 }
391             }
392         }
393
394         updateThing(thingBuilder.build());
395     }
396
397     // Various update helper functions
398
399     protected <T extends Quantity<T>> void updateState(String channelID, @Nullable BigDecimal number, Unit<T> unit) {
400         if (number != null) {
401             updateState(channelID, new QuantityType<>(number, unit));
402         } else {
403             updateState(channelID, UnDefType.UNDEF);
404         }
405     }
406
407     protected <T1, T2 extends Quantity<T2>> void updateState(String channelID, final @Nullable T1 object,
408             Function<? super T1, @Nullable BigDecimal> mapper, Unit<T2> unit) {
409         updateState(channelID, object != null ? mapper.apply(object) : null, unit);
410     }
411
412     protected <T1, T2, T3 extends Quantity<T3>> void updateState(String channelID, final @Nullable T1 object1,
413             Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable BigDecimal> mapper2,
414             Unit<T3> unit) {
415         final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
416         updateState(channelID, object2 != null ? mapper2.apply(object2) : null, unit);
417     }
418
419     protected <T1, T2, T3, T4 extends Quantity<T4>> void updateState(String channelID, final @Nullable T1 object1,
420             Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable T3> mapper2,
421             Function<? super T3, @Nullable BigDecimal> mapper3, Unit<T4> unit) {
422         final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
423         final @Nullable T3 object3 = object2 != null ? mapper2.apply(object2) : null;
424         updateState(channelID, object3 != null ? mapper3.apply(object3) : null, unit);
425     }
426
427     protected void updateState(String channelID, @Nullable ZonedDateTime date) {
428         if (date != null) {
429             updateState(channelID, new DateTimeType(date).toZone(timeZoneProvider.getTimeZone()));
430         } else {
431             updateState(channelID, UnDefType.UNDEF);
432         }
433     }
434
435     protected <T1> void updateStateDate(String channelID, @Nullable T1 object,
436             Function<? super T1, @Nullable ZonedDateTime> mapper) {
437         updateState(channelID, object != null ? mapper.apply(object) : null);
438     }
439
440     protected <T1, T2> void updateStateDate(String channelID, @Nullable T1 object1,
441             Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable ZonedDateTime> mapper2) {
442         final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
443         updateState(channelID, object2 != null ? mapper2.apply(object2) : null);
444     }
445
446     protected void updateState(String channelID, @Nullable String text) {
447         if (text != null) {
448             updateState(channelID, new StringType(text));
449         } else {
450             updateState(channelID, UnDefType.UNDEF);
451         }
452     }
453
454     protected <T1> void updateState(String channelID, @Nullable T1 object,
455             Function<? super T1, @Nullable String> mapper) {
456         updateState(channelID, object != null ? mapper.apply(object) : null);
457     }
458
459     protected <T1, T2> void updateState(String channelID, @Nullable T1 object1,
460             Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable String> mapper2) {
461         final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
462         updateState(channelID, object2 != null ? mapper2.apply(object2) : null);
463     }
464
465     protected void updateState(String channelID, @Nullable Boolean value) {
466         if (value != null) {
467             updateState(channelID, value ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
468         } else {
469             updateState(channelID, UnDefType.UNDEF);
470         }
471     }
472
473     protected <T1> void updateStateBoolean(String channelID, @Nullable T1 object,
474             Function<? super T1, @Nullable Boolean> mapper) {
475         updateState(channelID, object != null ? mapper.apply(object) : null);
476     }
477
478     protected <T1, T2> void updateStateBoolean(String channelID, final @Nullable T1 object1,
479             Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable Boolean> mapper2) {
480         final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
481         updateState(channelID, object2 != null ? mapper2.apply(object2) : null);
482     }
483 }