Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*

target/
.idea/
79 changes: 79 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# SwitchBotAPI-java Project

## Project Overview

A Java wrapper library for the SwitchBot API v1.1, providing easy integration with SwitchBot devices.

## Key Information

- **Language**: Java 17
- **Build Tool**: Maven
- **Version**: 1.2.5
- **Main Package**: `com.bigboxer23.switch_bot`

## Project Structure

```
src/
├── main/java/com/bigboxer23/switch_bot/
│ ├── SwitchBotApi.java # Main API client
│ ├── SwitchBotDeviceApi.java # Device-specific API operations
│ ├── IDeviceTypes.java # Device type constants
│ ├── IDeviceCommands.java # Device command constants
│ └── data/ # Data models and response classes
└── test/java/com/bigboxer23/switch_bot/ # Unit and integration tests
```

## Build Commands

- **Test**: `mvn test`
- **Integration Tests**: `mvn failsafe:integration-test -Dintegration=true`
- **Build**: `mvn compile`
- **Package**: `mvn package`
- **Format Code**: `mvn spotless:apply`
- **Coverage Report**: `mvn jacoco:report`

## Code Style

- Uses Spotless for code formatting with Google Java Format (AOSP style)
- Tab indentation (4 spaces per tab)
- Lombok for reducing boilerplate code

## Testing

- JUnit 5 for unit tests
- Mockito for mocking
- Integration tests with SwitchBot API
- Code coverage with JaCoCo

## Dependencies

- **utils**: Custom utility library (bigboxer23)
- **moshi**: JSON serialization
- **lombok**: Code generation
- **logback**: Logging (test scope)

## GitHub Actions

- Unit tests on push/PR
- CodeQL security analysis
- Code coverage reporting
- Automatic package publishing
- Auto-merge for dependency updates

## API Features

- Device listing and status retrieval
- Device control commands
- Battery status monitoring
- Support for curtains, plugs, and other SwitchBot devices
- HMAC-SHA256 authentication with SwitchBot API

## Example Usage

```java
SwitchBotApi instance = SwitchBotApi.getInstance(token, secret);
List<Device> devices = instance.getDeviceApi().getDevices();
Device status = instance.getDeviceApi().getDeviceStatus(devices.getFirst().getDeviceId());
```

10 changes: 9 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<lombok.version>1.18.40</lombok.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -48,7 +49,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.40</version>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
Expand Down Expand Up @@ -82,6 +83,13 @@
<version>3.14.0</version>
<configuration>
<release>17</release>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
Expand Down
55 changes: 55 additions & 0 deletions src/test/java/com/bigboxer23/switch_bot/IDeviceTypesTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.bigboxer23.switch_bot;

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

import org.junit.jupiter.api.Test;

public class IDeviceTypesTest {

@Test
public void testDeviceTypeConstants() {
assertEquals("Curtain", IDeviceTypes.CURTAIN);
assertEquals("Hub 2", IDeviceTypes.HUB2);
assertEquals("Meter", IDeviceTypes.METER);
assertEquals("WoIOSensor", IDeviceTypes.WOIOSENSOR);
assertEquals("Plug Mini (US)", IDeviceTypes.PLUG_MINI);
assertEquals("Water Detector", IDeviceTypes.WATER_DETECTOR);
assertEquals("MeterPro(CO2)", IDeviceTypes.METER_PRO_CO2);
assertEquals("Roller Shade", IDeviceTypes.ROLLER_SHADE);
}

@Test
public void testDeviceTypeConstantsAreNotNull() {
assertNotNull(IDeviceTypes.CURTAIN);
assertNotNull(IDeviceTypes.HUB2);
assertNotNull(IDeviceTypes.METER);
assertNotNull(IDeviceTypes.WOIOSENSOR);
assertNotNull(IDeviceTypes.PLUG_MINI);
assertNotNull(IDeviceTypes.WATER_DETECTOR);
assertNotNull(IDeviceTypes.METER_PRO_CO2);
assertNotNull(IDeviceTypes.ROLLER_SHADE);
}

@Test
public void testDeviceTypeUniqueness() {
String[] deviceTypes = {
IDeviceTypes.CURTAIN,
IDeviceTypes.HUB2,
IDeviceTypes.METER,
IDeviceTypes.WOIOSENSOR,
IDeviceTypes.PLUG_MINI,
IDeviceTypes.WATER_DETECTOR,
IDeviceTypes.METER_PRO_CO2,
IDeviceTypes.ROLLER_SHADE
};

for (int i = 0; i < deviceTypes.length; i++) {
for (int j = i + 1; j < deviceTypes.length; j++) {
assertNotEquals(
deviceTypes[i],
deviceTypes[j],
"Device types should be unique: " + deviceTypes[i] + " vs " + deviceTypes[j]);
}
}
}
}
55 changes: 49 additions & 6 deletions src/test/java/com/bigboxer23/switch_bot/SwitchBotApiUnitTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,15 @@ public void testSingletonBehavior() {

@Test
public void testGetInstanceWithNullToken() {
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
SwitchBotApi.getInstance(null, "secret");
});
RuntimeException exception =
assertThrows(RuntimeException.class, () -> SwitchBotApi.getInstance(null, "secret"));
assertEquals("need to define token and secret values.", exception.getMessage());
}

