Overview
The engineering problem
A scheduled light needs an unambiguous answer at each window boundary, including midnight. It also needs a defined response when its external time source fails, without treating a brief outage as a reason to immediately change the output.
Contribution
I separated the pure scheduling policy from the stateful controller, connected an HTTP time adapter and simulated output, and wired a cancellable hosted worker to apply the schedule.
What exists now
The released controller applies daily schedules to a simulated light, enters safe mode when time readings fail, and returns to scheduled control after recovery. Local tests exercise policy, adapter, and HTTP paths. No physical light is connected.
- Schedule, controller, and configuration validator tests exercise the core rules and state transitions.View unit tests (opens in a new tab)
- An in-process message handler checks the real HTTP adapter without contacting the time service.View http integration tests (opens in a new tab)
- In-memory controller scenarios cover scheduling, transient failure, safe-mode shutdown, and recovery.View acceptance scenarios (opens in a new tab)
- The workflow defines build, test, coverage, and audit steps. Its run for the current main commit completed successfully.View ci workflow and run (opens in a new tab)
- C# analysis completed successfully on the current main commit. The workflow also has a weekly schedule.View codeql workflow and run (opens in a new tab)
- CI checks vulnerable transitive runtime packages after a locked restore.View dependency audit (opens in a new tab)
- The published source release documents the simulation scope and test suites. It has no binary assets.View v1.0.0 release (opens in a new tab)
- The diagram traces the worker, policy, adapters, and failure state in the public source.
Engineering detail
Constraints
- One configured daily active window controls one simulated output. Start is inclusive, end is exclusive, and equal boundaries mean the light stays off.
- The runtime reads local time with its UTC offset from an HTTPS provider. A failed read cannot establish the current schedule state.
- Repeated provider failures must turn the light off at a configured threshold. A later successful read must restore scheduled operation.
- Configuration errors must fail at startup, and normal automated tests must run without an external service or wall-clock dependence.
Architecture at a glance
The .NET hosted worker checks once after startup and then polls with PeriodicTimer. LightController reads through ITimeProvider, evaluates the pure LightSchedule policy, and changes ILightOutput only when the requested state differs. It tracks consecutive time-provider failures. The threshold forces the output off, and a successful read clears safe mode and reapplies the schedule. Validated options configure the loop, schedule, and HTTP provider.
Validated configuration supplies the worker and controller. The worker invokes the controller, which reads time through an HTTP adapter, evaluates the schedule, and drives a simulated output. Consecutive provider failures move the controller into safe mode. The next successful read restores scheduled operation.
- Configuration: SmartLight options set the schedule, polling interval, failure threshold, time-zone path, HTTPS base URI, and timeout. Invalid values fail at startup.
- Worker: A BackgroundService runs one check after startup, then invokes the controller on each cancellable PeriodicTimer tick.
- Controller: LightController coordinates time reads, schedule decisions, output transitions, and the consecutive-failure count.
- Output: ILightOutput is implemented by a state-holding console simulation. No physical device is controlled.
- Schedule: LightSchedule is a pure policy for same-day and overnight windows with inclusive start and exclusive end.
- Safe mode: The controller retains output below the failure threshold, forces it off at the threshold, and leaves safe mode after a successful read.
- Time provider: ITimeProvider is implemented by HttpTimeProvider, which makes cancellable HTTP requests and wraps expected transport or data failures.
- Time service: The configured external HTTPS service returns an offset-bearing local datetime for the requested time-zone path.
Connections: Configuration to Worker (polling interval); Worker to Controller (check on each tick); Controller to Schedule (evaluate local time); Controller to Output (change when needed); Controller to Safe mode (track failures and recovery); Controller to Time provider (request current time); Time provider to Time service (HTTPS request).
Annotations: External HTTPS dependency at the time-service node.
Key decisions
Where should time reads and light output meet the control logic?
Direct HTTP calls and output writes inside schedule calculations would make behaviour depend on network and device side effects.
- Selected
- Put ITimeProvider and ILightOutput at the controller boundary. Keep LightSchedule free of I/O and mutable state. Supply HttpTimeProvider and ConsoleLightOutput through dependency injection.
- Alternatives
- Read system or network time and write output directly inside the schedule policy
- Trade-off
- The interfaces and adapters add a little structure to a single-output program.
- Consequence
- Scheduling is testable with supplied times, and a future time source or physical output adapter can be substituted without rewriting the policy.
When should an unavailable time source turn the light off?
One failed poll gives no trustworthy current time, but changing an enabled output immediately would react to a transient outage. Keeping the previous state forever would leave an unknown schedule state in place.
- Selected
- Count consecutive TimeProviderException failures. Preserve the last output below the validated threshold, force it off at the threshold, and clear the count and reapply the schedule on the next successful read.
- Alternatives
- Turn off after every failed read; Retain the last output indefinitely
- Trade-off
- The output may remain on briefly while time is unavailable below the threshold. The threshold and polling interval define that exposure.
- Consequence
- Safe-mode entry is a distinct logged transition, later failed polls do not repeat the output change, and recovery follows the configured schedule immediately.
How can the tests exercise failure and recovery without an external clock?
Wall-clock waits and real HTTP calls would make boundary and outage scenarios slow or unpredictable.
- Selected
- Supply fixed or mutable times and in-memory outputs to controller tests. Use an in-process HttpMessageHandler for HTTP provider integration tests. Keep acceptance scenarios at the controller boundary.
- Alternatives
- Run tests against the live time service or a manually started mock server
- Trade-off
- These suites validate controller behaviour and HTTP parsing, but they do not measure a deployed provider or physical device.
- Consequence
- Normal tests run locally and deterministically while covering schedule edges, transport failures, safe-mode shutdown, and recovery.
Reliability and quality
- Schedule and state checks
- xUnit tests cover same-day and overnight boundaries, equal start and end, failure counting, idempotent output transitions, cancellation, safe mode, recovery, and options validation.
- HTTP boundary checks
- Integration tests replace HttpMessageHandler in-process to exercise time-zone URI construction, offset parsing, HTTP errors, malformed responses, timeout, cancellation, and transient recovery.
- Acceptance scenarios
- In-memory scenarios follow an overnight window across boundaries and exercise one failed read, threshold shutdown, and restored operation after recovery.
- Delivery and security gates
- CI uses locked restore, format verification, a Release build, three test suites, coverage artifact collection, and a transitive runtime dependency audit. CodeQL analyzes C# on pushes, pull requests, and a weekly schedule. Dependabot proposes NuGet and Actions updates monthly. No coverage threshold is enforced.
Trace the control path
- Hosted worker (opens in a new tab) makes the first check and manages periodic, cancellable polls. LightController (opens in a new tab) owns the failure count, safe-mode transition, and output changes.
- LightSchedule (opens in a new tab) applies the half-open daily window:
08:00–12:00includes 08:00 and excludes 12:00.20:00–06:00spans midnight. Equal boundaries define an empty window. - HttpTimeProvider (opens in a new tab) reads an offset-bearing datetime through HTTPS. Program (opens in a new tab) wires it to
ITimeProvider, the simulatedILightOutput, and options validated on startup.
What next
A device adapter and operational measurements would be the next evidence needed for physical use. A local or NTP-backed time source could also reduce dependence on the configured HTTP service.
Limitations
- The supplied ILightOutput adapter only stores state for a console simulation. There is no physical-device adapter or evidence of hardware behaviour.
- Runtime accuracy and availability depend on the configured external HTTPS time provider. The tests replace its transport and do not establish live-service reliability.
- The scheduling model covers one daily window for one output. It has no calendars, exceptions, or multiple-device coordination.
- This is a single-process reference implementation, not a deployed home-automation system. The release contains source and no device package.