initial commit

This commit is contained in:
2025-09-15 15:12:59 +01:00
commit b2a6261fef
24 changed files with 2172 additions and 0 deletions

85
pom.xml Normal file
View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>space.hilltopgrove</groupId>
<artifactId>lightswitch</artifactId>
<version>0.0.2-SNAPSHOT</version>
<name>lightswitch</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
<pi4j.version>2.1.1</pi4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- include Pi4J Core -->
<dependency>
<groupId>com.pi4j</groupId>
<artifactId>pi4j-core</artifactId>
<version>${pi4j.version}</version>
</dependency>
<!-- include Pi4J Plugins (Platforms and I/O Providers) -->
<dependency>
<groupId>com.pi4j</groupId>
<artifactId>pi4j-plugin-raspberrypi</artifactId>
<version>${pi4j.version}</version>
</dependency>
<dependency>
<groupId>com.pi4j</groupId>
<artifactId>pi4j-plugin-pigpio</artifactId>
<version>${pi4j.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>**/application.yml</exclude>
</excludes>
</resource>
</resources>
</build>
</project>

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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<Integer, Map<Integer, Integer>> shelf;
private double latitude;
private double longitude;
}

View File

@@ -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<Integer, Rack> getRackMap(Map<Integer, Map<Integer, Integer>> 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<Integer, Light> getLightMap(Map<Integer, Integer> 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")
);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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<Integer, Light> lightMap;
public static Rack map(Map<Integer, Light> 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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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<Integer, Rack> rackMap;
public static Shelf map(Map<Integer, Rack> 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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,7 @@
package space.hilltopgrove.lightswitch.service;
import space.hilltopgrove.lightswitch.model.ScheduleBean;
public interface SunRiseSunSetService {
ScheduleBean getSchedule();
}

View File

@@ -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. <br>
* Also provides methods to convert between Gregorian and Julian dates.<br>
* The formulas used by this class are from the Wikipedia articles on Julian Day
* and Sunrise Equation. <br>
*
* @author Carmen Alvarez
* @see <a href="http://en.wikipedia.org/wiki/Julian_day">Julian Day on Wikipedia</a>
* @see <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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.
* <br>
* 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 <a href="http://en.wikipedia.org/wiki/Julian_day#Converting_Julian_or_Gregorian_calendar_date_to_Julian_Day_Number">Converting to Julian day number on Wikipedia</a>
*/
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.
* <br>
* 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 <a href="http://en.wikipedia.org/wiki/Julian_day#Gregorian_calendar_from_Julian_day_number">Converting from Julian day to Gregorian date, on Wikipedia</a>
*/
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 <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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 <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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 <a href="http://en.wikipedia.org/wiki/Solar_zenith_angle#Solar_elevation_angle">the angle between the horizon and the center of the sun's disc.</a>
* @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 <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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 <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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 <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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. <br>
* Also provides methods to convert between Gregorian and Julian dates.<br>
* The formulas used by this class are from the Wikipedia articles on Julian Day
* and Sunrise Equation. <br>
*
* @author Carmen Alvarez
* @see <a href="http://en.wikipedia.org/wiki/Julian_day">Julian Day on Wikipedia</a>
* @see <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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.
* <br>
* 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 <a href="http://en.wikipedia.org/wiki/Julian_day#Converting_Julian_or_Gregorian_calendar_date_to_Julian_Day_Number">Converting to Julian day number on Wikipedia</a>
*/
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.
* <br>
* 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 <a href="http://en.wikipedia.org/wiki/Julian_day#Gregorian_calendar_from_Julian_day_number">Converting from Julian day to Gregorian date, on Wikipedia</a>
*/
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 <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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 <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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 <a href="http://en.wikipedia.org/wiki/Solar_zenith_angle#Solar_elevation_angle">the angle between the horizon and the center of the sun's disc.</a>
* @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 <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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 <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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 <a href="http://en.wikipedia.org/wiki/Sunrise_equation">Sunrise equation on Wikipedia</a>
*/
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;
}
}
}

View File

@@ -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 * * *