Skip to content

Commit 5ef48b9

Browse files
committed
AI-instructions
1 parent 3f59374 commit 5ef48b9

1 file changed

Lines changed: 254 additions & 0 deletions

File tree

.github/copilot-instructions.md

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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

Comments
 (0)