refactor: move sun and wind automation into ShadeSensors

The sun and wind timers were spread over processFrame, checkMovement and
processInternalCommand. ShadeSensors now holds them and decides when the
weather extends or retracts a shade, when turning the sun flag on moves
it, and when wind blocks a remote Down. Each caller still sets the targets
itself, so no shade moves differently yet.

The timers are now 32-bit like millis(). As 64-bit values holding a 32-bit
millis() they fired at once after the rollover every 49.7 days. The debug
logs of each timer go: they were compiled out at CORE_DEBUG_LEVEL 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-13 21:12:34 +02:00
parent 0eb0e63631
commit 8276c1c3cd
6 changed files with 319 additions and 129 deletions

View file

@ -109,7 +109,7 @@ lib_deps =
platform = native platform = native
test_framework = unity test_framework = unity
test_build_src = yes test_build_src = yes
build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp> build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<ShadeSensors.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp>
build_flags = build_flags =
-std=gnu++17 -std=gnu++17
-Isrc -Isrc

77
src/ShadeSensors.cpp Normal file
View file

@ -0,0 +1,77 @@
#include "ShadeSensors.h"
// Whether a timer started at start has run timeoutMs by now. The unsigned
// subtraction keeps working across the millis() rollover.
static bool due(uint32_t start, uint32_t now, uint32_t timeoutMs) {
return start != 0 && now - start >= timeoutMs;
}
void ShadeSensors::clear() { *this = ShadeSensors(); }
void ShadeSensors::onSensorFrame(bool wasSunny, bool wasWindy, bool sunny, bool windy, uint32_t now) {
if(sunny) {
this->noSunStart = 0;
this->noSunDone = true;
}
else {
this->sunStart = 0;
this->sunDone = true;
}
if(windy) {
this->noWindStart = 0;
this->noWindDone = true;
this->windLast = now;
}
else {
this->windStart = 0;
this->windDone = true;
}
if(sunny && !wasSunny) {
this->sunStart = now;
this->sunDone = false;
}
else if(!sunny && wasSunny) {
this->noSunStart = now;
this->noSunDone = false;
}
if(windy && !wasWindy) {
this->windStart = now;
this->windDone = false;
}
else if(!windy && wasWindy) {
this->noWindStart = now;
this->noWindDone = false;
}
}
SensorMove ShadeSensors::check(bool sunFlag, bool sunny, bool windy, uint32_t now) {
SensorMove move = SensorMove::none;
if(sunFlag) {
if(sunny && !windy) {
// Sunny and calm, so the shade should be extended.
if(this->noWindDone && !this->sunDone && due(this->sunStart, now, SOMFY_SUN_TIMEOUT)) {
this->sunDone = true;
move = SensorMove::extend;
}
if(!this->noWindDone && due(this->noWindStart, now, SOMFY_NO_WIND_TIMEOUT)) {
this->noWindDone = true;
move = SensorMove::extend;
}
}
if(!sunny && !this->noSunDone && due(this->noSunStart, now, SOMFY_NO_SUN_TIMEOUT)) {
this->noSunDone = true;
move = SensorMove::retract;
}
}
if(windy && !this->windDone && due(this->windStart, now, SOMFY_WIND_TIMEOUT)) {
this->windDone = true;
move = SensorMove::retract;
}
return move;
}
SensorMove ShadeSensors::onSunFlag(bool sunny, bool windy) const {
if(windy) return SensorMove::none;
if(sunny && this->sunDone) return SensorMove::extend;
if(!sunny && this->noSunDone) return SensorMove::retract;
return SensorMove::none;
}
bool ShadeSensors::windBlocksDown(uint32_t now) const {
return this->windLast != 0 && now - this->windLast < SOMFY_NO_WIND_REMOTE_TIMEOUT;
}

52
src/ShadeSensors.h Normal file
View file