@Test
public void testGetInstanceWithNullSecret() {
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
SwitchBotApi.getInstance("token", null);
});
RuntimeException exception =
assertThrows(RuntimeException.class, () -> SwitchBotApi.getInstance("token", null));
assertEquals("need to define token and secret values.", exception.getMessage());
}

Expand Down Expand Up @@ -130,4 +128,49 @@ public void testDeviceNameCacheRefreshBehavior() throws IOException {
deviceApi.getDeviceNameFromId("12345");
verify(deviceApi, times(0)).getDevices();
}

@Test
public void testAddAuthCreatesValidHeaders() {
SwitchBotApi api = SwitchBotApi.getInstance("testToken", "testSecret");
com.bigboxer23.utils.http.RequestBuilderCallback callback = api.addAuth();

assertNotNull(callback);
}

@Test
public void testAddAuthWithRequestBuilder() {
SwitchBotApi api = SwitchBotApi.getInstance("testToken", "testSecret");
com.bigboxer23.utils.http.RequestBuilderCallback callback = api.addAuth();

okhttp3.Request.Builder mockBuilder = mock(okhttp3.Request.Builder.class);
when(mockBuilder.addHeader(anyString(), anyString())).thenReturn(mockBuilder);

okhttp3.Request.Builder result = callback.modifyBuilder(mockBuilder);

assertNotNull(result);
verify(mockBuilder, times(5)).addHeader(anyString(), anyString());
}

@Test
public void testGetMoshiReturnsInstance() {
SwitchBotApi api = SwitchBotApi.getInstance("testToken", "testSecret");
com.squareup.moshi.Moshi moshi = api.getMoshi();

assertNotNull(moshi);
assertSame(moshi, api.getMoshi(), "Should return the same Moshi instance");
}

@Test
public void testGetDeviceApiReturnsInstance() {
SwitchBotApi api = SwitchBotApi.getInstance("testToken", "testSecret");
SwitchBotDeviceApi deviceApi = api.getDeviceApi();

assertNotNull(deviceApi);
assertSame(deviceApi, api.getDeviceApi(), "Should return the same DeviceApi instance");
}

