diff --git a/cli/pom.xml b/cli/pom.xml index 6338690..dd509f2 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ de.spinscale.maxcube maxcube - ${version} + 0.0.2-SNAPSHOT cli @@ -15,7 +15,7 @@ de.spinscale.maxcube client - ${version} + ${project.parent.version} io.airlift diff --git a/cli/src/main/java/de/spinscale/maxcube/cli/Cli.java b/cli/src/main/java/de/spinscale/maxcube/cli/Cli.java index 0c48087..5c01279 100644 --- a/cli/src/main/java/de/spinscale/maxcube/cli/Cli.java +++ b/cli/src/main/java/de/spinscale/maxcube/cli/Cli.java @@ -55,14 +55,15 @@ public static void main(String[] args) throws IOException { // argument parsing io.airlift.airline.Cli.CliBuilder builder = io.airlift.airline.Cli.builder("eq3") - .withCommands(Help.class, Info.class, Discover.class, Version.class, Boost.class, Holiday.class) - .withDescription("Tool to manage Max!EQ3 cubes from the command line") - .withDefaultCommand(Help.class); + .withCommands(Help.class, Info.class, Discover.class, Version.class, Boost.class, Holiday.class, + ManualTemperature.class) + .withDescription("Tool to manage Max!EQ3 cubes from the command line") + .withDefaultCommand(Help.class); builder.withGroup("report") - .withDescription("Reporting to a configurable backend") - .withCommands(ReportCli.class) - .withDefaultCommand(Help.class); + .withDescription("Reporting to a configurable backend") + .withCommands(ReportCli.class) + .withDefaultCommand(Help.class); io.airlift.airline.Cli gitParser = builder.build(); gitParser.parse(args).run(); @@ -138,7 +139,7 @@ public abstract static class CubeHostCommand extends Eq3Command { @Arguments(description = "host of cube to query") public String host; - abstract void doRun(String host) throws Exception ; + abstract void doRun(String host) throws Exception; @Override void doRun() throws Exception { @@ -159,7 +160,7 @@ public static class Discover extends Eq3Command { @Arguments(description = "interface to scan on, i.e. eth0/en0", required = true) public String networkInterface; - @Option(name = { "-t", "timeout" } , description = "Time to wait for responses, in seconds, defaults to 2") + @Option(name = {"-t", "timeout"}, description = "Time to wait for responses, in seconds, defaults to 2") public Integer timeout = 2; public void doRun() throws Exception { @@ -172,7 +173,7 @@ public void doRun() throws Exception { @Command(name = "boost", description = "Boost a room") public static class Boost extends CubeHostCommand { - @Option(name = { "-r", "--room" } , description = "The name of the room to boost", required = true) + @Option(name = {"-r", "--room"}, description = "The name of the room to boost", required = true) public String roomName; public void doRun(String host) throws Exception { @@ -190,13 +191,13 @@ public void doRun(String host) throws Exception { @Command(name = "holiday", description = "Set holiday mode for a room") public static class Holiday extends CubeHostCommand { - @Option(name = { "-r", "--room" } , description = "The name of the room to boost", required = true) + @Option(name = {"-r", "--room"}, description = "The name of the room to boost", required = true) public String roomName; - @Option(name = { "-d", "--duration" } , description = "The duration of boosting", required = true) + @Option(name = {"-d", "--duration"}, description = "The duration of boosting", required = true) public String duration; - @Option(name = { "-t", "--temperature" } , description = "The temperature in °C", required = true) + @Option(name = {"-t", "--temperature"}, description = "The temperature in °C", required = true) public Integer temperature; public void doRun(String host) throws Exception { @@ -215,6 +216,28 @@ public void doRun(String host) throws Exception { } } + @Command(name = "manualtemp", description = "Set the temperature for a room (manual mode)") + public static class ManualTemperature extends CubeHostCommand { + + @Option(name = {"-r", "--room"}, description = "The name of the room to boost", required = true) + public String roomName; + + @Option(name = {"-t", "--temperature"}, description = "The temperature in °C in '.5'-steps from 0 to 31.", + required = true) + public Double temperature; + + public void doRun(String host) throws Exception { + try (CubeClient client = new SocketCubeClient(host)) { + Cube cube = client.connect(); + Room room = cube.findRoom(roomName); + boolean success = client.setManualTemp(room, temperature); + if (!success) { + logger.error("Executing manualtemp call was not successful!"); + } + } + } + } + @Command(name = "version", description = "Display version and exit") public static class Version extends Eq3Command { @@ -238,8 +261,10 @@ public void doRun(String host) throws Exception { } @Command(name = "info", description = "Return some standard information about the cube. Alias for `report cli`") - public static class Info extends AbstractCliReport {} + public static class Info extends AbstractCliReport { + } @Command(name = "cli", description = "Return some standard information about the cube to the terminal") - public static class ReportCli extends AbstractCliReport {} + public static class ReportCli extends AbstractCliReport { + } } diff --git a/client/pom.xml b/client/pom.xml index 46d8fb0..1b93139 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -5,7 +5,7 @@ de.spinscale.maxcube maxcube - ${version} + 0.0.2-SNAPSHOT client diff --git a/client/src/main/java/de/spinscale/maxcube/client/CubeClient.java b/client/src/main/java/de/spinscale/maxcube/client/CubeClient.java index 1addee7..57909c4 100644 --- a/client/src/main/java/de/spinscale/maxcube/client/CubeClient.java +++ b/client/src/main/java/de/spinscale/maxcube/client/CubeClient.java @@ -40,4 +40,12 @@ public interface CubeClient extends Closeable { * @return true if the command was send successfully, false otherwise */ boolean holiday(Room room, LocalDateTime endTime, int temperature) throws Exception; + + /** + * Sets manually the temperature for a room + * @param room The room to set the temperature + * @param temperature The target temperature in degrees celsius + * @return true if the command was send successfully, false otherwise + */ + boolean setManualTemp(Room room, double temperature) throws Exception; } diff --git a/client/src/main/java/de/spinscale/maxcube/client/SocketCubeClient.java b/client/src/main/java/de/spinscale/maxcube/client/SocketCubeClient.java index bed5c93..7c21299 100644 --- a/client/src/main/java/de/spinscale/maxcube/client/SocketCubeClient.java +++ b/client/src/main/java/de/spinscale/maxcube/client/SocketCubeClient.java @@ -89,6 +89,12 @@ public boolean holiday(Room room, LocalDateTime endTime, int temperature) throws return this.sendSetTemperatureRequest(data); } + @Override + public boolean setManualTemp(Room room, double temperature) throws Exception { + String data = Generator.writeSetTemperatureRequest(room, temperature); + return this.sendSetTemperatureRequest(data); + } + private boolean sendSetTemperatureRequest(String base64encodedData) throws Exception { String dataToSend = base64encodedData + "\r\n"; socket.getOutputStream().write(dataToSend.getBytes(UTF_8)); diff --git a/client/src/main/java/de/spinscale/maxcube/data/Generator.java b/client/src/main/java/de/spinscale/maxcube/data/Generator.java index a2e7280..f722d96 100644 --- a/client/src/main/java/de/spinscale/maxcube/data/Generator.java +++ b/client/src/main/java/de/spinscale/maxcube/data/Generator.java @@ -44,6 +44,56 @@ public static void writeRfAddress(int address, ByteArrayOutputStream bos) { bos.write(address); } + public static String writeBoostRequest(Room room) throws IOException { + Device thermostat = room.findThermostat(); + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + writeSetTemperatureRequest(bos, room.getId(), thermostat.getRfaddress()); + // mode & temperature, boost mode is 11 at the beginning, temperature does not matter + bos.write(192); + + String base64 = Base64.getEncoder().encodeToString(bos.toByteArray()); + return "s:" + base64; + } + } + + /** + * https://github.com/Bouni/max-cube-protocol/blob/master/S-Message.md + */ + public static String writeSetTemperatureRequest(Room room, double temperature) throws + IOException { + if (temperature < 0 || temperature > 31) { + throw new IllegalArgumentException("Temperature must be between 0 and 31 °C"); + } + + double doubledTemp = temperature * 2; + double isIntegerOrDotFive = doubledTemp % 2; + + if (isIntegerOrDotFive == 1 || isIntegerOrDotFive == 0) { + Device thermostat = room.findThermostat(); + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + writeSetTemperatureRequest(bos, room.getId(), thermostat.getRfaddress()); + /* + hex: | 66 | + dual: | 0110 1100 | + |||| |||| + ||++-++++-- temperature: 10 1100 -> 38 = temp * 2 + || (to get the temperature, the value must be divided by 2: 38/2 = 19) + ++--------- mode: + 00=auto/weekly program + 01=manual ( => 0100 0000 => Decimal 64) + 10=vacation + 11=boost */ + bos.write(64 + (int) doubledTemp); + + String base64 = Base64.getEncoder().encodeToString(bos.toByteArray()); + return "s:" + base64; + } + } + else { + throw new IllegalArgumentException("only xx.5 is supported"); + } + } + public static String writeHolidayRequest(Room room, LocalDateTime endDate, int temperature) throws IOException { if (temperature > 31) { throw new IllegalArgumentException("Temperature must be between 0 and 31 °C"); @@ -106,22 +156,13 @@ public static void writeDateTimeUntil(LocalDateTime dateTime, ByteArrayOutputStr bos.write(halfhours); } - public static String writeBoostRequest(Room room) throws IOException { - Device thermostat = room.findThermostat(); - try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { - writeSetTemperatureRequest(bos, room.getId(), thermostat.getRfaddress()); - // mode & temperature, boost mode is 11 at the beginning, temperature does not matter - bos.write(192); - - String base64 = Base64.getEncoder().encodeToString(bos.toByteArray()); - return "s:" + base64; - } - } - + /** + * https://github.com/Bouni/max-cube-protocol/blob/master/S-Message.md + */ private static void writeSetTemperatureRequest(ByteArrayOutputStream bos, int roomId, int thermostatRfAddress) { bos.write(0); // unknown bos.write(4); // rf flags - bos.write(64); // command + bos.write(64); // 0x40 => 64 (decimal) => Set temperature // rf address from Generator.writeRfAddress(0, bos); diff --git a/client/src/test/java/de/spinscale/maxcube/client/MinaCubeClient.java b/client/src/test/java/de/spinscale/maxcube/client/MinaCubeClient.java index 8ad5d17..b39557c 100644 --- a/client/src/test/java/de/spinscale/maxcube/client/MinaCubeClient.java +++ b/client/src/test/java/de/spinscale/maxcube/client/MinaCubeClient.java @@ -149,4 +149,9 @@ public void exceptionCaught(IoSession session, Throwable cause) throws Exception session.closeNow(); } } + + @Override + public boolean setManualTemp(Room room, double temperature) throws Exception { + return false; + } } diff --git a/client/src/test/java/de/spinscale/maxcube/data/GeneratorTest.java b/client/src/test/java/de/spinscale/maxcube/data/GeneratorTest.java index 1b6bdb9..d57a78f 100644 --- a/client/src/test/java/de/spinscale/maxcube/data/GeneratorTest.java +++ b/client/src/test/java/de/spinscale/maxcube/data/GeneratorTest.java @@ -174,4 +174,30 @@ public void testGeneratorHolidayRequest() throws IOException { String output = Generator.writeHolidayRequest(room, dateTime, 19); assertThat(output, is(expectedOutput)); } + + @Test + public void testSetTemperatureRequest() throws IOException { + Room room = new Room(1, "foo", 123456); + room.getDevices().add(new Device(DeviceType.THERMOSTAST, "foo", "serial", 1039085)); + + assertThat("Illegal temperature set", testTemperatureBounds(room, -1)); + assertThat("Illegal temperature set", testTemperatureBounds(room, 32)); + assertThat("Illegal temperature set", testTemperatureBounds(room, 18.8)); + assertThat("Illegal temperature set", testTemperatureBounds(room, 18.3)); + + String output = Generator.writeSetTemperatureRequest(room, 17.5); + assertThat(output, is("s:AARAAAAAD9rtAWM=")); + } + + private boolean testTemperatureBounds(Room room, double temp) throws IOException { + boolean exceptionWasThrown = false; + try{ + Generator.writeSetTemperatureRequest(room, temp); + } + catch(IllegalArgumentException e){ + exceptionWasThrown = true; + } + return exceptionWasThrown; + } + } diff --git a/pom.xml b/pom.xml index 094453e..8aa0b65 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,13 @@ de.spinscale.maxcube maxcube pom - ${version} + 0.0.2-SNAPSHOT maxcube-java cli client + web @@ -19,7 +20,6 @@ 1.8 1.8 2.5.0 - 0.0.2-SNAPSHOT @@ -243,14 +243,14 @@ true - + diff --git a/setup_pi.sh b/setup_pi.sh new file mode 100644 index 0000000..c5a983a --- /dev/null +++ b/setup_pi.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +echo "*** Use 'sudo raspi-config' to setup overclocking, networking, locale and timezone ***" +echo "*** Use 'passwd' to change default password ***" + +sudo systemctl enable ssh +sudo systemctl start ssh +sudo apt-get -y install docker git diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..ae6db0c --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,8 @@ +FROM airhacks/wildfly +COPY ./target/maxcube-web.war ${DEPLOYMENT_DIR} +ENV EQ3_HOST=192.168.178.28 + +# docker exec -i -t micro /bin/bash +# tail -f /opt/wildfly-11.0.0.Final/standalone/log/server.log +# OR +# docker logs micro diff --git a/web/pom.xml b/web/pom.xml new file mode 100644 index 0000000..af18c77 --- /dev/null +++ b/web/pom.xml @@ -0,0 +1,36 @@ + + 4.0.0 + + de.spinscale.maxcube + maxcube + 0.0.2-SNAPSHOT + + + war + + web + maxcube web interface + + + + de.spinscale.maxcube + cli + ${project.parent.version} + + + javax + javaee-api + 7.0 + provided + + + + 1.8 + 1.8 + false + + + maxcube-web + + diff --git a/web/src/main/java/de/spinscale/maxcube/cli/JAXRSConfiguration.java b/web/src/main/java/de/spinscale/maxcube/cli/JAXRSConfiguration.java new file mode 100644 index 0000000..d030a89 --- /dev/null +++ b/web/src/main/java/de/spinscale/maxcube/cli/JAXRSConfiguration.java @@ -0,0 +1,30 @@ +/* + * Copyright [2018] [Markus Schwarz] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.spinscale.maxcube.cli; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Configures a JAX-RS endpoint. Delete this class, if you are not exposing + * JAX-RS resources in your application. + * + * @author airhacks.com + */ +@ApplicationPath("resources") +public class JAXRSConfiguration extends Application { + +} diff --git a/web/src/main/java/de/spinscale/maxcube/cli/Thermostat.java b/web/src/main/java/de/spinscale/maxcube/cli/Thermostat.java new file mode 100644 index 0000000..71c6cc0 --- /dev/null +++ b/web/src/main/java/de/spinscale/maxcube/cli/Thermostat.java @@ -0,0 +1,44 @@ +/* + * Copyright [2018] [Markus Schwarz] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.spinscale.maxcube.cli; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; + +/** + * @author Markus Schwarz + */ +@Path("thermostat") +public class Thermostat { + + /* http://localhost:8080/maxcube-web/resources/thermostat */ + @GET + public String message() { + return "Only POST requests were supported"; + } + + + /* curl -v --data "room=Kitchen&temperature=20.5" http://localhost:8080/maxcube-web/resources/thermostat */ + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String setTemperature(@FormParam("room") String room, @FormParam("temperature") Double temperature) throws Exception { + Cli.ManualTemperature manualTemperature = new Cli.ManualTemperature(); + manualTemperature.roomName = room; + manualTemperature.temperature = temperature; + manualTemperature.doRun(); + return "setTemperature() executed"; + } +} diff --git a/web/src/main/webapp/WEB-INF/beans.xml b/web/src/main/webapp/WEB-INF/beans.xml new file mode 100644 index 0000000..2777559 --- /dev/null +++ b/web/src/main/webapp/WEB-INF/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/web/transferDockerImageToPI.sh b/web/transferDockerImageToPI.sh new file mode 100644 index 0000000..333f91d --- /dev/null +++ b/web/transferDockerImageToPI.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +docker history airhacks/micro +docker save -o max-web-docker-image dad12a31defd +scp max-web-docker-image pi: +ssh pi +curl -sSL get.docker.com | sh +sudo docker load -i max-web-docker-image + + +# in one command: +# +# docker save dad12a31defd | bzip2 | ssh pi 'bunzip2 | docker load'