@ -0,0 +1,52 @@
#ifndef SHADE_SENSORS_H
#define SHADE_SENSORS_H
#include <stdint.h>
// Sun and wind automation for a shade with a sun sensor. Kept free of the
// shade, radio and network so it can be unit tested on the PC
// (pio test -e native).
#define SECS_TO_MILLIS(x) ((x) * 1000)
#define MINS_TO_MILLIS(x) SECS_TO_MILLIS((x) * 60)
// How long the sensor must report a change before the shade follows it.
#define SOMFY_SUN_TIMEOUT MINS_TO_MILLIS(2)
#define SOMFY_NO_SUN_TIMEOUT MINS_TO_MILLIS(20)
#define SOMFY_WIND_TIMEOUT SECS_TO_MILLIS(2)
#define SOMFY_NO_WIND_TIMEOUT MINS_TO_MILLIS(12)
// A remote Down is ignored this long after the last windy sensor frame.
#define SOMFY_NO_WIND_REMOTE_TIMEOUT SECS_TO_MILLIS(30)
// What the weather tells a shade to do.
enum class SensorMove : uint8_t {
none,
extend, // To the favorite, or all the way down when there is none.
retract // All the way up.
};
// The sensor timers of one shade. Times are millis(); a start of 0 means the
// timer isn't running.
struct ShadeSensors {
uint32_t sunStart = 0;
uint32_t noSunStart = 0;
uint32_t windStart = 0;
uint32_t noWindStart = 0;
uint32_t windLast = 0; // Last sensor frame that reported wind.
bool sunDone = true;
bool noSunDone = true;
bool windDone = true;
bool noWindDone = true;
void clear();
// A sensor frame arrived. was* is what the shade knew before it, sunny and
// windy what the frame reports.
void onSensorFrame(bool wasSunny, bool wasWindy, bool sunny, bool windy, uint32_t now);
// Fires the timers that are due, each once. Sun only moves the shade while
// the sun flag is on; wind always does.
SensorMove check(bool sunFlag, bool sunny, bool windy, uint32_t now);
// Where turning the sun flag on sends the shade right away. Nothing while
// it's windy or while a timer is still running.
SensorMove onSunFlag(bool sunny, bool windy) const;
// Whether a remote Down is ignored because the sensor reported wind lately.
bool windBlocksDown(uint32_t now) const;
};
#endif

View file

