|
| 1 | +# Gardena Smart System Integration - Copilot Instructions |
| 2 | + |
| 3 | +## Architecture Overview |
| 4 | + |
| 5 | +This is a **Home Assistant custom integration** for Gardena Smart System devices using OAuth2 authentication and WebSocket real-time updates. The architecture follows Home Assistant's entity-platform pattern with custom Gardena API client. |
| 6 | + |
| 7 | +### Key Components |
| 8 | + |
| 9 | +- **`custom_components/gardena_smart_system/`** - Main integration package |
| 10 | +- **`gardena/`** - Internal API client library (embedded, not external dependency) |
| 11 | +- **`smartsystem.py`** - Core API client with OAuth2 + WebSocket handling |
| 12 | +- **Entity platforms**: `valve.py`, `sensor.py`, `lawn_mower.py`, `switch.py`, `binary_sensor.py` |
| 13 | + |
| 14 | +### Data Flow Architecture |
| 15 | + |
| 16 | +``` |
| 17 | +OAuth2 Auth → SmartSystem → Location → Devices → HA Entities |
| 18 | + ↓ |
| 19 | + WebSocket (real-time) |
| 20 | +``` |
| 21 | + |
| 22 | +**Critical Pattern**: The integration uses `hass.data[DOMAIN][GARDENA_LOCATION]` as the central data store where `GARDENA_LOCATION` contains a `Location` object with all devices. |
| 23 | + |
| 24 | +## Development Workflows |
| 25 | + |
| 26 | +### Running & Debugging |
| 27 | +```bash |
| 28 | +# Start HA in debug mode (uses config/ directory) |
| 29 | +scripts/develop |
| 30 | + |
| 31 | +# Lint code (ruff format + check) |
| 32 | +scripts/lint |
| 33 | +``` |
| 34 | + |
| 35 | +**Key**: The `scripts/develop` sets `PYTHONPATH` to include `custom_components/` directory, allowing local development without symlinking. |
| 36 | + |
| 37 | +### Project-Specific Patterns |
| 38 | + |
| 39 | +#### 1. Entity Setup Pattern |
| 40 | +All platforms use this pattern in `async_setup_entry()`: |
| 41 | +```python |
| 42 | +entities = [] |
| 43 | +for device in hass.data[DOMAIN][GARDENA_LOCATION].find_device_by_type("DEVICE_TYPE"): |
| 44 | + entities.append(EntityClass(device, config_entry.options)) |
| 45 | +``` |
| 46 | + |
| 47 | +#### 2. WebSocket Session Management |
| 48 | +**Critical**: The integration has sophisticated session cleanup to prevent "Simultaneous logins detected" errors: |
| 49 | +- `_check_and_cleanup_existing_sessions()` in `__init__.py` |
| 50 | +- Always reuse authenticated sessions via `GardenaSmartSystem` wrapper |
| 51 | +- WebSocket connections are managed centrally in `SmartSystem` class |
| 52 | + |
| 53 | +#### 3. Device Update Callbacks |
| 54 | +Devices use callback pattern for real-time updates: |
| 55 | +```python |
| 56 | +device.add_callback(entity.update_callback) |
| 57 | +``` |
| 58 | + |
| 59 | +#### 4. Rate Limiting & Backoff |
| 60 | +API calls use `@backoff` decorators with exponential backoff (`MAX_BACKOFF_VALUE = 900`). All HTTP calls go through `SmartSystem.create_header()` with proper authentication. |
| 61 | + |
| 62 | +## Integration Points |
| 63 | + |
| 64 | +### OAuth2 Flow |
| 65 | +- Uses `authlib` for OAuth2 with Husqvarna API |
| 66 | +- `TokenManager` handles token refresh automatically |
| 67 | +- Authentication host: `api.authentication.husqvarnagroup.dev` |
| 68 | +- API host: `api.smart.gardena.dev` |
| 69 | + |
| 70 | +### WebSocket Real-Time Updates |
| 71 | +- Established after OAuth2 authentication |
| 72 | +- Handles device state changes within seconds |
| 73 | +- Automatic reconnection on connection loss |
| 74 | +- Uses `websockets` library, not Home Assistant's WebSocket |
| 75 | + |
| 76 | +### Device Types & Services |
| 77 | +- **VALVE**: Water control, smart irrigation (with duration configuration) |
| 78 | +- **MOWER**: Robotic mowers with scheduling |
| 79 | +- **SENSOR**: Environmental sensors, soil sensors |
| 80 | +- **POWER_SOCKET**: Smart outlets |
| 81 | +- **Binary sensors**: Connectivity, error states |
| 82 | + |
| 83 | +## Configuration |
| 84 | + |
| 85 | +### manifest.json Dependencies |
| 86 | +```json |
| 87 | +"requirements": [ |
| 88 | + "oauthlib==3.2.2", "authlib>=1.2.0", "httpx>=0.24.0", |
| 89 | + "websockets", "backoff>=2.0.0" |
| 90 | +] |
| 91 | +``` |
| 92 | + |
| 93 | +### Config Flow |
| 94 | +Uses `CONFIG_SCHEMA` with `application_key` and `application_secret` from Gardena Developer Portal. Options include duration settings for different device types. |
| 95 | + |
| 96 | +## Error Handling Patterns |
| 97 | + |
| 98 | +### Simultaneous Login Prevention |
| 99 | +The integration has sophisticated logic to detect and cleanup existing sessions. Always check `__init__.py` session cleanup code when debugging authentication issues. |
| 100 | + |
| 101 | +### Pending State Protection |
| 102 | +Valve entities use `PENDING_STATE_TIMEOUT_SECONDS = 10` to prevent conflicts during user actions. |
| 103 | + |
| 104 | +## Common Debugging Scenarios |
| 105 | + |
| 106 | +### "Simultaneous logins detected" Error |
| 107 | +**Symptoms**: Integration fails to authenticate, logs show simultaneous login errors |
| 108 | +**Root Cause**: Multiple SmartSystem instances or lingering WebSocket connections |
| 109 | +**Debug Steps**: |
| 110 | +1. Check `_check_and_cleanup_existing_sessions()` in `__init__.py` |
| 111 | +2. Look for existing sessions in `hass.data[DOMAIN]` |
| 112 | +3. Verify WebSocket cleanup in SmartSystem destructor |
| 113 | +4. Restart Home Assistant if cleanup fails |
| 114 | + |
| 115 | +### WebSocket Connection Issues |
| 116 | +**Symptoms**: Device states not updating in real-time, WebSocket connection errors |
| 117 | +**Debug Steps**: |
| 118 | +1. Enable debug logging: `custom_components.gardena_smart_system: debug` |
| 119 | +2. Check `SmartSystem.start_ws()` connection establishment |
| 120 | +3. Monitor WebSocket task lifecycle in `_handle_ws_messages()` |
| 121 | +4. Verify SSL context configuration in `smart_system.py` |
| 122 | + |
| 123 | +### Valve State Synchronization Problems |
| 124 | +**Symptoms**: Valve shows wrong state, commands don't reflect immediately |
| 125 | +**Debug Steps**: |
| 126 | +1. Check pending state protection in `valve.py` (10-second window) |
| 127 | +2. Verify callback registration: `device.add_callback(entity.update_callback)` |
| 128 | +3. Monitor WebSocket messages for valve state changes |
| 129 | +4. Check duration timer updates in `_async_timer_update()` |
| 130 | + |
| 131 | +### Authentication Token Refresh Failures |
| 132 | +**Symptoms**: Integration stops working after period of time, 401/403 errors |
| 133 | +**Debug Steps**: |
| 134 | +1. Check `TokenManager` token refresh logic |
| 135 | +2. Verify OAuth2 client configuration in `SmartSystem` |
| 136 | +3. Monitor token expiration handling |
| 137 | +4. Check rate limiting backoff in API calls |
| 138 | + |
| 139 | +## Key Files for Understanding |
| 140 | + |
| 141 | +- **`__init__.py`**: Session management, service registration, cleanup logic |
| 142 | +- **`gardena/smart_system.py`**: Core API client, WebSocket, authentication |
| 143 | +- **`valve.py`**: Shows entity pattern with real-time updates and state management |
| 144 | +- **`const.py`**: All domain constants, default durations, rate limiting settings |
| 145 | +- **`config_flow.py`**: Configuration UI and validation patterns |
| 146 | + |
| 147 | +## Device-Specific Entity Patterns |
| 148 | + |
| 149 | +### Valve Entities (Water Control & Smart Irrigation) |
| 150 | +**Duration Configuration**: Each valve type has configurable default durations from config options: |
| 151 | +```python |
| 152 | +@property |
| 153 | +def option_smart_watering_duration(self) -> int: |
| 154 | + return self._options.get(CONF_SMART_WATERING_DURATION, DEFAULT_SMART_WATERING_DURATION) |
| 155 | +``` |
| 156 | + |
| 157 | +**Real-time Timer Updates**: Valves show live countdown during operation: |
| 158 | +```python |
| 159 | +async def _async_timer_update(self, now) -> None: |
| 160 | + """Update remaining time for active valves.""" |
| 161 | + if self.is_on and self._remaining_time > 0: |
| 162 | + self._remaining_time = max(0, self._remaining_time - 1) |
| 163 | + self.async_write_ha_state() |
| 164 | +``` |
| 165 | + |
| 166 | +**Pending State Protection**: Prevents conflicts during user actions: |
| 167 | +```python |
| 168 | +if self._last_state_change and (datetime.now(UTC) - self._last_state_change).total_seconds() < PENDING_STATE_TIMEOUT_SECONDS: |
| 169 | + return # Skip state updates during pending window |
| 170 | +``` |
| 171 | + |
| 172 | +### Mower Entities |
| 173 | +**Activity State Mapping**: Mowers have complex state mapping from Gardena API: |
| 174 | +```python |
| 175 | +@property |
| 176 | +def state(self): |
| 177 | + if self._activity == "PAUSED": return "paused" |
| 178 | + elif self._activity == "OK_CUTTING": return "mowing" |
| 179 | + elif self._activity == "PARKED_TIMER": return "docked" |
| 180 | +``` |
| 181 | + |
| 182 | +**Service Integration**: Mowers expose Home Assistant services for control: |
| 183 | +- `start_mowing_service()` - Duration-based mowing |
| 184 | +- `park_until_next_task_service()` - Scheduled parking |
| 185 | +- `park_until_further_notice_service()` - Manual parking |
| 186 | + |
| 187 | +### Sensor Entities |
| 188 | +**Multi-type Support**: Single platform handles multiple sensor types: |
| 189 | +```python |
| 190 | +# Environmental sensors (temperature, humidity, light) |
| 191 | +for sensor in hass.data[DOMAIN][GARDENA_LOCATION].find_device_by_type("SENSOR"): |
| 192 | + if hasattr(sensor, 'temperature'): |
| 193 | + entities.append(GardenaSmartSensorTemperature(sensor)) |
| 194 | + |
| 195 | +# Soil sensors (moisture, temperature) |
| 196 | +for sensor in hass.data[DOMAIN][GARDENA_LOCATION].find_device_by_type("SOIL_SENSOR"): |
| 197 | + entities.append(GardenaSmartSoilTemperature(sensor)) |
| 198 | +``` |
| 199 | + |
| 200 | +## WebSocket Message Handling |
| 201 | + |
| 202 | +### Connection Establishment |
| 203 | +WebSocket connects after OAuth2 authentication using secure WebSocket: |
| 204 | +```python |
| 205 | +async def start_ws(self) -> None: |
| 206 | + """Start WebSocket connection for real-time updates.""" |
| 207 | + if not self.token_manager.access_token: |
| 208 | + await self.authenticate() |
| 209 | + |
| 210 | + ws_url = f"wss://api.smart.gardena.dev/v1/websocket?access_token={self.token_manager.access_token}" |
| 211 | + self.ws = await connect(ws_url, ssl=self._ssl_context) |
| 212 | + self.is_ws_connected = True |
| 213 | +``` |
| 214 | + |
| 215 | +### Message Processing Pattern |
| 216 | +WebSocket messages trigger device updates via callback system: |
| 217 | +```python |
| 218 | +async def _handle_ws_messages(self) -> None: |
| 219 | + """Process incoming WebSocket messages.""" |
| 220 | + async for message in self.ws: |
| 221 | + try: |
| 222 | + data = json.loads(message) |
| 223 | + if data.get("type") == "LOCATION": |
| 224 | + # Update device data |
| 225 | + location = self.locations[data["id"]] |
| 226 | + location.update_devices(data) |
| 227 | + # Callbacks automatically notify HA entities |
| 228 | + except JSONDecodeError: |
| 229 | + self.logger.error("Invalid WebSocket message format") |
| 230 | +``` |
| 231 | + |
| 232 | +### Real-time State Updates |
| 233 | +Device callbacks trigger immediate Home Assistant state updates: |
| 234 | +```python |
| 235 | +def update_callback(self, device) -> None: |
| 236 | + """Called when device receives WebSocket update.""" |
| 237 | + if self._device.id == device.id: |
| 238 | + # Update entity state from device data |
| 239 | + self._state = device.activity |
| 240 | + self._remaining_time = device.remaining_time |
| 241 | + # Immediately update HA state |
| 242 | + self.async_write_ha_state() |
| 243 | +``` |
| 244 | + |
| 245 | +### WebSocket Error Handling & Reconnection |
| 246 | +Automatic reconnection on connection loss: |
| 247 | +```python |
| 248 | +except ConnectionClosed: |
| 249 | + self.logger.warning("WebSocket connection closed, attempting reconnection") |
| 250 | + self.is_ws_connected = False |
| 251 | + await asyncio.sleep(5) # Brief delay before reconnect |
| 252 | + if not self.should_stop: |
| 253 | + await self.start_ws() # Automatic reconnection |
| 254 | +``` |
0 commit comments