Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	src/main/java/ch/usi/inf/sa4/sanmarinoes/smarthut/controller/ThermostatController.java
This commit is contained in:
Jacob Salvi 2020-05-05 14:24:03 +02:00
commit 912d81e84f
40 changed files with 536 additions and 274 deletions

View file

@ -4,6 +4,7 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.AutomationFastUpdateRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DimmableState; 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.State;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SwitchableState; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.SwitchableState;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Trigger;
import com.google.gson.*; import com.google.gson.*;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -27,6 +28,7 @@ public class GsonConfig {
private static GsonBuilder configureBuilder() { private static GsonBuilder configureBuilder() {
final GsonBuilder builder = new GsonBuilder(); final GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter()); builder.registerTypeAdapter(Json.class, new SpringfoxJsonToGsonAdapter());
@SuppressWarnings({"rawTypes"})
RuntimeTypeAdapterFactory<State> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory<State> runtimeTypeAdapterFactory =
RuntimeTypeAdapterFactory.of(State.class, "kind") RuntimeTypeAdapterFactory.of(State.class, "kind")
.registerSubtype(SwitchableState.class, "switchableState") .registerSubtype(SwitchableState.class, "switchableState")
@ -43,6 +45,10 @@ public class GsonConfig {
"rangeTrigger"); "rangeTrigger");
builder.registerTypeAdapterFactory(runtimeTypeAdapterFactory); builder.registerTypeAdapterFactory(runtimeTypeAdapterFactory);
builder.registerTypeAdapterFactory(runtimeTypeAdapterFactoryII); builder.registerTypeAdapterFactory(runtimeTypeAdapterFactoryII);
builder.registerTypeAdapter(
Trigger.class,
(JsonSerializer<Trigger<?>>)
(src, typeOfSrc, context) -> context.serialize((Object) src));
return builder; return builder;
} }

View file

@ -31,6 +31,11 @@ public class ButtonDimmerController
this.buttonDimmerRepository = inputRepository; this.buttonDimmerRepository = inputRepository;
} }
@GetMapping("/{id}")
public ButtonDimmer findById(@PathVariable("id") long id) throws NotFoundException {
return buttonDimmerRepository.findById(id).orElseThrow(NotFoundException::new);
}
@PostMapping @PostMapping
public ButtonDimmer create( public ButtonDimmer create(
@Valid @RequestBody final GenericDeviceSaveReguest bd, final Principal principal) @Valid @RequestBody final GenericDeviceSaveReguest bd, final Principal principal)
@ -62,7 +67,7 @@ public class ButtonDimmerController
break; break;
} }
deviceService.saveAllAsOwner(buttonDimmer.getOutputs(), principal.getName(), false); deviceService.saveAllAsOwner(buttonDimmer.getOutputs(), principal.getName());
return buttonDimmer.getOutputs(); return buttonDimmer.getOutputs();
} }
@ -70,6 +75,6 @@ public class ButtonDimmerController
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void delete(@PathVariable("id") long id, final Principal principal) public void delete(@PathVariable("id") long id, final Principal principal)
throws NotFoundException { throws NotFoundException {
deviceService.delete(id, principal.getName()); deviceService.deleteByIdAsOwner(id, principal.getName());
} }
} }

View file

@ -49,7 +49,7 @@ public class CurtainsController {
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void delete(@PathVariable("id") long id, final Principal principal) public void delete(@PathVariable("id") long id, final Principal principal)
throws NotFoundException { throws NotFoundException {
deviceService.delete(id, principal.getName()); deviceService.deleteByIdAsOwner(id, principal.getName());
} }
@PostMapping("/{id}/state") @PostMapping("/{id}/state")

View file

@ -66,7 +66,9 @@ public class DimmableLightController extends GuestEnabledController<DimmableLigh
*/ */
@PutMapping @PutMapping
public DimmableLight update( public DimmableLight update(
@Valid @RequestBody DimmableSaveRequest sp, final Principal principal, Long hostId) @Valid @RequestBody DimmableSaveRequest sp,
final Principal principal,
@RequestParam(value = "hostId", required = false) Long hostId)
throws NotFoundException { throws NotFoundException {
return save( return save(
@ -79,7 +81,7 @@ public class DimmableLightController extends GuestEnabledController<DimmableLigh
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void delete(@PathVariable("id") long id, final Principal principal) public void delete(@PathVariable("id") long id, final Principal principal)
throws NotFoundException { throws NotFoundException {
deviceService.delete(id, principal.getName()); deviceService.deleteByIdAsOwner(id, principal.getName());
} }
// the full url should be: "/dimmableLight/{id}/state?sceneId={sceneId} // the full url should be: "/dimmableLight/{id}/state?sceneId={sceneId}

View file

@ -3,11 +3,16 @@ package ch.usi.inf.sa4.sanmarinoes.smarthut.controller;
import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList; import static ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils.toList;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GuestPermissionsRequest; import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GuestPermissionsRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.GuestsUpdateRequest;
import ch.usi.inf.sa4.sanmarinoes.smarthut.dto.UserResponse;
import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException; import ch.usi.inf.sa4.sanmarinoes.smarthut.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.EagerUserRepository;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
import java.security.Principal; import java.security.Principal;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.validation.Valid; import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -18,23 +23,48 @@ import org.springframework.web.bind.annotation.*;
@RequestMapping("/user") @RequestMapping("/user")
public class GuestController { public class GuestController {
@Autowired private UserRepository userRepository; @Autowired private EagerUserRepository userRepository;
@GetMapping @GetMapping
public List<User> findAll() { public List<UserResponse> findAll() {
return toList(userRepository.findAll()); return StreamSupport.stream(userRepository.findAll().spliterator(), false)
.map(UserResponse::fromUser)
.collect(Collectors.toList());
} }
@PostMapping("/guest") @GetMapping("/hosts")
public User addUserAsGuest(@RequestParam("userId") long id, final Principal principal) public List<UserResponse> findHosts(final Principal principal) {
final User u = userRepository.findByUsername(principal.getName());
return u.getHosts().stream().map(UserResponse::fromUser).collect(Collectors.toList());
}
@GetMapping("/guests")
public List<UserResponse> findGuests(final Principal principal) {
final User u = userRepository.findByUsername(principal.getName());
return u.getGuests().stream().map(UserResponse::fromUser).collect(Collectors.toList());
}
@PutMapping("/guests")
public List<User> setGuests(
@RequestBody @Valid GuestsUpdateRequest g, final Principal principal)
throws NotFoundException { throws NotFoundException {
User guest = userRepository.findById(id).orElseThrow(NotFoundException::new); Iterable<User> guests = userRepository.findAllById(g.ids);
User host = userRepository.findByUsername(principal.getName()); User host = userRepository.findByUsername(principal.getName());
host.addGuest(guest); for (final User oldGuest : host.getGuests()) {
guest.addHost(host); oldGuest.getHosts().remove(host);
userRepository.save(guest); }
return userRepository.save(host);
final Set<User> oldGuests = Set.copyOf(host.getGuests());
for (final User guest : guests) {
host.addGuest(guest);
guest.addHost(host);
}
userRepository.saveAll(oldGuests);
userRepository.save(host);
return toList(userRepository.saveAll(guests));
} }
@PutMapping("/permissions") @PutMapping("/permissions")
@ -44,16 +74,4 @@ public class GuestController {
currentUser.setCameraEnabled(g.isCameraEnabled()); currentUser.setCameraEnabled(g.isCameraEnabled());
return userRepository.save(currentUser); return userRepository.save(currentUser);
} }
@DeleteMapping("/guest")
public void removeUserAsGuest(@RequestParam("userId") long id, final Principal principal)
throws NotFoundException {
User guest = userRepository.findById(id).orElseThrow(NotFoundException::new);
User host = userRepository.findByUsername(principal.getName());
host.removeGuest(guest);
guest.getHosts().remove(host);
userRepository.save(host);
userRepository.save(guest);
}
} }

View file

@ -91,7 +91,7 @@ public abstract class InputDeviceConnectionController<
connector.connect(pair.input, o, true); connector.connect(pair.input, o, true);
} }
deviceService.saveAllAsOwner(pair.outputs, username, false); deviceService.saveAllAsOwner(pair.outputs, username);
return pair.input.getOutputs(); return pair.input.getOutputs();
} }
@ -111,7 +111,7 @@ public abstract class InputDeviceConnectionController<
connector.connect(pair.input, o, false); connector.connect(pair.input, o, false);
} }
deviceService.saveAllAsOwner(pair.outputs, username, false); deviceService.saveAllAsOwner(pair.outputs, username);
return pair.input.getOutputs(); return pair.input.getOutputs();
} }