@ -11,22 +11,13 @@
#define SOMFY_MAX_ROOMS 16 #define SOMFY_MAX_ROOMS 16
#define SOMFY_MAX_REPEATERS 7 #define SOMFY_MAX_REPEATERS 7
#define SECS_TO_MILLIS(x) ((x) * 1000)
#define MINS_TO_MILLIS(x) SECS_TO_MILLIS((x) * 60)
#define SOMFY_SUN_TIMEOUT MINS_TO_MILLIS(2)
#define SOMFY_NO_SUN_TIMEOUT MINS_TO_MILLIS(20)
#define SOMFY_WIND_TIMEOUT SECS_TO_MILLIS(2)
#define SOMFY_NO_WIND_TIMEOUT MINS_TO_MILLIS(12)
#define SOMFY_NO_WIND_REMOTE_TIMEOUT SECS_TO_MILLIS(30)
#include "SomfyFrame.h" #include "SomfyFrame.h"
#include "TxBuffer.h" #include "TxBuffer.h"
#include "RadioScheduler.h" #include "RadioScheduler.h"
#include "ShadeTypes.h" #include "ShadeTypes.h"
#include "ShadeMotion.h" #include "ShadeMotion.h"
#include "ShadeSensors.h"
#include "Transceiver.h" #include "Transceiver.h"
enum class somfy_flags_t : byte { enum class somfy_flags_t : byte {
@ -127,15 +118,7 @@ class SomfyShade : public SomfyRemote {
uint8_t shadeId = 255; uint8_t shadeId = 255;
uint64_t moveStart = 0; uint64_t moveStart = 0;
uint64_t tiltStart = 0; uint64_t tiltStart = 0;
uint64_t noSunStart = 0; ShadeSensors sensors;
uint64_t sunStart = 0;
uint64_t windStart = 0;
uint64_t windLast = 0;
uint64_t noWindStart = 0;
bool noSunDone = true;
bool sunDone = true;
bool windDone = true;
bool noWindDone = true;
float startPos = 0.0f; float startPos = 0.0f;
float startTiltPos = 0.0f; float startTiltPos = 0.0f;
bool settingMyPos = false; bool settingMyPos = false;

View file

@ -19,15 +19,7 @@ void SomfyShade::clear() {
this->setRemoteAddress(0); this->setRemoteAddress(0);
this->moveStart = 0; this->moveStart = 0;
this->tiltStart = 0; this->tiltStart = 0;
this->noSunStart = 0; this->sensors.clear();
this->sunStart = 0;
this->windStart = 0;
this->windLast = 0;
this->noWindStart = 0;
this->noSunDone = true;
this->sunDone = true;
this->windDone = true;
this->noWindDone = true;
this->startPos = 0.0f; this->startPos = 0.0f;
this->startTiltPos = 0.0f; this->startTiltPos = 0.0f;
this->settingMyPos = false; this->settingMyPos = false;
@ -160,49 +152,16 @@ void SomfyShade::checkMovement() {
case TxHoldCheck::none: case TxHoldCheck::none:
break; break;
} }
if (sunFlag) { switch(this->sensors.check(sunFlag, isSunny, isWindy, (uint32_t)curTime)) {
if (isSunny && !isWindy) { // It is sunny and there is no wind so we should be extended case SensorMove::extend:
if (this->noWindDone this->p_target(this->myPos >= 0 ? this->myPos : 100.0f);
&& !this->sunDone break;
&& this->sunStart case SensorMove::retract:
&& (curTime - this->sunStart) >= SOMFY_SUN_TIMEOUT)
{
this->p_target(this->myPos >= 0 ? this->myPos : 100.0f);
//this->target = this->myPos >= 0 ? this->myPos : 100.0f;
this->sunDone = true;
ESP_LOGD(TAG, "[%u] Sun -> done", this->shadeId);
}
if (!this->noWindDone
&& this->noWindStart
&& (curTime - this->noWindStart) >= SOMFY_NO_WIND_TIMEOUT)
{
this->p_target(this->myPos >= 0 ? this->myPos : 100.0f);
//this->target = this->myPos >= 0 ? this->myPos : 100.0f;
this->noWindDone = true;
ESP_LOGD(TAG, "[%u] No Wind -> done", this->shadeId);
}
}
if (!isSunny
&& !this->noSunDone
&& this->noSunStart
&& (curTime - this->noSunStart) >= SOMFY_NO_SUN_TIMEOUT)
{
if(this->tiltType == tilt_types::tiltonly) this->p_tiltTarget(0.0f); if(this->tiltType == tilt_types::tiltonly) this->p_tiltTarget(0.0f);
this->p_target(0.0f); this->p_target(0.0f);
this->noSunDone = true; break;
ESP_LOGD(TAG, "[%u] No Sun -> done", this->shadeId); case SensorMove::none:
} break;
}
if (isWindy
&& !this->windDone
&& this->windStart
&& (curTime - this->windStart) >= SOMFY_WIND_TIMEOUT)
{
if(this->tiltType == tilt_types::tiltonly) this->p_tiltTarget(0.0f);
this->p_target(0.0f);
this->windDone = true;
ESP_LOGD(TAG, "[%u] Wind -> done", this->shadeId);
} }
if(!tilt_first && this->direction > 0) { if(!tilt_first && this->direction > 0) {
@ -616,51 +575,7 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
this->flags &= ~(static_cast<uint8_t>(somfy_flags_t::DemoMode)); this->flags &= ~(static_cast<uint8_t>(somfy_flags_t::DemoMode));
const bool isSunny = this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny); const bool isSunny = this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny);
const bool isWindy = this->flags & static_cast<uint8_t>(somfy_flags_t::Windy); const bool isWindy = this->flags & static_cast<uint8_t>(somfy_flags_t::Windy);
if (isSunny) this->sensors.onSensorFrame(wasSunny, wasWindy, isSunny, isWindy, (uint32_t)curTime);
{
this->noSunStart = 0;
this->noSunDone = true;
}
else
{
this->sunStart = 0;
this->sunDone = true;
}
if (isWindy)
{
this->noWindStart = 0;
this->noWindDone = true;
this->windLast = curTime;
}
else
{
this->windStart = 0;
this->windDone = true;
}
if (isSunny && !wasSunny)
{
this->sunStart = curTime;
this->sunDone = false;
ESP_LOGD(TAG, "[%u] Sun -> start", this->shadeId);
}
else if (!isSunny && wasSunny)
{
this->noSunStart = curTime;
this->noSunDone = false;
ESP_LOGD(TAG, "[%u] No Sun -> start", this->shadeId);
}
if (isWindy && !wasWindy)
{
this->windStart = curTime;
this->windDone = false;
ESP_LOGD(TAG, "[%u] Wind -> start", this->shadeId);
}
else if (!isWindy && wasWindy)
{
this->noWindStart = curTime;
this->noWindDone = false;
ESP_LOGD(TAG, "[%u] No Wind -> start", this->shadeId);
}
this->emitState(); this->emitState();
somfy.updateGroupFlags(); somfy.updateGroupFlags();
} }
@ -691,23 +606,24 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
if(this->lastFrame.rollingCode & 0x8000) return; // Some sensors send bogus frames with a rollingCode >= 32768 that cause them to change the state. if(this->lastFrame.rollingCode & 0x8000) return; // Some sensors send bogus frames with a rollingCode >= 32768 that cause them to change the state.
{ {
const bool isWindy = this->flags & static_cast<uint8_t>(somfy_flags_t::Windy); const bool isWindy = this->flags & static_cast<uint8_t>(somfy_flags_t::Windy);
const bool isSunny = this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny);
//this->flags |= static_cast<uint8_t>(somfy_flags_t::SunFlag); //this->flags |= static_cast<uint8_t>(somfy_flags_t::SunFlag);
this->p_sunFlag(true); this->p_sunFlag(true);
if (!isWindy) switch(this->sensors.onSunFlag(isSunny, isWindy)) {
{ case SensorMove::extend:
const bool isSunny = this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny);
if (isSunny && this->sunDone) {
if(this->tiltType == tilt_types::tiltonly) if(this->tiltType == tilt_types::tiltonly)
this->p_tiltTarget(this->myTiltPos >= 0 ? this->myTiltPos : 100.0f); this->p_tiltTarget(this->myTiltPos >= 0 ? this->myTiltPos : 100.0f);
else else
this->p_target(this->myPos >= 0 ? this->myPos : 100.0f); this->p_target(this->myPos >= 0 ? this->myPos : 100.0f);
} break;
else if (!isSunny && this->noSunDone) { case SensorMove::retract:
if(this->tiltType == tilt_types::tiltonly) if(this->tiltType == tilt_types::tiltonly)
this->p_tiltTarget(0.0f); this->p_tiltTarget(0.0f);
else else
this->p_target(0.0f); this->p_target(0.0f);
} break;
case SensorMove::none:
break;
} }
somfy.isDirty = true; somfy.isDirty = true;
this->emitState(); this->emitState();
@ -755,7 +671,7 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) {
this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress); this->emitCommand(cmd, internal ? "internal" : "remote", frame.remoteAddress);
return; return;
} }
if (!this->windLast || (curTime - this->windLast) >= SOMFY_NO_WIND_REMOTE_TIMEOUT) { if (!this->sensors.windBlocksDown((uint32_t)curTime)) {
if(this->tiltType == tilt_types::tiltmotor || this->tiltType == tilt_types::euromode) { if(this->tiltType == tilt_types::tiltmotor || this->tiltType == tilt_types::euromode) {
// Wait another half seccond just in case we are potentially processing a tilt. // Wait another half seccond just in case we are potentially processing a tilt.
if(!internal) this->lastFrame.await = curTime + 500; if(!internal) this->lastFrame.await = curTime + 500;
@ -967,7 +883,7 @@ void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) {
} }
break; break;
case somfy_commands::Down: case somfy_commands::Down:
if (!this->windLast || (curTime - this->windLast) >= SOMFY_NO_WIND_REMOTE_TIMEOUT) { if (!this->sensors.windBlocksDown((uint32_t)curTime)) {
if(this->tiltType == tilt_types::tiltmotor) { if(this->tiltType == tilt_types::tiltmotor) {
if(repeat >= TILT_REPEATS) if(repeat >= TILT_REPEATS)
this->p_tiltTarget(100.0f); this->p_tiltTarget(100.0f);
@ -1088,15 +1004,18 @@ void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) {
case somfy_commands::SunFlag: case somfy_commands::SunFlag:
if(this->hasSunSensor()) { if(this->hasSunSensor()) {
const bool isWindy = this->flags & static_cast<uint8_t>(somfy_flags_t::Windy); const bool isWindy = this->flags & static_cast<uint8_t>(somfy_flags_t::Windy);
const bool isSunny = this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny);
this->p_sunFlag(true); this->p_sunFlag(true);
//this->flags |= static_cast<uint8_t>(somfy_flags_t::SunFlag); //this->flags |= static_cast<uint8_t>(somfy_flags_t::SunFlag);
if (!isWindy) switch(this->sensors.onSunFlag(isSunny, isWindy)) {
{ case SensorMove::extend:
const bool isSunny = this->flags & static_cast<uint8_t>(somfy_flags_t::Sunny);
if (isSunny && this->sunDone)
this->p_target(this->myPos >= 0 ? this->myPos : 100.0f); this->p_target(this->myPos >= 0 ? this->myPos : 100.0f);
else if (!isSunny && this->noSunDone) break;
case SensorMove::retract:
this->p_target(0.0f); this->p_target(0.0f);
break;
case SensorMove::none:
break;
} }
somfy.isDirty = true; somfy.isDirty = true;
this->emitState(); this->emitState();

View file

@ -0,0 +1,159 @@
#include <unity.h>
#include "ShadeSensors.h"
#define ASSERT_MOVE(expected, actual) TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(expected), static_cast<uint8_t>(actual))
static const bool SUN_FLAG = true;
static const uint32_t T0 = 1000;
void setUp() {}
void tearDown() {}
// Sensors that have just reported sun, with no wind, at T0.
static ShadeSensors sunnyAt(uint32_t now) {
ShadeSensors s;
s.onSensorFrame(false, false, true, false, now);
return s;
}
/*********************************************************************
* sun
********************************************************************/
static void test_sun_extends_after_its_timeout() {
ShadeSensors s = sunnyAt(T0);
ASSERT_MOVE(SensorMove::none, s.check(SUN_FLAG, true, false, T0 + SOMFY_SUN_TIMEOUT - 1));
ASSERT_MOVE(SensorMove::extend, s.check(SUN_FLAG, true, false, T0 + SOMFY_SUN_TIMEOUT));
}
static void test_sun_extends_once() {
ShadeSensors s = sunnyAt(T0);
ASSERT_MOVE(SensorMove::extend, s.check(SUN_FLAG, true, false, T0 + SOMFY_SUN_TIMEOUT));
ASSERT_MOVE(SensorMove::none, s.check(SUN_FLAG, true, false, T0 + SOMFY_SUN_TIMEOUT + 60000));
}
static void test_sun_needs_the_sun_flag() {
ShadeSensors s = sunnyAt(T0);
ASSERT_MOVE(SensorMove::none, s.check(!SUN_FLAG, true, false, T0 + SOMFY_SUN_TIMEOUT));
// The timer is still due once the flag goes on.
ASSERT_MOVE(SensorMove::extend, s.check(SUN_FLAG, true, false, T0 + SOMFY_SUN_TIMEOUT + 1));
}
static void test_sun_does_not_extend_in_wind() {
ShadeSensors s = sunnyAt(T0);
ASSERT_MOVE(SensorMove::none, s.check(SUN_FLAG, true, true, T0 + SOMFY_SUN_TIMEOUT));
}
static void test_steady_sun_does_not_restart_the_timer() {
ShadeSensors s = sunnyAt(T0);
s.onSensorFrame(true, false, true, false, T0 + 60000);
ASSERT_MOVE(SensorMove::extend, s.check(SUN_FLAG, true, false, T0 + SOMFY_SUN_TIMEOUT));
}
static void test_no_sun_retracts_after_its_timeout() {
ShadeSensors s = sunnyAt(T0);
s.onSensorFrame(true, false, false, false, T0 + 10);
ASSERT_MOVE(SensorMove::none, s.check(SUN_FLAG, false, false, T0 + 10 + SOMFY_NO_SUN_TIMEOUT - 1));
ASSERT_MOVE(SensorMove::retract, s.check(SUN_FLAG, false, false, T0 + 10 + SOMFY_NO_SUN_TIMEOUT));
ASSERT_MOVE(SensorMove::none, s.check(SUN_FLAG, false, false, T0 + 10 + SOMFY_NO_SUN_TIMEOUT + 1));
}
static void test_cloud_cancels_the_sun_timer() {
ShadeSensors s = sunnyAt(T0);
s.onSensorFrame(true, false, false, false, T0 + 10);
s.onSensorFrame(false, false, true, false, T0 + 20);
// Sun came back before the no-sun timeout: neither fires early.
ASSERT_MOVE(SensorMove::none, s.check(SUN_FLAG, true, false, T0 + 20 + SOMFY_SUN_TIMEOUT - 1));
ASSERT_MOVE(SensorMove::extend, s.check(SUN_FLAG, true, false, T0 + 20 + SOMFY_SUN_TIMEOUT));
}
/*********************************************************************
* wind
********************************************************************/
static void test_wind_retracts_without_the_sun_flag() {
ShadeSensors s;
s.onSensorFrame(false, false, false, true, T0);
ASSERT_MOVE(SensorMove::none, s.check(!SUN_FLAG, false, true, T0 + SOMFY_WIND_TIMEOUT - 1));
ASSERT_MOVE(SensorMove::retract, s.check(!SUN_FLAG, false, true, T0 + SOMFY_WIND_TIMEOUT));
ASSERT_MOVE(SensorMove::none, s.check(!SUN_FLAG, false, true, T0 + SOMFY_WIND_TIMEOUT + 1));
}
static void test_wind_dying_down_extends_again_in_sun() {
ShadeSensors s = sunnyAt(T0);
ASSERT_MOVE(SensorMove::extend, s.check(SUN_FLAG, true, false, T0 + SOMFY_SUN_TIMEOUT));
const uint32_t windy = T0 + SOMFY_SUN_TIMEOUT + 10;
s.onSensorFrame(true, false, true, true, windy);
ASSERT_MOVE(SensorMove::retract, s.check(SUN_FLAG, true, true, windy + SOMFY_WIND_TIMEOUT));
const uint32_t calm = windy + SOMFY_WIND_TIMEOUT + 10;
s.onSensorFrame(true, true, true, false, calm);
ASSERT_MOVE(SensorMove::none, s.check(SUN_FLAG, true, false, calm + SOMFY_NO_WIND_TIMEOUT - 1));
ASSERT_MOVE(SensorMove::extend, s.check(SUN_FLAG, true, false, calm + SOMFY_NO_WIND_TIMEOUT));
}
static void test_wind_blocks_a_remote_down_for_a_while() {
ShadeSensors s;
TEST_ASSERT_FALSE(s.windBlocksDown(T0));
s.onSensorFrame(false, false, false, true, T0);
TEST_ASSERT_TRUE(s.windBlocksDown(T0 + SOMFY_NO_WIND_REMOTE_TIMEOUT - 1));
TEST_ASSERT_FALSE(s.windBlocksDown(T0 + SOMFY_NO_WIND_REMOTE_TIMEOUT));
}
static void test_every_windy_frame_extends_the_down_block() {
ShadeSensors s;
s.onSensorFrame(false, false, false, true, T0);
s.onSensorFrame(false, true, false, true, T0 + 20000);
TEST_ASSERT_TRUE(s.windBlocksDown(T0 + SOMFY_NO_WIND_REMOTE_TIMEOUT + 1));
}
/*********************************************************************
* sun flag turned on
********************************************************************/
static void test_sun_flag_acts_on_a_finished_timer() {
ShadeSensors s;
// Nothing pending: the shade goes where the weather says.
ASSERT_MOVE(SensorMove::extend, s.onSunFlag(true, false));
ASSERT_MOVE(SensorMove::retract, s.onSunFlag(false, false));
}
static void test_sun_flag_waits_for_a_running_timer() {
ShadeSensors s = sunnyAt(T0);
ASSERT_MOVE(SensorMove::none, s.onSunFlag(true, false));
s.onSensorFrame(true, false, false, false, T0 + 10);
ASSERT_MOVE(SensorMove::none, s.onSunFlag(false, false));
}
static void test_sun_flag_does_nothing_in_wind() {
ShadeSensors s;
ASSERT_MOVE(SensorMove::none, s.onSunFlag(true, true));
ASSERT_MOVE(SensorMove::none, s.onSunFlag(false, true));
}
/*********************************************************************
* clear and millis() rollover
********************************************************************/
static void test_clear_forgets_pending_timers() {
ShadeSensors s = sunnyAt(T0);
s.onSensorFrame(false, false, false, true, T0);
s.clear();
ASSERT_MOVE(SensorMove::none, s.check(SUN_FLAG, true, false, T0 + SOMFY_NO_SUN_TIMEOUT));
TEST_ASSERT_FALSE(s.windBlocksDown(T0 + 1));
}
static void test_timers_run_across_a_millis_rollover() {
const uint32_t start = 0xFFFFFFFFUL - 1000;
ShadeSensors s = sunnyAt(start);
ASSERT_MOVE(SensorMove::none, s.check(SUN_FLAG, true, false, start + 2000));
ASSERT_MOVE(SensorMove::extend, s.check(SUN_FLAG, true, false, start + SOMFY_SUN_TIMEOUT));
s.onSensorFrame(false, false, false, true, start);
TEST_ASSERT_TRUE(s.windBlocksDown(start + 2000));
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_sun_extends_after_its_timeout);
RUN_TEST(test_sun_extends_once);
RUN_TEST(test_sun_needs_the_sun_flag);
RUN_TEST(test_sun_does_not_extend_in_wind);
RUN_TEST(test_steady_sun_does_not_restart_the_timer);
RUN_TEST(test_no_sun_retracts_after_its_timeout);
RUN_TEST(test_cloud_cancels_the_sun_timer);
RUN_TEST(test_wind_retracts_without_the_sun_flag);
RUN_TEST(test_wind_dying_down_extends_again_in_sun);
RUN_TEST(test_wind_blocks_a_remote_down_for_a_while);
RUN_TEST(test_every_windy_frame_extends_the_down_block);
RUN_TEST(test_sun_flag_acts_on_a_finished_timer);
RUN_TEST(test_sun_flag_waits_for_a_running_timer);
RUN_TEST(test_sun_flag_does_nothing_in_wind);
RUN_TEST(test_clear_forgets_pending_timers);
RUN_TEST(test_timers_run_across_a_millis_rollover);
return UNITY_END();
}