<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Florian Schaal</title><description>Technical articles, tutorials, and notes</description><link>https://florianschaal.com/</link><language>en-us</language><item><title>.NET 11 Preview 1: What Caught My Eye</title><link>https://florianschaal.com/blog/dotnet-11-preview-1/</link><guid isPermaLink="true">https://florianschaal.com/blog/dotnet-11-preview-1/</guid><description>A look at the highlights from .NET 11 Preview 1, covering runtime async, CoreCLR on WebAssembly, Zstandard compression, and SDK improvements.</description><pubDate>Sat, 14 Feb 2026 00:00:00 GMT</pubDate><content:encoded>Microsoft just shipped [.NET 11 Preview 1](https://devblogs.microsoft.com/dotnet/dotnet-11-preview-1/), and there&apos;s a lot to unpack. This is a preview release, not production-ready, but it gives a clear picture of where the platform is heading. Here&apos;s what stood out to me.

## Runtime: Async Gets a Rethink

The headline feature is **Runtime Async**, a fundamental change to how async/await works under the hood. Today, every `async` method allocates a state machine object on the heap. Runtime Async moves this into the runtime itself. That means fewer allocations and real performance gains for async-heavy workloads. If you&apos;re building services that do a lot of I/O (and in .NET, that&apos;s most of us), this is worth watching.

Alongside this, **CoreCLR now runs on WebAssembly**. Previously, Blazor WebAssembly used the Mono runtime. Moving to CoreCLR brings better performance and closer parity with server-side .NET behavior. For .NET MAUI, CoreCLR is now also the default runtime on Android.

## Libraries: Practical Additions

The libraries team delivered several additions that address real-world needs:

**Zstandard compression** (`System.IO.Compression`) is now built in. Zstandard offers better compression ratios than gzip at similar speeds. If you&apos;re compressing data for storage or transit, this is a welcome addition without needing third-party packages.

**BFloat16** is a new 16-bit floating-point type built for machine learning workloads. If you&apos;re doing ML inference in .NET, this gives you a native type that matches what most ML models use internally.

**FrozenDictionary collection expressions** let you create `FrozenDictionary` and `FrozenSet` using collection expression syntax, making immutable lookup tables cleaner to write:

```csharp
FrozenDictionary&lt;string, int&gt; statusCodes = [
    new(&quot;OK&quot;, 200),
    new(&quot;NotFound&quot;, 404),
    new(&quot;InternalServerError&quot;, 500)
];
```

Other notable additions:
- **MediaTypeMap** for MIME type lookups, so you can stop hardcoding content types
- **Hard link creation APIs** via `File.CreateHardLink`
- **Happy Eyeballs** support in `Socket.ConnectAsync` for faster dual-stack connections
- **HMAC/KMAC verification methods** for cleaner cryptographic validation

## SDK and Tooling

The developer experience improvements are subtle but appreciated:

`dotnet run` now supports **interactive target framework selection**. If your project multi-targets, it&apos;ll prompt you to choose instead of failing or picking one silently.

`dotnet watch` got **hot reload for reference changes** and **configurable ports**, making the inner dev loop smoother.

`dotnet test` now accepts **positional arguments**, so `dotnet test MyTests` works instead of requiring the full `--project` flag.

## ASP.NET Core and Blazor

Several quality-of-life improvements for web developers:

**EnvironmentBoundary component** lets you conditionally render Blazor components based on the hosting environment. Great for showing debug panels only in development.

**QuickGrid OnRowClick** adds row-level click handling to the built-in grid component, which was a common request.

**Blazor WebAssembly now supports `IHostedService`**, letting you run background tasks in the browser. Think periodic data refresh or WebSocket keep-alive.

On the API side, **OpenAPI gets binary file response schema support**, and there&apos;s a new **IOutputCachePolicyProvider** interface for more flexible output caching strategies.

## Entity Framework Core

EF Core gets some solid improvements:

**Complex types and JSON columns now work with TPT/TPC inheritance**. Previously you had to choose between these features, which was a frustrating limitation.

**Single-step migration creation and application** means `dotnet ef migrations add` can now also apply the migration in one command. No more create-then-update dance.

**Azure Cosmos DB transactional batches** and **bulk execution** bring better performance for Cosmos DB workloads.

## C# Language

C# picks up **collection expression arguments**, extending the collection expression syntax to support constructor parameters. This makes initializing collections with specific capacities or comparers more natural.

## F# Gets Faster

The F# compiler now has **parallel compilation enabled by default**, which should noticeably speed up build times for larger F# projects. There&apos;s also faster computation expression compilation and new FSI options for type-checking without execution.

## My Take

This preview is less about flashy new features and more about infrastructure improvements that make everyday .NET development better. Runtime Async and CoreCLR on WebAssembly are the big architectural bets, while the library and SDK additions address real friction points.