View file

@ -27,6 +27,11 @@ public class KnobDimmerController extends InputDeviceConnectionController<KnobDi
this.knobDimmerRepository = inputRepository; this.knobDimmerRepository = inputRepository;
} }
@GetMapping("/{id}")
public KnobDimmer findById(@PathVariable("id") long id) throws NotFoundException {
return knobDimmerRepository.findById(id).orElseThrow(NotFoundException::new);
}
@PostMapping @PostMapping
public KnobDimmer create( public KnobDimmer create(
@Valid @RequestBody GenericDeviceSaveReguest kd, final Principal principal) @Valid @RequestBody GenericDeviceSaveReguest kd, final Principal principal)
@ -49,7 +54,7 @@ public class KnobDimmerController extends InputDeviceConnectionController<KnobDi
.orElseThrow(NotFoundException::new); .orElseThrow(NotFoundException::new);
dimmer.setLightIntensity(bd.getIntensity()); dimmer.setLightIntensity(bd.getIntensity());
deviceService.saveAllAsOwner(dimmer.getOutputs(), principal.getName(), false); deviceService.saveAllAsOwner(dimmer.getOutputs(), principal.getName());
return dimmer.getOutputs(); return dimmer.getOutputs();
} }
@ -57,6 +62,6 @@ public class KnobDimmerController extends InputDeviceConnectionController<KnobDi
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void delete(@PathVariable("id") long id, final Principal principal) public void delete(@PathVariable("id") long id, final Principal principal)
throws NotFoundException { throws NotFoundException {
deviceService.delete(id, principal.getName()); deviceService.deleteByIdAsOwner(id, principal.getName());
} }
} }

View file

@ -53,6 +53,6 @@ public class MotionSensorController {
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void delete(@PathVariable("id") long id, final Principal principal) public void delete(@PathVariable("id") long id, final Principal principal)
throws NotFoundException { throws NotFoundException {
deviceService.delete(id, principal.getName()); deviceService.deleteByIdAsOwner(id, principal.getName());
} }
} }

View file

