diff --git a/build.gradle b/build.gradle index 3116923..c55c8c1 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' repositories { mavenCentral() + } dependencies { compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final' @@ -39,6 +40,13 @@ dependencies { // Fixes https://stackoverflow.com/a/60455550 testImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.11' } + +gradle.projectsEvaluated { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + } +} + test { useJUnitPlatform() } \ No newline at end of file diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java index 69c4fc9..67b25dc 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/GsonConfig.java @@ -1,5 +1,8 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.config; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableState; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.State; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SwitchableState; import com.google.gson.*; import java.lang.reflect.Type; import org.springframework.context.annotation.Bean; @@ -24,6 +27,11 @@ public class GsonConfig { final GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter()); builder.addSerializationExclusionStrategy(new AnnotationExclusionStrategy()); + RuntimeTypeAdapterFactory runtimeTypeAdapterFactory = + RuntimeTypeAdapterFactory.of(State.class, "kind") + .registerSubtype(SwitchableState.class, "switchableState") + .registerSubtype(DimmableState.class, "dimmableState"); + builder.registerTypeAdapterFactory(runtimeTypeAdapterFactory); return builder.create(); } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactory.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactory.java new file mode 100644 index 0000000..e4a9107 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/RuntimeTypeAdapterFactory.java @@ -0,0 +1,315 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.config; + +/* + * Copyright (C) 2011 Google Inc. + * + * 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. + */ + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Adapts values whose runtime type may differ from their declaration type. This is necessary when a + * field's type is not the same type that GSON should create when deserializing that field. For + * example, consider these types: + * + *
{@code
+ * abstract class Shape {
+ *   int x;
+ *   int y;
+ * }
+ * class Circle extends Shape {
+ *   int radius;
+ * }
+ * class Rectangle extends Shape {
+ *   int width;
+ *   int height;
+ * }
+ * class Diamond extends Shape {
+ *   int width;
+ *   int height;
+ * }
+ * class Drawing {
+ *   Shape bottomShape;
+ *   Shape topShape;
+ * }
+ * }
+ * + *

Without additional type information, the serialized JSON is ambiguous. Is the bottom shape in + * this drawing a rectangle or a diamond? + * + *

{@code
+ * {
+ *   "bottomShape": {
+ *     "width": 10,
+ *     "height": 5,
+ *     "x": 0,
+ *     "y": 0
+ *   },
+ *   "topShape": {
+ *     "radius": 2,
+ *     "x": 4,
+ *     "y": 1
+ *   }
+ * }
+ * }
+ * + * This class addresses this problem by adding type information to the serialized JSON and honoring + * that type information when the JSON is deserialized: + * + *
{@code
+ * {
+ *   "bottomShape": {
+ *     "type": "Diamond",
+ *     "width": 10,
+ *     "height": 5,
+ *     "x": 0,
+ *     "y": 0
+ *   },
+ *   "topShape": {
+ *     "type": "Circle",
+ *     "radius": 2,
+ *     "x": 4,
+ *     "y": 1
+ *   }
+ * }
+ * }
+ * + * Both the type field name ({@code "type"}) and the type labels ({@code "Rectangle"}) are + * configurable. + * + *

Registering Types

+ * + * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field name to the + * {@link #of} factory method. If you don't supply an explicit type field name, {@code "type"} will + * be used. + * + *
{@code
+ * RuntimeTypeAdapterFactory shapeAdapterFactory
+ *     = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * + * Next register all of your subtypes. Every subtype must be explicitly registered. This protects + * your application from injection attacks. If you don't supply an explicit type label, the type's + * simple name will be used. + * + *
{@code
+ * shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ * shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * + * Finally, register the type adapter factory in your application's GSON builder: + * + *
{@code
+ * Gson gson = new GsonBuilder()
+ *     .registerTypeAdapterFactory(shapeAdapterFactory)
+ *     .create();
+ * }
+ * + * Like {@code GsonBuilder}, this API supports chaining: + * + *
{@code
+ * RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *     .registerSubtype(Rectangle.class)
+ *     .registerSubtype(Circle.class)
+ *     .registerSubtype(Diamond.class);
+ * }
+ * + *

Serialization and deserialization

+ * + * In order to serialize and deserialize a polymorphic object, you must specify the base type + * explicitly. + * + *
{@code
+ * Diamond diamond = new Diamond();
+ * String json = gson.toJson(diamond, Shape.class);
+ * }
+ * + * And then: + * + *
{@code
+ * Shape shape = gson.fromJson(json, Shape.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap>(); + private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); + private final boolean maintainType; + + private RuntimeTypeAdapterFactory( + Class baseType, String typeFieldName, boolean maintainType) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + this.maintainType = maintainType; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code typeFieldName} as + * the type field name. Type field names are case sensitive. {@code maintainType} flag decide if + * the type will be stored in pojo or not. + */ + public static RuntimeTypeAdapterFactory of( + Class baseType, String typeFieldName, boolean maintainType) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType); + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code typeFieldName} as + * the type field name. Type field names are case sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName, false); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as the type + * field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory(baseType, "type", false); + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} have already been + * registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple name}. Labels are + * case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name have already been + * registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map> labelToDelegate = + new LinkedHashMap>(); + final Map, TypeAdapter> subtypeToDelegate = + new LinkedHashMap, TypeAdapter>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = + gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter() { + @Override + public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement; + if (maintainType) { + labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + } else { + labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + } + + if (labelJsonElement == null) { + throw new JsonParseException( + "cannot deserialize " + + baseType + + " because it does not define a field named " + + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException( + "cannot deserialize " + + baseType + + " subtype named " + + label + + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override + public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException( + "cannot serialize " + + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + if (maintainType) { + Streams.write(jsonObject, out); + return; + } + + JsonObject clone = new JsonObject(); + + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException( + "cannot serialize " + + srcType.getName() + + " because it already defines a field named " + + typeFieldName); + } + clone.add(typeFieldName, new JsonPrimitive(label)); + + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + Streams.write(clone, out); + } + }.nullSafe(); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java index 193c18b..2f45d22 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/config/SpringFoxConfig.java @@ -75,6 +75,9 @@ public class SpringFoxConfig { .or(PathSelectors.regex("/securityCamera.*")::apply) .or(PathSelectors.regex("/sensor.*")::apply) .or(PathSelectors.regex("/smartPlug.*")::apply) + .or(PathSelectors.regex("/scene.*")::apply) + .or(PathSelectors.regex("/switchableState.*")::apply) + .or(PathSelectors.regex("/dimmableState.*")::apply) .or(PathSelectors.regex("/switch.*")::apply) .or(PathSelectors.regex("/motionSensor.*")::apply) .or(PathSelectors.regex("/curtains.*")::apply) diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java index 54541cf..5eb00be 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/CurtainsController.java @@ -3,9 +3,10 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.DimmableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Curtains; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.CurtainsRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.security.Principal; import java.util.List; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; @@ -17,6 +18,8 @@ import org.springframework.web.bind.annotation.*; @RequestMapping("/curtains") public class CurtainsController { @Autowired private CurtainsRepository curtainsService; + @Autowired private SceneRepository sceneRepository; + @Autowired private StateRepository> stateRepository; @GetMapping public List findAll() { @@ -53,4 +56,23 @@ public class CurtainsController { public void delete(@PathVariable("id") long id) { curtainsService.deleteById(id); } + + @PostMapping("/{id}/state") + public State sceneBinding( + @PathVariable("id") long deviceId, + @RequestParam long sceneId, + final Principal principal) + throws NotFoundException, DuplicateStateException { + + Curtains c = + curtainsService + .findByIdAndUsername(deviceId, principal.getName()) + .orElseThrow(NotFoundException::new); + State s = c.cloneState(); + sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); + s.setSceneId(sceneId); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java index ee33933..cb7cc00 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableLightController.java @@ -3,9 +3,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.DimmableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLight; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableLightRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.security.Principal; import java.util.List; import javax.validation.Valid; @@ -19,6 +19,8 @@ import org.springframework.web.bind.annotation.*; public class DimmableLightController { @Autowired private DimmableLightRepository dimmableLightService; + @Autowired private SceneRepository sceneRepository; + @Autowired private StateRepository> stateRepository; @GetMapping public List findAll() { @@ -58,4 +60,25 @@ public class DimmableLightController { public void delete(@PathVariable("id") long id) { dimmableLightService.deleteById(id); } + + // the full url should be: "/dimmableLight/{id}/state?sceneId={sceneId} + // however it is not necessary to specify the query in the mapping + @PostMapping("/{id}/state") + public State sceneBinding( + @PathVariable("id") long deviceId, + @RequestParam long sceneId, + final Principal principal) + throws NotFoundException, DuplicateStateException { + + DimmableLight d = + dimmableLightService + .findByIdAndUsername(deviceId, principal.getName()) + .orElseThrow(NotFoundException::new); + State s = d.cloneState(); + sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); + s.setSceneId(sceneId); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableStateController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableStateController.java new file mode 100644 index 0000000..d866d78 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/DimmableStateController.java @@ -0,0 +1,33 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.DimmableStateSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableState; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableStateRepository; +import javax.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.web.bind.annotation.*; + +@RestController +@EnableAutoConfiguration +@RequestMapping("/dimmableState") +public class DimmableStateController { + + @Autowired private DimmableStateRepository dimmableStateRepository; + + @PutMapping + public DimmableState update(@Valid @RequestBody DimmableStateSaveRequest ss) + throws NotFoundException { + final DimmableState initial = + dimmableStateRepository.findById(ss.getId()).orElseThrow(NotFoundException::new); + initial.setIntensity(ss.getIntensity()); + dimmableStateRepository.save(initial); + return initial; + } + + @DeleteMapping("/{id}") + public void delete(@PathVariable("id") long id) { + dimmableStateRepository.deleteById(id); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java index cdb3e9d..ac0fd47 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/RegularLightController.java @@ -3,9 +3,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLight; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.RegularLightRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.security.Principal; import java.util.List; import javax.validation.Valid; @@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -26,6 +27,8 @@ import org.springframework.web.bind.annotation.RestController; public class RegularLightController { @Autowired private RegularLightRepository regularLightService; + @Autowired private SceneRepository sceneRepository; + @Autowired private StateRepository> stateRepository; @GetMapping public List findAll() { @@ -65,4 +68,24 @@ public class RegularLightController { public void delete(@PathVariable("id") long id) { regularLightService.deleteById(id); } + + // the full url should be: "/dimmableLight/{id}/state?sceneId={sceneId} + // however it is not necessary to specify the query in the mapping + @PostMapping("/{id}/state") + public State sceneBinding( + @PathVariable("id") long deviceId, + @RequestParam long sceneId, + final Principal principal) + throws NotFoundException, DuplicateStateException { + RegularLight d = + regularLightService + .findByIdAndUsername(deviceId, principal.getName()) + .orElseThrow(NotFoundException::new); + State s = d.cloneState(); + sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); + s.setSceneId(sceneId); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SceneController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SceneController.java new file mode 100644 index 0000000..2b4c9c1 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SceneController.java @@ -0,0 +1,112 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SceneSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import javax.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@EnableAutoConfiguration +@RequestMapping("/scene") +public class SceneController { + + @Autowired SceneRepository sceneService; + @Autowired UserRepository userService; + @Autowired StateRepository> stateService; + @Autowired DeviceRepository deviceRepository; + + @GetMapping + public List findAll(Principal principal) { + return toList(sceneService.findByUsername(principal.getName())); + } + + @GetMapping("/{id}") + public @ResponseBody Scene findById(@PathVariable("id") long id, Principal principal) + throws NotFoundException { + return sceneService + .findByIdAndUsername(id, principal.getName()) + .orElseThrow(NotFoundException::new); + } + + @PostMapping + public @ResponseBody Scene create( + @Valid @RequestBody SceneSaveRequest s, final Principal principal) { + + final String username = principal.getName(); + final Long userId = userService.findByUsername(username).getId(); + + final Scene newScene = new Scene(); + + newScene.setUserId(userId); + newScene.setName(s.getName()); + + return sceneService.save(newScene); + } + + @PostMapping("/{id}/apply") + public @ResponseBody List apply(@PathVariable("id") long id, final Principal principal) + throws NotFoundException { + final Scene newScene = + sceneService + .findByIdAndUsername(id, principal.getName()) + .orElseThrow(NotFoundException::new); + + final List updated = new ArrayList<>(); + + for (final State s : newScene.getStates()) { + s.apply(); + updated.add(s.getDevice()); + } + deviceRepository.saveAll(updated); + + return updated; + } + + @PutMapping("/{id}") + public @ResponseBody Scene update( + @PathVariable("id") long id, @RequestBody SceneSaveRequest s, final Principal principal) + throws NotFoundException { + final Scene newScene = + sceneService + .findByIdAndUsername(id, principal.getName()) + .orElseThrow(NotFoundException::new); + + if (s.getName() != null) { + newScene.setName(s.getName()); + } + + return sceneService.save(newScene); + } + + @DeleteMapping("/{id}") + public void deleteById(@PathVariable("id") long id) { + stateService.deleteAllBySceneId(id); + sceneService.deleteById(id); + } + + /** + * Returns a List of all Devices that are associated to a given scene (identified by its + * id). + */ + @GetMapping(path = "/{sceneId}/states") + public List> getDevices(@PathVariable("sceneId") long sceneId) { + Iterable> states = stateService.findBySceneId(sceneId); + return toList(states); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java index dcaafc4..d7fdc8e 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SecurityCameraController.java @@ -3,9 +3,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SecurityCamera; -import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SecurityCameraRepository; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.security.Principal; import java.util.List; import javax.validation.Valid; @@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -26,6 +27,8 @@ import org.springframework.web.bind.annotation.RestController; public class SecurityCameraController { @Autowired SecurityCameraRepository securityCameraService; + @Autowired private SceneRepository sceneRepository; + @Autowired private StateRepository> stateRepository; @GetMapping public List findAll() { @@ -65,4 +68,23 @@ public class SecurityCameraController { public void delete(@PathVariable("id") long id) { securityCameraService.deleteById(id); } + + @PostMapping("/{id}/state") + public State sceneBinding( + @PathVariable("id") long deviceId, + @RequestParam long sceneId, + final Principal principal) + throws NotFoundException, DuplicateStateException { + + SecurityCamera d = + securityCameraService + .findByIdAndUsername(deviceId, principal.getName()) + .orElseThrow(NotFoundException::new); + State s = d.cloneState(); + sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); + s.setSceneId(sceneId); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java index 45c33ff..d90c9ac 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SmartPlugController.java @@ -3,6 +3,7 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchableSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import java.security.Principal; @@ -18,6 +19,8 @@ import org.springframework.web.bind.annotation.*; public class SmartPlugController { @Autowired private SmartPlugRepository smartPlugRepository; + @Autowired private SceneRepository sceneRepository; + @Autowired private StateRepository> stateRepository; @GetMapping public List findAll() { @@ -69,4 +72,23 @@ public class SmartPlugController { public void deleteById(@PathVariable("id") long id) { smartPlugRepository.deleteById(id); } + + @PostMapping("/{id}/state") + public State sceneBinding( + @PathVariable("id") long deviceId, + @RequestParam long sceneId, + final Principal principal) + throws NotFoundException, DuplicateStateException { + + SmartPlug d = + smartPlugRepository + .findByIdAndUsername(deviceId, principal.getName()) + .orElseThrow(NotFoundException::new); + State s = d.cloneState(); + sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); + s.setSceneId(sceneId); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchableStateController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchableStateController.java new file mode 100644 index 0000000..ed6f30c --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/SwitchableStateController.java @@ -0,0 +1,38 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.SwitchableStateSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SwitchableState; +import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SwitchableStateRepository; +import javax.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@EnableAutoConfiguration +@RequestMapping("/switchableState") +public class SwitchableStateController { + + @Autowired private SwitchableStateRepository switchableStateRepository; + + @PutMapping + public SwitchableState update(@Valid @RequestBody SwitchableStateSaveRequest ss) + throws NotFoundException { + final SwitchableState initial = + switchableStateRepository.findById(ss.getId()).orElseThrow(NotFoundException::new); + initial.setOn(ss.isOn()); + switchableStateRepository.save(initial); + return initial; + } + + @DeleteMapping("/{id}") + public void delete(@PathVariable("id") long id) { + switchableStateRepository.deleteById(id); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java index 57e1ca9..ee566ed 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java @@ -1,11 +1,11 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.ThermostatSaveRequest; +import ch.usi.inf.sa4.sanmarinoes.smarthut.error.DuplicateStateException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import ch.usi.inf.sa4.sanmarinoes.smarthut.service.ThermostatService; import java.security.Principal; -import java.util.*; import java.util.List; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; @@ -21,6 +21,9 @@ public class ThermostatController { @Autowired private ThermostatService thermostatService; + @Autowired private SceneRepository sceneRepository; + @Autowired private StateRepository> stateRepository; + @GetMapping public List findAll(Principal user) { return thermostatService.findAll(user.getName()); @@ -40,13 +43,7 @@ public class ThermostatController { newT.setName(t.getName()); newT.setRoomId(t.getRoomId()); newT.setUseExternalSensors(t.isUseExternalSensors()); - - if (t.isTurnOn()) { - newT.setState(Thermostat.ThermostatState.IDLE); - thermostatService.computeState(newT); - } else { - newT.setState(Thermostat.ThermostatState.OFF); - } + newT.setOn(t.isTurnOn()); newT = thermostatRepository.save(newT); thermostatService.populateMeasuredTemperature(newT); @@ -72,4 +69,23 @@ public class ThermostatController { public void deleteById(@PathVariable("id") long id) { thermostatRepository.deleteById(id); } + + @PostMapping("/{id}/state") + public State sceneBinding( + @PathVariable("id") long deviceId, + @RequestParam long sceneId, + final Principal principal) + throws NotFoundException, DuplicateStateException { + + Thermostat d = + thermostatRepository + .findByIdAndUsername(deviceId, principal.getName()) + .orElseThrow(NotFoundException::new); + State s = d.cloneState(); + sceneRepository.findById(sceneId).orElseThrow(NotFoundException::new); + s.setSceneId(sceneId); + if (stateRepository.countByDeviceIdAndSceneId(deviceId, sceneId) > 0) + throw new DuplicateStateException(); + return stateRepository.save(s); + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableStateSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableStateSaveRequest.java new file mode 100644 index 0000000..ebf99eb --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/DimmableStateSaveRequest.java @@ -0,0 +1,28 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +public class DimmableStateSaveRequest { + + /** Device id (used only for update requests) */ + @NotNull private Long id; + + @NotNull + @Min(0) + @Max(100) + private Integer intensity = 0; + + public Integer getIntensity() { + return intensity; + } + + public void setIntensity(Integer intensity) { + this.intensity = intensity; + } + + public Long getId() { + return id; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SceneSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SceneSaveRequest.java new file mode 100644 index 0000000..ea3c6bd --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SceneSaveRequest.java @@ -0,0 +1,24 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import com.sun.istack.NotNull; + +public class SceneSaveRequest { + + /** Room identifier */ + private long id; + + /** The user given name of this room (e.g. 'Master bedroom') */ + @NotNull private String name; + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableStateSaveRequest.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableStateSaveRequest.java new file mode 100644 index 0000000..ab03f27 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/dto/SwitchableStateSaveRequest.java @@ -0,0 +1,23 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; + +import javax.validation.constraints.NotNull; + +public class SwitchableStateSaveRequest { + + /** Device id (used only for update requests) */ + @NotNull private Long id; + + @NotNull private boolean on; + + public boolean isOn() { + return on; + } + + public void setOn(boolean on) { + this.on = on; + } + + public Long getId() { + return id; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/DuplicateStateException.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/DuplicateStateException.java new file mode 100644 index 0000000..4e9f3d5 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/error/DuplicateStateException.java @@ -0,0 +1,12 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.error; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.BAD_REQUEST) +public class DuplicateStateException extends Exception { + public DuplicateStateException() { + super( + "Cannot create state since it has already been created for this scene and this device"); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java index 4302774..0114ac3 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Device.java @@ -3,6 +3,8 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude; import com.google.gson.annotations.SerializedName; import io.swagger.annotations.ApiModelProperty; +import java.util.HashSet; +import java.util.Set; import javax.persistence.*; import javax.validation.constraints.NotNull; @@ -57,6 +59,10 @@ public abstract class Device { */ @Transient private final FlowType flowType; + @OneToMany(mappedBy = "device", orphanRemoval = true) + @GsonExclude + private Set states = new HashSet<>(); + public long getId() { return id; } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java index d8c083a..a3ae206 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmable.java @@ -1,23 +1,20 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude; - +import java.util.Set; import javax.persistence.*; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; -import java.util.Set; @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) public class Dimmable extends Switchable { - public static final Connector - KNOB_DIMMER_DIMMABLE_CONNECTOR = + public static final Connector KNOB_DIMMER_DIMMABLE_CONNECTOR = Connector.basic(KnobDimmer::getOutputs, Dimmable::getDimmers); - public static final Connector - BUTTON_DIMMER_DIMMABLE_CONNECTOR = + public static final Connector BUTTON_DIMMER_DIMMABLE_CONNECTOR = Connector.basic(ButtonDimmer::getOutputs, Dimmable::getDimmers); protected Dimmable(String kind) { @@ -75,4 +72,17 @@ public class Dimmable extends Switchable { public Set getDimmers() { return this.dimmers; } + + public void readStateAndSet(DimmableState state) { + setIntensity(state.getIntensity()); + } + + @Override + public State cloneState() { + final DimmableState newState = new DimmableState<>(); + newState.setDeviceId(getId()); + newState.setDevice(this); + newState.setIntensity(getIntensity()); + return newState; + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableState.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableState.java new file mode 100644 index 0000000..7c0b8d2 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableState.java @@ -0,0 +1,28 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import javax.persistence.Entity; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + +/** Represent a state for an IDimmable device */ +@Entity +public class DimmableState extends State { + + /** The light intensity value. Goes from 0 (off) to 100 (on) */ + @Min(0) + @Max(100) + private int intensity = 0; + + public int getIntensity() { + return intensity; + } + + public void setIntensity(int dimAmount) { + this.intensity = dimAmount; + } + + @Override + public void apply() { + getDevice().readStateAndSet(this); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableStateRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableStateRepository.java new file mode 100644 index 0000000..00edb96 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/DimmableStateRepository.java @@ -0,0 +1,3 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +public interface DimmableStateRepository extends StateRepository> {} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java index 29d0c6b..7ef0808 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Dimmer.java @@ -34,7 +34,7 @@ public abstract class Dimmer extends InputDevice { } /** Add a light to be controller by this dimmer */ - public void addDimmable(Dimmable dimmableLight) { - dimmables.add(dimmableLight); + public void addDimmable(Dimmable dimmable) { + dimmables.add(dimmable); } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java index 56e68c5..ba1813e 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/OutputDevice.java @@ -12,4 +12,12 @@ public abstract class OutputDevice extends Device { public OutputDevice(String kind) { super(kind, FlowType.OUTPUT); } + + /** + * Creates a State object initialized to point at this device and with values copied from + * this device's state + * + * @return a new State object + */ + public abstract State cloneState(); } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java index 34f3824..4f0f592 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Room.java @@ -145,6 +145,7 @@ public class Room { */ @NotNull @Column(name = "user_id", nullable = false) + @GsonExclude private Long userId; /** The user given name of this room (e.g. 'Master bedroom') */ diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Scene.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Scene.java new file mode 100644 index 0000000..e762087 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Scene.java @@ -0,0 +1,81 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude; +import io.swagger.annotations.ApiModelProperty; +import java.util.HashSet; +import java.util.Set; +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +/** + * Represent a collection of state changes to devices even in different rooms but belonging to the + * same user + */ +@Entity +public class Scene { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "id", updatable = false, nullable = false, unique = true) + @ApiModelProperty(hidden = true) + private long id; + + @ManyToOne + @JoinColumn(name = "user_id", updatable = false, insertable = false) + @GsonExclude + private User user; + + @OneToMany(mappedBy = "scene", orphanRemoval = true) + @GsonExclude + private Set> states = new HashSet<>(); + + @NotNull + @Column(name = "user_id", nullable = false) + @GsonExclude + private Long userId; + + /** The user given name of this room (e.g. 'Master bedroom') */ + @NotNull + @Column(nullable = false) + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Set> getStates() { + return states; + } + + public void setStates(Set> states) { + this.states = states; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SceneRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SceneRepository.java new file mode 100644 index 0000000..de08a53 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SceneRepository.java @@ -0,0 +1,22 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; + +public interface SceneRepository extends CrudRepository { + + /** + * Finds a room by their id and a username + * + * @param id the scene id + * @param username a User's username + * @return an optional scene, empty if none found + */ + @Query("SELECT r FROM Room r JOIN r.user u WHERE r.id = ?1 AND u.username = ?2") + Optional findByIdAndUsername(Long id, String username); + + @Query("SELECT r FROM Room r JOIN r.user u WHERE u.username = ?1") + List findByUsername(String username); +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java index 8424bde..546c746 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SmartPlug.java @@ -46,12 +46,4 @@ public class SmartPlug extends Switchable { public SmartPlug() { super("smartPlug"); } - - @Override - public String toString() { - return Objects.toStringHelper(this) - .add("totalConsumption", totalConsumption) - .add("on", on) - .toString(); - } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/State.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/State.java new file mode 100644 index 0000000..01398b8 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/State.java @@ -0,0 +1,96 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude; +import io.swagger.annotations.ApiModelProperty; +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +/** + * Represents instructions on how to change the state of a particular device. Many states (plus + * other properties) form a Scene + */ +@Entity +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"device_id", "scene_id"})}) +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +public abstract class State { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "id", updatable = false, nullable = false, unique = true) + @ApiModelProperty(hidden = true) + private long id; + + @ManyToOne(targetEntity = OutputDevice.class) + @JoinColumn(name = "device_id", updatable = false, insertable = false) + @GsonExclude + private D device; + + /** + * The device this state belongs in, as a foreign key id. To use when updating and inserting + * from a REST call. + */ + @Column(name = "device_id", nullable = false) + @NotNull + private Long deviceId; + + @ManyToOne + @JoinColumn(name = "scene_id", updatable = false, insertable = false) + @GsonExclude + private Scene scene; + + @Column(name = "scene_id", nullable = false) + @NotNull + private Long sceneId; + + /** Sets the state of the connected device to the state represented by this object. */ + public abstract void apply(); + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public D getDevice() { + return device; + } + + public void setDevice(D device) { + this.device = device; + } + + public Long getDeviceId() { + return deviceId; + } + + public void setDeviceId(Long deviceId) { + this.deviceId = deviceId; + } + + public Scene getScene() { + return scene; + } + + public void setScene(Scene scene) { + this.scene = scene; + } + + public Long getSceneId() { + return sceneId; + } + + public void setSceneId(Long sceneId) { + this.sceneId = sceneId; + } + + @PreRemove + public void removeDeviceAndScene() { + this.setScene(null); + this.setSceneId(null); + + this.setDevice(null); + this.setDeviceId(null); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateRepository.java new file mode 100644 index 0000000..d2d1278 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/StateRepository.java @@ -0,0 +1,16 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import java.util.List; +import javax.transaction.Transactional; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; + +public interface StateRepository> extends CrudRepository { + + @Transactional + void deleteAllBySceneId(long roomId); + + List findBySceneId(@Param("sceneId") long sceneId); + + Integer countByDeviceIdAndSceneId(long deviceId, long sceneId); +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java index 6d7260f..7fbc64a 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Switchable.java @@ -1,9 +1,9 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.models; import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude; -import javax.persistence.*; import java.util.HashSet; import java.util.Set; +import javax.persistence.*; /** A device that can be turned either on or off */ @Entity @@ -38,4 +38,17 @@ public abstract class Switchable extends OutputDevice { public Set getSwitches() { return inputs; } + + public void readStateAndSet(SwitchableState state) { + setOn(state.isOn()); + } + + @Override + public State cloneState() { + final SwitchableState newState = new SwitchableState<>(); + newState.setDeviceId(getId()); + newState.setDevice(this); + newState.setOn(isOn()); + return newState; + } } diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableState.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableState.java new file mode 100644 index 0000000..67b3118 --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableState.java @@ -0,0 +1,25 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +import javax.persistence.Column; +import javax.persistence.Entity; + +/** Represents a state for a Switchable device */ +@Entity +public class SwitchableState extends State { + + @Column(name = "switchable_on") + private boolean on; + + public boolean isOn() { + return on; + } + + public void setOn(boolean on) { + this.on = on; + } + + @Override + public void apply() { + getDevice().readStateAndSet(this); + } +} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableStateRepository.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableStateRepository.java new file mode 100644 index 0000000..933ac6c --- /dev/null +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/SwitchableStateRepository.java @@ -0,0 +1,3 @@ +package ch.usi.inf.sa4.sanmarinoes.smarthut.models; + +public interface SwitchableStateRepository extends StateRepository> {} diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Thermostat.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Thermostat.java index 8524b8f..7c54d54 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Thermostat.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/models/Thermostat.java @@ -9,9 +9,47 @@ import javax.validation.constraints.NotNull; /** A thermostat capable of controlling cooling and heating. */ @Entity -public class Thermostat extends OutputDevice { +public class Thermostat extends Switchable { - public enum ThermostatState { + @Override + public boolean isOn() { + return mode != Mode.OFF; + } + + @Override + public void setOn(boolean on) { + mode = on ? Mode.IDLE : Mode.OFF; + computeState(); + } + + /** + * Computes the new thermostat state, for when the thermostat is on; + * @return true if the state changed, false if not; + */ + public boolean computeState() { + if (mode == Thermostat.Mode.OFF) { + return false; + } + + BigDecimal measured = this.getMeasuredTemperature(); + BigDecimal target = this.getTargetTemperature(); + BigDecimal delta = target.subtract(measured); + + if (delta.abs().doubleValue() < 0.25) { + if (this.getMode() == Thermostat.Mode.IDLE) return false; + this.setMode(Thermostat.Mode.IDLE); + } else if (delta.signum() > 0) { + if (this.getMode() == Thermostat.Mode.HEATING) return false; + this.setMode(Thermostat.Mode.HEATING); + } else { + if (this.getMode() == Thermostat.Mode.COOLING) return false; + this.setMode(Thermostat.Mode.COOLING); + } + + return true; + } + + public enum Mode { @SerializedName("OFF") OFF, @SerializedName("IDLE") @@ -31,7 +69,7 @@ public class Thermostat extends OutputDevice { Sensor.TYPICAL_VALUES.get(Sensor.SensorType.TEMPERATURE); /** State of this thermostat */ - @Column @NotNull private ThermostatState state; + @Column @NotNull private Thermostat.Mode mode; @Transient private BigDecimal measuredTemperature; @@ -40,15 +78,15 @@ public class Thermostat extends OutputDevice { /** Creates a thermostat with a temperature sensor and its initial OFF state */ public Thermostat() { super("thermostat"); - this.state = ThermostatState.OFF; + this.mode = Mode.OFF; } - public void setState(ThermostatState state) { - this.state = state; + public void setMode(Mode state) { + this.mode = state; } - public ThermostatState getState() { - return this.state; + public Mode getMode() { + return this.mode; } public BigDecimal getTargetTemperature() { diff --git a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatService.java b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatService.java index fca88ee..2352cf9 100644 --- a/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatService.java +++ b/src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/service/ThermostatService.java @@ -43,27 +43,8 @@ public class ThermostatService { } public boolean computeState(Thermostat t) { - if (t.getState() == Thermostat.ThermostatState.OFF) { - return false; - } - populateMeasuredTemperature(t); - BigDecimal measured = t.getMeasuredTemperature(); - BigDecimal target = t.getTargetTemperature(); - BigDecimal delta = target.subtract(measured); - - if (delta.abs().doubleValue() < 0.25) { - if (t.getState() == Thermostat.ThermostatState.IDLE) return false; - t.setState(Thermostat.ThermostatState.IDLE); - } else if (delta.signum() > 0) { - if (t.getState() == Thermostat.ThermostatState.HEATING) return false; - t.setState(Thermostat.ThermostatState.HEATING); - } else { - if (t.getState() == Thermostat.ThermostatState.COOLING) return false; - t.setState(Thermostat.ThermostatState.COOLING); - } - - return true; + return t.computeState(); } private void updateState(Thermostat t) {