├── smart-meter-dashboard ├── src │ └── main │ │ ├── resources │ │ ├── application.properties │ │ └── templates │ │ │ ├── error │ │ │ └── 404.html │ │ │ ├── index.html │ │ │ └── zone.html │ │ └── java │ │ └── com │ │ └── example │ │ └── meter │ │ └── dashboard │ │ ├── generator │ │ ├── ZoneDescriptorRepository.java │ │ ├── ZoneDescriptor.java │ │ ├── MeasuresCollector.java │ │ └── ElectricityMeasure.java │ │ ├── web │ │ ├── MissingDataException.java │ │ └── DashboardController.java │ │ ├── SmartMeterDashboardApplication.java │ │ ├── sampling │ │ ├── PowerGridSampleRepository.java │ │ ├── PowerGridSampleKey.java │ │ ├── PowerGridSample.java │ │ └── PowerGridSampler.java │ │ ├── MetricAggregatorHealthIndicator.java │ │ ├── DashboardProperties.java │ │ └── MongoCollectionInitializer.java └── pom.xml ├── .gitignore ├── smart-meter-aggregator ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── meter │ │ │ └── aggregator │ │ │ ├── SmartMeterAggregatorApplication.java │ │ │ ├── domain │ │ │ └── ElectricityMeasure.java │ │ │ ├── web │ │ │ ├── SmartMeterAggregatorRoutes.java │ │ │ └── ElectricityMeasureHandler.java │ │ │ ├── generator │ │ │ ├── ZoneInfo.java │ │ │ ├── ElectricityMeasureGenerator.java │ │ │ ├── ElectricityMeasureFluxGenerator.java │ │ │ └── ElectricityMeasureGeneratorProperties.java │ │ │ └── management │ │ │ └── ZonesEndpoint.java │ │ └── resources │ │ └── application.yml └── pom.xml ├── pom.xml └── README.adoc /smart-meter-dashboard/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | management.endpoints.web.exposure.include=* -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/generator/ZoneDescriptorRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard.generator; 2 | 3 | import org.springframework.data.mongodb.repository.ReactiveMongoRepository; 4 | 5 | public interface ZoneDescriptorRepository extends ReactiveMongoRepository { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /smart-meter-aggregator/src/main/java/com/example/meter/aggregator/SmartMeterAggregatorApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.aggregator; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SmartMeterAggregatorApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SmartMeterAggregatorApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /smart-meter-aggregator/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | management: 2 | endpoints: 3 | web: 4 | exposure: 5 | include: "*" 6 | meter: 7 | aggregator: 8 | generator: 9 | zones: 10 | urb1: 11 | name: Urban Area One 12 | devicesCount: 50 13 | urb2: 14 | name: Urban Area Two 15 | devicesCount: 60 16 | fin1: 17 | name: Financial District 18 | devicesCount: 20 19 | powerLow: 4000 20 | powerHigh: 6000 21 | server: 22 | port: 8081 23 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/web/MissingDataException.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard.web; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.server.ResponseStatusException; 5 | 6 | public class MissingDataException extends ResponseStatusException { 7 | 8 | private final String zoneId; 9 | 10 | public MissingDataException(String zoneId) { 11 | super(HttpStatus.NOT_FOUND, "Missing power data for Zone " + zoneId); 12 | this.zoneId = zoneId; 13 | } 14 | 15 | public String getZoneId() { 16 | return zoneId; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/resources/templates/error/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ooops, page not found 5 | 15 | 16 | 17 |
18 |
19 |

¯\_(ツ)_/¯

20 |
21 |
22 | 23 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/SmartMeterDashboardApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | 7 | @SpringBootApplication 8 | @EnableConfigurationProperties(DashboardProperties.class) 9 | public class SmartMeterDashboardApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(SmartMeterDashboardApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/sampling/PowerGridSampleRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard.sampling; 2 | 3 | import java.time.Instant; 4 | 5 | import reactor.core.publisher.Flux; 6 | 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.data.mongodb.repository.Tailable; 9 | import org.springframework.data.repository.reactive.ReactiveSortingRepository; 10 | 11 | public interface PowerGridSampleRepository extends ReactiveSortingRepository { 12 | 13 | Flux findAllByZoneId(String zoneId, Pageable pageable); 14 | 15 | @Tailable 16 | Flux findWithTailableCursorByZoneIdAndTimestampAfter(String zoneId, 17 | Instant timestamp); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/sampling/PowerGridSampleKey.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard.sampling; 2 | 3 | import java.time.Instant; 4 | 5 | class PowerGridSampleKey { 6 | 7 | public final String zoneId; 8 | 9 | public final Instant timestamp; 10 | 11 | PowerGridSampleKey(String zoneId, Instant timestamp) { 12 | this.zoneId = zoneId; 13 | this.timestamp = timestamp; 14 | } 15 | 16 | @Override 17 | public boolean equals(Object o) { 18 | if (this == o) return true; 19 | if (o == null || getClass() != o.getClass()) return false; 20 | 21 | PowerGridSampleKey that = (PowerGridSampleKey) o; 22 | 23 | if (!zoneId.equals(that.zoneId)) return false; 24 | return timestamp.equals(that.timestamp); 25 | } 26 | 27 | @Override 28 | public int hashCode() { 29 | int result = zoneId.hashCode(); 30 | result = 31 * result + timestamp.hashCode(); 31 | return result; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/generator/ZoneDescriptor.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard.generator; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.annotation.TypeAlias; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | @Document(collection = "zonedescriptors") 8 | @TypeAlias("zonedescriptor") 9 | public class ZoneDescriptor { 10 | 11 | @Id 12 | private String id; 13 | 14 | private String name; 15 | 16 | public ZoneDescriptor() { 17 | } 18 | 19 | ZoneDescriptor(String id, String name) { 20 | this.id = id; 21 | this.name = name; 22 | } 23 | 24 | public String getId() { 25 | return this.id; 26 | } 27 | 28 | public String getName() { 29 | return this.name; 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "ZoneDescriptor{" + 35 | "id='" + id + '\'' + 36 | ", name='" + name + '\'' + 37 | '}'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /smart-meter-aggregator/src/main/java/com/example/meter/aggregator/domain/ElectricityMeasure.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.aggregator.domain; 2 | 3 | import java.io.Serializable; 4 | import java.time.Instant; 5 | 6 | public final class ElectricityMeasure implements Serializable { 7 | 8 | private final String deviceId; 9 | 10 | private final String zoneId; 11 | 12 | private final Instant timestamp; 13 | 14 | private final float power; 15 | 16 | public ElectricityMeasure(String deviceId, String zoneId, Instant timestamp, 17 | float power) { 18 | this.deviceId = deviceId; 19 | this.zoneId = zoneId; 20 | this.timestamp = timestamp; 21 | this.power = power; 22 | } 23 | 24 | public String getDeviceId() { 25 | return this.deviceId; 26 | } 27 | 28 | public String getZoneId() { 29 | return this.zoneId; 30 | } 31 | 32 | public Instant getTimestamp() { 33 | return this.timestamp; 34 | } 35 | 36 | public float getPower() { 37 | return this.power; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/generator/MeasuresCollector.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard.generator; 2 | 3 | import com.example.meter.dashboard.DashboardProperties; 4 | import reactor.core.publisher.Flux; 5 | 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.web.reactive.function.client.WebClient; 8 | 9 | @Service 10 | public class MeasuresCollector { 11 | 12 | private final Flux electricityMeasures; 13 | 14 | public MeasuresCollector(DashboardProperties properties, 15 | WebClient.Builder webClientBuilder) { 16 | WebClient webClient = webClientBuilder 17 | .baseUrl(properties.getGenerator().getServiceUrl()).build(); 18 | 19 | this.electricityMeasures = webClient 20 | .get().uri("/measures/firehose") 21 | .retrieve().bodyToFlux(ElectricityMeasure.class) 22 | .share(); 23 | } 24 | 25 | public Flux getElectricityMeasures() { 26 | return this.electricityMeasures; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /smart-meter-aggregator/src/main/java/com/example/meter/aggregator/web/SmartMeterAggregatorRoutes.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.aggregator.web; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.web.reactive.function.server.RouterFunction; 7 | import org.springframework.web.reactive.function.server.ServerResponse; 8 | 9 | import static org.springframework.web.reactive.function.server.RequestPredicates.GET; 10 | import static org.springframework.web.reactive.function.server.RequestPredicates.accept; 11 | import static org.springframework.web.reactive.function.server.RouterFunctions.route; 12 | 13 | @Configuration 14 | public class SmartMeterAggregatorRoutes { 15 | 16 | @Bean 17 | public RouterFunction electricityMeasureRouter( 18 | ElectricityMeasureHandler electricityMeasureHandler) { 19 | return route(GET("/measures/firehose"), electricityMeasureHandler::firehose) 20 | .andRoute(GET("/measures/zones/{id}"), electricityMeasureHandler::forZone); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/generator/ElectricityMeasure.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard.generator; 2 | 3 | import java.io.Serializable; 4 | import java.time.Instant; 5 | 6 | import com.fasterxml.jackson.annotation.JsonCreator; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | 9 | public final class ElectricityMeasure implements Serializable { 10 | 11 | private final String deviceId; 12 | 13 | private final String zoneId; 14 | 15 | private final Instant timestamp; 16 | 17 | private final float power; 18 | 19 | @JsonCreator 20 | public ElectricityMeasure(@JsonProperty("deviceId") String deviceId, 21 | @JsonProperty("zoneId") String zoneId, 22 | @JsonProperty("timestamp") Instant timestamp, 23 | @JsonProperty("power") float power) { 24 | this.deviceId = deviceId; 25 | this.zoneId = zoneId; 26 | this.timestamp = timestamp; 27 | this.power = power; 28 | } 29 | 30 | public String getDeviceId() { 31 | return this.deviceId; 32 | } 33 | 34 | public String getZoneId() { 35 | return this.zoneId; 36 | } 37 | 38 | public Instant getTimestamp() { 39 | return this.timestamp; 40 | } 41 | 42 | public float getPower() { 43 | return this.power; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/MetricAggregatorHealthIndicator.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; 6 | import org.springframework.boot.actuate.health.Health; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.reactive.function.client.ClientResponse; 9 | import org.springframework.web.reactive.function.client.WebClient; 10 | 11 | @Component 12 | class MetricAggregatorHealthIndicator extends AbstractReactiveHealthIndicator { 13 | 14 | private final WebClient webClient; 15 | 16 | private MetricAggregatorHealthIndicator(DashboardProperties properties, 17 | WebClient.Builder webClientBuilder) { 18 | this.webClient = webClientBuilder 19 | .baseUrl(properties.getGenerator().getServiceUrl()).build(); 20 | } 21 | 22 | @Override 23 | protected Mono doHealthCheck(Health.Builder builder) { 24 | return webClient.get().uri("/actuator/health") 25 | .exchange() 26 | .map(ClientResponse::statusCode) 27 | .map((status -> status.is2xxSuccessful() ? Health.up() : Health.down())) 28 | .map(Health.Builder::build); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/DashboardProperties.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard; 2 | 3 | import javax.validation.constraints.Max; 4 | 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.boot.context.properties.NestedConfigurationProperty; 7 | import org.springframework.validation.annotation.Validated; 8 | 9 | @ConfigurationProperties("meter.dashboard") 10 | @Validated 11 | public class DashboardProperties { 12 | 13 | @NestedConfigurationProperty 14 | private final Generator generator = new Generator(); 15 | 16 | @NestedConfigurationProperty 17 | private final Renderer renderer = new Renderer(); 18 | 19 | public Generator getGenerator() { 20 | return generator; 21 | } 22 | 23 | public Renderer getRenderer() { 24 | return this.renderer; 25 | } 26 | 27 | public static class Generator { 28 | 29 | private String serviceUrl = "http://localhost:8081"; 30 | 31 | public String getServiceUrl() { 32 | return serviceUrl; 33 | } 34 | 35 | public void setServiceUrl(String serviceUrl) { 36 | this.serviceUrl = serviceUrl; 37 | } 38 | } 39 | 40 | @Validated 41 | public static class Renderer { 42 | 43 | @Max(40) 44 | private int historySize = 40; 45 | 46 | public int getHistorySize() { 47 | return this.historySize; 48 | } 49 | 50 | public void setHistorySize(int historySize) { 51 | this.historySize = historySize; 52 | } 53 | 54 | 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /smart-meter-aggregator/src/main/java/com/example/meter/aggregator/web/ElectricityMeasureHandler.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.aggregator.web; 2 | 3 | import java.util.Map; 4 | 5 | import com.example.meter.aggregator.domain.ElectricityMeasure; 6 | import com.example.meter.aggregator.generator.ElectricityMeasureGenerator; 7 | import reactor.core.publisher.Flux; 8 | import reactor.core.publisher.Mono; 9 | 10 | import org.springframework.http.MediaType; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.web.reactive.function.server.ServerRequest; 13 | import org.springframework.web.reactive.function.server.ServerResponse; 14 | 15 | @Component 16 | class ElectricityMeasureHandler { 17 | 18 | private final Map> content; 19 | 20 | private final Flux firehose; 21 | 22 | ElectricityMeasureHandler(ElectricityMeasureGenerator generator) { 23 | this.content = generator.generateSensorData(); 24 | this.firehose = Flux.merge(content.values()).share(); 25 | } 26 | 27 | public Mono firehose(ServerRequest request) { 28 | return ServerResponse.ok().contentType(MediaType.APPLICATION_STREAM_JSON) 29 | .body(this.firehose, ElectricityMeasure.class); 30 | } 31 | 32 | public Mono forZone(ServerRequest request) { 33 | String id = request.pathVariable("id"); 34 | return ServerResponse.ok().contentType(MediaType.APPLICATION_STREAM_JSON) 35 | .body(this.content.get(id), ElectricityMeasure.class); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /smart-meter-aggregator/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.example.meter 8 | smart-meter 9 | 0.0.1-SNAPSHOT 10 | 11 | smart-meter-aggregator 12 | jar 13 | Smart Meter :: Aggregator 14 | Collect and publish electricity measures 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-webflux 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-actuator 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-configuration-processor 29 | true 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | test 36 | 37 | 38 | io.projectreactor 39 | reactor-test 40 | test 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Smart Meter Dashboard 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |

16 |  Smart Meter Dashboard 17 |

18 |
19 |
20 |
21 |
22 |
23 |
24 |

Receiving power data for Zones:

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
Zone IDZone NameLinks
[[${zone.id}]][[${zone.name}]]Show
41 | 42 |
43 |
44 |
45 | 46 | -------------------------------------------------------------------------------- /smart-meter-aggregator/src/main/java/com/example/meter/aggregator/generator/ZoneInfo.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.aggregator.generator; 2 | 3 | import java.util.Random; 4 | 5 | public final class ZoneInfo { 6 | 7 | private static final Random random = new Random(); 8 | 9 | private final String zoneId; 10 | 11 | private int devicesCount; 12 | 13 | private PowerRange powerRange; 14 | 15 | ZoneInfo(String zoneId, int devicesCount, float powerLow, float powerHigh) { 16 | this.zoneId = zoneId; 17 | this.devicesCount = devicesCount; 18 | this.powerRange = new PowerRange(powerLow, powerHigh); 19 | } 20 | 21 | public String getZoneId() { 22 | return this.zoneId; 23 | } 24 | 25 | public int getDevicesCount() { 26 | return this.devicesCount; 27 | } 28 | 29 | public PowerRange getPowerRange() { 30 | return this.powerRange; 31 | } 32 | 33 | public void updatePowerRange(Float powerLow, Float powerHigh) { 34 | float effectivePowerLow = (powerLow != null ? powerLow 35 | : this.powerRange.powerLow); 36 | float effectivePowerHigh = (powerHigh != null ? powerHigh 37 | : this.powerRange.powerHigh); 38 | this.powerRange = new PowerRange(effectivePowerLow, effectivePowerHigh); 39 | } 40 | 41 | public float randomPower() { 42 | PowerRange powerRange = this.powerRange; 43 | return powerRange.randomPower(); 44 | } 45 | 46 | public static class PowerRange { 47 | 48 | private final float powerLow; 49 | 50 | private final float powerHigh; 51 | 52 | PowerRange(float powerLow, float powerHigh) { 53 | this.powerLow = powerLow; 54 | this.powerHigh = powerHigh; 55 | } 56 | 57 | public float getPowerLow() { 58 | return this.powerLow; 59 | } 60 | 61 | public float getPowerHigh() { 62 | return this.powerHigh; 63 | } 64 | 65 | float randomPower() { 66 | float delta = this.powerHigh - this.powerLow; 67 | return this.powerLow + (random.nextFloat() * delta); 68 | } 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.0.4.RELEASE 10 | 11 | 12 | com.example.meter 13 | smart-meter 14 | 0.0.1-SNAPSHOT 15 | pom 16 | Smart Meter 17 | A demo of smart meters reporting electricity usage 18 | 19 | 20 | UTF-8 21 | UTF-8 22 | 1.8 23 | 24 | 25 | 26 | smart-meter-aggregator 27 | smart-meter-dashboard 28 | 29 | 30 | 31 | 32 | 33 | org.webjars.npm 34 | bulma 35 | 0.6.0 36 | 37 | 38 | org.webjars.npm 39 | font-awesome 40 | 4.7.0 41 | 42 | 43 | org.webjars 44 | highcharts 45 | 6.0.2 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-maven-plugin 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /smart-meter-aggregator/src/main/java/com/example/meter/aggregator/generator/ElectricityMeasureGenerator.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.aggregator.generator; 2 | 3 | import java.time.Duration; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import com.example.meter.aggregator.domain.ElectricityMeasure; 9 | import reactor.core.publisher.Flux; 10 | 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.util.Assert; 13 | 14 | @Component 15 | public class ElectricityMeasureGenerator { 16 | 17 | private final Map zones; 18 | private final Duration periodicity; 19 | 20 | public ElectricityMeasureGenerator(ElectricityMeasureGeneratorProperties properties) { 21 | this.zones = extractConfiguration(properties); 22 | this.periodicity = properties.getPeriodicity(); 23 | } 24 | 25 | public Map> generateSensorData() { 26 | Map> content = new HashMap<>(); 27 | this.zones.forEach((id, zone) -> { 28 | ElectricityMeasureFluxGenerator generator = new ElectricityMeasureFluxGenerator( 29 | this.periodicity, zone); 30 | content.put(id, generator.sensorData()); 31 | }); 32 | return content; 33 | } 34 | 35 | public Map getZones() { 36 | return Collections.unmodifiableMap(this.zones); 37 | } 38 | 39 | public void updatePowerRangeFor(String zoneId, Float powerLow, Float powerHigh) { 40 | ZoneInfo zoneInfo = this.zones.get(zoneId); 41 | Assert.notNull(zoneInfo, "Zone with id " + zoneId + " does not exist"); 42 | zoneInfo.updatePowerRange(powerLow, powerHigh); 43 | } 44 | 45 | private static Map extractConfiguration( 46 | ElectricityMeasureGeneratorProperties properties) { 47 | Map zones = new HashMap<>(); 48 | properties.getZones().forEach((id, zone) -> zones.put(id, 49 | new ZoneInfo(id, zone.getDevicesCount(), zone.getPowerLow(), zone.getPowerHigh()))); 50 | return zones; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /smart-meter-aggregator/src/main/java/com/example/meter/aggregator/generator/ElectricityMeasureFluxGenerator.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.aggregator.generator; 2 | 3 | import java.time.Duration; 4 | import java.time.Instant; 5 | import java.time.LocalDateTime; 6 | import java.time.ZoneOffset; 7 | import java.util.Random; 8 | import java.util.UUID; 9 | 10 | import com.example.meter.aggregator.domain.ElectricityMeasure; 11 | import reactor.core.publisher.Flux; 12 | import reactor.core.publisher.Mono; 13 | 14 | class ElectricityMeasureFluxGenerator { 15 | 16 | private static final Random random = new Random(); 17 | 18 | private final int periodicity; 19 | 20 | private final ZoneInfo zoneInfo; 21 | 22 | private final String sensorIdPrefix; 23 | 24 | ElectricityMeasureFluxGenerator(Duration periodicity, ZoneInfo zoneInfo) { 25 | this.periodicity = (int) periodicity.getSeconds(); 26 | this.zoneInfo = zoneInfo; 27 | this.sensorIdPrefix = UUID.randomUUID().toString().substring(0, 16); 28 | } 29 | 30 | public Flux sensorData() { 31 | return Flux.interval(Duration.ZERO, Duration.ofSeconds(this.periodicity)) 32 | .map(i -> generateReportTimestamp()) 33 | .flatMap(reportTimestamp -> 34 | Flux.range(1, this.zoneInfo.getDevicesCount()) 35 | .map(sensorIndex 36 | -> generateMeasure(sensorIndex, reportTimestamp)) 37 | .delayUntil(i -> randomDelay())) 38 | .share(); 39 | } 40 | 41 | private ElectricityMeasure generateMeasure(int sensorIndex, Instant reportTimestamp) { 42 | return new ElectricityMeasure(generateSensorId(sensorIndex), this.zoneInfo.getZoneId(), 43 | reportTimestamp, this.zoneInfo.randomPower()); 44 | } 45 | 46 | private Mono randomDelay() { 47 | int max = (this.periodicity * 1000) / this.zoneInfo.getDevicesCount(); 48 | return Mono.delay(Duration.ofMillis(10 + random.nextInt(max))); 49 | } 50 | 51 | private String generateSensorId(long sensorIndex) { 52 | return String.format("%s-%04d", this.sensorIdPrefix, sensorIndex); 53 | } 54 | 55 | private Instant generateReportTimestamp() { 56 | LocalDateTime now = LocalDateTime.now(); 57 | return now.withSecond((now.getSecond() / this.periodicity) * this.periodicity) 58 | .withNano(0) 59 | .toInstant(ZoneOffset.UTC); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/sampling/PowerGridSample.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard.sampling; 2 | 3 | import java.time.Instant; 4 | 5 | import com.example.meter.dashboard.generator.ElectricityMeasure; 6 | 7 | import org.springframework.data.annotation.Id; 8 | import org.springframework.data.annotation.TypeAlias; 9 | import org.springframework.data.mongodb.core.mapping.Document; 10 | 11 | @Document(collection = "powergridsamples") 12 | @TypeAlias("powergridsample") 13 | public class PowerGridSample { 14 | 15 | @Id 16 | private String id; 17 | 18 | private long deviceCount; 19 | 20 | private String zoneId; 21 | 22 | private Instant timestamp; 23 | 24 | private float totalPower; 25 | 26 | public PowerGridSample() { 27 | } 28 | 29 | public PowerGridSample(String zoneId, Instant timestamp) { 30 | this.zoneId = zoneId; 31 | this.timestamp = timestamp; 32 | } 33 | 34 | public PowerGridSample(long deviceCount, String zoneId, Instant timestamp, float totalPower) { 35 | this.deviceCount = deviceCount; 36 | this.zoneId = zoneId; 37 | this.timestamp = timestamp; 38 | this.totalPower = totalPower; 39 | } 40 | 41 | public void addMeasure(ElectricityMeasure measure) { 42 | this.deviceCount++; 43 | this.totalPower += measure.getPower(); 44 | } 45 | 46 | public String getId() { 47 | return id; 48 | } 49 | 50 | public long getDeviceCount() { 51 | return deviceCount; 52 | } 53 | 54 | public void setDeviceCount(long deviceCount) { 55 | this.deviceCount = deviceCount; 56 | } 57 | 58 | public String getZoneId() { 59 | return zoneId; 60 | } 61 | 62 | public void setZoneId(String zoneId) { 63 | this.zoneId = zoneId; 64 | } 65 | 66 | public Instant getTimestamp() { 67 | return timestamp; 68 | } 69 | 70 | public void setTimestamp(Instant timestamp) { 71 | this.timestamp = timestamp; 72 | } 73 | 74 | public float getTotalPower() { 75 | return totalPower; 76 | } 77 | 78 | public void setTotalPower(float totalPower) { 79 | this.totalPower = totalPower; 80 | } 81 | 82 | @Override 83 | public String toString() { 84 | return "PowerGridSample{" + 85 | "id='" + id + '\'' + 86 | ", deviceCount=" + deviceCount + 87 | ", zoneId='" + zoneId + '\'' + 88 | ", timestamp=" + timestamp + 89 | ", totalPower=" + totalPower + 90 | '}'; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Smart Meter 2 | 3 | This sample project is used as live demo material for "Spring Boot 2.0 Web Applications". 4 | The recording of the session is https://www.youtube.com/watch?v=Q-9bUsrLVpI[available 5 | here]. 6 | 7 | == Building 8 | To build this project, you need Java 1.8, Maven 3.5 and a bash-like shell. 9 | 10 | Just invoke the following at the root of the project: 11 | 12 | [indent=0] 13 | ---- 14 | $ mvn clean install 15 | ---- 16 | 17 | == Running 18 | To run this project, you also need a mongo server running your machine with default 19 | settings. If mongo is running somewhere else, please refer 20 | https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-connecting-to-mongodb[to the documentation] 21 | to know more how you can configure Spring Boot to use that custom location. 22 | 23 | TIP: This assumes that you are running the application on the command line. You can also 24 | very easily run this application from your IDE. 25 | 26 | First start the aggregator, from the root of the project: 27 | 28 | [indent=0] 29 | ---- 30 | $ cd smart-meter-aggregator 31 | $ mvn spring-boot:run 32 | ---- 33 | 34 | You should be able to check that the aggregator is running by issuing the following 35 | command: 36 | 37 | [indent=0] 38 | ---- 39 | $ curl http://localhost:8081/measures/firehose 40 | ---- 41 | 42 | If you prefer to use `HTTPie` you can use this command: 43 | 44 | [indent=0] 45 | ---- 46 | $ http :8081/measures/firehose --stream 47 | ---- 48 | 49 | Once you've made sure the aggregator is running, you can start the dasbhoard. From the 50 | root of the project: 51 | 52 | [indent=0] 53 | ---- 54 | $ cd smart-meter-dashboard 55 | $ mvn spring-boot:run 56 | ---- 57 | 58 | If you go to `http://localhost:8080` you should see a list of zones. If you click on one 59 | of them, you should see a graph updating itself every 10s 60 | 61 | TIP: Initially the graph is empty as there is no data. Please wait a bit for the 62 | application to collect metrics from the aggregator. 63 | 64 | == Cleaning old data 65 | If you've been running this project a long time ago, the application will try to fetch 66 | the latest 40 entries which may create a huge gap. To restart from scratch you can remove 67 | the collection in MongoDB. 68 | 69 | If the dashboard is running, stop it first and then invoke the following: 70 | 71 | [indent=0] 72 | ---- 73 | $ mongo 74 | > db.powergridsamples.drop() 75 | ---- 76 | 77 | -------------------------------------------------------------------------------- /smart-meter-aggregator/src/main/java/com/example/meter/aggregator/generator/ElectricityMeasureGeneratorProperties.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.aggregator.generator; 2 | 3 | import java.time.Duration; 4 | import java.time.temporal.ChronoUnit; 5 | import java.util.LinkedHashMap; 6 | import java.util.Map; 7 | 8 | import javax.validation.constraints.Min; 9 | import javax.validation.constraints.NotNull; 10 | 11 | import org.hibernate.validator.constraints.time.DurationMax; 12 | import org.hibernate.validator.constraints.time.DurationMin; 13 | 14 | import org.springframework.boot.context.properties.ConfigurationProperties; 15 | import org.springframework.boot.convert.DurationUnit; 16 | import org.springframework.stereotype.Component; 17 | import org.springframework.validation.annotation.Validated; 18 | 19 | @Component 20 | @ConfigurationProperties("meter.aggregator.generator") 21 | @Validated 22 | public class ElectricityMeasureGeneratorProperties { 23 | 24 | /** 25 | * Interval between measures. 26 | */ 27 | @DurationMin(seconds = 5) 28 | @DurationMax(seconds = 60) 29 | @DurationUnit(ChronoUnit.SECONDS) 30 | private Duration periodicity = Duration.ofSeconds(10); 31 | 32 | private final Map zones = new LinkedHashMap<>(); 33 | 34 | public Duration getPeriodicity() { 35 | return this.periodicity; 36 | } 37 | 38 | public void setPeriodicity(Duration periodicity) { 39 | this.periodicity = periodicity; 40 | } 41 | 42 | public Map getZones() { 43 | return this.zones; 44 | } 45 | 46 | @Validated 47 | public static class Zone { 48 | 49 | @NotNull 50 | private String name; 51 | 52 | @Min(10) 53 | private int devicesCount = 50; 54 | 55 | private float powerLow = 2000; 56 | 57 | private float powerHigh = 4000; 58 | 59 | public String getName() { 60 | return this.name; 61 | } 62 | 63 | public void setName(String name) { 64 | this.name = name; 65 | } 66 | 67 | public int getDevicesCount() { 68 | return this.devicesCount; 69 | } 70 | 71 | public void setDevicesCount(int devicesCount) { 72 | this.devicesCount = devicesCount; 73 | } 74 | 75 | public float getPowerLow() { 76 | return this.powerLow; 77 | } 78 | 79 | public void setPowerLow(float powerLow) { 80 | this.powerLow = powerLow; 81 | } 82 | 83 | public float getPowerHigh() { 84 | return this.powerHigh; 85 | } 86 | 87 | public void setPowerHigh(float powerHigh) { 88 | this.powerHigh = powerHigh; 89 | } 90 | 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /smart-meter-dashboard/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example.meter 7 | smart-meter 8 | 0.0.1-SNAPSHOT 9 | 10 | smart-meter-dashboard 11 | jar 12 | Smart Meter :: Dashboard 13 | Display electricity measures 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-webflux 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-tomcat 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-thymeleaf 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-mongodb-reactive 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-actuator 35 | 36 | 37 | 38 | org.webjars.npm 39 | bulma 40 | runtime 41 | 42 | 43 | org.webjars.npm 44 | font-awesome 45 | runtime 46 | 47 | 48 | org.webjars 49 | highcharts 50 | runtime 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-configuration-processor 56 | true 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-devtools 61 | true 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-test 67 | test 68 | 69 | 70 | io.projectreactor 71 | reactor-test 72 | test 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/MongoCollectionInitializer.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard; 2 | 3 | import java.time.Duration; 4 | import java.util.List; 5 | 6 | import com.example.meter.dashboard.generator.ZoneDescriptor; 7 | import com.example.meter.dashboard.sampling.PowerGridSample; 8 | import com.mongodb.reactivestreams.client.MongoCollection; 9 | import org.bson.Document; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.Mono; 14 | 15 | import org.springframework.beans.factory.SmartInitializingSingleton; 16 | import org.springframework.core.Ordered; 17 | import org.springframework.core.annotation.Order; 18 | import org.springframework.data.mongodb.core.CollectionOptions; 19 | import org.springframework.data.mongodb.core.ReactiveMongoTemplate; 20 | import org.springframework.stereotype.Component; 21 | import org.springframework.web.reactive.function.client.WebClient; 22 | 23 | @Component 24 | @Order(Ordered.HIGHEST_PRECEDENCE) 25 | class MongoCollectionInitializer implements SmartInitializingSingleton { 26 | 27 | private static final Logger logger = LoggerFactory.getLogger(MongoCollectionInitializer.class); 28 | 29 | private final ReactiveMongoTemplate mongoTemplate; 30 | 31 | private final DashboardProperties properties; 32 | 33 | public MongoCollectionInitializer(ReactiveMongoTemplate mongoTemplate, 34 | DashboardProperties properties) { 35 | this.mongoTemplate = mongoTemplate; 36 | this.properties = properties; 37 | } 38 | 39 | @Override 40 | public void afterSingletonsInstantiated() { 41 | logger.info("Initializing MongoDB store if necessary"); 42 | Flux zoneDescriptors = WebClient 43 | .create(properties.getGenerator().getServiceUrl()) 44 | .get().uri("/actuator/zones") 45 | .retrieve().bodyToFlux(ZoneReport.class) 46 | .flatMapIterable(ZoneReport::getZones); 47 | 48 | // Retry if the remote service is not available yet 49 | Flux resilientZoneDescriptors = zoneDescriptors.onErrorResume(ex -> 50 | zoneDescriptors.delaySubscription(Duration.ofSeconds(1)).retry(3)); 51 | 52 | mongoTemplate.collectionExists(PowerGridSample.class) 53 | .filter(available -> !available).flatMap(i -> createSampleCollection()) 54 | .then(mongoTemplate.dropCollection(ZoneDescriptor.class)) 55 | .then(mongoTemplate.createCollection(ZoneDescriptor.class)) 56 | .thenMany(resilientZoneDescriptors.flatMap(mongoTemplate::insert)) 57 | .blockLast(); 58 | } 59 | 60 | private Mono> createSampleCollection() { 61 | CollectionOptions options = CollectionOptions.empty().size(104857600).capped(); 62 | return mongoTemplate.createCollection(PowerGridSample.class, options); 63 | } 64 | 65 | private static class ZoneReport { 66 | private List zones; 67 | 68 | public List getZones() { 69 | return this.zones; 70 | } 71 | 72 | public void setZones(List zones) { 73 | this.zones = zones; 74 | } 75 | 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/sampling/PowerGridSampler.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard.sampling; 2 | 3 | import java.time.Instant; 4 | import java.util.concurrent.atomic.AtomicReference; 5 | import java.util.function.Predicate; 6 | 7 | import com.example.meter.dashboard.generator.ElectricityMeasure; 8 | import com.example.meter.dashboard.generator.MeasuresCollector; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import reactor.core.Disposable; 12 | import reactor.core.publisher.Flux; 13 | 14 | import org.springframework.beans.factory.DisposableBean; 15 | import org.springframework.boot.ApplicationArguments; 16 | import org.springframework.boot.ApplicationRunner; 17 | import org.springframework.core.Ordered; 18 | import org.springframework.core.annotation.Order; 19 | import org.springframework.stereotype.Service; 20 | 21 | @Service 22 | @Order(Ordered.HIGHEST_PRECEDENCE) 23 | public class PowerGridSampler implements ApplicationRunner, DisposableBean { 24 | 25 | private static final Logger logger = LoggerFactory.getLogger(PowerGridSampler.class); 26 | 27 | private final MeasuresCollector measuresCollector; 28 | 29 | private final PowerGridSampleRepository repository; 30 | 31 | private final AtomicReference currentTimestamp = new AtomicReference<>(Instant.now()); 32 | 33 | private Disposable subscription; 34 | 35 | 36 | public PowerGridSampler(MeasuresCollector measuresCollector, 37 | PowerGridSampleRepository repository) { 38 | this.measuresCollector = measuresCollector; 39 | this.repository = repository; 40 | } 41 | 42 | private Flux sampleMeasuresForPowerGrid() { 43 | Flux samples = measuresCollector.getElectricityMeasures() 44 | // buffer until the timestamp of the measures changes 45 | .windowUntil(timestampBoundaryTrigger(), true) 46 | // group measures by zoneIds + timestamp 47 | .flatMap(window -> window.groupBy(measure -> 48 | new PowerGridSampleKey(measure.getZoneId(), measure.getTimestamp()))) 49 | // for each group, reduce all measures into a PowerGrid sample for that timestamp 50 | .flatMap(windowForZone -> { 51 | PowerGridSampleKey key = windowForZone.key(); 52 | PowerGridSample initial = new PowerGridSample(key.zoneId, key.timestamp); 53 | return windowForZone.reduce(initial, 54 | (powerGridSample, measure) -> { 55 | powerGridSample.addMeasure(measure); 56 | return powerGridSample; 57 | }); 58 | }); 59 | 60 | // save the generated samples and return them 61 | return this.repository.saveAll(samples); 62 | } 63 | 64 | private Predicate timestampBoundaryTrigger() { 65 | return measure -> { 66 | if (this.currentTimestamp.get().isBefore(measure.getTimestamp())) { 67 | this.currentTimestamp.set(measure.getTimestamp()); 68 | return true; 69 | } 70 | return false; 71 | }; 72 | } 73 | 74 | @Override 75 | public void run(ApplicationArguments args) { 76 | logger.info("Starting subscription with aggregator service"); 77 | this.subscription = sampleMeasuresForPowerGrid().subscribe(); 78 | } 79 | 80 | @Override 81 | public void destroy() { 82 | this.subscription.dispose(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /smart-meter-aggregator/src/main/java/com/example/meter/aggregator/management/ZonesEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.aggregator.management; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.List; 6 | 7 | import com.example.meter.aggregator.generator.ElectricityMeasureGenerator; 8 | import com.example.meter.aggregator.generator.ElectricityMeasureGeneratorProperties; 9 | import com.example.meter.aggregator.generator.ZoneInfo; 10 | 11 | import org.springframework.boot.actuate.endpoint.annotation.Endpoint; 12 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; 13 | import org.springframework.boot.actuate.endpoint.annotation.Selector; 14 | import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; 15 | import org.springframework.lang.Nullable; 16 | import org.springframework.stereotype.Component; 17 | 18 | @Component 19 | @Endpoint(id = "zones") 20 | public class ZonesEndpoint { 21 | 22 | private final ElectricityMeasureGenerator electricityMeasureGenerator; 23 | private final ElectricityMeasureGeneratorProperties properties; 24 | 25 | public ZonesEndpoint(ElectricityMeasureGenerator electricityMeasureGenerator, 26 | ElectricityMeasureGeneratorProperties properties) { 27 | this.electricityMeasureGenerator = electricityMeasureGenerator; 28 | this.properties = properties; 29 | } 30 | 31 | @ReadOperation 32 | public ZoneReport zones() { 33 | Collection descriptors = new ArrayList<>(); 34 | this.electricityMeasureGenerator.getZones().forEach((id, zi) -> { 35 | descriptors.add(toZoneDescriptor(id, zi)); 36 | }); 37 | return new ZoneReport(descriptors); 38 | } 39 | 40 | @ReadOperation 41 | public ZoneDescriptor zone(@Selector String id) { 42 | ZoneInfo zoneInfo = this.electricityMeasureGenerator.getZones().get(id); 43 | if (zoneInfo != null) { 44 | return toZoneDescriptor(id, zoneInfo); 45 | } 46 | return null; 47 | } 48 | 49 | @WriteOperation 50 | public void updatePowerRange(@Selector String id, 51 | @Nullable Float powerLow, @Nullable Float powerHigh) { 52 | this.electricityMeasureGenerator.updatePowerRangeFor(id, powerLow, powerHigh); 53 | } 54 | 55 | private ZoneDescriptor toZoneDescriptor(String id, ZoneInfo zoneInfo) { 56 | ZoneInfo.PowerRange powerRange = zoneInfo.getPowerRange(); 57 | return new ZoneDescriptor(id, this.properties.getZones().get(id).getName(), 58 | powerRange.getPowerLow(), powerRange.getPowerHigh()); 59 | } 60 | 61 | public static class ZoneReport { 62 | 63 | private final Collection zones; 64 | 65 | ZoneReport(Collection zones) { 66 | this.zones = zones; 67 | } 68 | 69 | public Collection getZones() { 70 | return this.zones; 71 | } 72 | } 73 | 74 | public static class ZoneDescriptor { 75 | 76 | private final String id; 77 | private final String name; 78 | private final float powerLow; 79 | private final float powerHigh; 80 | 81 | ZoneDescriptor(String id, String name, float powerLow, 82 | float powerHigh) { 83 | this.id = id; 84 | this.name = name; 85 | this.powerLow = powerLow; 86 | this.powerHigh = powerHigh; 87 | } 88 | 89 | public String getId() { 90 | return this.id; 91 | } 92 | 93 | public String getName() { 94 | return this.name; 95 | } 96 | 97 | public float getPowerLow() { 98 | return this.powerLow; 99 | } 100 | 101 | public float getPowerHigh() { 102 | return this.powerHigh; 103 | } 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/java/com/example/meter/dashboard/web/DashboardController.java: -------------------------------------------------------------------------------- 1 | package com.example.meter.dashboard.web; 2 | 3 | import java.time.Instant; 4 | import java.time.LocalDateTime; 5 | import java.time.ZoneOffset; 6 | 7 | import com.example.meter.dashboard.DashboardProperties; 8 | import com.example.meter.dashboard.generator.ElectricityMeasure; 9 | import com.example.meter.dashboard.generator.MeasuresCollector; 10 | import com.example.meter.dashboard.generator.ZoneDescriptorRepository; 11 | import com.example.meter.dashboard.sampling.PowerGridSample; 12 | import com.example.meter.dashboard.sampling.PowerGridSampleRepository; 13 | import org.thymeleaf.spring5.context.webflux.ReactiveDataDriverContextVariable; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | import org.springframework.data.domain.PageRequest; 18 | import org.springframework.data.domain.Sort; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.stereotype.Controller; 21 | import org.springframework.web.bind.annotation.GetMapping; 22 | import org.springframework.web.bind.annotation.PathVariable; 23 | import org.springframework.web.bind.annotation.ResponseBody; 24 | import org.springframework.web.reactive.result.view.Rendering; 25 | 26 | @Controller 27 | public class DashboardController { 28 | 29 | private final int historySize; 30 | 31 | private final PowerGridSampleRepository powerGridSampleRepository; 32 | 33 | private final ZoneDescriptorRepository zoneDescriptorRepository; 34 | 35 | private final MeasuresCollector measuresCollector; 36 | 37 | public DashboardController(DashboardProperties properties, 38 | PowerGridSampleRepository powerGridSampleRepository, 39 | ZoneDescriptorRepository zoneDescriptorRepository, 40 | MeasuresCollector measuresCollector) { 41 | this.historySize = properties.getRenderer().getHistorySize(); 42 | this.powerGridSampleRepository = powerGridSampleRepository; 43 | this.zoneDescriptorRepository = zoneDescriptorRepository; 44 | this.measuresCollector = measuresCollector; 45 | } 46 | 47 | @GetMapping("/") 48 | public Rendering home() { 49 | return Rendering 50 | .view("index") 51 | .modelAttribute("zones", this.zoneDescriptorRepository.findAll()) 52 | .build(); 53 | } 54 | 55 | @GetMapping("/zones/{zoneId}") 56 | public Mono displayZone(@PathVariable String zoneId) { 57 | PageRequest pageRequest = PageRequest.of(0, this.historySize, 58 | Sort.by("timestamp").descending()); 59 | Flux latestSamples = this.powerGridSampleRepository 60 | .findAllByZoneId(zoneId, pageRequest); 61 | 62 | return this.zoneDescriptorRepository.findById(zoneId) 63 | .switchIfEmpty(Mono.error(new MissingDataException(zoneId))) 64 | .map(zoneDescriptor -> Rendering 65 | .view("zone") 66 | .modelAttribute("zone", zoneDescriptor) 67 | .modelAttribute("samples", latestSamples) 68 | .build()); 69 | } 70 | 71 | @GetMapping(path = "/zones/{zoneId}/updates", produces = MediaType.TEXT_EVENT_STREAM_VALUE) 72 | @ResponseBody 73 | public Flux streamUpdates(@PathVariable String zoneId) { 74 | Instant startup = LocalDateTime.now().withSecond(0).toInstant(ZoneOffset.UTC); 75 | return this.powerGridSampleRepository 76 | .findWithTailableCursorByZoneIdAndTimestampAfter(zoneId, startup); 77 | } 78 | 79 | @GetMapping(path = "/zones/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE) 80 | public Rendering streamZoneEvents() { 81 | Flux events = this.measuresCollector 82 | .getElectricityMeasures() 83 | .filter(measure -> measure.getPower() == 0); 84 | 85 | ReactiveDataDriverContextVariable eventsDataDriver = 86 | new ReactiveDataDriverContextVariable(events, 1); 87 | 88 | return Rendering.view("zone :: #events") 89 | .modelAttribute("eventStream", eventsDataDriver) 90 | .build(); 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /smart-meter-dashboard/src/main/resources/templates/zone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Smart Meter Dashboard for Zone [[|${zone.name}|]] 7 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | 20 |
21 |
22 |
23 |

24 |  Smart Meter Dashboard 25 |

26 |
27 |
28 |
29 |
30 |
31 |

32 | Power data for Zone "[[|${zone.name}|]]" 33 |

34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |

46 |  Zone events 47 |

48 |
49 |
50 |

[[${#temporals.formatISO(event.timestamp)}]] - 51 | [[${event.deviceId}]] - Zone [[|${event.zoneId}|]]

52 |
53 |
54 |
55 | 56 | 107 | 108 | --------------------------------------------------------------------------------