[paradoxalarm] Add SYNC_TIME command to sync panel clock on demand (#20744)

* [paradoxalarm] Add SYNC_TIME command to sync panel clock on demand

Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>
This commit is contained in:
Konstantin Polihronov
2026-06-06 11:35:40 +02:00
committed by GitHub
parent 212f19f57a
commit b757e91cd2
13 changed files with 160 additions and 14 deletions
@@ -41,18 +41,23 @@ Currently binding supports the following panels: EVO192, EVO48 (not tested), EVO
### IP150 Bridge Channels
| Channel | Description |
|----------------------|-------------------------------------------------------------------------------------------------------|
| communicationCommand | Possible values [LOGOUT, LOGIN, RESET] |
| communicationState | Shows the communication status to Paradox (may differ from Bridge status). Values: [Offline, Online]. |
| Channel | Description |
|----------------------|----------------------------------------------------------------------------------------------------------------------------------------|
| communicationCommand | Possible values [LOGOUT, LOGIN, RESET, SYNC_TIME] |
| communicationState | Shows the communication status to Paradox (may differ from Bridge status). Values: [Offline, Online]. |
#### Communication Command Values
| Value | Description |
|--------|------------------------------------------------------------------------------------------------------------------------|
| LOGOUT | Logs out and disconnects from Paradox alarm system. |
| LOGIN | Creates socket if necessary, connects to Paradox system, and uses the logon data from the Thing parameters to connect. |
| RESET | Does logout and then login with recreation of communicator objects inside the code. |
| Value | Description |
|-----------|--------------------------------------------------------------------------------------------------------------------------------|
| LOGOUT | Logs out and disconnects from Paradox alarm system. |
| LOGIN | Creates socket if necessary, connects to Paradox system, and uses the logon data from the Thing parameters to connect. |
| RESET | Does logout and then login with recreation of communicator objects inside the code. |
| SYNC_TIME | Pushes the current openHAB host time to the panel clock on demand. |
**Note on SYNC_TIME timezone:** The time sent to the panel uses the JVM default timezone of the openHAB host (i.e. the timezone configured for the Java process or the OS).
If the host runs in UTC (e.g. a Docker container without an explicit timezone setting) but the panel is installed in a different timezone, the panel clock will be set to the wrong local time.
To avoid this, ensure the openHAB host timezone matches the timezone where the panel is located — for example, by setting the `TZ` environment variable on the container.
### Entities (zones, partitions) Configuration
@@ -183,7 +188,7 @@ Currently binding supports the following panels: EVO192, EVO48 (not tested), EVO
Text label="Security" icon="lock" {
Frame label="IP150 communication" {
Text item=panelState valuecolor=[panelState=="Online"="green", panelState=="Offline"="red"]
Selection item=paradoxSendCommand mappings=["LOGOUT"="Logout", "LOGIN"="Login", "RESET"="Reset"]
Selection item=paradoxSendCommand mappings=["LOGOUT"="Logout", "LOGIN"="Login", "RESET"="Reset", "SYNC_TIME"="Sync Time"]
}
Frame label="Panel" {
Text item=paradoxTime
@@ -120,7 +120,11 @@ public abstract class AbstractCommunicator implements IParadoxInitialLoginCommun
byte[] packetBytes = request.getRequestPacket().getBytes();
ParadoxUtil.printPacket("Tx Packet:", packetBytes);
tx.write(packetBytes);
syncQueue.moveRequest();
if (request.isResponseExpected()) {
syncQueue.moveRequest();
} else {
syncQueue.removeSendRequest();
}
} catch (SocketException e) {
logger.debug("Socket time out occurred. Informing listener. Request={}. Exception=", request, e);
syncQueue.removeSendRequest();
@@ -15,6 +15,7 @@ package org.openhab.binding.paradoxalarm.internal.communication;
import java.io.IOException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -28,6 +29,7 @@ import org.openhab.binding.paradoxalarm.internal.communication.messages.HeaderMe
import org.openhab.binding.paradoxalarm.internal.communication.messages.IPayload;
import org.openhab.binding.paradoxalarm.internal.communication.messages.ParadoxIPPacket;
import org.openhab.binding.paradoxalarm.internal.communication.messages.RamRequestPayload;
import org.openhab.binding.paradoxalarm.internal.communication.messages.SyncTimePayload;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxException;
import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxRuntimeException;
import org.openhab.binding.paradoxalarm.internal.model.EntityType;
@@ -326,6 +328,9 @@ public class EvoCommunicator extends GenericCommunicator implements IParadoxComm
CommunicationState.LOGOUT.runPhase(this);
scheduler.schedule(this::startLoginSequence, 5, TimeUnit.SECONDS);
return;
case SYNC_TIME:
syncTime(ZonedDateTime.now());
return;
default:
logger.debug("Command {} not implemented.", command);
}
@@ -367,6 +372,15 @@ public class EvoCommunicator extends GenericCommunicator implements IParadoxComm
}
}
@Override
public void syncTime(ZonedDateTime time) {
logger.debug("Submitting SYNC_TIME request for {}", time);
IPayload payload = new SyncTimePayload(time);
ParadoxIPPacket packet = createSerialPassthroughPacket(payload);
IRequest request = new SyncTimeRequest(packet, this);
submitRequest(request);
}
public static class EvoCommunicatorBuilder implements ICommunicatorBuilder {
private final Logger logger = LoggerFactory.getLogger(EvoCommunicatorBuilder.class);
@@ -132,7 +132,7 @@ public class GenericCommunicator extends AbstractCommunicator implements IRespon
} else if (type == RequestType.EPROM) {
receiveEpromResponse(response);
} else {
logger.debug("Probably wrong sender in the request. Request type is not one of the supported methods.");
logger.debug("Received response for unhandled request type: {}", type);
}
}
}
@@ -22,5 +22,6 @@ public enum IP150Command {
LOGOUT,
LOGIN,
RESET,
SYNC_TIME,
UNIMPLEMENTED
}
@@ -12,6 +12,7 @@
*/
package org.openhab.binding.paradoxalarm.internal.communication;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
@@ -40,4 +41,13 @@ public interface IParadoxCommunicator extends IParadoxInitialLoginCommunicator {
void initializeData();
MemoryMap getMemoryMap();
/**
* Pushes the supplied time to the panel clock via the EVO serial passthrough command.
*
* @param time the date/time to set on the panel; must include a valid time-zone so the correct wall-clock values
* are
* encoded
*/
void syncTime(ZonedDateTime time);
}
@@ -30,4 +30,8 @@ public interface IRequest {
boolean isTimeStampExpired(long expirationTreshold);
RequestType getType();
default boolean isResponseExpected() {
return true;
}
}
@@ -22,5 +22,6 @@ public enum RequestType {
RAM,
EPROM,
PARTITION_COMMAND,
ZONE_COMMAND
ZONE_COMMAND,
SYNC_TIME
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2010-2026 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.paradoxalarm.internal.communication;
import org.openhab.binding.paradoxalarm.internal.communication.messages.IPPacket;
/**
* The {@link SyncTimeRequest}. Request for setting the date/time on a Paradox EVO panel.
*
* @author Konstantin Polihronov - Initial contribution
*/
public class SyncTimeRequest extends Request {
public SyncTimeRequest(IPPacket packet, IResponseReceiver receiver) {
super(RequestType.SYNC_TIME, packet, receiver);
}
@Override
public boolean isResponseExpected() {
return false;
}
@Override
public String toString() {
return "SyncTimeRequest [getType()=" + getType() + "]";
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2010-2026 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.paradoxalarm.internal.communication.messages;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SyncTimePayload} builds the 13-byte EVO serial command to set the panel clock (command 0x30).
* Seconds are not supported by the protocol.
*
* @author Konstantin Polihronov - Initial contribution
*/
@NonNullByDefault
public class SyncTimePayload implements IPayload {
private static final int LENGTH = 13;
private static final byte COMMAND = 0x30;
private static final byte PAYLOAD_LENGTH = 0x0D;
private final ZonedDateTime time;
public SyncTimePayload(ZonedDateTime time) {
this.time = time;
}
@Override
public byte[] getBytes() {
byte[] buf = new byte[LENGTH];
buf[0] = COMMAND;
buf[1] = PAYLOAD_LENGTH;
// bytes 2-5: padding (zero)
buf[6] = (byte) (time.getYear() / 100);
buf[7] = (byte) (time.getYear() % 100);
buf[8] = (byte) time.getMonthValue();
buf[9] = (byte) time.getDayOfMonth();
buf[10] = (byte) time.getHour();
buf[11] = (byte) time.getMinute();
// buf[12] left as 0x00 — ParadoxIPPacket(byte[]) overwrites the last byte with the checksum
return buf;
}
}
@@ -14,6 +14,8 @@ package org.openhab.binding.paradoxalarm.internal.handlers;
import static org.openhab.binding.paradoxalarm.internal.handlers.ParadoxAlarmBindingConstants.*;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxInformation;
import org.openhab.binding.paradoxalarm.internal.model.ParadoxPanel;
@@ -57,7 +59,10 @@ public class ParadoxPanelHandler extends EntityBaseHandler {
panelInformation.getApplicationVersion().toString());
updateProperty(PANEL_BOOTLOADER_VERSION_PROPERTY_NAME, panelInformation.getBootLoaderVersion().toString());
updateState(PANEL_TIME, new DateTimeType(panel.getPanelTime()));
ZonedDateTime panelTime = panel.getPanelTime();
if (panelTime != null) {
updateState(PANEL_TIME, new DateTimeType(panelTime));
}
updateState(PANEL_INPUT_VOLTAGE, new QuantityType<>(panel.getVdcLevel(), Units.VOLT));
updateState(PANEL_BOARD_VOLTAGE, new QuantityType<>(panel.getDcLevel(), Units.VOLT));
updateState(PANEL_BATTERY_VOLTAGE, new QuantityType<>(panel.getBatteryLevel(), Units.VOLT));
@@ -91,6 +91,10 @@ public class ParadoxPanel implements IDataUpdateListener {
}
ZoneStateFlags zoneStateFlags = communicator.getZoneStateFlags();
if (zoneStateFlags == null) {
logger.debug("Zone state flags not yet available, skipping zone update.");
return;
}
for (int i = 0; i < zones.size(); i++) {
Zone zone = zones.get(i);
zone.setZoneState(parser.calculateZoneState(zone.getId(), zoneStateFlags));
@@ -72,6 +72,14 @@
<item-type>String</item-type>
<label>Communicator Command</label>
<description>Send Command</description>
<state>
<options>
<option value="LOGIN">Login</option>
<option value="LOGOUT">Logout</option>
<option value="RESET">Reset</option>
<option value="SYNC_TIME">Sync Panel Time</option>
</options>
</state>
</channel-type>
<channel-type id="communicationState">
<item-type>String</item-type>