Skip to content

Commit 8dbb13d

Browse files
authored
[solax] Add handler integration tests and fix missing X3 Mic/Pro G2 temperature channels (#20675)
* [solax] Add handler-level integration tests for all supported inverter types and EV charger Introduces Mockito-based handler tests that verify the full data flow from raw JSON API response through parsing, inverter type detection, and handler channel population. Tests cover X1 Hybrid G4, X1 Boost Air Mini, X3 Hybrid G4, X3 Mic/Pro G2, and the EV charger — all 17 tests pass. Signed-off-by: Konstantin Polihronov <polychronov@gmail.com>
1 parent cb52ed3 commit 8dbb13d

8 files changed

Lines changed: 672 additions & 0 deletions

File tree

bundles/org.openhab.binding.solax/src/main/java/org/openhab/binding/solax/internal/handlers/SolaxLocalAccessInverterHandler.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ private void updateChannels(RawDataParser parser, LocalData inverterData) {
192192
updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_OUTPUT_FREQUENCY_PHASE3, inverterData.getFrequencyPhase3(),
193193
Units.HERTZ, supportedChannels);
194194

195+
updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_TEMPERATURE1, inverterData.getInverterTemperature1(),
196+
SIUnits.CELSIUS, supportedChannels);
197+
updateChannel(SolaxBindingConstants.CHANNEL_INVERTER_TEMPERATURE2, inverterData.getInverterTemperature2(),
198+
SIUnits.CELSIUS, supportedChannels);
199+
195200
// Binding provided data
196201
updateState(SolaxBindingConstants.CHANNEL_TIMESTAMP, new DateTimeType());
197202
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright (c) 2010-2026 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.solax.internal.local.handlers;
14+
15+
import static org.mockito.Mockito.*;
16+
17+
import java.util.TimeZone;
18+
19+
import javax.measure.Quantity;
20+
import javax.measure.Unit;
21+
22+
import org.eclipse.jdt.annotation.NonNullByDefault;
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
import org.mockito.Mock;
27+
import org.mockito.junit.jupiter.MockitoExtension;
28+
import org.openhab.binding.solax.internal.SolaxBindingConstants;
29+
import org.openhab.binding.solax.internal.exceptions.SolaxUpdateException;
30+
import org.openhab.binding.solax.internal.handlers.SolaxLocalAccessChargerHandler;
31+
import org.openhab.core.i18n.TimeZoneProvider;
32+
import org.openhab.core.i18n.TranslationProvider;
33+
import org.openhab.core.library.types.QuantityType;
34+
import org.openhab.core.library.types.StringType;
35+
import org.openhab.core.thing.ChannelUID;
36+
import org.openhab.core.thing.Thing;
37+
import org.openhab.core.thing.ThingUID;
38+
import org.openhab.core.thing.binding.ThingHandlerCallback;
39+
40+
/**
41+
* The {@link AbstractChargerHandlerTest} is the abstract base for handler-level tests that verify the full flow from
42+
* raw JSON API data through parsing to channel state updates on {@link SolaxLocalAccessChargerHandler}.
43+
*
44+
* @author Konstantin Polihronov - Initial contribution
45+
*/
46+
@ExtendWith(MockitoExtension.class)
47+
@NonNullByDefault
48+
public abstract class AbstractChargerHandlerTest {
49+
50+
protected static final ThingUID THING_UID = new ThingUID(SolaxBindingConstants.THING_TYPE_LOCAL_CONNECT_CHARGER,
51+
"test");
52+
53+
@Mock
54+
@NonNullByDefault({})
55+
protected ThingHandlerCallback callbackMock;
56+
57+
@Mock
58+
@NonNullByDefault({})
59+
protected Thing thingMock;
60+
61+
@Mock
62+
@NonNullByDefault({})
63+
protected TranslationProvider i18nProvider;
64+
65+
@Mock
66+
@NonNullByDefault({})
67+
protected TimeZoneProvider timeZoneProvider;
68+
69+
@NonNullByDefault({})
70+
protected TestableChargerHandler handler;
71+
72+
/**
73+
* Subclass of {@link SolaxLocalAccessChargerHandler} that exposes the protected {@code updateFromData} method for
74+
* direct invocation in tests, bypassing the HTTP retrieval layer.
75+
*/
76+
protected static class TestableChargerHandler extends SolaxLocalAccessChargerHandler {
77+
public TestableChargerHandler(Thing thing, TranslationProvider i18nProvider,
78+
TimeZoneProvider timeZoneProvider) {
79+
super(thing, i18nProvider, timeZoneProvider);
80+
}
81+
82+
@Override
83+
public void updateFromData(String rawJsonData) throws SolaxUpdateException {
84+
super.updateFromData(rawJsonData);
85+
}
86+
}
87+
88+
@BeforeEach
89+
public void setUp() {
90+
when(thingMock.getUID()).thenReturn(THING_UID);
91+
when(timeZoneProvider.getTimeZone()).thenReturn(TimeZone.getDefault().toZoneId());
92+
93+
handler = new TestableChargerHandler(thingMock, i18nProvider, timeZoneProvider);
94+
handler.setCallback(callbackMock);
95+
}
96+
97+
@Test
98+
public void testChannelUpdates() throws SolaxUpdateException {
99+
handler.updateFromData(getRawData());
100+
assertChannels();
101+
}
102+
103+
protected abstract String getRawData();
104+
105+
protected abstract void assertChannels();
106+
107+
protected <T extends Quantity<T>> void assertQuantityChannel(String channelId, double expectedValue, Unit<T> unit) {
108+
verify(callbackMock).stateUpdated(eq(new ChannelUID(THING_UID, channelId)),
109+
eq(new QuantityType<>(expectedValue, unit)));
110+
}
111+
112+
protected void assertStringChannel(String channelId, String expectedValue) {
113+
verify(callbackMock).stateUpdated(eq(new ChannelUID(THING_UID, channelId)), eq(new StringType(expectedValue)));
114+
}
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright (c) 2010-2026 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.solax.internal.local.handlers;
14+
15+
import static org.mockito.Mockito.*;
16+
17+
import java.util.List;
18+
import java.util.TimeZone;
19+
20+
import javax.measure.Quantity;
21+
import javax.measure.Unit;
22+
23+
import org.eclipse.jdt.annotation.NonNull;
24+
import org.eclipse.jdt.annotation.NonNullByDefault;
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.extension.ExtendWith;
28+
import org.mockito.Mock;
29+
import org.mockito.junit.jupiter.MockitoExtension;
30+
import org.openhab.binding.solax.internal.SolaxBindingConstants;
31+
import org.openhab.binding.solax.internal.handlers.SolaxLocalAccessInverterHandler;
32+
import org.openhab.core.i18n.TimeZoneProvider;
33+
import org.openhab.core.i18n.TranslationProvider;
34+
import org.openhab.core.library.types.QuantityType;
35+
import org.openhab.core.library.types.StringType;
36+
import org.openhab.core.thing.ChannelUID;
37+
import org.openhab.core.thing.Thing;
38+
import org.openhab.core.thing.ThingUID;
39+
import org.openhab.core.thing.binding.ThingHandlerCallback;
40+
41+
/**
42+
* The {@link AbstractInverterHandlerTest} is the abstract base for handler-level tests that verify the full flow from
43+
* raw JSON API data through parsing to channel state updates on {@link SolaxLocalAccessInverterHandler}.
44+
*
45+
* @author Konstantin Polihronov - Initial contribution
46+
*/
47+
@ExtendWith(MockitoExtension.class)
48+
@NonNullByDefault
49+
public abstract class AbstractInverterHandlerTest {
50+
51+
protected static final ThingUID THING_UID = new ThingUID(SolaxBindingConstants.THING_TYPE_LOCAL_CONNECT_INVERTER,
52+
"test");
53+
54+
@Mock
55+
@NonNullByDefault({})
56+
protected ThingHandlerCallback callbackMock;
57+
58+
@Mock
59+
@NonNullByDefault({})
60+
protected Thing thingMock;
61+
62+
@Mock
63+
@NonNullByDefault({})
64+
protected TranslationProvider i18nProvider;
65+
66+
@Mock
67+
@NonNullByDefault({})
68+
protected TimeZoneProvider timeZoneProvider;
69+
70+
@NonNullByDefault({})
71+
protected TestableInverterHandler handler;
72+
73+
/**
74+
* Subclass of {@link SolaxLocalAccessInverterHandler} that exposes the protected {@code updateFromData} method for
75+
* direct invocation in tests, bypassing the HTTP retrieval layer.
76+
*/
77+
protected static class TestableInverterHandler extends SolaxLocalAccessInverterHandler {
78+
public TestableInverterHandler(Thing thing, TranslationProvider i18nProvider,
79+
TimeZoneProvider timeZoneProvider) {
80+
super(thing, i18nProvider, timeZoneProvider);
81+
}
82+
83+
@Override
84+
public void updateFromData(@NonNull String rawJsonData) {
85+
super.updateFromData(rawJsonData);
86+
}
87+
}
88+
89+
@BeforeEach
90+
public void setUp() {
91+
when(thingMock.getUID()).thenReturn(THING_UID);
92+
when(thingMock.getChannels()).thenReturn(List.of());
93+
when(timeZoneProvider.getTimeZone()).thenReturn(TimeZone.getDefault().toZoneId());
94+
95+
handler = new TestableInverterHandler(thingMock, i18nProvider, timeZoneProvider);
96+
handler.setCallback(callbackMock);
97+
}
98+
99+
@Test
100+
public void testChannelUpdates() {
101+
handler.updateFromData(getRawData());
102+
assertChannels();
103+
}
104+
105+
protected abstract String getRawData();
106+
107+
protected abstract void assertChannels();
108+
109+
protected <T extends Quantity<T>> void assertQuantityChannel(String channelId, double expectedValue, Unit<T> unit) {
110+
verify(callbackMock).stateUpdated(eq(new ChannelUID(THING_UID, channelId)),
111+
eq(new QuantityType<>(expectedValue, unit)));
112+
}
113+
114+
protected void assertStringChannel(String channelId, String expectedValue) {
115+
verify(callbackMock).stateUpdated(eq(new ChannelUID(THING_UID, channelId)), eq(new StringType(expectedValue)));
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright (c) 2010-2026 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.solax.internal.local.handlers;
14+
15+
import org.eclipse.jdt.annotation.NonNullByDefault;
16+
import org.openhab.binding.solax.internal.SolaxBindingConstants;
17+
import org.openhab.core.library.unit.SIUnits;
18+
import org.openhab.core.library.unit.Units;
19+
20+
/**
21+
* The {@link TestEvChargerHandlerTest} verifies the full flow from raw JSON API data through the
22+
* {@link org.openhab.binding.solax.internal.handlers.SolaxLocalAccessChargerHandler} to channel state updates for the
23+
* EV Charger.
24+
*
25+
* @author Konstantin Polihronov - Initial contribution
26+
*/
27+
@NonNullByDefault
28+
public class TestEvChargerHandlerTest extends AbstractChargerHandlerTest {
29+
30+
private static final String RAW_DATA = """
31+
{
32+
"SN":"SQBLABLA",
33+
"ver":"3.004.11",
34+
"type":1,
35+
"Data":[
36+
2,2,23914,23991,23895,1517,1513,1519,3654,3657,
37+
3656,10968,44,0,346,0,65434,35463,65459,65508,
38+
65513,27,402,0,43,0,2,15,0,0,
39+
0,0,0,5004,5000,4996,10518,1547,6150,4,
40+
0,0,0,0,0,0,0,0,1,100,
41+
0,0,0,0,0,0,0,0,0,0,
42+
0,0,0,0,0,0,0,0,0,0,
43+
0,0,0,0,0,0,0,0,0,0,
44+
1717,0,3114,1547,6150,0,1,1,1,0,
45+
0,121,584,266,0,50,0,0,1,1,0],
46+
"Information":[11.000,1,"CXXXXXXXXXX",1,1.13,1.01,0.00,0.00,0.00,1],
47+
"OCPPServer":"",
48+
"OCPPChargerId":""
49+
}
50+
""";
51+
52+
@Override
53+
protected String getRawData() {
54+
return RAW_DATA;
55+
}
56+
57+
@Override
58+
protected void assertChannels() {
59+
assertStringChannel(SolaxBindingConstants.CHANNEL_CHARGER_STATE, "2");
60+
assertStringChannel(SolaxBindingConstants.CHANNEL_CHARGER_MODE, "2");
61+
62+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_EQ_SINGLE_SESSION, 4.4, Units.KILOWATT_HOUR);
63+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_EQ_TOTAL, 34.6, Units.KILOWATT_HOUR);
64+
65+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_OUTPUT_CURRENT_PHASE1, 15.17, Units.AMPERE);
66+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_OUTPUT_CURRENT_PHASE2, 15.13, Units.AMPERE);
67+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_OUTPUT_CURRENT_PHASE3, 15.19, Units.AMPERE);
68+
69+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_OUTPUT_VOLTAGE_PHASE1, 239.14, Units.VOLT);
70+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_OUTPUT_VOLTAGE_PHASE2, 239.91, Units.VOLT);
71+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_OUTPUT_VOLTAGE_PHASE3, 238.95, Units.VOLT);
72+
73+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_OUTPUT_POWER_PHASE1, 3654, Units.WATT);
74+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_OUTPUT_POWER_PHASE2, 3657, Units.WATT);
75+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_OUTPUT_POWER_PHASE3, 3656, Units.WATT);
76+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_TOTAL_OUTPUT_POWER, 10968, Units.WATT);
77+
78+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_EXTERNAL_CURRENT_PHASE1, -1.02, Units.AMPERE);
79+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_EXTERNAL_CURRENT_PHASE2, -300.73, Units.AMPERE);
80+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_EXTERNAL_CURRENT_PHASE3, -0.77, Units.AMPERE);
81+
82+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_EXTERNAL_POWER_PHASE1, -28, Units.WATT);
83+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_EXTERNAL_POWER_PHASE2, -23, Units.WATT);
84+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_EXTERNAL_POWER_PHASE3, 27, Units.WATT);
85+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_TOTAL_EXTERNAL_POWER, 402, Units.WATT);
86+
87+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_PLUG_TEMPERATURE, 0, SIUnits.CELSIUS);
88+
assertQuantityChannel(SolaxBindingConstants.CHANNEL_CHARGER_INTERNAL_TEMPERATURE, 43, SIUnits.CELSIUS);
89+
}
90+
}

0 commit comments

Comments
 (0)