@ -80,7 +80,9 @@ public class RegularLightController extends GuestEnabledController<RegularLight>
@PutMapping @PutMapping
public RegularLight update( public RegularLight update(
@Valid @RequestBody SwitchableSaveRequest rl, final Principal principal, Long hostId) @Valid @RequestBody SwitchableSaveRequest rl,
final Principal principal,
@RequestParam(value = "hostId", required = false) Long hostId)
throws NotFoundException { throws NotFoundException {
return save( return save(
fetchIfOwnerOrGuest(principal, rl.getId(), hostId), fetchIfOwnerOrGuest(principal, rl.getId(), hostId),
@ -92,7 +94,7 @@ public class RegularLightController extends GuestEnabledController<RegularLight>
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void delete(@PathVariable("id") long id, final Principal principal) public void delete(@PathVariable("id") long id, final Principal principal)
throws NotFoundException { throws NotFoundException {
deviceService.delete(id, principal.getName()); deviceService.deleteByIdAsOwner(id, principal.getName());
} }
// the full url should be: "/regularLight/{id}/state?sceneId={sceneId} // the full url should be: "/regularLight/{id}/state?sceneId={sceneId}

View file

@ -75,7 +75,7 @@ public class RoomController {
final String username = principal.getName(); final String username = principal.getName();
final Long userId = userRepository.findByUsername(username).getId(); final Long userId = userRepository.findByUsername(username).getId();
final String img = r.getImage(); final String img = r.getImage();
final Room.Icon icon = r.getIcon(); final Icon icon = r.getIcon();
final Room newRoom = new Room(); final Room newRoom = new Room();
newRoom.setUserId(userId); newRoom.setUserId(userId);
@ -95,7 +95,7 @@ public class RoomController {
.findByIdAndUsername(id, principal.getName()) .findByIdAndUsername(id, principal.getName())
.orElseThrow(NotFoundException::new); .orElseThrow(NotFoundException::new);
final String img = r.getImage(); final String img = r.getImage();
final Room.Icon icon = r.getIcon(); final Icon icon = r.getIcon();
if (r.getName() != null) { if (r.getName() != null) {
newRoom.setName(r.getName()); newRoom.setName(r.getName());
@ -124,6 +124,11 @@ public class RoomController {
roomRepository roomRepository
.findByIdAndUsername(id, principal.getName()) .findByIdAndUsername(id, principal.getName())
.orElseThrow(NotFoundException::new); .orElseThrow(NotFoundException::new);
List<Device> devices = deviceService.findAll(r.getId(), null, principal.getName());
for (Device d : devices) {
deviceService.deleteByIdAsOwner(d.getId(), principal.getName());
}
roomRepository.delete(r); roomRepository.delete(r);
} }

View file

@ -6,20 +6,13 @@ 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.error.NotFoundException;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import ch.usi.inf.sa4.sanmarinoes.smarthut.service.SceneService; import ch.usi.inf.sa4.sanmarinoes.smarthut.service.SceneService;
import ch.usi.inf.sa4.sanmarinoes.smarthut.utils.Utils;
import java.security.Principal; import java.security.Principal;
import java.util.List; import java.util.List;
import javax.validation.Valid; import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.*;
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 @RestController
@EnableAutoConfiguration @EnableAutoConfiguration
@ -27,21 +20,20 @@ import org.springframework.web.bind.annotation.RestController;
public class SceneController { public class SceneController {
@Autowired private SceneRepository sceneRepository; @Autowired private SceneRepository sceneRepository;
@Autowired private UserRepository userRepository;
@Autowired private SceneService sceneService; @Autowired private SceneService sceneService;
@Autowired private UserRepository userService; @Autowired private StateRepository<State<?>> stateRepository;
@Autowired private StateRepository<State<?>> stateService;
@GetMapping @GetMapping
public List<Scene> findAll(Principal principal) { public List<Scene> findAll(
return toList(sceneRepository.findByUsername(principal.getName())); Principal principal, @RequestParam(value = "hostId", required = false) Long hostId)
}
@GetMapping("/{id}")
public @ResponseBody Scene findById(@PathVariable("id") long id, Principal principal)
throws NotFoundException { throws NotFoundException {
return sceneRepository if (hostId == null) {
.findByIdAndUsername(id, principal.getName()) return toList(sceneRepository.findByUsername(principal.getName()));
.orElseThrow(NotFoundException::new); } else {
Utils.returnIfGuest(userRepository, null, hostId, principal);
return sceneRepository.findByHostId(hostId);
}
} }
@PostMapping @PostMapping
@ -49,26 +41,58 @@ public class SceneController {
@Valid @RequestBody SceneSaveRequest s, final Principal principal) { @Valid @RequestBody SceneSaveRequest s, final Principal principal) {
final String username = principal.getName(); final String username = principal.getName();
final Long userId = userService.findByUsername(username).getId(); final Long userId = userRepository.findByUsername(username).getId();
final Scene newScene = new Scene(); final Scene newScene = new Scene();
newScene.setUserId(userId); newScene.setUserId(userId);
newScene.setName(s.getName()); newScene.setName(s.getName());
newScene.setGuestAccessEnabled(s.isGuestAccessEnabled()); newScene.setGuestAccessEnabled(s.isGuestAccessEnabled());
newScene.setIcon(s.getIcon());
return sceneRepository.save(newScene); return sceneRepository.save(newScene);
} }
@PostMapping("/{id}/apply") @PostMapping("/{id}/apply")
public @ResponseBody List<Device> apply(@PathVariable("id") long id, final Principal principal) public @ResponseBody List<Device> apply(
@PathVariable("id") long id,
final Principal principal,
@RequestParam(value = "hostId", required = false) Long hostId)
throws NotFoundException { throws NotFoundException {
final Scene newScene = if (hostId == null) {
return sceneService.apply(
sceneRepository
.findByIdAndUsername(id, principal.getName())
.orElseThrow(NotFoundException::new),
principal.getName(),
false);
} else {
Utils.returnIfGuest(userRepository, null, hostId, principal);
return sceneService.applyAsGuest(
sceneRepository
.findByIdAndUserId(id, hostId)
.orElseThrow(NotFoundException::new),
principal.getName(),
hostId);
}
}
@PostMapping("/{id}/copyFrom/{copyId}")
public @ResponseBody List<State<?>> copy(
@PathVariable("id") long id,
@PathVariable("copyId") long copyId,
final Principal principal)
throws NotFoundException {
final Scene scene =
sceneRepository sceneRepository
.findByIdAndUsername(id, principal.getName()) .findByIdAndUsername(id, principal.getName())
.orElseThrow(NotFoundException::new); .orElseThrow(NotFoundException::new);
final Scene copyFrom =
sceneRepository
.findByIdAndUsername(copyId, principal.getName())
.orElseThrow(NotFoundException::new);
return sceneService.apply(newScene); return sceneService.copyStates(scene, copyFrom);
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@ -84,6 +108,8 @@ public class SceneController {
newScene.setName(s.getName()); newScene.setName(s.getName());
} }
newScene.setIcon(s.getIcon());
newScene.setGuestAccessEnabled(s.isGuestAccessEnabled()); newScene.setGuestAccessEnabled(s.isGuestAccessEnabled());
return sceneRepository.save(newScene); return sceneRepository.save(newScene);
@ -91,7 +117,7 @@ public class SceneController {
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") long id) { public void deleteById(@PathVariable("id") long id) {
stateService.deleteAllBySceneId(id); stateRepository.deleteAllBySceneId(id);
sceneRepository.deleteById(id); sceneRepository.deleteById(id);
} }
@ -101,7 +127,7 @@ public class SceneController {
*/ */
@GetMapping(path = "/{sceneId}/states") @GetMapping(path = "/{sceneId}/states")
public List<State<?>> getDevices(@PathVariable("sceneId") long sceneId) { public List<State<?>> getDevices(@PathVariable("sceneId") long sceneId) {
Iterable<State<?>> states = stateService.findBySceneId(sceneId); Iterable<State<?>> states = stateRepository.findBySceneId(sceneId);
return toList(states); return toList(states);
} }
} }

View file

@ -61,7 +61,7 @@ public class SecurityCameraController {
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void delete(@PathVariable("id") long id, final Principal principal) public void delete(@PathVariable("id") long id, final Principal principal)
throws NotFoundException { throws NotFoundException {
deviceService.delete(id, principal.getName()); deviceService.deleteByIdAsOwner(id, principal.getName());
} }
@PostMapping("/{id}/state") @PostMapping("/{id}/state")

View file

@ -57,6 +57,6 @@ public class SensorController {
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") long id, final Principal principal) public void deleteById(@PathVariable("id") long id, final Principal principal)
throws NotFoundException { throws NotFoundException {
deviceService.delete(id, principal.getName()); deviceService.deleteByIdAsOwner(id, principal.getName());
} }
} }

View file

@ -63,7 +63,7 @@ public class SmartPlugController {
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") long id, final Principal principal) public void deleteById(@PathVariable("id") long id, final Principal principal)
throws NotFoundException { throws NotFoundException {
deviceService.delete(id, principal.getName()); deviceService.deleteByIdAsOwner(id, principal.getName());
} }
@PostMapping("/{id}/state") @PostMapping("/{id}/state")

View file

@ -37,6 +37,11 @@ public class SwitchController extends InputDeviceConnectionController<Switch, Sw
this.switchRepository = inputRepository; this.switchRepository = inputRepository;
} }
@GetMapping("/{id}")
public Switch findById(@PathVariable("id") long id) throws NotFoundException {
return switchRepository.findById(id).orElseThrow(NotFoundException::new);
}
@PostMapping @PostMapping
public Switch create(@Valid @RequestBody GenericDeviceSaveReguest s, final Principal principal) public Switch create(@Valid @RequestBody GenericDeviceSaveReguest s, final Principal principal)
throws NotFoundException { throws NotFoundException {
@ -70,12 +75,12 @@ public class SwitchController extends InputDeviceConnectionController<Switch, Sw
} }
deviceService.saveAsOwner(s, principal.getName()); deviceService.saveAsOwner(s, principal.getName());
return deviceService.saveAllAsOwner(s.getOutputs(), principal.getName(), false); return deviceService.saveAllAsOwner(s.getOutputs(), principal.getName());
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") long id, final Principal principal) public void deleteById(@PathVariable("id") long id, final Principal principal)
throws NotFoundException { throws NotFoundException {
deviceService.delete(id, principal.getName()); deviceService.deleteByIdAsOwner(id, principal.getName());
} }
} }

View file

@ -25,14 +25,17 @@ public class ThermostatController {
private Thermostat save(Thermostat newT, ThermostatSaveRequest t, final Principal principal) { private Thermostat save(Thermostat newT, ThermostatSaveRequest t, final Principal principal) {
newT.setTargetTemperature(t.getTargetTemperature()); newT.setTargetTemperature(t.getTargetTemperature());
thermostatService.populateMeasuredTemperature(newT);
newT.setId(t.getId()); newT.setId(t.getId());
newT.setName(t.getName()); newT.setName(t.getName());
newT.setRoomId(t.getRoomId()); newT.setRoomId(t.getRoomId());
newT.setMeasuredTemperature(t.getMeasuredTemperature());
newT.setUseExternalSensors(t.isUseExternalSensors()); newT.setUseExternalSensors(t.isUseExternalSensors());
newT.setOn(t.isTurnOn()); newT.setOn(false);
System.out.println(newT);
thermostatService.populateMeasuredTemperature(newT);
newT = thermostatRepository.save(newT);
newT.setOn(t.isTurnOn());
newT = deviceService.saveAsOwner(newT, principal.getName()); newT = deviceService.saveAsOwner(newT, principal.getName());
return newT; return newT;
} }
@ -58,7 +61,7 @@ public class ThermostatController {
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") long id, final Principal principal) public void deleteById(@PathVariable("id") long id, final Principal principal)
throws NotFoundException { throws NotFoundException {
deviceService.delete(id, principal.getName()); deviceService.deleteByIdAsOwner(id, principal.getName());
} }
@PostMapping("/{id}/state") @PostMapping("/{id}/state")

View file

@ -0,0 +1,8 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import java.util.List;
import javax.validation.constraints.NotNull;
public class GuestsUpdateRequest {
@NotNull public List<Long> ids;
}

View file

@ -1,6 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Room; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Icon;
import javax.persistence.Lob; import javax.persistence.Lob;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@ -9,7 +9,7 @@ public class RoomSaveRequest {
/** Room identifier */ /** Room identifier */
private long id; private long id;
@NotNull private Room.Icon icon; @NotNull private Icon icon;
/** /**
* Image is to be given as byte[]. In order to get an encoded string from it, the * Image is to be given as byte[]. In order to get an encoded string from it, the
@ -38,11 +38,11 @@ public class RoomSaveRequest {
this.name = name; this.name = name;
} }
public Room.Icon getIcon() { public Icon getIcon() {
return icon; return icon;
} }
public void setIcon(Room.Icon icon) { public void setIcon(Icon icon) {
this.icon = icon; this.icon = icon;
} }

View file

@ -1,5 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto; package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Icon;
import com.sun.istack.NotNull; import com.sun.istack.NotNull;
import javax.persistence.Column; import javax.persistence.Column;
@ -11,6 +12,8 @@ public class SceneSaveRequest {
/** The user given name of this room (e.g. 'Master bedroom') */ /** The user given name of this room (e.g. 'Master bedroom') */
@NotNull private String name; @NotNull private String name;
@NotNull private Icon icon;
/** Determines whether a guest can access this scene */ /** Determines whether a guest can access this scene */
@Column @NotNull private boolean guestAccessEnabled; @Column @NotNull private boolean guestAccessEnabled;
@ -33,4 +36,12 @@ public class SceneSaveRequest {
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }
public Icon getIcon() {
return icon;
}
public void setIcon(Icon icon) {
this.icon = icon;
}
} }

View file

@ -22,8 +22,6 @@ public class ThermostatSaveRequest {
@NotNull private boolean useExternalSensors; @NotNull private boolean useExternalSensors;
@NotNull private BigDecimal measuredTemperature;
/** State of this thermostat */ /** State of this thermostat */
@NotNull private boolean turnOn; @NotNull private boolean turnOn;
@ -74,12 +72,4 @@ public class ThermostatSaveRequest {
public void setId(long id) { public void setId(long id) {
this.id = id; this.id = id;
} }
public BigDecimal getMeasuredTemperature() {
return measuredTemperature;
}
public void setMeasuredTemperature(BigDecimal measuredTemperature) {
this.measuredTemperature = measuredTemperature;
}
} }

View file

@ -0,0 +1,19 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.dto;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
public class UserResponse {
private Long id;
private String username;
private String name;
private UserResponse() {}
public static UserResponse fromUser(User u) {
final UserResponse us = new UserResponse();
us.name = u.getName();
us.id = u.getId();
us.username = u.getUsername();
return us;
}
}

View file

@ -27,7 +27,7 @@ public class Automation {
@GsonExclude @GsonExclude
private Long userId; private Long userId;
@OneToMany(mappedBy = "automation", orphanRemoval = true) @OneToMany(mappedBy = "automation", orphanRemoval = true, cascade = CascadeType.REMOVE)
private Set<Trigger<?>> triggers = new HashSet<>(); private Set<Trigger<?>> triggers = new HashSet<>();
@OneToMany(mappedBy = "automation", cascade = CascadeType.REMOVE) @OneToMany(mappedBy = "automation", cascade = CascadeType.REMOVE)

View file

@ -71,14 +71,18 @@ public abstract class Device {
@SocketGsonExclude @SocketGsonExclude
private Set<State<?>> states = new HashSet<>(); private Set<State<?>> states = new HashSet<>();
@Transient @GsonExclude private boolean fromHost = false; @Transient @GsonExclude private Long fromHostId = null;
@Transient @GsonExclude private boolean fromGuest = false; @Transient @GsonExclude private boolean fromGuest = false;
@Transient @GsonExclude private boolean deleted = false; @Transient @GsonExclude private boolean deleted = false;
public boolean isFromHost() { public Long getFromHostId() {
return fromHost; return fromHostId;
}
public void setFromHostId(Long fromHostId) {
this.fromHostId = fromHostId;
} }
public boolean isDeleted() { public boolean isDeleted() {
@ -97,10 +101,6 @@ public abstract class Device {
this.fromGuest = fromGuest; this.fromGuest = fromGuest;
} }
public void setFromHost(boolean fromHost) {
this.fromHost = fromHost;
}
public long getId() { public long getId() {
return id; return id;
} }

View file

@ -25,4 +25,11 @@ public class DimmableState<T extends Dimmable> extends State<T> {
public void apply() { public void apply() {
getDevice().readStateAndSet(this); getDevice().readStateAndSet(this);
} }
@Override
protected State<T> copy() {
final DimmableState<T> d = new DimmableState<>();
d.setIntensity(intensity);
return d;
}
} }

View file

@ -0,0 +1,103 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import com.google.gson.annotations.SerializedName;
/** A collection of Semantic UI icons */
@SuppressWarnings("unused")
public enum Icon {
@SerializedName("home")
HOME("home"),
@SerializedName("coffee")
COFFEE("coffee"),
@SerializedName("beer")
BEER("beer"),
@SerializedName("glass martini")
GLASS_MARTINI("glass martini"),
@SerializedName("film")
FILM("film"),
@SerializedName("video")
VIDEO("video"),
@SerializedName("music")
MUSIC("music"),
@SerializedName("headphones")
HEADPHONES("headphones"),
@SerializedName("fax")
FAX("fax"),
@SerializedName("phone")
PHONE("phone"),
@SerializedName("laptop")
LAPTOP("laptop"),
@SerializedName("bath")
BATH("bath"),
@SerializedName("shower")
SHOWER("shower"),
@SerializedName("bed")
BED("bed"),
@SerializedName("child")
CHILD("child"),
@SerializedName("warehouse")
WAREHOUSE("warehouse"),
@SerializedName("car")
CAR("car"),
@SerializedName("bicycle")
BICYCLE("bicycle"),
@SerializedName("motorcycle")
MOTORCYCLE("motorcycle"),
@SerializedName("archive")
ARCHIVE("archive"),
@SerializedName("boxes")
BOXES("boxes"),
@SerializedName("cubes")
CUBES("cubes"),
@SerializedName("chess")
CHESS("chess"),
@SerializedName("gamepad")
GAMEPAD("gamepad"),
@SerializedName("futbol")
FUTBOL("futbol"),
@SerializedName("table tennis")
TABLE_TENNIS("table tennis"),
@SerializedName("server")
SERVER("server"),
@SerializedName("tv")
TV("tv"),
@SerializedName("heart")
HEART("heart"),
@SerializedName("camera")
CAMERA("camera"),
@SerializedName("trophy")
TROPHY("trophy"),
@SerializedName("wrench")
WRENCH("wrench"),
@SerializedName("image")
IMAGE("image"),
@SerializedName("book")
BOOK("book"),
@SerializedName("university")
UNIVERSITY("university"),
@SerializedName("medkit")
MEDKIT("medkit"),
@SerializedName("paw")
PAW("paw"),
@SerializedName("tree")
TREE("tree"),
@SerializedName("utensils")
UTENSILS("utensils"),
@SerializedName("male")
MALE("male"),
@SerializedName("female")
FEMALE("female"),
@SerializedName("life ring outline")
LIFE_RING_OUTLINE("life ring outline");
private String iconName;
Icon(String s) {
this.iconName = s;
}
@Override
public String toString() {
return iconName;
}
}

View file

@ -1,7 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.models; package ch.usi.inf.sa4.sanmarinoes.smarthut.models;
import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude; import ch.usi.inf.sa4.sanmarinoes.smarthut.config.GsonExclude;
import com.google.gson.annotations.SerializedName;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -12,106 +11,6 @@ import javax.validation.constraints.NotNull;
@Entity @Entity
public class Room { public class Room {
/** A collection of Semantic UI icons */
@SuppressWarnings("unused")
public enum Icon {
@SerializedName("home")
HOME("home"),
@SerializedName("coffee")
COFFEE("coffee"),
@SerializedName("beer")
BEER("beer"),
@SerializedName("glass martini")
GLASS_MARTINI("glass martini"),
@SerializedName("film")
FILM("film"),
@SerializedName("video")
VIDEO("video"),
@SerializedName("music")
MUSIC("music"),
@SerializedName("headphones")
HEADPHONES("headphones"),
@SerializedName("fax")
FAX("fax"),
@SerializedName("phone")
PHONE("phone"),
@SerializedName("laptop")
LAPTOP("laptop"),
@SerializedName("bath")
BATH("bath"),
@SerializedName("shower")
SHOWER("shower"),
@SerializedName("bed")
BED("bed"),
@SerializedName("child")
CHILD("child"),
@SerializedName("warehouse")
WAREHOUSE("warehouse"),
@SerializedName("car")
CAR("car"),
@SerializedName("bicycle")
BICYCLE("bicycle"),
@SerializedName("motorcycle")
MOTORCYCLE("motorcycle"),
@SerializedName("archive")
ARCHIVE("archive"),
@SerializedName("boxes")
BOXES("boxes"),
@SerializedName("cubes")
CUBES("cubes"),
@SerializedName("chess")
CHESS("chess"),
@SerializedName("gamepad")
GAMEPAD("gamepad"),
@SerializedName("futbol")
FUTBOL("futbol"),
@SerializedName("table tennis")
TABLE_TENNIS("table tennis"),
@SerializedName("server")
SERVER("server"),
@SerializedName("tv")
TV("tv"),
@SerializedName("heart")
HEART("heart"),
@SerializedName("camera")
CAMERA("camera"),
@SerializedName("trophy")
TROPHY("trophy"),
@SerializedName("wrench")
WRENCH("wrench"),
@SerializedName("image")
IMAGE("image"),
@SerializedName("book")
BOOK("book"),
@SerializedName("university")
UNIVERSITY("university"),
@SerializedName("medkit")
MEDKIT("medkit"),
@SerializedName("paw")
PAW("paw"),
@SerializedName("tree")
TREE("tree"),
@SerializedName("utensils")
UTENSILS("utensils"),
@SerializedName("male")
MALE("male"),
@SerializedName("female")
FEMALE("female"),
@SerializedName("life ring outline")
LIFE_RING_OUTLINE("life ring outline");
private String iconName;
Icon(String s) {
this.iconName = s;
}
@Override
public String toString() {
return iconName;
}
}
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, nullable = false, unique = true) @Column(name = "id", updatable = false, nullable = false, unique = true)

View file

@ -39,9 +39,21 @@ public class Scene {
@Column(nullable = false) @Column(nullable = false)
private String name; private String name;
@Column(nullable = false)
@NotNull
private Icon icon;
/** Determines whether a guest can access this scene */ /** Determines whether a guest can access this scene */
@Column private boolean guestAccessEnabled; @Column private boolean guestAccessEnabled;
public Icon getIcon() {
return icon;
}
public void setIcon(Icon icon) {
this.icon = icon;
}
public boolean isGuestAccessEnabled() { public boolean isGuestAccessEnabled() {
return guestAccessEnabled; return guestAccessEnabled;
} }

View file

@ -19,4 +19,9 @@ public interface SceneRepository extends CrudRepository<Scene, Long> {
@Query("SELECT s FROM Scene s JOIN s.user u WHERE u.username = ?1") @Query("SELECT s FROM Scene s JOIN s.user u WHERE u.username = ?1")
List<Scene> findByUsername(String username); List<Scene> findByUsername(String username);
@Query("SELECT s FROM Scene s JOIN s.user u WHERE u.id = ?1 AND s.guestAccessEnabled = true")
List<Scene> findByHostId(Long hostId);
Optional<Scene> findByIdAndUserId(Long id, Long userId);
} }

View file

@ -45,6 +45,16 @@ public abstract class State<D extends OutputDevice> {
/** Sets the state of the connected device to the state represented by this object. */ /** Sets the state of the connected device to the state represented by this object. */
public abstract void apply(); public abstract void apply();
/** Creates a perfect copy of this state, except for the id field and the sceneId/scene */
protected abstract State<D> copy();
public State<D> copyToSceneId(Long sceneId) {
final State<D> s = copy();
s.setDeviceId(this.deviceId);
s.setSceneId(sceneId);
return s;
}
public long getId() { public long getId() {
return id; return id;
} }

View file

@ -10,7 +10,13 @@ import javax.persistence.*;
@Entity @Entity
public class Switch extends InputDevice implements BooleanTriggerable { public class Switch extends InputDevice implements BooleanTriggerable {
@ManyToMany(cascade = CascadeType.DETACH) @ManyToMany(
cascade = {
CascadeType.DETACH,
CascadeType.MERGE,
CascadeType.REFRESH,
CascadeType.PERSIST
})
@GsonExclude @GsonExclude
@SocketGsonExclude @SocketGsonExclude
@JoinTable( @JoinTable(

View file

@ -14,7 +14,14 @@ public abstract class Switchable extends OutputDevice {
public static final Connector<Switch, Switchable> SWITCH_SWITCHABLE_CONNECTOR = public static final Connector<Switch, Switchable> SWITCH_SWITCHABLE_CONNECTOR =
Connector.basic(Switch::getOutputs, Switchable::getSwitches); Connector.basic(Switch::getOutputs, Switchable::getSwitches);
@ManyToMany(mappedBy = "switchables", cascade = CascadeType.DETACH) @ManyToMany(
mappedBy = "switchables",
cascade = {
CascadeType.DETACH,
CascadeType.MERGE,
CascadeType.REFRESH,
CascadeType.PERSIST
})
@GsonExclude @GsonExclude
@SocketGsonExclude @SocketGsonExclude
private Set<Switch> inputs = new HashSet<>(); private Set<Switch> inputs = new HashSet<>();

View file

@ -22,4 +22,11 @@ public class SwitchableState<T extends Switchable> extends State<T> {
public void apply() { public void apply() {
getDevice().readStateAndSet(this); getDevice().readStateAndSet(this);
} }
@Override
protected State<T> copy() {
final SwitchableState<T> d = new SwitchableState<>();
d.setOn(on);
return d;
}
} }

View file

@ -6,9 +6,11 @@ import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Transient; import javax.persistence.Transient;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import org.springframework.stereotype.Component;
/** A thermostat capable of controlling cooling and heating. */ /** A thermostat capable of controlling cooling and heating. */
@Entity @Entity
@Component
public class Thermostat extends Switchable implements BooleanTriggerable { public class Thermostat extends Switchable implements BooleanTriggerable {
@Override @Override
@ -22,32 +24,29 @@ public class Thermostat extends Switchable implements BooleanTriggerable {
computeState(); computeState();
} }
/** /** Computes the new thermostat state, for when the thermostat is on */
* Computes the new thermostat state, for when the thermostat is on; public void computeState() {
*
* @return true if the state changed, false if not;
*/
public boolean computeState() {
if (mode == Thermostat.Mode.OFF) { if (mode == Thermostat.Mode.OFF) {
return false; return;
} }
BigDecimal measured = this.getMeasuredTemperature(); BigDecimal measured = this.getMeasuredTemperature();
BigDecimal target = this.getTargetTemperature(); BigDecimal target = this.getTargetTemperature();
if (measured == null) {
this.setMode(Thermostat.Mode.IDLE);
return;
}
BigDecimal delta = target.subtract(measured); BigDecimal delta = target.subtract(measured);
if (delta.abs().doubleValue() < 0.25) { if (delta.abs().doubleValue() < 0.25) {
if (this.getMode() == Thermostat.Mode.IDLE) return false;
this.setMode(Thermostat.Mode.IDLE); this.setMode(Thermostat.Mode.IDLE);
} else if (delta.signum() > 0) { } else if (delta.signum() > 0) {
if (this.getMode() == Thermostat.Mode.HEATING) return false;
this.setMode(Thermostat.Mode.HEATING); this.setMode(Thermostat.Mode.HEATING);
} else { } else {
if (this.getMode() == Thermostat.Mode.COOLING) return false;
this.setMode(Thermostat.Mode.COOLING); this.setMode(Thermostat.Mode.COOLING);
} }
return true;
} }
@Override @Override
@ -87,6 +86,18 @@ public class Thermostat extends Switchable implements BooleanTriggerable {
this.mode = Mode.OFF; this.mode = Mode.OFF;
} }
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Thermostat{");
sb.append("targetTemperature=").append(targetTemperature);
sb.append(", internalSensorTemperature=").append(internalSensorTemperature);
sb.append(", mode=").append(mode);
sb.append(", measuredTemperature=").append(measuredTemperature);
sb.append(", useExternalSensors=").append(useExternalSensors);
sb.append('}');
return sb.toString();
}
public void setMode(Mode state) { public void setMode(Mode state) {
this.mode = state; this.mode = state;
} }

View file

@ -78,7 +78,7 @@ public class UpdateTasks {
c.forEach( c.forEach(
s -> s ->
sensorSocketEndpoint.queueDeviceUpdate( sensorSocketEndpoint.queueDeviceUpdate(
s, sensorRepository.findUser(s.getId()))); s, sensorRepository.findUser(s.getId()), false, null, false));
} }
/** Sends device updates through sensor socket in batch every one second */ /** Sends device updates through sensor socket in batch every one second */

View file

@ -9,6 +9,8 @@ import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -37,7 +39,7 @@ public class DeviceService {
} }
} }
private void triggerTriggers(Device device) { private void triggerTriggers(Device device, final String username) {
final long deviceId = device.getId(); final long deviceId = device.getId();
@ -57,7 +59,7 @@ public class DeviceService {
sceneRepository sceneRepository
.findById(t.getSceneId()) .findById(t.getSceneId())
.orElseThrow(IllegalStateException::new)) .orElseThrow(IllegalStateException::new))
.forEach(sceneService::apply); .forEach((s) -> sceneService.apply(s, username, true));
} }
public List<Device> findAll(Long hostId, String username) throws NotFoundException { public List<Device> findAll(Long hostId, String username) throws NotFoundException {
@ -68,6 +70,7 @@ public class DeviceService {
throws NotFoundException { throws NotFoundException {
try { try {
Iterable<Device> devices; Iterable<Device> devices;
User host = null;
if (hostId == null) { if (hostId == null) {
if (roomId != null) { if (roomId != null) {
roomRepository roomRepository
@ -79,8 +82,7 @@ public class DeviceService {
} }
} else { } else {
final User guest = userRepository.findByUsername(username); final User guest = userRepository.findByUsername(username);
final User host = host = userRepository.findById(hostId).orElseThrow(NotFoundException::new);
userRepository.findById(hostId).orElseThrow(NotFoundException::new);
if (!guest.getHosts().contains(host)) { if (!guest.getHosts().contains(host)) {
throw new NotFoundException(); throw new NotFoundException();
@ -97,98 +99,148 @@ public class DeviceService {
} }
} }
for (Device d : devices) { populateComputedFields(devices);
if (d instanceof Thermostat) {
thermostatService.populateMeasuredTemperature((Thermostat) d);
}
}
return toList(devices); if (host != null && !host.isCameraEnabled()) {
return StreamSupport.stream(devices.spliterator(), true)
.filter(d -> !(d instanceof SecurityCamera))
.collect(Collectors.toList());
} else {
return toList(devices);
}
} catch (NotFoundException e) { } catch (NotFoundException e) {
e.printStackTrace(); e.printStackTrace();
throw e; throw e;
} }
} }
public void populateComputedFields(Iterable<Device> devices) {
for (Device d : devices) {
if (d instanceof Thermostat) {
thermostatService.populateMeasuredTemperature((Thermostat) d);
}
}
}
public <T extends Device> T saveAsGuest(T device, String guestUsername, Long hostId) public <T extends Device> T saveAsGuest(T device, String guestUsername, Long hostId)
throws NotFoundException { throws NotFoundException {
final User currentUser = userRepository.findByUsername(guestUsername); final User currentUser = userRepository.findByUsername(guestUsername);
final User host = userRepository.findById(hostId).orElseThrow(NotFoundException::new); final User host = userRepository.findById(hostId).orElseThrow(NotFoundException::new);
if (!host.getGuests().contains(currentUser)) throw new NotFoundException(); if (!host.getGuests().contains(currentUser)) throw new NotFoundException();
renameIfDuplicate(device, host.getUsername()); renameIfDuplicate(device, host.getUsername());
device = deviceRepository.save(device); device = deviceRepository.save(device);
propagateUpdateAsGuest(device, host, currentUser);
return device;
}
private void propagateUpdateAsGuest(Device device, User host, User guest) {
final Set<User> guests = Set.copyOf(host.getGuests()); final Set<User> guests = Set.copyOf(host.getGuests());
// We're telling the host that a guest has modified a device. Therefore, fromGuest becomes // We're telling the host that a guest has modified a device. Therefore, fromGuest becomes
// true. // true.
device.setFromHost(false);
device.setFromGuest(true);
// broadcast device update to host // broadcast device update to host
endpoint.queueDeviceUpdate(device, host); endpoint.queueDeviceUpdate(device, host, true, null, false);
// We're telling all guests that a higher entity has issued a device update. Therefore, // We're telling all guests that a higher entity has issued a device update. Therefore,
// fromHost becomes true. // fromHost becomes true.
device.setFromHost(true); for (final User aGuest : guests) {
device.setFromGuest(false); if (aGuest.equals(guest)) {
for (final User guest : guests) {
if (guest.equals(currentUser)) {
continue; continue;
} }
// enqueue all device updates for all other guests // enqueue all device updates for all other guests
endpoint.queueDeviceUpdate(device, guest); endpoint.queueDeviceUpdate(device, aGuest, false, host.getId(), false);
} }
return device;
} }
private void propagateUpdateAsOwner(Device device, String username) { List<Device> saveAllAsGuestSceneApplication(
List<Device> devices, String guestUsername, Long hostId) {
final User guest = userRepository.findByUsername(guestUsername);
final User host = userRepository.findById(hostId).orElseThrow(IllegalStateException::new);
deviceRepository.saveAll(devices);
devices.forEach(d -> this.propagateUpdateAsGuest(d, host, guest));
return devices;
}
/**
* Propagates the update through the socket assuming that the user that modified the device is
* the owner of that device
*
* @param device the updated device
* @param username the username of the owner of that device
* @param causedByTrigger if true, send the update to the owner as well
*/
private void propagateUpdateAsOwner(Device device, String username, boolean causedByTrigger) {
final User user = userRepository.findByUsername(username); final User user = userRepository.findByUsername(username);
final Set<User> guests = user.getGuests(); final Set<User> guests = user.getGuests();
// make sure we're broadcasting from host // make sure we're broadcasting from host
device.setFromHost(true);
device.setFromGuest(false);
for (final User guest : guests) { for (final User guest : guests) {
// broadcast to endpoint the object device, with receiving user set to guest // broadcast to endpoint the object device, with receiving user set to guest
endpoint.queueDeviceUpdate(device, guest); endpoint.queueDeviceUpdate(device, guest, false, user.getId(), false);
}
if (causedByTrigger) {
endpoint.queueDeviceUpdate(device, user, false, user.getId(), false);
} }
} }
/**
* Saves all the devices given in devices assuming that the owner updated them in one way or
* another. Takes care of the appropriate websocket updates and trigger checking as well. No
* checking is done to verify that the user whose username is given is in fact the owner of
* these devices
*
* @param devices the list of devices to save
* @param username the username of the owner of these devices
* @param fromScene true if the update comes from the a scene application side effect. Disables
* trigger checking to avoid recursive invocations of automations
* @param fromTrigger true if the update comes from a scene application executed by an
* automation. Propagates updated through socket to owner as well. No effect if fromScene is
* false.
* @param <T> the type of device contained in the list
* @return the updated list of devices, ready to be fed to GSON
*/
public <T extends Device> List<T> saveAllAsOwner( public <T extends Device> List<T> saveAllAsOwner(
Iterable<T> devices, String username, boolean fromScene) { Iterable<T> devices, String username, boolean fromScene, boolean fromTrigger) {
devices.forEach(d -> renameIfDuplicate(d, username)); devices.forEach(d -> renameIfDuplicate(d, username));
devices = deviceRepository.saveAll(devices); devices = deviceRepository.saveAll(devices);
devices.forEach((d) -> propagateUpdateAsOwner(d, username)); devices.forEach((d) -> propagateUpdateAsOwner(d, username, fromScene && fromTrigger));
if (!fromScene) { if (!fromScene) {
devices.forEach(this::triggerTriggers); devices.forEach((d) -> triggerTriggers(d, username));
} }
return toList(devices); return toList(devices);
} }
public <T extends Device> T saveAsOwner(T device, String username, boolean fromScene) { public <T extends Device> List<T> saveAllAsOwner(Iterable<T> devices, String username) {
return saveAllAsOwner(devices, username, false, false);
}
public <T extends Device> T saveAsOwner(T device, String username) {
renameIfDuplicate(device, username); renameIfDuplicate(device, username);
device = deviceRepository.save(device); device = deviceRepository.save(device);
propagateUpdateAsOwner(device, username); propagateUpdateAsOwner(device, username, false);
if (!fromScene) { triggerTriggers(device, username);
triggerTriggers(device);
}
return device; return device;
} }
public <T extends Device> T saveAsOwner(T device, String username) { public void deleteByIdAsOwner(Long id, String username) throws NotFoundException {
return saveAsOwner(device, username, false); Device d =
}
public void delete(Long id, String username) throws NotFoundException {
Device device =
deviceRepository deviceRepository
.findByIdAndUsername(id, username) .findByIdAndUsername(id, username)
.orElseThrow(NotFoundException::new); .orElseThrow(NotFoundException::new);
deviceRepository.delete(device);
propagateUpdateAsOwner(device, username); final User user = userRepository.findByUsername(username);
final Set<User> guests = user.getGuests();
// make sure we're broadcasting from host
for (final User guest : guests) {
// broadcast to endpoint the object device, with receiving user set to guest
endpoint.queueDeviceUpdate(d, guest, false, user.getId(), true);
}
deviceRepository.delete(d);
} }
} }

View file

@ -14,7 +14,7 @@ public class MotionSensorService {
@Autowired private MotionSensorRepository motionSensorRepository; @Autowired private MotionSensorRepository motionSensorRepository;
/** /**
* Updates detection status of given motion sensor and propagates update throgh socket * Updates detection status of given motion sensor and propagates update through socket
* *
* @param sensor the motion sensor to update * @param sensor the motion sensor to update
* @param detected the new detection status * @param detected the new detection status
@ -26,7 +26,7 @@ public class MotionSensorService {
final MotionSensor toReturn = deviceService.saveAsOwner(sensor, username); final MotionSensor toReturn = deviceService.saveAsOwner(sensor, username);
sensorSocketEndpoint.queueDeviceUpdate( sensorSocketEndpoint.queueDeviceUpdate(
sensor, motionSensorRepository.findUser(sensor.getId())); sensor, motionSensorRepository.findUser(sensor.getId()), false, null, false);
return toReturn; return toReturn;
} }

View file

@ -1,9 +1,6 @@
package ch.usi.inf.sa4.sanmarinoes.smarthut.service; package ch.usi.inf.sa4.sanmarinoes.smarthut.service;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.*;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.DeviceRepository;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Scene;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.State;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -13,16 +10,38 @@ import org.springframework.stereotype.Component;
public class SceneService { public class SceneService {
@Autowired private DeviceRepository<Device> deviceRepository; @Autowired private DeviceRepository<Device> deviceRepository;
@Autowired private DeviceService deviceService;
@Autowired private StateRepository<State<?>> stateRepository;
public List<Device> apply(Scene newScene) { private List<Device> copyStatesToDevices(Scene fromScene) {
final List<Device> updated = new ArrayList<>(); final List<Device> updated = new ArrayList<>();
for (final State<?> s : newScene.getStates()) { for (final State<?> s : fromScene.getStates()) {
s.apply(); s.apply();
updated.add(s.getDevice()); updated.add(s.getDevice());
} }
deviceRepository.saveAll(updated);
deviceService.populateComputedFields(updated);
return updated; return updated;
} }
public List<Device> apply(Scene newScene, String username, boolean fromTrigger) {
List<Device> updated = copyStatesToDevices(newScene);
deviceService.saveAllAsOwner(updated, username, true, fromTrigger);
return updated;
}
public List<Device> applyAsGuest(Scene newScene, String username, Long hostId) {
List<Device> updated = copyStatesToDevices(newScene);
deviceService.saveAllAsGuestSceneApplication(updated, username, hostId);
return updated;
}
public List<State<?>> copyStates(Scene to, Scene from) {
final ArrayList<State<?>> states = new ArrayList<>();
for (final State<?> s : from.getStates()) {
states.add(stateRepository.save(s.copyToSceneId(to.getId())));
}
return states;
}
} }

View file

@ -43,7 +43,8 @@ public class SensorService {
sensor = sensor =
deviceService.saveAsOwner( deviceService.saveAsOwner(
sensor, sensorRepository.findUser(sensor.getId()).getUsername()); sensor, sensorRepository.findUser(sensor.getId()).getUsername());
endpoint.queueDeviceUpdate(sensor, sensorRepository.findUser(sensor.getId())); endpoint.queueDeviceUpdate(
sensor, sensorRepository.findUser(sensor.getId()), false, null, false);
return sensor; return sensor;
} }
} }

View file

@ -45,18 +45,16 @@ public class ThermostatService {
return Utils.toList(all); return Utils.toList(all);
} }
public boolean computeState(Thermostat t) { public void computeState(Thermostat t) {
populateMeasuredTemperature(t); populateMeasuredTemperature(t);
return t.computeState(); t.computeState();
} }
private void updateState(Thermostat t) { private void updateState(Thermostat t) {
boolean shouldUpdate = this.computeState(t); this.computeState(t);
if (shouldUpdate) { deviceService.saveAsOwner(t, thermostatRepository.findUser(t.getId()).getUsername());
deviceService.saveAsOwner(t, thermostatRepository.findUser(t.getId()).getUsername()); endpoint.queueDeviceUpdate(t, thermostatRepository.findUser(t.getId()), false, null, false);
endpoint.queueDeviceUpdate(t, thermostatRepository.findUser(t.getId()));
}
} }
public void updateStates() { public void updateStates() {
@ -72,7 +70,6 @@ public class ThermostatService {
populateMeasuredTemperature(u); populateMeasuredTemperature(u);
t = Optional.of(u); t = Optional.of(u);
} }
return t; return t;
} }

View file

@ -5,6 +5,7 @@ import ch.usi.inf.sa4.sanmarinoes.smarthut.config.JWTTokenUtils;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.Device;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.User;
import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository; import ch.usi.inf.sa4.sanmarinoes.smarthut.models.UserRepository;
import ch.usi.inf.sa4.sanmarinoes.smarthut.service.DeviceService;
import com.google.common.collect.HashMultimap; import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps; import com.google.common.collect.Multimaps;
@ -21,6 +22,8 @@ public class SensorSocketEndpoint extends Endpoint {
private Gson gson = GsonConfig.socketGson(); private Gson gson = GsonConfig.socketGson();
@Autowired private DeviceService deviceService;
private UserRepository userRepository; private UserRepository userRepository;
private JWTTokenUtils jwtTokenUtils; private JWTTokenUtils jwtTokenUtils;
@ -28,7 +31,10 @@ public class SensorSocketEndpoint extends Endpoint {
private Multimap<User, Session> authorizedClients = private Multimap<User, Session> authorizedClients =
Multimaps.synchronizedMultimap(HashMultimap.create()); Multimaps.synchronizedMultimap(HashMultimap.create());
private final Map<User, Map<Long, Device>> messages = new HashMap<>(); // messages are now stored as strings as a "hack" to capture and clone the state of the device,
// since
// fromHost and fromGuest are just mutable properties and hibernate caches the object.
private final Map<User, Map<Long, String>> messages = new HashMap<>();
@Autowired @Autowired
public SensorSocketEndpoint(UserRepository userRepository, JWTTokenUtils jwtTokenUtils) { public SensorSocketEndpoint(UserRepository userRepository, JWTTokenUtils jwtTokenUtils) {
@ -41,18 +47,33 @@ public class SensorSocketEndpoint extends Endpoint {
* *
* @param device the device update to be sent * @param device the device update to be sent
* @param u the user the device belongs * @param u the user the device belongs
* @param fromGuest value for device.fromGuest. This will be put in the device passed.
* @param fromHostId value for device.fromHostId. This will be put in the device passed.
* @param deleted value for device.deleted. This will be put in the device passed.
*/ */
public void queueDeviceUpdate(Device device, User u) { public void queueDeviceUpdate(
Device device, User u, boolean fromGuest, Long fromHostId, boolean deleted) {
synchronized (messages) { synchronized (messages) {
device.setFromGuest(fromGuest);
device.setFromHostId(fromHostId);
device.setDeleted(deleted);
// sort of an hack: force the population of thermostat measureTemperature and other
// possible
// computed fields in the future. This should already be done by the callers of this
// method but for
// whatever reason they don't do it.
deviceService.populateComputedFields(List.of(device));
messages.putIfAbsent(u, new HashMap<>()); messages.putIfAbsent(u, new HashMap<>());
messages.get(u).put(device.getId(), device); messages.get(u).put(device.getId(), gson.toJson(device));
} }
} }
/** Sends all device updates queued to be sent in a unique WebSocket message */ /** Sends all device updates queued to be sent in a unique WebSocket message */
public void flushDeviceUpdates() { public void flushDeviceUpdates() {
synchronized (messages) { synchronized (messages) {
for (Map.Entry<User, Map<Long, Device>> batchForUser : messages.entrySet()) { for (Map.Entry<User, Map<Long, String>> batchForUser : messages.entrySet()) {
broadcast(batchForUser.getKey(), batchForUser.getValue().values()); broadcast(batchForUser.getKey(), batchForUser.getValue().values());
batchForUser.getValue().clear(); batchForUser.getValue().clear();
} }
@ -66,13 +87,13 @@ public class SensorSocketEndpoint extends Endpoint {
* @param messages the message batch to send * @param messages the message batch to send
* @param u the user to which to send the message * @param u the user to which to send the message
*/ */
private void broadcast(User u, Collection<?> messages) { private void broadcast(User u, Collection<String> messages) {
if (messages.isEmpty()) return; if (messages.isEmpty()) return;
final HashSet<Session> sessions = new HashSet<>(authorizedClients.get(u)); final HashSet<Session> sessions = new HashSet<>(authorizedClients.get(u));
for (Session s : sessions) { for (Session s : sessions) {
try { try {
if (s.isOpen()) { if (s.isOpen()) {
s.getBasicRemote().sendText(gson.toJson(messages)); s.getBasicRemote().sendText("[" + String.join(",", messages) + "]");
} else { } else {
authorizedClients.remove(u, s); authorizedClients.remove(u, s);
} }