For those of us in the .NET ecosystem, the Zstandard support, EF Core inheritance fixes, and SDK quality-of-life improvements are the kind of things that reduce the number of workarounds in production code. That&apos;s always welcome.

You can try it out today by [downloading .NET 11 Preview 1](https://dotnet.microsoft.com/download/dotnet/11.0) or updating your global.json. Just don&apos;t deploy it to production yet.</content:encoded></item><item><title>Smart Garden Watering with Node-RED and Home Assistant</title><link>https://florianschaal.com/blog/smart-garden-watering-node-red/</link><guid isPermaLink="true">https://florianschaal.com/blog/smart-garden-watering-node-red/</guid><description>How I built an automated irrigation system that adapts to temperature and rainfall using Node-RED, Gardena smart valves, and weather data.</description><pubDate>Sun, 25 Jan 2026 00:00:00 GMT</pubDate><content:encoded>After spending one too many summers manually turning on sprinklers during a heatwave (or worse, forgetting to), I decided it was time to automate my garden watering. The result is a Node-RED flow that evaluates weather conditions daily at sunrise and decides whether to water and for how long.

## What the System Does

The core idea is simple: water the garden based on three factors:

1. **Maximum temperature** since the last watering
2. **Days elapsed** since the last watering
3. **Total rainfall** (actual + expected) since the last watering

Every morning at sunrise, the system evaluates these conditions and either starts a watering cycle or skips the day. When it waters, it sequences through four zones one at a time to maintain adequate water pressure.

## Hardware Setup

My setup uses two types of [Gardena](https://www.gardena.com/int/products/smart-system/) smart irrigation products:

```
                         ┌─────────────────┐
                         │   Water Main    │
                         └────────┬────────┘
                                  │
              ┌───────────────────┼───────────────────┐
              │                                       │
              ▼                                       ▼
    ┌─────────────────┐                    ┌─────────────────┐
    │ Gardena Smart   │                    │  Outdoor Tap    │
    │ Gateway         │                    └────────┬────────┘
    └────────┬────────┘                             │
             │                                      ▼
    ┌────────▼────────┐                    ┌─────────────────┐
    │ 3-Valve Smart   │                    │ Bluetooth Valve │
    │ Distribution Box│                    │ (Gardena)       │
    └────────┬────────┘                    └────────┬────────┘
             │                                      │
     ┌───────┼───────┐                              ▼
     │       │       │                     ┌─────────────────┐
     ▼       ▼       ▼                     │ Gardena Water   │
   Lawn   Planter  Planter                 │ Stop + Spray    │
          Left     Right                   └─────────────────┘
          (Drip)   (Drip)                     Front Yard
```

**Backyard (via Smart Gateway):**
- **Lawn** - Standard sprinkler coverage
- **Left Planter** - Drip irrigation system
- **Right Planter** - Drip irrigation system

**Front yard (via Bluetooth valve):**
- **Front Yard** - Connected to a Gardena water stop with spray nozzle

The zones are watered sequentially rather than simultaneously because running multiple zones would drop the water pressure too much for effective coverage.

## The Decision Logic

Every day at sunrise, the system runs through this decision tree:

```
                    ┌─────────────┐
                    │   Sunrise   │
                    └──────┬──────┘
                           │
                           ▼
              ┌────────────────────────┐
              │ Days since watering &gt;4?│
              └────────────┬───────────┘
                     │           │
                    Yes          No
                     │           │
                     ▼           ▼
              ┌──────────┐ ┌─────────────────────┐
              │Reset all │ │Check max temperature│
              │counters  │ └──────────┬──────────┘
              └──────────┘            │
                                      ▼
                          ┌───────────────────────┐
                          │ Match temperature tier │
                          │ &amp; check days threshold │
                          └───────────┬───────────┘
                                      │
                                      ▼
                          ┌───────────────────────┐
                          │ Rain &lt; threshold for  │
                          │ this tier?            │
                          └───────────┬───────────┘
                                │           │
                               Yes          No
                                │           │
                                ▼           ▼
                        ┌────────────┐ ┌──────────┐
                        │Start cycle │ │Skip today│
                        └────────────┘ └──────────┘
```

## Temperature Tiers

The watering duration and conditions depend on how hot it&apos;s been:

| Max Temp | Days Since Last | Rain Threshold | Duration |
|----------|-----------------|----------------|----------|
| &gt; 31°C   | any             | &lt; 20.1mm       | 90 min   |
| 26-31°C  | ≥ 2 days        | &lt; 15.1mm       | 75 min   |
| 21-26°C  | ≥ 3 days        | &lt; 10.1mm       | 60 min   |
| 15-21°C  | ≥ 3 days        | &lt; 10.1mm       | 45 min   |
| Any      | &gt; 4 days        | - (safety)     | reset    |

**Visual representation:**

```
Duration (min)
    │
 90 ├────────────────────────────────────────────── &gt; 31°C (extreme heat)
    │
 75 ├──────────────────────────────────── 26-31°C (hot)
    │
 60 ├────────────────────────── 21-26°C (warm)
    │
 45 ├──────────────── 15-21°C (mild)
    │
  0 ├──── &lt; 15°C (skip watering)
    └────┬─────┬─────┬─────┬─────┬────► Temperature (°C)
        15    21    26    31    35
```

The logic is intentionally conservative: during extreme heat, water every day regardless of how recently you watered. During milder temperatures, wait a few days between cycles.

## Rain Tracking

Rain data comes from the [OpenWeatherMap](https://openweathermap.org/) integration in Home Assistant. The system tracks two values:

1. **Actual rainfall** - How much rain has fallen since the last watering
2. **Expected rainfall** - Today&apos;s forecast: `precipitation amount × probability`

For example, if the forecast shows 5mm with 60% probability, the expected rain is `5 × 0.6 = 3mm`.

Both values are accumulated between watering sessions and summed to determine if we&apos;ve had &quot;enough&quot; rain. This approach isn&apos;t perfect (sometimes forecasts are wrong), but it&apos;s been reliable enough in practice.

```javascript
// Simplified rain calculation
const actualRain = msg.payload; // from weather sensor
const currentTotal = parseFloat(global.get(&apos;rain_since_watering&apos;)) || 0;
const newTotal = currentTotal + actualRain;
global.set(&apos;rain_since_watering&apos;, newTotal);
```

## Temperature Tracking

Temperature tracking follows a similar pattern, but we only care about the **maximum** temperature since the last watering:

```javascript
// Temperature tracking logic
const currentTemp = msg.payload;
const storedMax = parseFloat(global.get(&apos;max_temp_since_watering&apos;)) || 0;

if (currentTemp &gt; storedMax) {
    global.set(&apos;max_temp_since_watering&apos;, currentTemp);
}
```

This runs every time the temperature sensor updates. After watering (or after 4+ days), the maximum is reset to start fresh.

## The Watering Sequence

When watering is triggered, each zone runs sequentially. Here&apos;s what a 90-minute cycle looks like:

```
Time ────────────────────────────────────────────────────────────────────►

Sunrise                                                              ~6h later
   │                                                                      │
   ▼                                                                      ▼
   ┌─────────┐
   │Evaluate │
   │Conditions│
   └────┬────┘
        │
        ▼
   ┌─────────────────────────────────────────────────────────────────────┐
   │ Notification: &quot;Watering started for 90 minutes&quot;                     │
   └─────────────────────────────────────────────────────────────────────┘
        │
        ▼
   ┌─────────────────────────────────────────────────────────────────────┐
   │ Front Yard                                                          │
   │ ████████████████████████████████████████████████████████████████    │
   │ 0 ──────────────────────────────────────────────────────────► 90min │
   └─────────────────────────────────────────────────────────────────────┘
        │
        ▼
   ┌─────────────────────────────────────────────────────────────────────┐
   │ Lawn                                                                │
   │ ████████████████████████████████████████████████████████████████    │
   │ 0 ──────────────────────────────────────────────────────────► 90min │
   └─────────────────────────────────────────────────────────────────────┘
        │
        ▼
   ┌─────────────────────────────────────────────────────────────────────┐
   │ Left Planter (drip)                                                 │
   │ ████████████████████████████████████████████████████████████████    │
   │ 0 ──────────────────────────────────────────────────────────► 90min │
   └─────────────────────────────────────────────────────────────────────┘
        │
        ▼
   ┌─────────────────────────────────────────────────────────────────────┐
   │ Right Planter (drip)                                                │
   │ ████████████████████████████████████████████████████████████████    │
   │ 0 ──────────────────────────────────────────────────────────► 90min │
   └─────────────────────────────────────────────────────────────────────┘
        │
        ▼
   ┌─────────────────────────────────────────────────────────────────────┐
   │ Notification: &quot;Watering stopped&quot;                                    │
   └─────────────────────────────────────────────────────────────────────┘
        │
        ▼
   ┌─────────────────────────────────────────────────────────────────────┐
   │ Reset: days counter, max temp, rain accumulator                     │
   └─────────────────────────────────────────────────────────────────────┘
```

A full 90-minute cycle across all four zones takes 6 hours to complete. The morning start time ensures everything is done by early afternoon.

## Node-RED Flow Overview

The flow uses these key node types:

**Triggers:**
- `state-change` nodes watching `sun.sun` for sunrise
- `state-change` nodes watching temperature and rain sensors

**Logic:**
- `function` nodes for calculations and threshold comparisons
- `switch` nodes for routing based on conditions
- `current-state` nodes to check sensor values

**Actions:**
- `call-service` nodes to turn valves on/off
- `delay` nodes for watering duration
- `api-call-service` for mobile notifications

**Flow control:**
- `link-in` and `link-out` nodes for the reset sequence

Here&apos;s a simplified version of the main evaluation function:

```javascript
const maxTemp = parseFloat(global.get(&apos;max_temp_since_watering&apos;)) || 0;
const daysSince = parseInt(global.get(&apos;days_since_watering&apos;)) || 0;
const totalRain = parseFloat(global.get(&apos;rain_since_watering&apos;)) || 0;

// Safety reset after 4+ days
if (daysSince &gt; 4) {
    return [null, null, null, null, { payload: &apos;reset&apos; }];
}

// Extreme heat: &gt;31°C, any days, &lt;20.1mm rain
if (maxTemp &gt; 31 &amp;&amp; totalRain &lt; 20.1) {
    return [{ payload: 90 }, null, null, null, null]; // 90 min
}

// Hot: 26-31°C, 2+ days, &lt;15.1mm rain
if (maxTemp &gt; 26 &amp;&amp; maxTemp &lt;= 31 &amp;&amp; daysSince &gt;= 2 &amp;&amp; totalRain &lt; 15.1) {
    return [null, { payload: 75 }, null, null, null]; // 75 min
}

// Warm: 21-26°C, 3+ days, &lt;10.1mm rain
if (maxTemp &gt; 21 &amp;&amp; maxTemp &lt;= 26 &amp;&amp; daysSince &gt;= 3 &amp;&amp; totalRain &lt; 10.1) {
    return [null, null, { payload: 60 }, null, null]; // 60 min
}

// Mild: 15-21°C, 3+ days, &lt;10.1mm rain
if (maxTemp &gt; 15 &amp;&amp; maxTemp &lt;= 21 &amp;&amp; daysSince &gt;= 3 &amp;&amp; totalRain &lt; 10.1) {
    return [null, null, null, { payload: 45 }, null]; // 45 min
}

// Skip watering
return null;
```

Each output connects to a different watering duration flow.

## Lessons Learned After One Season

After running this system through a full summer, I&apos;ve made several adjustments:

**Temperature thresholds needed tweaking.** My initial thresholds were too conservative. During a 35°C+ heatwave, even 90 minutes wasn&apos;t enough for some plants. I added a manual boost option for extreme conditions.

**Drip systems need longer.** The planters with drip irrigation needed roughly 50% longer run times compared to the lawn sprinklers to achieve equivalent saturation. I adjusted the per-zone durations accordingly.

**Rain prediction is imperfect.** Occasionally the system skips a day expecting rain that never comes, or waters just before an unexpected shower. It&apos;s still better than manual management, but there&apos;s room for improvement.

**Notifications are essential.** Getting a mobile notification when watering starts lets me catch any issues (forgotten hose still connected, etc.) before 6 hours of watering begins.

## Known Limitations

The current system has a few gaps I&apos;m aware of:

**No mid-cycle abort.** If it starts raining heavily while watering is in progress, the system will continue until the cycle completes. Adding a rain sensor trigger to stop early is on my to-do list.

**No soil moisture feedback.** The system uses weather data as a proxy for soil conditions, but actual soil moisture sensors would provide ground truth. The Gardena ecosystem doesn&apos;t include these, so it would require additional hardware.

**Manual intervention during heatwaves.** Extended heat periods sometimes need manual watering boosts beyond what the automation provides. A &quot;heatwave mode&quot; with more aggressive settings could help.

## Future Improvements

Some enhancements I&apos;m considering:

- **Rain sensor integration** - Abort watering if significant rain is detected during a cycle
- **Soil moisture sensors** - Replace weather-based guessing with actual measurements
- **Per-zone adjustments** - Different plants have different needs; the planters could use separate logic from the lawn
- **Seasonal presets** - Automatically adjust thresholds based on time of year

## Was It Worth It?

Automating garden watering with Node-RED and [Home Assistant](https://www.home-assistant.io/) has been one of my more satisfying home automation projects. The system handles 90% of watering decisions correctly, which is enough to keep the garden healthy while freeing me from the daily chore of manual irrigation.

If you&apos;re considering a similar setup, start simple. Begin with one zone and basic temperature-based logic, then add complexity as you learn what your garden actually needs. The beauty of [Node-RED](https://nodered.org/) is that you can iterate quickly. Just drag, connect, and deploy.

For questions or suggestions, feel free to reach out on [GitHub](https://github.com/fschaal).</content:encoded></item></channel></rss>