Skip to content

Commit

Permalink
Feature/download openweather data during sensor data fetching (#226)
Browse files Browse the repository at this point in the history
  • Loading branch information
saschadoemer authored Aug 14, 2024
1 parent 3fdf7d6 commit 101b5f1
Show file tree
Hide file tree
Showing 27 changed files with 639 additions and 27 deletions.
3 changes: 2 additions & 1 deletion src/main/java/de/app/fivegla/api/Error.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ public enum Error {
DEFAULT_GROUP_FOR_TENANT_NOT_FOUND(errorOf(40)),
THIRD_PARTY_API_CONFIGURATION_NOT_FOUND(errorOf(41)),
COULD_NOT_STORE_IMAGE_ON_S3(errorOf(42)),
ORTHOPHOTO_COULD_NOT_TRIGGER_CALCULATION(errorOf(43));
ORTHOPHOTO_COULD_NOT_TRIGGER_CALCULATION(errorOf(43)),
COULD_NOT_IMPORT_DATA_FROM_OPEN_WEATHER(errorOf(44));

private static String errorOf(int i) {
return ERR_ + String.format("%05d", i);
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/de/app/fivegla/api/Manufacturer.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ public enum Manufacturer {
AGVOLUTION,
SENSOTERRA,
SENTEK,
WEENAT
WEENAT,
OPEN_WEATHER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package de.app.fivegla.business;

import de.app.fivegla.persistence.RegisteredDeviceRepository;
import de.app.fivegla.persistence.entity.RegisteredDevice;
import de.app.fivegla.persistence.entity.Tenant;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class RegisteredDevicesService {

private final GroupService groupService;
private final RegisteredDeviceRepository registeredDeviceRepository;

/**
* Registers a device.
*
* @param prefilledEntity The prefilled device entity to register.
* @return The registered device entity.
*/
public RegisteredDevice registerDevice(RegisteredDevice prefilledEntity, String groupId) {
log.info("Registering device: {}", prefilledEntity);
prefilledEntity.setOid(UUID.randomUUID().toString());
groupService.getOrDefault(prefilledEntity.getTenant(), groupId);
return registeredDeviceRepository.save(prefilledEntity);
}

/**
* Finds all registered devices for a tenant.
*
* @param tenant The tenant to find the registered devices for.
* @return The list of registered devices.
*/
public List<RegisteredDevice> findAll(Tenant tenant) {
log.info("Finding all registered devices for tenant: {}", tenant);
return registeredDeviceRepository.findAllByTenant(tenant);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package de.app.fivegla.business;

import de.app.fivegla.api.Manufacturer;
import de.app.fivegla.event.events.HistoricalDataImportEvent;
import de.app.fivegla.persistence.ThirdPartyApiConfigurationRepository;
import de.app.fivegla.persistence.entity.Tenant;
import de.app.fivegla.persistence.entity.ThirdPartyApiConfiguration;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -99,9 +101,17 @@ public Optional<ThirdPartyApiConfiguration> findById(Long id) {
* @param startDateInThePast the start date from which the import will begin, in the past
*/
public void triggerImport(String tenantId, String uuid, LocalDate startDateInThePast) {
getThirdPartyApiConfigurations(tenantId, uuid).forEach(thirdPartyApiConfiguration -> {
applicationEventPublisher.publishEvent(new HistoricalDataImportEvent(thirdPartyApiConfiguration.getId(),
startDateInThePast.atStartOfDay(ZoneId.systemDefault()).toInstant()));
});
getThirdPartyApiConfigurations(tenantId, uuid).forEach(thirdPartyApiConfiguration -> applicationEventPublisher.publishEvent(new HistoricalDataImportEvent(this, thirdPartyApiConfiguration.getId(),
startDateInThePast.atStartOfDay(ZoneId.systemDefault()).toInstant())));
}

/**
* Finds all third-party API configurations for a given tenant and manufacturer.
*
* @param tenant the tenant for which the configurations will be retrieved
* @param manufacturer the manufacturer for which the configurations will be retrieved
*/
public Optional<ThirdPartyApiConfiguration> findByManufacturer(Tenant tenant, Manufacturer manufacturer) {
return thirdPartyApiConfigurationRepository.findFirstByTenantAndManufacturer(tenant, manufacturer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ public class BaseMappings {
public static final String DEVICE_POSITION = SECURED_BY_TENANT + "/device-position";
public static final String GROUPS = SECURED_BY_TENANT + "/groups";
public static final String SUBSCRIPTION = SECURED_BY_TENANT + "/subscription";
public static final String REGISTERED_DEVICES = SECURED_BY_TENANT + "/registered-devices";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package de.app.fivegla.controller.dto.request;


import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
@Schema(description = "Request to register a device.")
public class RegisterDeviceRequest {

@NotBlank
@Schema(description = "The name.", requiredMode = Schema.RequiredMode.REQUIRED)
private String name;

@Schema(description = "The description.")
private String description;

@Schema(description = "The longitude.", requiredMode = Schema.RequiredMode.REQUIRED)
private double longitude;

@Schema(description = "The latitude.", requiredMode = Schema.RequiredMode.REQUIRED)
private double latitude;

@Schema(description = "The id of the group, if not set, the default group will be used.")
private String groupId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package de.app.fivegla.controller.dto.response;

import de.app.fivegla.api.Response;
import de.app.fivegla.controller.dto.response.inner.RegisteredDevice;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Builder
@Schema(description = "Response to register a device.")
public class RegisterDeviceResponse extends Response {

@Schema(description = "The registered device.")
private RegisteredDevice registeredDevice;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package de.app.fivegla.controller.dto.response.inner;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

/**
* Image.
*/
@Getter
@Setter
@Builder
@Schema(description = "A registered device.")
public class RegisteredDevice {

/**
* The oid.
*/
@Schema(description = "The oid.")
private String oid;

/**
* The name.
*/
@Schema(description = "The name.")
private String name;

/**
* The description.
*/
@Schema(description = "The description.")
private String description;

/**
* The location of the image.
*/
@Schema(description = "The longitude.")
private double longitude;

/**
* The location.
*/
@Schema(description = "The latitude.")
private double latitude;

/**
* The group.
*/
@Schema(description = "The ID of the group.")
private String groupId;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package de.app.fivegla.controller.tenant;

import de.app.fivegla.api.Response;
import de.app.fivegla.business.RegisteredDevicesService;
import de.app.fivegla.config.security.marker.TenantCredentialApiAccess;
import de.app.fivegla.controller.api.BaseMappings;
import de.app.fivegla.controller.dto.request.RegisterDeviceRequest;
import de.app.fivegla.controller.dto.response.RegisterDeviceResponse;
import de.app.fivegla.persistence.entity.RegisteredDevice;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(BaseMappings.REGISTERED_DEVICES)
public class RegisteredDevicesController implements TenantCredentialApiAccess {

private final RegisteredDevicesService registeredDevicesService;

@Operation(
operationId = "registered-devices.register",
description = "Registers a device.",
tags = BaseMappings.REGISTERED_DEVICES
)
@ApiResponse(
responseCode = "201",
description = "The device was registered.",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = RegisterDeviceResponse.class)
)
)
@ApiResponse(
responseCode = "400",
description = "The request is invalid.",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Response.class)
)
)
@PostMapping(value = "/register", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<? extends Response> register(@Valid @RequestBody RegisterDeviceRequest request) {
var registeredDevice = new RegisteredDevice();
registeredDevice.setName(request.getName());
registeredDevice.setDescription(request.getDescription());
registeredDevice.setLongitude(request.getLongitude());
registeredDevice.setLatitude(request.getLatitude());
var registeredAndSavedDevice = registeredDevicesService.registerDevice(registeredDevice, request.getGroupId());
return ResponseEntity.ok(RegisterDeviceResponse.builder()
.registeredDevice(de.app.fivegla.controller.dto.response.inner.RegisteredDevice.builder()
.oid(registeredAndSavedDevice.getOid())
.name(registeredAndSavedDevice.getName())
.description(registeredAndSavedDevice.getDescription())
.longitude(registeredAndSavedDevice.getLongitude())
.latitude(registeredAndSavedDevice.getLatitude())
.groupId(registeredAndSavedDevice.getGroup().getOid())
.build())
.build());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public ResponseEntity<? extends Response> createThirdPartyApiConfiguration(@Vali
var thirdPartyApiConfiguration = request.toEntity();
thirdPartyApiConfiguration.setTenant(tenant);
var thirdPartyApiConfigurationCreated = thirdPartyApiConfigurationService.createThirdPartyApiConfiguration(thirdPartyApiConfiguration);
applicationEventPublisher.publishEvent(new DataImportEvent(thirdPartyApiConfigurationCreated.getId()));
applicationEventPublisher.publishEvent(new DataImportEvent(this, thirdPartyApiConfigurationCreated.getId()));
var response = CreateThirdPartyApiConfigurationResponse.builder()
.thirdPartyApiConfiguration(ThirdPartyApiConfiguration.builder()
.tenantId(thirdPartyApiConfigurationCreated.getTenant().getTenantId())
Expand Down
25 changes: 16 additions & 9 deletions src/main/java/de/app/fivegla/event/DataImportEventHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class DataImportEventHandler {

@EventListener(DataImportEvent.class)
public void handleDataImportEvent(DataImportEvent dataImportEvent) {
var thirdPartyApiConfiguration = thirdPartyApiConfigurationService.findById(dataImportEvent.thirdPartyApiConfigurationId())
var thirdPartyApiConfiguration = thirdPartyApiConfigurationService.findById(dataImportEvent.getThirdPartyApiConfigurationId())
.orElseThrow(() -> new BusinessException(ErrorMessage.builder()
.error(Error.THIRD_PARTY_API_CONFIGURATION_NOT_FOUND)
.message("Third party API configuration not found.")
Expand Down Expand Up @@ -68,7 +68,7 @@ public void handleDataImportEvent(DataImportEvent dataImportEvent) {

@EventListener(HistoricalDataImportEvent.class)
public void handleHistoricalDataImportEvent(HistoricalDataImportEvent historicalDataImportEvent) {
var thirdPartyApiConfiguration = thirdPartyApiConfigurationService.findById(historicalDataImportEvent.thirdPartyApiConfigurationId())
var thirdPartyApiConfiguration = thirdPartyApiConfigurationService.findById(historicalDataImportEvent.getThirdPartyApiConfigurationId())
.orElseThrow(() -> new BusinessException(ErrorMessage.builder()
.error(Error.THIRD_PARTY_API_CONFIGURATION_NOT_FOUND)
.message("Third party API configuration not found.")
Expand All @@ -82,13 +82,20 @@ public void handleHistoricalDataImportEvent(HistoricalDataImportEvent historical
} else {
var tenant = optionalTenant.get();
switch (manufacturer) {
case SOILSCOUT -> soilScoutScheduledMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.startDate());
case AGVOLUTION -> agvolutionMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.startDate());
case AGRANIMO -> agranimoMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.startDate());
case FARM21 -> farm21MeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.startDate());
case SENSOTERRA -> sensoterraMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.startDate());
case SENTEK -> sentekMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.startDate());
case WEENAT -> weenatMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.startDate());
case SOILSCOUT ->
soilScoutScheduledMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.getStartDate());
case AGVOLUTION ->
agvolutionMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.getStartDate());
case AGRANIMO ->
agranimoMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.getStartDate());
case FARM21 ->
farm21MeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.getStartDate());
case SENSOTERRA ->
sensoterraMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.getStartDate());
case SENTEK ->
sentekMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.getStartDate());
case WEENAT ->
weenatMeasurementImport.run(tenant, thirdPartyApiConfiguration, historicalDataImportEvent.getStartDate());
default -> throw new IllegalArgumentException("Unknown manufacturer: " + manufacturer);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package de.app.fivegla.event;

import de.app.fivegla.event.events.OpenWeatherImportEvent;
import de.app.fivegla.integration.openweather.OpenWeatherIntegrationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class OpenWeatherImportEventHandler {

public final OpenWeatherIntegrationService openWeatherIntegrationService;

@EventListener(OpenWeatherImportEvent.class)
public void handleOpenWeatherImportEvent(OpenWeatherImportEvent openWeatherImportEvent) {
log.info("Handling OpenWeather import event for longitude {} and latitude {}.", openWeatherImportEvent.getLongitude(), openWeatherImportEvent.getLatitude());
var openWeatherData = openWeatherIntegrationService.fetchWeatherData(openWeatherImportEvent.getThirdPartyApiConfiguration().getApiToken(), openWeatherImportEvent.getLatitude(), openWeatherImportEvent.getLongitude());
log.info("Successfully imported weather data from OpenWeather for longitude {} and latitude {}.", openWeatherImportEvent.getLongitude(), openWeatherImportEvent.getLatitude());
log.debug("OpenWeather data: {}", openWeatherData);
}

}
15 changes: 12 additions & 3 deletions src/main/java/de/app/fivegla/event/events/DataImportEvent.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package de.app.fivegla.event.events;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

/**
* Event for data import.
*
* @param thirdPartyApiConfigurationId The ID of the third party API configuration.
*/
public record DataImportEvent(Long thirdPartyApiConfigurationId) {
@Getter
public class DataImportEvent extends ApplicationEvent {

private final Long thirdPartyApiConfigurationId;

public DataImportEvent(Object source, Long thirdPartyApiConfigurationId) {
super(source);
this.thirdPartyApiConfigurationId = thirdPartyApiConfigurationId;
}
}
Loading

0 comments on commit 101b5f1

Please sign in to comment.