From b2a6261fef7b9a7d0b766408370caf3925a0d082 Mon Sep 17 00:00:00 2001 From: noisymouse27f Date: Mon, 15 Sep 2025 15:12:59 +0100 Subject: [PATCH] initial commit --- pom.xml | 85 +++ .../lightswitch/LightswitchApplication.java | 26 + .../lightswitch/config/CalculatorConfig.java | 21 + .../lightswitch/config/Pi4JContext.java | 14 + .../lightswitch/config/PropertiesConfig.java | 14 + .../lightswitch/config/ShelfConfig.java | 59 ++ .../lightswitch/controller/LightingAPI.java | 7 + .../controller/impl/LightingController.java | 50 ++ .../controller/impl/ScheduleController.java | 37 + .../hilltopgrove/lightswitch/model/Light.java | 94 +++ .../hilltopgrove/lightswitch/model/Rack.java | 62 ++ .../lightswitch/model/ScheduleBean.java | 99 +++ .../hilltopgrove/lightswitch/model/Shelf.java | 48 ++ .../lightswitch/service/LightingService.java | 7 + .../service/RackLightingPattern.java | 12 + .../service/SchedulingService.java | 8 + .../service/SunRiseSunSetService.java | 7 + .../lightswitch/service/SunriseSunset.java | 649 ++++++++++++++++++ .../service/impl/LightingServiceImpl.java | 29 + .../service/impl/RackLightingPatternImpl.java | 71 ++ .../service/impl/SchedulingServiceImpl.java | 88 +++ .../impl/SunRiseSunSetServiceImpl.java | 28 + .../service/impl/SunriseSunset.java | 649 ++++++++++++++++++ src/main/resources/application.yml | 8 + 24 files changed, 2172 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/space/hilltopgrove/lightswitch/LightswitchApplication.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/config/CalculatorConfig.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/config/Pi4JContext.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/config/PropertiesConfig.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/config/ShelfConfig.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/controller/LightingAPI.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/controller/impl/LightingController.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/controller/impl/ScheduleController.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/model/Light.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/model/Rack.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/model/ScheduleBean.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/model/Shelf.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/service/LightingService.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/service/RackLightingPattern.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/service/SchedulingService.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/service/SunRiseSunSetService.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/service/SunriseSunset.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/service/impl/LightingServiceImpl.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/service/impl/RackLightingPatternImpl.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/service/impl/SchedulingServiceImpl.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/service/impl/SunRiseSunSetServiceImpl.java create mode 100644 src/main/java/space/hilltopgrove/lightswitch/service/impl/SunriseSunset.java create mode 100644 src/main/resources/application.yml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..47f848c --- /dev/null +++ b/pom.xml @@ -0,0 +1,85 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.3 + + + + space.hilltopgrove + lightswitch + 0.0.2-SNAPSHOT + lightswitch + Demo project for Spring Boot + + + 17 + 2.1.1 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + true + + + + + com.pi4j + pi4j-core + ${pi4j.version} + + + + + com.pi4j + pi4j-plugin-raspberrypi + ${pi4j.version} + + + com.pi4j + pi4j-plugin-pigpio + ${pi4j.version} + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + src/main/resources + true + + **/application.yml + + + + + + diff --git a/src/main/java/space/hilltopgrove/lightswitch/LightswitchApplication.java b/src/main/java/space/hilltopgrove/lightswitch/LightswitchApplication.java new file mode 100644 index 0000000..1dca983 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/LightswitchApplication.java @@ -0,0 +1,26 @@ +package space.hilltopgrove.lightswitch; + +import com.pi4j.context.Context; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; + +import javax.annotation.PreDestroy; + +@SpringBootApplication +@RequiredArgsConstructor +@ConfigurationPropertiesScan +public class LightswitchApplication { + private final Context pi4jContext; + + public static void main(String[] args) { + SpringApplication.run(LightswitchApplication.class, args); + } + + @PreDestroy + public void shutdown() { + pi4jContext.shutdown(); + } + +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/config/CalculatorConfig.java b/src/main/java/space/hilltopgrove/lightswitch/config/CalculatorConfig.java new file mode 100644 index 0000000..7a19772 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/config/CalculatorConfig.java @@ -0,0 +1,21 @@ +package space.hilltopgrove.lightswitch.config; + +import com.luckycatlabs.sunrisesunset.SunriseSunsetCalculator; +import com.luckycatlabs.sunrisesunset.dto.Location; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +import java.util.TimeZone; + +@Component +public class CalculatorConfig { + + @Bean + public SunriseSunsetCalculator calculator(PropertiesConfig propertiesConfig) { + return new SunriseSunsetCalculator(getLocation(propertiesConfig), TimeZone.getDefault().getID()); + } + + private Location getLocation(PropertiesConfig propertiesConfig) { + return new Location(propertiesConfig.getLatitude(), propertiesConfig.getLongitude()); + } +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/config/Pi4JContext.java b/src/main/java/space/hilltopgrove/lightswitch/config/Pi4JContext.java new file mode 100644 index 0000000..de2f836 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/config/Pi4JContext.java @@ -0,0 +1,14 @@ +package space.hilltopgrove.lightswitch.config; + +import com.pi4j.Pi4J; +import com.pi4j.context.Context; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +@Component +public class Pi4JContext { + @Bean + public Context pi4jContext() { + return Pi4J.newAutoContext(); + } +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/config/PropertiesConfig.java b/src/main/java/space/hilltopgrove/lightswitch/config/PropertiesConfig.java new file mode 100644 index 0000000..2b7dfd9 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/config/PropertiesConfig.java @@ -0,0 +1,14 @@ +package space.hilltopgrove.lightswitch.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Map; + +@Data +@ConfigurationProperties +public class PropertiesConfig { + private Map> shelf; + private double latitude; + private double longitude; +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/config/ShelfConfig.java b/src/main/java/space/hilltopgrove/lightswitch/config/ShelfConfig.java new file mode 100644 index 0000000..c2b991b --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/config/ShelfConfig.java @@ -0,0 +1,59 @@ +package space.hilltopgrove.lightswitch.config; + +import com.pi4j.context.Context; +import com.pi4j.io.gpio.digital.DigitalOutput; +import com.pi4j.io.gpio.digital.DigitalState; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import space.hilltopgrove.lightswitch.model.Light; +import space.hilltopgrove.lightswitch.model.Rack; +import space.hilltopgrove.lightswitch.model.Shelf; + +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class ShelfConfig { + + @Bean + public Shelf shelf(PropertiesConfig propertiesConfig, Context pi4jContext) { + var rawShelfMap = propertiesConfig.getShelf(); + log.info("converting raw shelfMap: {}", rawShelfMap); + var rackMap = getRackMap(rawShelfMap, pi4jContext); + return Shelf.map(rackMap); + } + + private Map getRackMap(Map> rawRackMap, Context pi4jContext) { + var result = rawRackMap.keySet().stream() + .collect(Collectors.toMap( + rackKey -> rackKey, + rackKey -> Rack.map(getLightMap(rawRackMap.get(rackKey), pi4jContext)) + )); + log.info("rackMap: " + result); + return result; + } + + private Map getLightMap(Map rawLightMap, Context pi4jContext) { + var result = rawLightMap.keySet().stream() + .collect(Collectors.toMap( + lightKey -> lightKey, + lightKey -> Light.map(createDigitalOutput(rawLightMap.get(lightKey), pi4jContext)))); + log.info("lightMap: " + result); + return result; + } + + private DigitalOutput createDigitalOutput(int pin, Context pi4jContext) { + return pi4jContext.create( + DigitalOutput.newConfigBuilder(pi4jContext) + .id("led" + pin) + .name("LED" + pin) + .address(pin) + .shutdown(DigitalState.LOW) + .initial(DigitalState.LOW) + .provider("pigpio-digital-output") + ); + } + +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/controller/LightingAPI.java b/src/main/java/space/hilltopgrove/lightswitch/controller/LightingAPI.java new file mode 100644 index 0000000..c39682d --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/controller/LightingAPI.java @@ -0,0 +1,7 @@ +package space.hilltopgrove.lightswitch.controller; + +public interface LightingAPI { + void on(Integer rack, Integer pin); + void off(Integer rack, Integer pin); + void toggle(Integer rack, Integer pin); +} \ No newline at end of file diff --git a/src/main/java/space/hilltopgrove/lightswitch/controller/impl/LightingController.java b/src/main/java/space/hilltopgrove/lightswitch/controller/impl/LightingController.java new file mode 100644 index 0000000..bb5cc2e --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/controller/impl/LightingController.java @@ -0,0 +1,50 @@ +package space.hilltopgrove.lightswitch.controller.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import space.hilltopgrove.lightswitch.controller.LightingAPI; +import space.hilltopgrove.lightswitch.model.Shelf; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping(path = "/lights/api/v2") +public class LightingController implements LightingAPI { + + private final Shelf shelf; + + @Override + @PostMapping(path = {"/on", "/on/{rack}", "/on/{rack}/{light}"}) + public void on( + @PathVariable(value = "rack", required = false) + Integer rack, + @PathVariable(value = "light", required = false) + Integer pin) { + shelf.on(rack, pin); + } + + @Override + @PostMapping(path = {"/off", "/off/{rack}", "/off/{rack}/{light}"}) + public void off( + @PathVariable(value = "rack", required = false) + Integer rack, + @PathVariable(value = "light", required = false) + Integer pin) { + shelf.off(rack, pin); + } + + + @Override + @PostMapping(path = {"/toggle", "/toggle/{rack}", "/toggle/{rack}/{light}"}) + public void toggle( + @PathVariable(value = "rack", required = false) + Integer rack, + @PathVariable(value = "light", required = false) + Integer pin) { + shelf.toggle(rack, pin); + } +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/controller/impl/ScheduleController.java b/src/main/java/space/hilltopgrove/lightswitch/controller/impl/ScheduleController.java new file mode 100644 index 0000000..b334698 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/controller/impl/ScheduleController.java @@ -0,0 +1,37 @@ +package space.hilltopgrove.lightswitch.controller.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import space.hilltopgrove.lightswitch.model.Shelf; +import space.hilltopgrove.lightswitch.service.SunRiseSunSetService; + +import javax.annotation.PostConstruct; + +@Slf4j +@Component +@EnableScheduling +@RequiredArgsConstructor +public class ScheduleController { + + private final SunRiseSunSetService sunRiseSunSetService; + private final Shelf shelf; + + @PostConstruct + public void runAtPostConstruct() { + setSchedule(); + } + + @Scheduled(cron = "${cronExpression}") + public void newDay() { + log.info("newDay::Started"); + setSchedule(); + log.info("newDay::Completed"); + } + + private void setSchedule() { + shelf.setRacksSchedule(sunRiseSunSetService.getSchedule()); + } +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/model/Light.java b/src/main/java/space/hilltopgrove/lightswitch/model/Light.java new file mode 100644 index 0000000..4c02d14 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/model/Light.java @@ -0,0 +1,94 @@ +package space.hilltopgrove.lightswitch.model; + +import com.pi4j.io.gpio.digital.DigitalOutput; +import lombok.Builder; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +@Data +@Slf4j +@Builder +public class Light { + private DigitalOutput light; + + public static Light map(DigitalOutput digitalOutput) { + return Light.builder() + .light(digitalOutput) + .build(); + } + + public void setAsDawnLight(ScheduleBean scheduleBean) { + if (scheduleBean.isNowBeforeDawn()) { + log.info("{}: Seconds until dawn: {}", this.light.getName(), scheduleBean.nowTillDawn()); + secondsTillOn(scheduleBean.nowTillDawn()); + log.info("{}: Seconds until dusk: {}", this.light.getName(), scheduleBean.nowTillDusk()); + secondsTillOff(scheduleBean.nowTillDusk()); + } + if (scheduleBean.isNowBetweenDawnAndDusk()) { + this.on(); + log.info("{}: Seconds until dusk: {}", this.light.getName(), scheduleBean.nowTillDusk()); + secondsTillOff(scheduleBean.nowTillDusk()); + } + } + + public void setAsSunriseLight(ScheduleBean scheduleBean) { + if (scheduleBean.isNowBeforeSunrise()) { + log.info("{}: Seconds until sunrise: {}", this.light.getName(), scheduleBean.nowTillSunrise()); + secondsTillOn(scheduleBean.nowTillSunrise()); + log.info("{}: Seconds until sunset: {}", this.light.getName(), scheduleBean.nowTillSunset()); + secondsTillOff(scheduleBean.nowTillSunset()); + } + if (scheduleBean.isNowBetweenSunriseAndSunset()) { + this.on(); + log.info("{}: Seconds until sunset: {}", this.light.getName(), scheduleBean.nowTillSunset()); + secondsTillOff(scheduleBean.nowTillSunset()); + } + } + + public void setAsMorningLight(ScheduleBean scheduleBean) { + if (scheduleBean.isNowBeforeMorning()) { + log.info("{}: Seconds until morning: {}", this.light.getName(), scheduleBean.nowTillMorning()); + secondsTillOn(scheduleBean.nowTillMorning()); + log.info("{}: Seconds until afternoon: {}", this.light.getName(), scheduleBean.nowTillAfternoon()); + secondsTillOff(scheduleBean.nowTillAfternoon()); + } + if (scheduleBean.isNowBetweenMorningAndAfternoon()) { + this.on(); + log.info("{}: Seconds until afternoon: {}", this.light.getName(), scheduleBean.nowTillAfternoon()); + secondsTillOff(scheduleBean.nowTillAfternoon()); + } + } + + private void secondsTillOn(long seconds) { + secondsTill(seconds, this::on); + } + + private void secondsTillOff(long seconds) { + secondsTill(seconds, this::off); + } + + private void secondsTill(long seconds, Runnable onOff) { + var ses = Executors.newSingleThreadScheduledExecutor(); + ses.schedule(onOff, seconds, TimeUnit.SECONDS); + ses.shutdown(); + } + + public void toggle() { + log.info("toggling {}", this.light.getName()); + this.light.toggle(); + } + + public void on() { + log.info("{}: Turning on!", this.light.getName()); + this.light.high(); + } + + public void off() { + log.info("{}: Turning off!", this.light.getName()); + this.light.low(); + } +} + diff --git a/src/main/java/space/hilltopgrove/lightswitch/model/Rack.java b/src/main/java/space/hilltopgrove/lightswitch/model/Rack.java new file mode 100644 index 0000000..4127e52 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/model/Rack.java @@ -0,0 +1,62 @@ +package space.hilltopgrove.lightswitch.model; + +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +@Data +@Builder +public class Rack { + private Map lightMap; + + public static Rack map(Map lightMap) { + return Rack.builder() + .lightMap(lightMap) + .build(); + } + + public void toggle(Integer lightPosition) { + if (lightPosition == null) { + this.lightMap.values().forEach(Light::toggle); + } else { + this.lightMap.get(lightPosition).toggle(); + } + } + + public void on(Integer lightPosition) { + if (lightPosition == null) { + this.lightMap.values().forEach(Light::on); + } else { + this.lightMap.get(lightPosition).on(); + } + } + + public void off(Integer lightPosition) { + if (lightPosition == null) { + this.lightMap.values().forEach(Light::off); + } else { + this.lightMap.get(lightPosition).off(); + } + } + + public int size() { + return lightMap.size(); + } + + public void setRackLights(ScheduleBean scheduleBean) { + if (this.size() == 1) { + this.lightMap.get(1).setAsSunriseLight(scheduleBean); + } + if (this.size() == 2) { + this.lightMap.get(1).setAsSunriseLight(scheduleBean); + this.lightMap.get(2).setAsMorningLight(scheduleBean); + } + if (this.size() == 3) { + this.lightMap.get(2).setAsDawnLight(scheduleBean); + this.lightMap.get(1).setAsSunriseLight(scheduleBean); + this.lightMap.get(3).setAsMorningLight(scheduleBean); + } + } + +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/model/ScheduleBean.java b/src/main/java/space/hilltopgrove/lightswitch/model/ScheduleBean.java new file mode 100644 index 0000000..9487cf5 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/model/ScheduleBean.java @@ -0,0 +1,99 @@ +package space.hilltopgrove.lightswitch.model; + +import lombok.Builder; +import space.hilltopgrove.lightswitch.service.impl.SunriseSunset; + +import java.time.Duration; +import java.time.Instant; +import java.util.Calendar; + +@Builder +public record ScheduleBean(Instant dawn, Instant sunrise, Instant sunset, Instant dusk) { + + public static ScheduleBean build(Calendar day, double latitude, double longitude) { + var sunsetSunrise = SunriseSunset.getSunriseSunset(day, latitude, longitude); + var dawnDusk = SunriseSunset.getCivilTwilight(day, latitude, longitude); + return ScheduleBean.builder() + .dawn(dawnDusk[0].toInstant()) + .sunrise(sunsetSunrise[0].toInstant()) + .sunset(sunsetSunrise[1].toInstant()) + .dusk(dawnDusk[1].toInstant()) + .build(); + } + + public boolean isNowBeforeDawn() { + return Instant.now().isBefore(this.dawn); + } + + public boolean isNowBeforeSunrise() { + return Instant.now().isBefore(this.sunrise); + } + + public boolean isNowBeforeMorning() { + return Instant.now().isBefore(this.getMorning()); + } + + public boolean isNowBetweenDawnAndDusk() { + var now = Instant.now(); + return now.isAfter(this.dawn) && now.isBefore(this.dusk); + } + + public boolean isNowBetweenSunriseAndSunset() { + var now = Instant.now(); + return now.isAfter(this.sunrise) && now.isBefore(this.sunset); + } + + public boolean isNowBetweenMorningAndAfternoon() { + var now = Instant.now(); + return now.isAfter(this.getMorning()) && now.isBefore(this.getAfterNoon()); + } + + public long nowTillDawn() { + return nowTill(this.dawn); + } + + public long nowTillDusk() { + return nowTill(this.dusk); + } + + public long nowTillSunrise() { + return nowTill(this.sunrise()); + } + + public long nowTillSunset() { + return nowTill(this.sunset()); + } + + public long nowTillMorning() { + return nowTill(this.getMorning()); + } + + public long nowTillAfternoon() { + return nowTill(this.getAfterNoon()); + } + + private long nowTill(Instant instant) { + return Duration.between(Instant.now(), instant).getSeconds(); + } + + public Instant getMorning() { + return Instant.ofEpochSecond(sunrise.getEpochSecond() + (getDayLength() / 6 * 2)); + } + + public Instant getNoon() { + return Instant.ofEpochSecond(sunrise.getEpochSecond() + (getDayLength() / 2)); + } + + public Instant getAfterNoon() { + return Instant.ofEpochSecond(sunrise.getEpochSecond() + (getDayLength() / 6 * 4)); + } + + public Instant getEvening() { + return Instant.ofEpochSecond(sunrise.getEpochSecond() + (getDayLength() / 6 * 5)); + } + + private long getDayLength() { + return sunset.minusSeconds(sunrise.getEpochSecond()).getEpochSecond(); + } + +} \ No newline at end of file diff --git a/src/main/java/space/hilltopgrove/lightswitch/model/Shelf.java b/src/main/java/space/hilltopgrove/lightswitch/model/Shelf.java new file mode 100644 index 0000000..1a869a4 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/model/Shelf.java @@ -0,0 +1,48 @@ +package space.hilltopgrove.lightswitch.model; + +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +@Data +@Builder +public class Shelf { + private Map rackMap; + + public static Shelf map(Map rackMap) { + return Shelf.builder() + .rackMap(rackMap) + .build(); + } + + public void setRacksSchedule(ScheduleBean scheduleBean) { + this.rackMap.values().forEach(rack -> { + rack.setRackLights(scheduleBean); + }); + } + + public void toggle(Integer rackPosition, Integer lightPosition) { + if (rackPosition == null) { + this.rackMap.values().forEach(rack -> rack.toggle(lightPosition)); + } else { + this.rackMap.get(rackPosition).toggle(lightPosition); + } + } + + public void on(Integer rackPosition, Integer lightPosition) { + if (rackPosition == null) { + this.rackMap.values().forEach(rack -> rack.on(lightPosition)); + } else { + this.rackMap.get(rackPosition).on(lightPosition); + } + } + + public void off(Integer rackPosition, Integer lightPosition) { + if (rackPosition == null) { + this.rackMap.values().forEach(rack -> rack.off(lightPosition)); + } else { + this.rackMap.get(rackPosition).off(lightPosition); + } + } +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/service/LightingService.java b/src/main/java/space/hilltopgrove/lightswitch/service/LightingService.java new file mode 100644 index 0000000..8f62d6b --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/service/LightingService.java @@ -0,0 +1,7 @@ +package space.hilltopgrove.lightswitch.service; + +public interface LightingService { + void on(Integer rack, Integer light); + void off(Integer rack, Integer light); + void toggle(Integer rack, Integer light); +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/service/RackLightingPattern.java b/src/main/java/space/hilltopgrove/lightswitch/service/RackLightingPattern.java new file mode 100644 index 0000000..e02c023 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/service/RackLightingPattern.java @@ -0,0 +1,12 @@ +package space.hilltopgrove.lightswitch.service; + +import space.hilltopgrove.lightswitch.model.Rack; + +public interface RackLightingPattern { + void sunRise(Rack rack); + void morning(Rack rack); + void noon(Rack rack); + void afterNoon(Rack rack); + void evening(Rack rack); + void sunSet(Rack rack); +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/service/SchedulingService.java b/src/main/java/space/hilltopgrove/lightswitch/service/SchedulingService.java new file mode 100644 index 0000000..a08b257 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/service/SchedulingService.java @@ -0,0 +1,8 @@ +package space.hilltopgrove.lightswitch.service; + +import space.hilltopgrove.lightswitch.model.ScheduleBean; + +public interface SchedulingService { + void setLightingSchedule(ScheduleBean scheduleBean); + void setCurrentLighting(ScheduleBean scheduleBean); +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/service/SunRiseSunSetService.java b/src/main/java/space/hilltopgrove/lightswitch/service/SunRiseSunSetService.java new file mode 100644 index 0000000..a79f9af --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/service/SunRiseSunSetService.java @@ -0,0 +1,7 @@ +package space.hilltopgrove.lightswitch.service; + +import space.hilltopgrove.lightswitch.model.ScheduleBean; + +public interface SunRiseSunSetService { + ScheduleBean getSchedule(); +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/service/SunriseSunset.java b/src/main/java/space/hilltopgrove/lightswitch/service/SunriseSunset.java new file mode 100644 index 0000000..e18f505 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/service/SunriseSunset.java @@ -0,0 +1,649 @@ +package space.hilltopgrove.lightswitch.service; +/* + * Sunrise Sunset Calculator. + * Copyright (C) 2013-2017 Carmen Alvarez + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.TimeZone; + +/** + * Provides methods to determine the sunrise, sunset, civil twilight, + * nautical twilight, and astronomical twilight times of a given + * location, or if it is currently day or night at a given location.
+ * Also provides methods to convert between Gregorian and Julian dates.
+ * The formulas used by this class are from the Wikipedia articles on Julian Day + * and Sunrise Equation.
+ * + * @author Carmen Alvarez + * @see Julian Day on Wikipedia + * @see Sunrise equation on Wikipedia + */ +public final class SunriseSunset { + + /** + * The altitude of the sun (solar elevation angle) at the moment of sunrise or sunset: -0.833 + */ + public static final double SUN_ALTITUDE_SUNRISE_SUNSET = -0.833; + /** + * The altitude of the sun (solar elevation angle) at the moment of civil twilight: -6.0 + */ + public static final double SUN_ALTITUDE_CIVIL_TWILIGHT = -6.0; + /** + * The altitude of the sun (solar elevation angle) at the moment of nautical twilight: -12.0 + */ + public static final double SUN_ALTITUDE_NAUTICAL_TWILIGHT = -12.0; + /** + * The altitude of the sun (solar elevation angle) at the moment of astronomical twilight: -18.0 + */ + public static final double SUN_ALTITUDE_ASTRONOMICAL_TWILIGHT = -18.0; + private static final int JULIAN_DATE_2000_01_01 = 2451545; + private static final double CONST_0009 = 0.0009; + private static final double CONST_360 = 360; + private static final long MILLISECONDS_IN_DAY = 60 * 60 * 24 * 1000; + + private SunriseSunset() { + // Prevent instantiation of this utility class + } + + /** + * Convert a Gregorian calendar date to a Julian date. Accuracy is to the + * second. + *
+ * This is based on the Wikipedia article for Julian day. + * + * @param gregorianDate Gregorian date in any time zone. + * @return the Julian date for the given Gregorian date. + * @see Converting to Julian day number on Wikipedia + */ + public static double getJulianDate(final Calendar gregorianDate) { + // Convert the date to the UTC time zone. + TimeZone tzUTC = TimeZone.getTimeZone("UTC"); + Calendar gregorianDateUTC = Calendar.getInstance(tzUTC); + gregorianDateUTC.setTimeInMillis(gregorianDate.getTimeInMillis()); + // For the year (Y) astronomical year numbering is used, thus 1 BC is 0, + // 2 BC is -1, and 4713 BC is -4712. + int year = gregorianDateUTC.get(Calendar.YEAR); + // The months (M) January to December are 1 to 12 + int month = gregorianDateUTC.get(Calendar.MONTH) + 1; + // D is the day of the month. + int day = gregorianDateUTC.get(Calendar.DAY_OF_MONTH); + int a = (14 - month) / 12; + int y = year + 4800 - a; + int m = month + 12 * a - 3; + + int julianDay = day + (153 * m + 2) / 5 + 365 * y + (y / 4) - (y / 100) + + (y / 400) - 32045; + int hour = gregorianDateUTC.get(Calendar.HOUR_OF_DAY); + int minute = gregorianDateUTC.get(Calendar.MINUTE); + int second = gregorianDateUTC.get(Calendar.SECOND); + + return julianDay + ((double) hour - 12) / 24 + + ((double) minute) / 1440 + ((double) second) / 86400; + } + + /** + * Convert a Julian date to a Gregorian date. The Gregorian date will be in + * the local time zone. Accuracy is to the second. + *
+ * This is based on the Wikipedia article for Julian day. + * + * @param julianDate The date to convert + * @return a Gregorian date in the local time zone. + * @see Converting from Julian day to Gregorian date, on Wikipedia + */ + public static Calendar getGregorianDate(final double julianDate) { + + final int DAYS_PER_4000_YEARS = 146097; + final int DAYS_PER_CENTURY = 36524; + final int DAYS_PER_4_YEARS = 1461; + final int DAYS_PER_5_MONTHS = 153; + + // Let J = JD + 0.5: (note: this shifts the epoch back by one half day, + // to start it at 00:00UTC, instead of 12:00 UTC); + int J = (int) (julianDate + 0.5); + + // let j = J + 32044; (note: this shifts the epoch back to astronomical + // year -4800 instead of the start of the Christian era in year AD 1 of + // the proleptic Gregorian calendar). + int j = J + 32044; + + // let g = j div 146097; let dg = j mod 146097; + int g = j / DAYS_PER_4000_YEARS; + int dg = j % DAYS_PER_4000_YEARS; + + // let c = (dg div 36524 + 1) * 3 div 4; let dc = dg - c * 36524; + int c = ((dg / DAYS_PER_CENTURY + 1) * 3) / 4; + int dc = dg - c * DAYS_PER_CENTURY; + + // let b = dc div 1461; let db = dc mod 1461; + int b = dc / DAYS_PER_4_YEARS; + int db = dc % DAYS_PER_4_YEARS; + + // let a = (db div 365 + 1) * 3 div 4; let da = db - a * 365; + int a = ((db / 365 + 1) * 3) / 4; + int da = db - a * 365; + + // let y = g * 400 + c * 100 + b * 4 + a; (note: this is the integer + // number of full years elapsed since March 1, 4801 BC at 00:00 UTC); + int y = g * 400 + c * 100 + b * 4 + a; + + // let m = (da * 5 + 308) div 153 - 2; (note: this is the integer number + // of full months elapsed since the last March 1 at 00:00 UTC); + int m = (da * 5 + 308) / DAYS_PER_5_MONTHS - 2; + + // let d = da -(m + 4) * 153 div 5 + 122; (note: this is the number of + // days elapsed since day 1 of the month at 00:00 UTC, including + // fractions of one day); + int d = da - ((m + 4) * DAYS_PER_5_MONTHS) / 5 + 122; + + // let Y = y - 4800 + (m + 2) div 12; + int year = y - 4800 + (m + 2) / 12; + + // let M = (m + 2) mod 12 + 1; + int month = (m + 2) % 12; + + // let D = d + 1; + int day = d + 1; + + // Apply the fraction of the day in the Julian date to the Gregorian + // date. + // Example: dayFraction = 0.717 + final double dayFraction = (julianDate + 0.5) - J; + + // Ex: 0.717*24 = 17.208 hours. We truncate to 17 hours. + final int hours = (int) (dayFraction * 24); + // Ex: 17.208 - 17 = 0.208 days. 0.208*60 = 12.48 minutes. We truncate + // to 12 minutes. + final int minutes = (int) ((dayFraction * 24 - hours) * 60d); + // Ex: 17.208*60 - (17*60 + 12) = 1032.48 - 1032 = 0.48 minutes. 0.48*60 + // = 28.8 seconds. + // We round to 29 seconds. + final int seconds = (int) ((dayFraction * 24 * 3600 - (hours * 3600 + minutes * 60)) + .5); + + // Create the gregorian date in UTC. + final Calendar gregorianDateUTC = Calendar.getInstance(TimeZone + .getTimeZone("UTC")); + gregorianDateUTC.set(Calendar.YEAR, year); + gregorianDateUTC.set(Calendar.MONTH, month); + gregorianDateUTC.set(Calendar.DAY_OF_MONTH, day); + gregorianDateUTC.set(Calendar.HOUR_OF_DAY, hours); + gregorianDateUTC.set(Calendar.MINUTE, minutes); + gregorianDateUTC.set(Calendar.SECOND, seconds); + gregorianDateUTC.set(Calendar.MILLISECOND, 0); + + // Convert to a Gregorian date in the local time zone. + Calendar gregorianDate = Calendar.getInstance(); + gregorianDate.setTimeInMillis(gregorianDateUTC.getTimeInMillis()); + return gregorianDate; + } + + /** + * Calculate the civil twilight time for the given date and given location. + * + * @param day The day for which to calculate civil twilight + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return a two-element Gregorian Calendar array. The first element is the + * civil twilight dawn, the second element is the civil twilight dusk. + * This will return null if there is no civil twilight. (Ex: no twilight in Antarctica in December) + */ + public static Calendar[] getCivilTwilight(final Calendar day, + final double latitude, double longitude) { + return getSunriseSunset(day, latitude, longitude, SUN_ALTITUDE_CIVIL_TWILIGHT); + } + + /** + * Calculate the nautical twilight time for the given date and given location. + * + * @param day The day for which to calculate nautical twilight + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return a two-element Gregorian Calendar array. The first element is the + * nautical twilight dawn, the second element is the nautical twilight dusk. + * This will return null if there is no nautical twilight. (Ex: no twilight in Antarctica in December) + */ + public static Calendar[] getNauticalTwilight(final Calendar day, + final double latitude, double longitude) { + return getSunriseSunset(day, latitude, longitude, SUN_ALTITUDE_NAUTICAL_TWILIGHT); + } + + /** + * Calculate the astronomical twilight time for the given date and given location. + * + * @param day The day for which to calculate astronomical twilight + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return a two-element Gregorian Calendar array. The first element is the + * astronomical twilight dawn, the second element is the astronomical twilight dusk. + * This will return null if there is no astronomical twilight. (Ex: no twilight in Antarctica in December) + */ + public static Calendar[] getAstronomicalTwilight(final Calendar day, + final double latitude, double longitude) { + return getSunriseSunset(day, latitude, longitude, SUN_ALTITUDE_ASTRONOMICAL_TWILIGHT); + } + + /** + * Calculate the sunrise and sunset times for the given date and given + * location. This is based on the Wikipedia article on the Sunrise equation. + * + * @param day The day for which to calculate sunrise and sunset + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return a two-element Gregorian Calendar array. The first element is the + * sunrise, the second element is the sunset. This will return null if there is no sunrise or sunset. (Ex: no sunrise in Antarctica in June) + * @see Sunrise equation on Wikipedia + */ + public static Calendar[] getSunriseSunset(final Calendar day, + final double latitude, double longitude) { + return getSunriseSunset(day, latitude, longitude, SUN_ALTITUDE_SUNRISE_SUNSET); + } + + /** + * Return intermediate variables used for calculating sunrise, sunset, and solar noon. + * + * @param day The day for which to calculate the ecliptic longitude and jtransit + * @param longitude the longitude of the location in degrees (West is negative) + * @return a 2-element array with the ecliptic longitude (lambda) as the first element, and solar transit (jtransit) as the second element + * @see Sunrise equation on Wikipedia + */ + private static SolarEquationVariables getSolarEquationVariables(final Calendar day, double longitude) { + + longitude = -longitude; + + // Get the given date as a Julian date. + final double julianDate = getJulianDate(day); + + // Calculate current Julian cycle (number of days since 2000-01-01). + final double nstar = julianDate - JULIAN_DATE_2000_01_01 - CONST_0009 + - longitude / CONST_360; + final double n = Math.round(nstar); + + // Approximate solar noon + final double jstar = JULIAN_DATE_2000_01_01 + CONST_0009 + longitude + / CONST_360 + n; + // Solar mean anomaly + final double m = Math + .toRadians((357.5291 + 0.98560028 * (jstar - JULIAN_DATE_2000_01_01)) + % CONST_360); + + // Equation of center + final double c = 1.9148 * Math.sin(m) + 0.0200 * Math.sin(2 * m) + + 0.0003 * Math.sin(3 * m); + + // Ecliptic longitude + final double lambda = Math + .toRadians((Math.toDegrees(m) + 102.9372 + c + 180) % CONST_360); + + // Solar transit (hour angle for solar noon) + final double jtransit = jstar + 0.0053 * Math.sin(m) - 0.0069 + * Math.sin(2 * lambda); + + // Declination of the sun. + final double delta = Math.asin(Math.sin(lambda) + * Math.sin(Math.toRadians(23.439))); + + + return new SolarEquationVariables(n, m, lambda, jtransit, delta); + } + + /** + * Calculate the sunrise and sunset times for the given date, given + * location, and sun altitude. + * This is based on the Wikipedia article on the Sunrise equation. + * + * @param day The day for which to calculate sunrise and sunset + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @param sunAltitude the angle between the horizon and the center of the sun's disc. + * @return a two-element Gregorian Calendar array. The first element is the + * sunrise, the second element is the sunset. This will return null if there is no sunrise or sunset. (Ex: no sunrise in Antarctica in June) + * @see Sunrise equation on Wikipedia + */ + public static Calendar[] getSunriseSunset(final Calendar day, + final double latitude, double longitude, double sunAltitude) { + + final SolarEquationVariables solarEquationVariables = getSolarEquationVariables(day, longitude); + + longitude = -longitude; + final double latitudeRad = Math.toRadians(latitude); + + // Hour angle + final double omega = Math.acos((Math.sin(Math.toRadians(sunAltitude)) - Math + .sin(latitudeRad) * Math.sin(solarEquationVariables.delta)) + / (Math.cos(latitudeRad) * Math.cos(solarEquationVariables.delta))); + + if (Double.isNaN(omega)) { + return null; + } + + // Sunset + final double jset = JULIAN_DATE_2000_01_01 + + CONST_0009 + + ((Math.toDegrees(omega) + longitude) / CONST_360 + solarEquationVariables.n + 0.0053 + * Math.sin(solarEquationVariables.m) - 0.0069 * Math.sin(2 * solarEquationVariables.lambda)); + + // Sunrise + final double jrise = solarEquationVariables.jtransit - (jset - solarEquationVariables.jtransit); + // Convert sunset and sunrise to Gregorian dates, in UTC + final Calendar gregRiseUTC = getGregorianDate(jrise); + final Calendar gregSetUTC = getGregorianDate(jset); + + // Convert the sunset and sunrise to the timezone of the day parameter + final Calendar gregRise = Calendar.getInstance(day.getTimeZone()); + gregRise.setTimeInMillis(gregRiseUTC.getTimeInMillis()); + final Calendar gregSet = Calendar.getInstance(day.getTimeZone()); + gregSet.setTimeInMillis(gregSetUTC.getTimeInMillis()); + return new Calendar[]{gregRise, gregSet}; + } + + /** + * Calculate the solar noon time for the given date and given location. + * This is based on the Wikipedia article on the Sunrise equation. + * + * @param day The day for which to calculate sunrise and sunset + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return a Calendar with the time set to solar noon for the given day. + * @see Sunrise equation on Wikipedia + */ + public static Calendar getSolarNoon(final Calendar day, final double latitude, double longitude) { + SolarEquationVariables solarEquationVariables = getSolarEquationVariables(day, longitude); + + // Add a check for Antarctica in June and December (sun always down or up, respectively). + // In this case, jtransit will be filled in, but we need to check the hour angle omega for + // sunrise. + // If there's no sunrise (omega is NaN), there's no solar noon. + final double latitudeRad = Math.toRadians(latitude); + + // Hour angle + final double omega = Math.acos((Math.sin(Math.toRadians(SUN_ALTITUDE_SUNRISE_SUNSET)) - Math + .sin(latitudeRad) * Math.sin(solarEquationVariables.delta)) + / (Math.cos(latitudeRad) * Math.cos(solarEquationVariables.delta))); + + if (Double.isNaN(omega)) { + return null; + } + + // Convert jtransit Gregorian dates, in UTC + final Calendar gregNoonUTC = getGregorianDate(solarEquationVariables.jtransit); + final Calendar gregNoon = Calendar.getInstance(day.getTimeZone()); + gregNoon.setTimeInMillis(gregNoonUTC.getTimeInMillis()); + return gregNoon; + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is currently day at the given location. This returns + * true if the current time at the location is after the sunrise and + * before the sunset for that location. + */ + public static boolean isDay(double latitude, double longitude) { + Calendar now = Calendar.getInstance(); + return isDay(now, latitude, longitude); + } + + /** + * @param calendar a datetime + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is day at the given location and given datetime. This returns + * true if the given datetime at the location is after the sunrise and + * before the sunset for that location. + */ + public static boolean isDay(Calendar calendar, double latitude, double longitude) { + Calendar[] sunriseSunset = getSunriseSunset(calendar, latitude, longitude); + // In extreme latitudes, there may be no sunrise/sunset time in summer or + // winter, because it will be day or night 24 hours + if (sunriseSunset == null) { + int month = calendar.get(Calendar.MONTH); // Reminder: January = 0 + if (latitude > 0) { + // Always night at the north pole in December + return month >= 3 && month <= 10; // Always day at the north pole in June + } else { + // Always day at the south pole in December + return month < 3 || month > 10; // Always night at the south pole in June + } + } + Calendar sunrise = sunriseSunset[0]; + Calendar sunset = sunriseSunset[1]; + return calendar.after(sunrise) && calendar.before(sunset); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is night at the given location currently. This returns + * true if the current time at the location is after the astronomical twilight dusk and + * before the astronomical twilight dawn for that location. + */ + public static boolean isNight(double latitude, double longitude) { + Calendar now = Calendar.getInstance(); + return isNight(now, latitude, longitude); + } + + /** + * @param calendar a datetime + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is night at the given location and datetime. This returns + * true if the given datetime at the location is after the astronomical twilight dusk and before + * the astronomical twilight dawn. + */ + public static boolean isNight(Calendar calendar, double latitude, double longitude) { + Calendar[] astronomicalTwilight = getAstronomicalTwilight(calendar, latitude, longitude); + if (astronomicalTwilight == null) { + int month = calendar.get(Calendar.MONTH); // Reminder: January = 0 + if (latitude > 0) { + // Always night at the north pole in December + return month < 3 || month > 10; // Always day at the north pole in June + } else { + // Always day at the south pole in December + return month >= 3 && month <= 10; // Always night at the south pole in June + } + } + Calendar dawn = astronomicalTwilight[0]; + Calendar dusk = astronomicalTwilight[1]; + SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss z"); + format.setTimeZone(calendar.getTimeZone()); + return calendar.before(dawn) || calendar.after(dusk); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is currently civil twilight at the current time at the given location. + * This returns true if the current time at the location is between sunset and civil twilight dusk + * or between civil twilight dawn and sunrise. + */ + @SuppressWarnings("unused") + public static boolean isCivilTwilight(double latitude, double longitude) { + Calendar today = Calendar.getInstance(); + return isCivilTwilight(today, latitude, longitude); + } + + /** + * @param calendar the datetime for which to determine if it's civil twilight in the given location + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is civil twilight at the given location and the given calendar. + * This returns true if the given time at the location is between sunset and civil twilight dusk + * or between civil twilight dawn and sunrise. + */ + public static boolean isCivilTwilight(Calendar calendar, double latitude, double longitude) { + Calendar[] sunriseSunset = getSunriseSunset(calendar, latitude, longitude); + if (sunriseSunset == null) return false; + Calendar[] civilTwilight = getCivilTwilight(calendar, latitude, longitude); + if (civilTwilight == null) return false; + + return (calendar.after(sunriseSunset[1]) && calendar.before(civilTwilight[1]) + || (calendar.after(civilTwilight[0]) && calendar.before(sunriseSunset[0]))); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is currently nautical twilight at the current time at the given location. + * This returns true if the current time at the location is between civil and nautical twilight dusk + * or between nautical and civil twilight dawn. + */ + @SuppressWarnings("unused") + public static boolean isNauticalTwilight(double latitude, double longitude) { + Calendar today = Calendar.getInstance(); + return isNauticalTwilight(today, latitude, longitude); + } + + /** + * @param calendar the datetime for which to determine if it's nautical twilight in the given location + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is nautical twilight at the given location and the given calendar. + * This returns true if the given time at the location is between civil and nautical twilight dusk + * or between nautical and civil twilight dawn. + */ + public static boolean isNauticalTwilight(Calendar calendar, double latitude, double longitude) { + Calendar[] civilTwilight = getCivilTwilight(calendar, latitude, longitude); + if (civilTwilight == null) return false; + Calendar[] nauticalTwilight = getNauticalTwilight(calendar, latitude, longitude); + if (nauticalTwilight == null) return false; + + return (calendar.after(civilTwilight[1]) && calendar.before(nauticalTwilight[1]) + || (calendar.after(nauticalTwilight[0]) && calendar.before(civilTwilight[0]))); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is currently astronomical twilight at the current time at the given location. + * This returns true if the current time at the location is between nautical and astronomical twilight dusk + * or between astronomical and nautical twilight dawn. + */ + @SuppressWarnings("unused") + public static boolean isAstronomicalTwilight(double latitude, double longitude) { + Calendar today = Calendar.getInstance(); + return isAstronomicalTwilight(today, latitude, longitude); + } + + /** + * @param calendar the datetime for which to determine if it's astronomical twilight in the given location + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is astronomical twilight at the given location and the given calendar. + * This returns true if the given time at the location is between nautical and astronomical twilight dusk + * or between astronomical and nautical twilight dawn. + */ + public static boolean isAstronomicalTwilight(Calendar calendar, double latitude, double longitude) { + Calendar[] nauticalTwilight = getNauticalTwilight(calendar, latitude, longitude); + if (nauticalTwilight == null) return false; + Calendar[] astronomicalTwilight = getAstronomicalTwilight(calendar, latitude, longitude); + if (astronomicalTwilight == null) return false; + + return (calendar.after(nauticalTwilight[1]) && calendar.before(astronomicalTwilight[1]) + || (calendar.after(astronomicalTwilight[0]) && calendar.before(nauticalTwilight[0]))); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is civil, nautical, or astronomical twilight currently at the given location. + */ + @SuppressWarnings("unused") + public static boolean isTwilight(double latitude, double longitude) { + Calendar today = Calendar.getInstance(); + return isTwilight(today, latitude, longitude); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @param calendar the given datetime to check for twilight + * @return true if at the given location and calendar, it is civil, nautical, or astronomical twilight. + */ + public static boolean isTwilight(Calendar calendar, double latitude, double longitude) { + return isCivilTwilight(calendar, latitude, longitude) + || isNauticalTwilight(calendar, latitude, longitude) + || isAstronomicalTwilight(calendar, latitude, longitude); + } + + public static DayPeriod getDayPeriod(Calendar calendar, double latitude, double longitude) { + if (isDay(calendar, latitude, longitude)) return DayPeriod.DAY; + if (isCivilTwilight(calendar, latitude, longitude)) return DayPeriod.CIVIL_TWILIGHT; + if (isNauticalTwilight(calendar, latitude, longitude)) return DayPeriod.NAUTICAL_TWILIGHT; + if (isAstronomicalTwilight(calendar, latitude, longitude)) return DayPeriod.ASTRONOMICAL_TWILIGHT; + if (isNight(calendar, latitude, longitude)) return DayPeriod.NIGHT; + return DayPeriod.NIGHT; + } + + /** + * @param calendar the datetime for which to determine the day length + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return the number of milliseconds between sunrise and sunset. + */ + public static long getDayLength(Calendar calendar, double latitude, double longitude) { + Calendar[] sunriseSunset = getSunriseSunset(calendar, latitude, longitude); + if (sunriseSunset == null) { + int month = calendar.get(Calendar.MONTH); // Reminder: January = 0 + if (latitude > 0) { + if (month >= 3 && month <= 10) { + return MILLISECONDS_IN_DAY; // Always day at the north pole in June + } else { + return 0; // Always night at the north pole in December + } + } else { + if (month >= 3 && month <= 10) { + return 0; // Always night at the south pole in June + } else { + return MILLISECONDS_IN_DAY; // Always day at the south pole in December + } + } + } + return sunriseSunset[1].getTimeInMillis() - sunriseSunset[0].getTimeInMillis(); + } + + public enum DayPeriod { + DAY, + CIVIL_TWILIGHT, + NAUTICAL_TWILIGHT, + ASTRONOMICAL_TWILIGHT, + NIGHT + } + + /** + * Intermediate variables used in the sunrise equation + * + * @see Sunrise equation on Wikipedia + */ + private static class SolarEquationVariables { + final double n;// Julian cycle (number of days since 2000-01-01). + final double m; // solar mean anomaly + final double lambda; // ecliptic longitude + final double jtransit; // Solar transit (hour angle for solar noon) + final double delta; // Declination of the sun + + private SolarEquationVariables(double n, double m, double lambda, double jtransit, double delta) { + this.n = n; + this.m = m; + this.lambda = lambda; + this.jtransit = jtransit; + this.delta = delta; + } + } + +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/service/impl/LightingServiceImpl.java b/src/main/java/space/hilltopgrove/lightswitch/service/impl/LightingServiceImpl.java new file mode 100644 index 0000000..19838b3 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/service/impl/LightingServiceImpl.java @@ -0,0 +1,29 @@ +package space.hilltopgrove.lightswitch.service.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import space.hilltopgrove.lightswitch.model.Shelf; +import space.hilltopgrove.lightswitch.service.LightingService; + +@Service +@RequiredArgsConstructor +public class LightingServiceImpl implements LightingService { + + private final Shelf shelf; + + @Override + public void on(Integer rack, Integer light) { + shelf.on(rack, light); + } + + @Override + public void off(Integer rack, Integer light) { + shelf.off(rack, light); + } + + @Override + public void toggle(Integer rack, Integer light) { + shelf.toggle(rack, light); + } + +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/service/impl/RackLightingPatternImpl.java b/src/main/java/space/hilltopgrove/lightswitch/service/impl/RackLightingPatternImpl.java new file mode 100644 index 0000000..d904dcb --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/service/impl/RackLightingPatternImpl.java @@ -0,0 +1,71 @@ +package space.hilltopgrove.lightswitch.service.impl; + +import org.springframework.stereotype.Service; +import space.hilltopgrove.lightswitch.model.Rack; +import space.hilltopgrove.lightswitch.service.RackLightingPattern; + +@Service +public class RackLightingPatternImpl implements RackLightingPattern { + + @Override + public void sunRise(Rack rack) { + if (rack.size() == 2) { + rack.off(1); + rack.off(2); + } + if (rack.size() == 3) { + rack.off(1); + rack.on(2); + rack.off(3); + } + } + + @Override + public void morning(Rack rack) { + if (rack.size() == 2) { + rack.on(1); + rack.off(2); + } + if (rack.size() == 3) { + rack.on(1); + rack.on(2); + rack.off(3); + } + } + + @Override + public void noon(Rack rack) { + rack.on(null); + } + + @Override + public void afterNoon(Rack rack) { + if (rack.size() == 2) { + rack.off(1); + rack.on(2); + } + if (rack.size() == 3) { + rack.off(1); + rack.on(2); + rack.on(3); + } + } + + @Override + public void evening(Rack rack) { + if (rack.size() == 2) { + rack.off(1); + rack.off(2); + } + if (rack.size() == 3) { + rack.off(1); + rack.on(2); + rack.off(3); + } + } + + @Override + public void sunSet(Rack rack) { + rack.off(null); + } +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/service/impl/SchedulingServiceImpl.java b/src/main/java/space/hilltopgrove/lightswitch/service/impl/SchedulingServiceImpl.java new file mode 100644 index 0000000..1229fee --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/service/impl/SchedulingServiceImpl.java @@ -0,0 +1,88 @@ +package space.hilltopgrove.lightswitch.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import space.hilltopgrove.lightswitch.model.ScheduleBean; +import space.hilltopgrove.lightswitch.model.Shelf; +import space.hilltopgrove.lightswitch.service.RackLightingPattern; +import space.hilltopgrove.lightswitch.service.SchedulingService; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SchedulingServiceImpl implements SchedulingService { + + private final RackLightingPattern rackLightingPattern; + private final Shelf shelf; + + @Override + public void setLightingSchedule(ScheduleBean scheduleBean) { + var now = Instant.now(); + var ses = Executors.newSingleThreadScheduledExecutor(); + log.info("setLightingSchedule:: now: " + now); + log.info("setLightingSchedule:: scheduleBean: " + scheduleBean); + for (var rack : shelf.getRackMap().values()) { + if (now.isBefore(scheduleBean.sunrise())) { + log.info("setLightingSchedule:: sunrise: " + scheduleBean.sunrise()); + ses.schedule(() -> rackLightingPattern.sunRise(rack), Duration.between(now, scheduleBean.sunrise()).getSeconds(), TimeUnit.SECONDS); + } + if (now.isBefore(scheduleBean.getMorning())) { + log.info("setLightingSchedule:: morning: " + scheduleBean.getMorning()); + ses.schedule(() -> rackLightingPattern.morning(rack), Duration.between(now, scheduleBean.getMorning()).getSeconds(), TimeUnit.SECONDS); + } + if (now.isBefore(scheduleBean.getNoon())) { + log.info("setLightingSchedule:: noon: " + scheduleBean.getNoon()); + ses.schedule(() -> rackLightingPattern.noon(rack), Duration.between(now, scheduleBean.getNoon()).getSeconds(), TimeUnit.SECONDS); + } + if (now.isBefore(scheduleBean.getAfterNoon())) { + log.info("setLightingSchedule:: afterNoon: " + scheduleBean.getAfterNoon()); + ses.schedule(() -> rackLightingPattern.afterNoon(rack), Duration.between(now, scheduleBean.getAfterNoon()).getSeconds(), TimeUnit.SECONDS); + } + if (now.isBefore(scheduleBean.getEvening())) { + log.info("setLightingSchedule:: evening: " + scheduleBean.getEvening()); + ses.schedule(() -> rackLightingPattern.evening(rack), Duration.between(now, scheduleBean.getEvening()).getSeconds(), TimeUnit.SECONDS); + } + if (now.isBefore(scheduleBean.sunset())) { + log.info("setLightingSchedule:: sunset: " + scheduleBean.sunset()); + ses.schedule(() -> rackLightingPattern.sunSet(rack), Duration.between(now, scheduleBean.sunset()).getSeconds(), TimeUnit.SECONDS); + } + } + ses.shutdown(); + } + + @Override + public void setCurrentLighting(ScheduleBean scheduleBean) { + var now = Instant.now(); + log.info("setCurrentLighting:: now: " + now); + log.info("setCurrentLighting:: scheduleBean: " + scheduleBean); + shelf.getRackMap().values().forEach(rack -> { + if (now.isAfter(scheduleBean.sunset())) { + log.info("setCurrentLighting:: sunset: " + scheduleBean.sunset()); + rackLightingPattern.sunSet(rack); + } else if (now.isAfter(scheduleBean.getEvening())) { + log.info("setCurrentLighting:: evening: " + scheduleBean.getEvening()); + rackLightingPattern.evening(rack); + } else if (now.isAfter(scheduleBean.getAfterNoon())) { + log.info("setCurrentLighting:: afternoon: " + scheduleBean.getAfterNoon()); + rackLightingPattern.afterNoon(rack); + } else if (now.isAfter(scheduleBean.getNoon())) { + log.info("setCurrentLighting:: noon: " + scheduleBean.getNoon()); + rackLightingPattern.noon(rack); + } else if (now.isAfter(scheduleBean.getMorning())) { + log.info("setCurrentLighting:: morning: " + scheduleBean.getMorning()); + rackLightingPattern.morning(rack); + } else { + log.info("setCurrentLighting:: sunrise: " + scheduleBean.sunrise()); + rackLightingPattern.sunRise(rack); + } + }); + log.info("setCurrentLighting:: now: " + Instant.now()); + } + +} diff --git a/src/main/java/space/hilltopgrove/lightswitch/service/impl/SunRiseSunSetServiceImpl.java b/src/main/java/space/hilltopgrove/lightswitch/service/impl/SunRiseSunSetServiceImpl.java new file mode 100644 index 0000000..f7110f3 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/service/impl/SunRiseSunSetServiceImpl.java @@ -0,0 +1,28 @@ +package space.hilltopgrove.lightswitch.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import space.hilltopgrove.lightswitch.config.PropertiesConfig; +import space.hilltopgrove.lightswitch.model.ScheduleBean; +import space.hilltopgrove.lightswitch.service.SunRiseSunSetService; + +import java.time.Instant; +import java.util.Calendar; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SunRiseSunSetServiceImpl implements SunRiseSunSetService { + + private final PropertiesConfig propertiesConfig; + + @Override + public ScheduleBean getSchedule() { + var result = ScheduleBean.build(Calendar.getInstance(), propertiesConfig.getLatitude(), propertiesConfig.getLongitude()); + log.info(result.toString()); + log.info(Instant.now().toString()); + return result; + } + +} \ No newline at end of file diff --git a/src/main/java/space/hilltopgrove/lightswitch/service/impl/SunriseSunset.java b/src/main/java/space/hilltopgrove/lightswitch/service/impl/SunriseSunset.java new file mode 100644 index 0000000..9849d17 --- /dev/null +++ b/src/main/java/space/hilltopgrove/lightswitch/service/impl/SunriseSunset.java @@ -0,0 +1,649 @@ +package space.hilltopgrove.lightswitch.service.impl; +/* + * Sunrise Sunset Calculator. + * Copyright (C) 2013-2017 Carmen Alvarez + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.TimeZone; + +/** + * Provides methods to determine the sunrise, sunset, civil twilight, + * nautical twilight, and astronomical twilight times of a given + * location, or if it is currently day or night at a given location.
+ * Also provides methods to convert between Gregorian and Julian dates.
+ * The formulas used by this class are from the Wikipedia articles on Julian Day + * and Sunrise Equation.
+ * + * @author Carmen Alvarez + * @see Julian Day on Wikipedia + * @see Sunrise equation on Wikipedia + */ +public final class SunriseSunset { + + /** + * The altitude of the sun (solar elevation angle) at the moment of sunrise or sunset: -0.833 + */ + public static final double SUN_ALTITUDE_SUNRISE_SUNSET = -0.833; + /** + * The altitude of the sun (solar elevation angle) at the moment of civil twilight: -6.0 + */ + public static final double SUN_ALTITUDE_CIVIL_TWILIGHT = -6.0; + /** + * The altitude of the sun (solar elevation angle) at the moment of nautical twilight: -12.0 + */ + public static final double SUN_ALTITUDE_NAUTICAL_TWILIGHT = -12.0; + /** + * The altitude of the sun (solar elevation angle) at the moment of astronomical twilight: -18.0 + */ + public static final double SUN_ALTITUDE_ASTRONOMICAL_TWILIGHT = -18.0; + private static final int JULIAN_DATE_2000_01_01 = 2451545; + private static final double CONST_0009 = 0.0009; + private static final double CONST_360 = 360; + private static final long MILLISECONDS_IN_DAY = 60 * 60 * 24 * 1000; + + private SunriseSunset() { + // Prevent instantiation of this utility class + } + + /** + * Convert a Gregorian calendar date to a Julian date. Accuracy is to the + * second. + *
+ * This is based on the Wikipedia article for Julian day. + * + * @param gregorianDate Gregorian date in any time zone. + * @return the Julian date for the given Gregorian date. + * @see Converting to Julian day number on Wikipedia + */ + public static double getJulianDate(final Calendar gregorianDate) { + // Convert the date to the UTC time zone. + TimeZone tzUTC = TimeZone.getTimeZone("UTC"); + Calendar gregorianDateUTC = Calendar.getInstance(tzUTC); + gregorianDateUTC.setTimeInMillis(gregorianDate.getTimeInMillis()); + // For the year (Y) astronomical year numbering is used, thus 1 BC is 0, + // 2 BC is -1, and 4713 BC is -4712. + int year = gregorianDateUTC.get(Calendar.YEAR); + // The months (M) January to December are 1 to 12 + int month = gregorianDateUTC.get(Calendar.MONTH) + 1; + // D is the day of the month. + int day = gregorianDateUTC.get(Calendar.DAY_OF_MONTH); + int a = (14 - month) / 12; + int y = year + 4800 - a; + int m = month + 12 * a - 3; + + int julianDay = day + (153 * m + 2) / 5 + 365 * y + (y / 4) - (y / 100) + + (y / 400) - 32045; + int hour = gregorianDateUTC.get(Calendar.HOUR_OF_DAY); + int minute = gregorianDateUTC.get(Calendar.MINUTE); + int second = gregorianDateUTC.get(Calendar.SECOND); + + return julianDay + ((double) hour - 12) / 24 + + ((double) minute) / 1440 + ((double) second) / 86400; + } + + /** + * Convert a Julian date to a Gregorian date. The Gregorian date will be in + * the local time zone. Accuracy is to the second. + *
+ * This is based on the Wikipedia article for Julian day. + * + * @param julianDate The date to convert + * @return a Gregorian date in the local time zone. + * @see Converting from Julian day to Gregorian date, on Wikipedia + */ + public static Calendar getGregorianDate(final double julianDate) { + + final int DAYS_PER_4000_YEARS = 146097; + final int DAYS_PER_CENTURY = 36524; + final int DAYS_PER_4_YEARS = 1461; + final int DAYS_PER_5_MONTHS = 153; + + // Let J = JD + 0.5: (note: this shifts the epoch back by one half day, + // to start it at 00:00UTC, instead of 12:00 UTC); + int J = (int) (julianDate + 0.5); + + // let j = J + 32044; (note: this shifts the epoch back to astronomical + // year -4800 instead of the start of the Christian era in year AD 1 of + // the proleptic Gregorian calendar). + int j = J + 32044; + + // let g = j div 146097; let dg = j mod 146097; + int g = j / DAYS_PER_4000_YEARS; + int dg = j % DAYS_PER_4000_YEARS; + + // let c = (dg div 36524 + 1) * 3 div 4; let dc = dg - c * 36524; + int c = ((dg / DAYS_PER_CENTURY + 1) * 3) / 4; + int dc = dg - c * DAYS_PER_CENTURY; + + // let b = dc div 1461; let db = dc mod 1461; + int b = dc / DAYS_PER_4_YEARS; + int db = dc % DAYS_PER_4_YEARS; + + // let a = (db div 365 + 1) * 3 div 4; let da = db - a * 365; + int a = ((db / 365 + 1) * 3) / 4; + int da = db - a * 365; + + // let y = g * 400 + c * 100 + b * 4 + a; (note: this is the integer + // number of full years elapsed since March 1, 4801 BC at 00:00 UTC); + int y = g * 400 + c * 100 + b * 4 + a; + + // let m = (da * 5 + 308) div 153 - 2; (note: this is the integer number + // of full months elapsed since the last March 1 at 00:00 UTC); + int m = (da * 5 + 308) / DAYS_PER_5_MONTHS - 2; + + // let d = da -(m + 4) * 153 div 5 + 122; (note: this is the number of + // days elapsed since day 1 of the month at 00:00 UTC, including + // fractions of one day); + int d = da - ((m + 4) * DAYS_PER_5_MONTHS) / 5 + 122; + + // let Y = y - 4800 + (m + 2) div 12; + int year = y - 4800 + (m + 2) / 12; + + // let M = (m + 2) mod 12 + 1; + int month = (m + 2) % 12; + + // let D = d + 1; + int day = d + 1; + + // Apply the fraction of the day in the Julian date to the Gregorian + // date. + // Example: dayFraction = 0.717 + final double dayFraction = (julianDate + 0.5) - J; + + // Ex: 0.717*24 = 17.208 hours. We truncate to 17 hours. + final int hours = (int) (dayFraction * 24); + // Ex: 17.208 - 17 = 0.208 days. 0.208*60 = 12.48 minutes. We truncate + // to 12 minutes. + final int minutes = (int) ((dayFraction * 24 - hours) * 60d); + // Ex: 17.208*60 - (17*60 + 12) = 1032.48 - 1032 = 0.48 minutes. 0.48*60 + // = 28.8 seconds. + // We round to 29 seconds. + final int seconds = (int) ((dayFraction * 24 * 3600 - (hours * 3600 + minutes * 60)) + .5); + + // Create the gregorian date in UTC. + final Calendar gregorianDateUTC = Calendar.getInstance(TimeZone + .getTimeZone("UTC")); + gregorianDateUTC.set(Calendar.YEAR, year); + gregorianDateUTC.set(Calendar.MONTH, month); + gregorianDateUTC.set(Calendar.DAY_OF_MONTH, day); + gregorianDateUTC.set(Calendar.HOUR_OF_DAY, hours); + gregorianDateUTC.set(Calendar.MINUTE, minutes); + gregorianDateUTC.set(Calendar.SECOND, seconds); + gregorianDateUTC.set(Calendar.MILLISECOND, 0); + + // Convert to a Gregorian date in the local time zone. + Calendar gregorianDate = Calendar.getInstance(); + gregorianDate.setTimeInMillis(gregorianDateUTC.getTimeInMillis()); + return gregorianDate; + } + + /** + * Calculate the civil twilight time for the given date and given location. + * + * @param day The day for which to calculate civil twilight + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return a two-element Gregorian Calendar array. The first element is the + * civil twilight dawn, the second element is the civil twilight dusk. + * This will return null if there is no civil twilight. (Ex: no twilight in Antarctica in December) + */ + public static Calendar[] getCivilTwilight(final Calendar day, + final double latitude, double longitude) { + return getSunriseSunset(day, latitude, longitude, SUN_ALTITUDE_CIVIL_TWILIGHT); + } + + /** + * Calculate the nautical twilight time for the given date and given location. + * + * @param day The day for which to calculate nautical twilight + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return a two-element Gregorian Calendar array. The first element is the + * nautical twilight dawn, the second element is the nautical twilight dusk. + * This will return null if there is no nautical twilight. (Ex: no twilight in Antarctica in December) + */ + public static Calendar[] getNauticalTwilight(final Calendar day, + final double latitude, double longitude) { + return getSunriseSunset(day, latitude, longitude, SUN_ALTITUDE_NAUTICAL_TWILIGHT); + } + + /** + * Calculate the astronomical twilight time for the given date and given location. + * + * @param day The day for which to calculate astronomical twilight + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return a two-element Gregorian Calendar array. The first element is the + * astronomical twilight dawn, the second element is the astronomical twilight dusk. + * This will return null if there is no astronomical twilight. (Ex: no twilight in Antarctica in December) + */ + public static Calendar[] getAstronomicalTwilight(final Calendar day, + final double latitude, double longitude) { + return getSunriseSunset(day, latitude, longitude, SUN_ALTITUDE_ASTRONOMICAL_TWILIGHT); + } + + /** + * Calculate the sunrise and sunset times for the given date and given + * location. This is based on the Wikipedia article on the Sunrise equation. + * + * @param day The day for which to calculate sunrise and sunset + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return a two-element Gregorian Calendar array. The first element is the + * sunrise, the second element is the sunset. This will return null if there is no sunrise or sunset. (Ex: no sunrise in Antarctica in June) + * @see Sunrise equation on Wikipedia + */ + public static Calendar[] getSunriseSunset(final Calendar day, + final double latitude, double longitude) { + return getSunriseSunset(day, latitude, longitude, SUN_ALTITUDE_SUNRISE_SUNSET); + } + + /** + * Return intermediate variables used for calculating sunrise, sunset, and solar noon. + * + * @param day The day for which to calculate the ecliptic longitude and jtransit + * @param longitude the longitude of the location in degrees (West is negative) + * @return a 2-element array with the ecliptic longitude (lambda) as the first element, and solar transit (jtransit) as the second element + * @see Sunrise equation on Wikipedia + */ + private static SolarEquationVariables getSolarEquationVariables(final Calendar day, double longitude) { + + longitude = -longitude; + + // Get the given date as a Julian date. + final double julianDate = getJulianDate(day); + + // Calculate current Julian cycle (number of days since 2000-01-01). + final double nstar = julianDate - JULIAN_DATE_2000_01_01 - CONST_0009 + - longitude / CONST_360; + final double n = Math.round(nstar); + + // Approximate solar noon + final double jstar = JULIAN_DATE_2000_01_01 + CONST_0009 + longitude + / CONST_360 + n; + // Solar mean anomaly + final double m = Math + .toRadians((357.5291 + 0.98560028 * (jstar - JULIAN_DATE_2000_01_01)) + % CONST_360); + + // Equation of center + final double c = 1.9148 * Math.sin(m) + 0.0200 * Math.sin(2 * m) + + 0.0003 * Math.sin(3 * m); + + // Ecliptic longitude + final double lambda = Math + .toRadians((Math.toDegrees(m) + 102.9372 + c + 180) % CONST_360); + + // Solar transit (hour angle for solar noon) + final double jtransit = jstar + 0.0053 * Math.sin(m) - 0.0069 + * Math.sin(2 * lambda); + + // Declination of the sun. + final double delta = Math.asin(Math.sin(lambda) + * Math.sin(Math.toRadians(23.439))); + + + return new SolarEquationVariables(n, m, lambda, jtransit, delta); + } + + /** + * Calculate the sunrise and sunset times for the given date, given + * location, and sun altitude. + * This is based on the Wikipedia article on the Sunrise equation. + * + * @param day The day for which to calculate sunrise and sunset + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @param sunAltitude the angle between the horizon and the center of the sun's disc. + * @return a two-element Gregorian Calendar array. The first element is the + * sunrise, the second element is the sunset. This will return null if there is no sunrise or sunset. (Ex: no sunrise in Antarctica in June) + * @see Sunrise equation on Wikipedia + */ + public static Calendar[] getSunriseSunset(final Calendar day, + final double latitude, double longitude, double sunAltitude) { + + final SolarEquationVariables solarEquationVariables = getSolarEquationVariables(day, longitude); + + longitude = -longitude; + final double latitudeRad = Math.toRadians(latitude); + + // Hour angle + final double omega = Math.acos((Math.sin(Math.toRadians(sunAltitude)) - Math + .sin(latitudeRad) * Math.sin(solarEquationVariables.delta)) + / (Math.cos(latitudeRad) * Math.cos(solarEquationVariables.delta))); + + if (Double.isNaN(omega)) { + return null; + } + + // Sunset + final double jset = JULIAN_DATE_2000_01_01 + + CONST_0009 + + ((Math.toDegrees(omega) + longitude) / CONST_360 + solarEquationVariables.n + 0.0053 + * Math.sin(solarEquationVariables.m) - 0.0069 * Math.sin(2 * solarEquationVariables.lambda)); + + // Sunrise + final double jrise = solarEquationVariables.jtransit - (jset - solarEquationVariables.jtransit); + // Convert sunset and sunrise to Gregorian dates, in UTC + final Calendar gregRiseUTC = getGregorianDate(jrise); + final Calendar gregSetUTC = getGregorianDate(jset); + + // Convert the sunset and sunrise to the timezone of the day parameter + final Calendar gregRise = Calendar.getInstance(day.getTimeZone()); + gregRise.setTimeInMillis(gregRiseUTC.getTimeInMillis()); + final Calendar gregSet = Calendar.getInstance(day.getTimeZone()); + gregSet.setTimeInMillis(gregSetUTC.getTimeInMillis()); + return new Calendar[]{gregRise, gregSet}; + } + + /** + * Calculate the solar noon time for the given date and given location. + * This is based on the Wikipedia article on the Sunrise equation. + * + * @param day The day for which to calculate sunrise and sunset + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return a Calendar with the time set to solar noon for the given day. + * @see Sunrise equation on Wikipedia + */ + public static Calendar getSolarNoon(final Calendar day, final double latitude, double longitude) { + SolarEquationVariables solarEquationVariables = getSolarEquationVariables(day, longitude); + + // Add a check for Antarctica in June and December (sun always down or up, respectively). + // In this case, jtransit will be filled in, but we need to check the hour angle omega for + // sunrise. + // If there's no sunrise (omega is NaN), there's no solar noon. + final double latitudeRad = Math.toRadians(latitude); + + // Hour angle + final double omega = Math.acos((Math.sin(Math.toRadians(SUN_ALTITUDE_SUNRISE_SUNSET)) - Math + .sin(latitudeRad) * Math.sin(solarEquationVariables.delta)) + / (Math.cos(latitudeRad) * Math.cos(solarEquationVariables.delta))); + + if (Double.isNaN(omega)) { + return null; + } + + // Convert jtransit Gregorian dates, in UTC + final Calendar gregNoonUTC = getGregorianDate(solarEquationVariables.jtransit); + final Calendar gregNoon = Calendar.getInstance(day.getTimeZone()); + gregNoon.setTimeInMillis(gregNoonUTC.getTimeInMillis()); + return gregNoon; + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is currently day at the given location. This returns + * true if the current time at the location is after the sunrise and + * before the sunset for that location. + */ + public static boolean isDay(double latitude, double longitude) { + Calendar now = Calendar.getInstance(); + return isDay(now, latitude, longitude); + } + + /** + * @param calendar a datetime + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is day at the given location and given datetime. This returns + * true if the given datetime at the location is after the sunrise and + * before the sunset for that location. + */ + public static boolean isDay(Calendar calendar, double latitude, double longitude) { + Calendar[] sunriseSunset = getSunriseSunset(calendar, latitude, longitude); + // In extreme latitudes, there may be no sunrise/sunset time in summer or + // winter, because it will be day or night 24 hours + if (sunriseSunset == null) { + int month = calendar.get(Calendar.MONTH); // Reminder: January = 0 + if (latitude > 0) { + // Always night at the north pole in December + return month >= 3 && month <= 10; // Always day at the north pole in June + } else { + // Always day at the south pole in December + return month < 3 || month > 10; // Always night at the south pole in June + } + } + Calendar sunrise = sunriseSunset[0]; + Calendar sunset = sunriseSunset[1]; + return calendar.after(sunrise) && calendar.before(sunset); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is night at the given location currently. This returns + * true if the current time at the location is after the astronomical twilight dusk and + * before the astronomical twilight dawn for that location. + */ + public static boolean isNight(double latitude, double longitude) { + Calendar now = Calendar.getInstance(); + return isNight(now, latitude, longitude); + } + + /** + * @param calendar a datetime + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is night at the given location and datetime. This returns + * true if the given datetime at the location is after the astronomical twilight dusk and before + * the astronomical twilight dawn. + */ + public static boolean isNight(Calendar calendar, double latitude, double longitude) { + Calendar[] astronomicalTwilight = getAstronomicalTwilight(calendar, latitude, longitude); + if (astronomicalTwilight == null) { + int month = calendar.get(Calendar.MONTH); // Reminder: January = 0 + if (latitude > 0) { + // Always night at the north pole in December + return month < 3 || month > 10; // Always day at the north pole in June + } else { + // Always day at the south pole in December + return month >= 3 && month <= 10; // Always night at the south pole in June + } + } + Calendar dawn = astronomicalTwilight[0]; + Calendar dusk = astronomicalTwilight[1]; + SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss z"); + format.setTimeZone(calendar.getTimeZone()); + return calendar.before(dawn) || calendar.after(dusk); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is currently civil twilight at the current time at the given location. + * This returns true if the current time at the location is between sunset and civil twilight dusk + * or between civil twilight dawn and sunrise. + */ + @SuppressWarnings("unused") + public static boolean isCivilTwilight(double latitude, double longitude) { + Calendar today = Calendar.getInstance(); + return isCivilTwilight(today, latitude, longitude); + } + + /** + * @param calendar the datetime for which to determine if it's civil twilight in the given location + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is civil twilight at the given location and the given calendar. + * This returns true if the given time at the location is between sunset and civil twilight dusk + * or between civil twilight dawn and sunrise. + */ + public static boolean isCivilTwilight(Calendar calendar, double latitude, double longitude) { + Calendar[] sunriseSunset = getSunriseSunset(calendar, latitude, longitude); + if (sunriseSunset == null) return false; + Calendar[] civilTwilight = getCivilTwilight(calendar, latitude, longitude); + if (civilTwilight == null) return false; + + return (calendar.after(sunriseSunset[1]) && calendar.before(civilTwilight[1]) + || (calendar.after(civilTwilight[0]) && calendar.before(sunriseSunset[0]))); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is currently nautical twilight at the current time at the given location. + * This returns true if the current time at the location is between civil and nautical twilight dusk + * or between nautical and civil twilight dawn. + */ + @SuppressWarnings("unused") + public static boolean isNauticalTwilight(double latitude, double longitude) { + Calendar today = Calendar.getInstance(); + return isNauticalTwilight(today, latitude, longitude); + } + + /** + * @param calendar the datetime for which to determine if it's nautical twilight in the given location + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is nautical twilight at the given location and the given calendar. + * This returns true if the given time at the location is between civil and nautical twilight dusk + * or between nautical and civil twilight dawn. + */ + public static boolean isNauticalTwilight(Calendar calendar, double latitude, double longitude) { + Calendar[] civilTwilight = getCivilTwilight(calendar, latitude, longitude); + if (civilTwilight == null) return false; + Calendar[] nauticalTwilight = getNauticalTwilight(calendar, latitude, longitude); + if (nauticalTwilight == null) return false; + + return (calendar.after(civilTwilight[1]) && calendar.before(nauticalTwilight[1]) + || (calendar.after(nauticalTwilight[0]) && calendar.before(civilTwilight[0]))); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is currently astronomical twilight at the current time at the given location. + * This returns true if the current time at the location is between nautical and astronomical twilight dusk + * or between astronomical and nautical twilight dawn. + */ + @SuppressWarnings("unused") + public static boolean isAstronomicalTwilight(double latitude, double longitude) { + Calendar today = Calendar.getInstance(); + return isAstronomicalTwilight(today, latitude, longitude); + } + + /** + * @param calendar the datetime for which to determine if it's astronomical twilight in the given location + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is astronomical twilight at the given location and the given calendar. + * This returns true if the given time at the location is between nautical and astronomical twilight dusk + * or between astronomical and nautical twilight dawn. + */ + public static boolean isAstronomicalTwilight(Calendar calendar, double latitude, double longitude) { + Calendar[] nauticalTwilight = getNauticalTwilight(calendar, latitude, longitude); + if (nauticalTwilight == null) return false; + Calendar[] astronomicalTwilight = getAstronomicalTwilight(calendar, latitude, longitude); + if (astronomicalTwilight == null) return false; + + return (calendar.after(nauticalTwilight[1]) && calendar.before(astronomicalTwilight[1]) + || (calendar.after(astronomicalTwilight[0]) && calendar.before(nauticalTwilight[0]))); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return true if it is civil, nautical, or astronomical twilight currently at the given location. + */ + @SuppressWarnings("unused") + public static boolean isTwilight(double latitude, double longitude) { + Calendar today = Calendar.getInstance(); + return isTwilight(today, latitude, longitude); + } + + /** + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @param calendar the given datetime to check for twilight + * @return true if at the given location and calendar, it is civil, nautical, or astronomical twilight. + */ + public static boolean isTwilight(Calendar calendar, double latitude, double longitude) { + return isCivilTwilight(calendar, latitude, longitude) + || isNauticalTwilight(calendar, latitude, longitude) + || isAstronomicalTwilight(calendar, latitude, longitude); + } + + public static DayPeriod getDayPeriod(Calendar calendar, double latitude, double longitude) { + if (isDay(calendar, latitude, longitude)) return DayPeriod.DAY; + if (isCivilTwilight(calendar, latitude, longitude)) return DayPeriod.CIVIL_TWILIGHT; + if (isNauticalTwilight(calendar, latitude, longitude)) return DayPeriod.NAUTICAL_TWILIGHT; + if (isAstronomicalTwilight(calendar, latitude, longitude)) return DayPeriod.ASTRONOMICAL_TWILIGHT; + if (isNight(calendar, latitude, longitude)) return DayPeriod.NIGHT; + return DayPeriod.NIGHT; + } + + /** + * @param calendar the datetime for which to determine the day length + * @param latitude the latitude of the location in degrees. + * @param longitude the longitude of the location in degrees (West is negative) + * @return the number of milliseconds between sunrise and sunset. + */ + public static long getDayLength(Calendar calendar, double latitude, double longitude) { + Calendar[] sunriseSunset = getSunriseSunset(calendar, latitude, longitude); + if (sunriseSunset == null) { + int month = calendar.get(Calendar.MONTH); // Reminder: January = 0 + if (latitude > 0) { + if (month >= 3 && month <= 10) { + return MILLISECONDS_IN_DAY; // Always day at the north pole in June + } else { + return 0; // Always night at the north pole in December + } + } else { + if (month >= 3 && month <= 10) { + return 0; // Always night at the south pole in June + } else { + return MILLISECONDS_IN_DAY; // Always day at the south pole in December + } + } + } + return sunriseSunset[1].getTimeInMillis() - sunriseSunset[0].getTimeInMillis(); + } + + public enum DayPeriod { + DAY, + CIVIL_TWILIGHT, + NAUTICAL_TWILIGHT, + ASTRONOMICAL_TWILIGHT, + NIGHT + } + + /** + * Intermediate variables used in the sunrise equation + * + * @see Sunrise equation on Wikipedia + */ + private static class SolarEquationVariables { + final double n;// Julian cycle (number of days since 2000-01-01). + final double m; // solar mean anomaly + final double lambda; // ecliptic longitude + final double jtransit; // Solar transit (hour angle for solar noon) + final double delta; // Declination of the sun + + private SolarEquationVariables(double n, double m, double lambda, double jtransit, double delta) { + this.n = n; + this.m = m; + this.lambda = lambda; + this.jtransit = jtransit; + this.delta = delta; + } + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..990d5d5 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,8 @@ +shelf: + '1': #rack + '1': 26 #light/pin + '2': 20 + '3': 21 +latitude: 51.751187549834086 +longitude: -0.33916135825242166 +cronExpression: 0 5 1 * * * \ No newline at end of file