@Test
public void testBaseUrlConstant() {
assertEquals("https://api.switch-bot.com/", SwitchBotApi.baseUrl);
}
}
100 changes: 87 additions & 13 deletions src/test/java/com/bigboxer23/switch_bot/SwitchBotDeviceApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
Expand Down Expand Up @@ -53,7 +54,7 @@ public void testGetDeviceNameFromIdWithUnknownDevice() throws IOException {
device.setDeviceName("Known Device");

SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
doReturn(Arrays.asList(device)).when(spyDeviceApi).getDevices();
doReturn(List.of(device)).when(spyDeviceApi).getDevices();

String result = spyDeviceApi.getDeviceNameFromId("unknown-device");
assertEquals("unknown-device", result);
Expand All @@ -68,7 +69,7 @@ public void testGetDeviceNameFromIdCacheExpiry() throws IOException {
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
spyDeviceApi.deviceIdToNamesCacheTime = System.currentTimeMillis() - (ITimeConstants.HOUR * 2);

doReturn(Arrays.asList(device)).when(spyDeviceApi).getDevices();
doReturn(List.of(device)).when(spyDeviceApi).getDevices();

String result = spyDeviceApi.getDeviceNameFromId("device1");
assertEquals("Test Device", result);
Expand All @@ -85,7 +86,7 @@ public void testGetDeviceNameFromIdWithValidCache() throws IOException {
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
spyDeviceApi.deviceIdToNamesCacheTime = System.currentTimeMillis();

doReturn(Arrays.asList(device)).when(spyDeviceApi).getDevices();
doReturn(List.of(device)).when(spyDeviceApi).getDevices();
spyDeviceApi.getDeviceNameFromId("device1");

reset(spyDeviceApi);
Expand All @@ -112,9 +113,7 @@ public void testGetDeviceStatusWithNullDeviceId() throws IOException {

@Test
public void testGetDeviceStatusInputValidation() {
assertDoesNotThrow(() -> {
assertNotNull(deviceApi);
});
assertDoesNotThrow(() -> assertNotNull(deviceApi));
}

@Test
Expand Down Expand Up @@ -145,18 +144,14 @@ public void testConcurrentCacheRefresh() throws InterruptedException {
device.setDeviceId("concurrent-device");
device.setDeviceName("Concurrent Test");
try {
doReturn(Arrays.asList(device)).when(spyDeviceApi).getDevices();
doReturn(List.of(device)).when(spyDeviceApi).getDevices();
} catch (IOException e) {
fail("Setup failed: " + e.getMessage());
}

Thread thread1 = new Thread(() -> {
spyDeviceApi.getDeviceNameFromId("concurrent-device");
});
Thread thread1 = new Thread(() -> spyDeviceApi.getDeviceNameFromId("concurrent-device"));

Thread thread2 = new Thread(() -> {
spyDeviceApi.getDeviceNameFromId("concurrent-device");
});
Thread thread2 = new Thread(() -> spyDeviceApi.getDeviceNameFromId("concurrent-device"));

thread1.start();
thread2.start();
Expand Down Expand Up @@ -192,4 +187,83 @@ public void testGetDeviceStatusReturnsNullForNullInput() throws IOException {
public void testDeviceApiNotNull() {
assertNotNull(deviceApi);
}

@Test
public void testParseResponseWithIOException() {
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
when(mockSwitchBotApi.getMoshi()).thenReturn(new com.squareup.moshi.Moshi.Builder().build());

DeviceCommand command = new DeviceCommand("turnOn", "default");
assertDoesNotThrow(() -> {
String json =
mockSwitchBotApi.getMoshi().adapter(DeviceCommand.class).toJson(command);
assertNotNull(json);
});
}

@Test
public void testSendDeviceControlCommandsWithValidInput() {
DeviceCommand command = new DeviceCommand("turnOn", "default");
String deviceId = "valid-device-id";

when(mockSwitchBotApi.getMoshi()).thenReturn(new com.squareup.moshi.Moshi.Builder().build());

assertNotNull(command.getCommand());
assertNotNull(command.getParameter());
assertNotNull(deviceId);
assertEquals("turnOn", command.getCommand());
assertEquals("default", command.getParameter());
}

@Test
public void testDeviceStatusValidation() throws IOException {
assertNull(deviceApi.getDeviceStatus(null));
}

@Test
public void testGetDeviceNameFromIdWithNullDeviceNameMap() throws IOException {
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);
spyDeviceApi.deviceIdToNamesCacheTime = -1;

doThrow(new IOException("Network error")).when(spyDeviceApi).getDevices();

String result = spyDeviceApi.getDeviceNameFromId("test-device");
assertEquals("test-device", result);
}

@Test
public void testCacheTimeValidation() {
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);

spyDeviceApi.deviceIdToNamesCacheTime = System.currentTimeMillis() - (ITimeConstants.HOUR * 2);
assertTrue(spyDeviceApi.deviceIdToNamesCacheTime < System.currentTimeMillis() - ITimeConstants.HOUR);

spyDeviceApi.deviceIdToNamesCacheTime = System.currentTimeMillis();
assertTrue(spyDeviceApi.deviceIdToNamesCacheTime > System.currentTimeMillis() - ITimeConstants.HOUR);
}

@Test
public void testDeviceApiConstructor() {
SwitchBotDeviceApi newDeviceApi = new SwitchBotDeviceApi(mockSwitchBotApi);
assertNotNull(newDeviceApi);
assertEquals(-1, newDeviceApi.deviceIdToNamesCacheTime);
}

@Test
public void testGetDeviceStatusWithEmptyDeviceId() {
assertDoesNotThrow(() -> assertNotNull(deviceApi));
}

@Test
public void testGetDeviceNameFromIdWithCacheRefreshFailure() throws IOException {
SwitchBotDeviceApi spyDeviceApi = spy(deviceApi);

spyDeviceApi.deviceIdToNamesCacheTime = System.currentTimeMillis() - (ITimeConstants.HOUR * 2);

doThrow(new IOException("Network failure")).when(spyDeviceApi).getDevices();

String result = spyDeviceApi.getDeviceNameFromId("test-device");
assertEquals("test-device", result);
assertEquals(-1, spyDeviceApi.deviceIdToNamesCacheTime);
}
}
Loading