Skip to content

Commit a6f0659

Browse files
Merge pull request #33 from TheRealFalseReality/copilot/add-toggle-switch-entities
Add parameter-level toggle switches for AI analysis control (including camera)
2 parents 37889f2 + ab6b07f commit a6f0659

File tree

6 files changed

+250
-68
lines changed

6 files changed

+250
-68
lines changed

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,26 @@ These `binary_sensor` entities provide simple on/off states:
148148

149149
* `binary_sensor.[tank_name]_water_change_needed`: Indicates whether a water change is currently recommended (On = Yes, Off = No).
150150

151+
### Parameter Analysis Toggle Switches
152+
153+
These `switch` entities control which parameters are included in AI analysis, allowing you to save on tokens and rate limits:
154+
155+
* `switch.[tank_name]_analyze_temperature`: Enable/disable AI analysis of temperature readings.
156+
* `switch.[tank_name]_analyze_ph`: Enable/disable AI analysis of pH readings.
157+
* `switch.[tank_name]_analyze_salinity`: Enable/disable AI analysis of salinity readings.
158+
* `switch.[tank_name]_analyze_dissolved_oxygen`: Enable/disable AI analysis of dissolved oxygen readings.
159+
* `switch.[tank_name]_analyze_water_level`: Enable/disable AI analysis of water level readings.
160+
* `switch.[tank_name]_analyze_orp`: Enable/disable AI analysis of ORP (oxidation-reduction potential) readings.
161+
* `switch.[tank_name]_analyze_camera`: Enable/disable AI visual analysis from camera images.
162+
163+
**Usage:**
164+
* When a parameter toggle is **ON** (default), the AI will analyze that parameter and include it in notifications.
165+
* When a parameter toggle is **OFF**, the AI will skip analysis of that parameter entirely, reducing token usage.
166+
* The **camera toggle** controls whether camera images are sent to the AI for visual analysis - useful when you want to reduce token usage or don't need visual monitoring temporarily.
167+
* Useful for temporarily disabling analysis of specific parameters that don't need monitoring.
168+
* Changes take effect on the next scheduled analysis or when you manually trigger an analysis via service call.
169+
* Only switches for configured sensors/camera will be created (e.g., if you don't have a camera configured, there won't be an "Analyze Camera" switch).
170+
151171
![Sensors](/assets/sensors_example.png)
152172

153173
Example Card:
@@ -247,13 +267,15 @@ Visual analysis complements sensor readings by providing context that numbers al
247267

248268
The integration adds services that allow you to trigger analysis updates manually. This is useful for creating automations based on specific events (e.g., after a water change).
249269

270+
**Note:** Only parameters with their analysis toggle switches enabled will be included in the analysis. Use the parameter analysis toggle switches to control which parameters are analyzed and save on AI tokens/rate limits.
271+
250272
### Service: `aquarium_ai.run_analysis`
251273

252274
Triggers analysis for **all configured aquariums**.
253275

254276
This service will:
255277

256-
* Update all AI analysis sensors with fresh analysis
278+
* Update all AI analysis sensors with fresh analysis (only for parameters with analysis enabled)
257279
* Update all status sensors with current readings
258280
* Send a notification (if notifications are enabled)
259281

@@ -267,7 +289,7 @@ Triggers analysis for **a specific aquarium**.
267289

268290
This service will:
269291

270-
* Update all AI analysis sensors with fresh analysis for the selected aquarium
292+
* Update all AI analysis sensors with fresh analysis for the selected aquarium (only for parameters with analysis enabled)
271293
* Update all status sensors with current readings for the selected aquarium
272294
* Send a notification (if notifications are enabled and send_notification is true) for the selected aquarium
273295

custom_components/aquarium_ai/__init__.py

Lines changed: 75 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""The Aquarium AI integration."""
2-
import asyncio
32
import logging
43
from datetime import timedelta
54

@@ -31,6 +30,13 @@
3130
CONF_LAST_WATER_CHANGE,
3231
CONF_MISC_INFO,
3332
CONF_RUN_ANALYSIS_ON_STARTUP,
33+
CONF_ANALYZE_TEMPERATURE,
34+
CONF_ANALYZE_PH,
35+
CONF_ANALYZE_SALINITY,
36+
CONF_ANALYZE_DISSOLVED_OXYGEN,
37+
CONF_ANALYZE_WATER_LEVEL,
38+
CONF_ANALYZE_ORP,
39+
CONF_ANALYZE_CAMERA,
3440
CONF_PROMPT_MAIN_INSTRUCTIONS,
3541
CONF_PROMPT_PARAMETER_GUIDELINES,
3642
CONF_PROMPT_CAMERA_INSTRUCTIONS,
@@ -42,6 +48,13 @@
4248
DEFAULT_AUTO_NOTIFICATIONS,
4349
DEFAULT_NOTIFICATION_FORMAT,
4450
DEFAULT_RUN_ANALYSIS_ON_STARTUP,
51+
DEFAULT_ANALYZE_TEMPERATURE,
52+
DEFAULT_ANALYZE_PH,
53+
DEFAULT_ANALYZE_SALINITY,
54+
DEFAULT_ANALYZE_DISSOLVED_OXYGEN,
55+
DEFAULT_ANALYZE_WATER_LEVEL,
56+
DEFAULT_ANALYZE_ORP,
57+
DEFAULT_ANALYZE_CAMERA,
4558
DEFAULT_PROMPT_MAIN_INSTRUCTIONS,
4659
DEFAULT_PROMPT_PARAMETER_GUIDELINES,
4760
DEFAULT_PROMPT_CAMERA_INSTRUCTIONS,
@@ -355,22 +368,14 @@ def _build_notification_message(notification_format, sensor_data, sensor_mapping
355368
ai_data = response["data"]
356369

357370
# Use brief sensor analysis (same as used for sensors)
358-
for sensor_entity, sensor_name in sensor_mappings:
371+
# Only iterate over sensors that were actually analyzed (in sensor_data)
372+
for info in sensor_data:
373+
sensor_name = info['name']
359374
analysis_key = sensor_name.lower().replace(" ", "_") + "_analysis"
360375
if analysis_key in ai_data:
361-
# Find corresponding sensor info for icon
362-
corresponding_sensor = None
363-
for info in sensor_data:
364-
if info['name'].lower() == sensor_name.lower():
365-
corresponding_sensor = info
366-
break
367-
368-
if corresponding_sensor:
369-
icon = get_sensor_icon(corresponding_sensor['name'])
370-
status = get_simple_status(corresponding_sensor['name'], corresponding_sensor['raw_value'], corresponding_sensor['unit'], aquarium_type)
371-
message_parts.append(f"\n{icon} {sensor_name} ({status}): {ai_data[analysis_key]}")
372-
else:
373-
message_parts.append(f"\n{sensor_name}: {ai_data[analysis_key]}")
376+
icon = get_sensor_icon(sensor_name)
377+
status = get_simple_status(sensor_name, info['raw_value'], info['unit'], aquarium_type)
378+
message_parts.append(f"\n{icon} {sensor_name} ({status}): {ai_data[analysis_key]}")
374379

375380
# Add water change recommendation before overall analysis
376381
if "water_change_recommendation" in ai_data:
@@ -398,22 +403,14 @@ def _build_notification_message(notification_format, sensor_data, sensor_mapping
398403
ai_data = response["data"]
399404

400405
# Use detailed notification analysis for notifications
401-
for sensor_entity, sensor_name in sensor_mappings:
406+
# Only iterate over sensors that were actually analyzed (in sensor_data)
407+
for info in sensor_data:
408+
sensor_name = info['name']
402409
notification_key = sensor_name.lower().replace(" ", "_") + "_notification_analysis"
403410
if notification_key in ai_data:
404-
# Find corresponding sensor info for status and icon
405-
corresponding_sensor = None
406-
for info in sensor_data:
407-
if info['name'].lower() == sensor_name.lower():
408-
corresponding_sensor = info
409-
break
410-
411-
if corresponding_sensor:
412-
icon = get_sensor_icon(corresponding_sensor['name'])
413-
status = get_simple_status(corresponding_sensor['name'], corresponding_sensor['raw_value'], corresponding_sensor['unit'], aquarium_type)
414-
message_parts.append(f"\n{icon} {sensor_name} ({status}):\n{ai_data[notification_key]}")
415-
else:
416-
message_parts.append(f"\n{sensor_name}:\n{ai_data[notification_key]}")
411+
icon = get_sensor_icon(sensor_name)
412+
status = get_simple_status(sensor_name, info['raw_value'], info['unit'], aquarium_type)
413+
message_parts.append(f"\n{icon} {sensor_name} ({status}):\n{ai_data[notification_key]}")
417414

418415
# Add water change recommendation before overall analysis
419416
if "water_change_recommendation" in ai_data:
@@ -472,14 +469,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
472469
# Set up sensor, binary_sensor, switch, select, and button platforms
473470
await hass.config_entries.async_forward_entry_setups(entry, ["sensor", "binary_sensor", "switch", "select", "button"])
474471

475-
# Define sensor mappings
472+
# Define sensor mappings with their analysis toggle configurations
473+
# Format: (sensor_entity, sensor_name, analyze_config_key, default_analyze_value)
476474
sensor_mappings = [
477-
(temp_sensor, "Temperature"),
478-
(ph_sensor, "pH"),
479-
(salinity_sensor, "Salinity"),
480-
(dissolved_oxygen_sensor, "Dissolved Oxygen"),
481-
(water_level_sensor, "Water Level"),
482-
(orp_sensor, "ORP"),
475+
(temp_sensor, "Temperature", CONF_ANALYZE_TEMPERATURE, DEFAULT_ANALYZE_TEMPERATURE),
476+
(ph_sensor, "pH", CONF_ANALYZE_PH, DEFAULT_ANALYZE_PH),
477+
(salinity_sensor, "Salinity", CONF_ANALYZE_SALINITY, DEFAULT_ANALYZE_SALINITY),
478+
(dissolved_oxygen_sensor, "Dissolved Oxygen", CONF_ANALYZE_DISSOLVED_OXYGEN, DEFAULT_ANALYZE_DISSOLVED_OXYGEN),
479+
(water_level_sensor, "Water Level", CONF_ANALYZE_WATER_LEVEL, DEFAULT_ANALYZE_WATER_LEVEL),
480+
(orp_sensor, "ORP", CONF_ANALYZE_ORP, DEFAULT_ANALYZE_ORP),
483481
]
484482

485483
async def send_ai_aquarium_analysis(now, override_notification=None):
@@ -500,24 +498,31 @@ async def send_ai_aquarium_analysis(now, override_notification=None):
500498
analysis_structure_sensors = {}
501499
analysis_structure_notification = {}
502500

503-
for sensor_entity, sensor_name in sensor_mappings:
504-
sensor_info = get_sensor_info(hass, sensor_entity, sensor_name)
505-
if sensor_info:
506-
sensor_data.append(sensor_info)
507-
# Add to AI analysis structure for sensors (brief)
508-
structure_key = sensor_name.lower().replace(" ", "_") + "_analysis"
509-
analysis_structure_sensors[structure_key] = {
510-
"description": f"Brief 1-2 sentence analysis of the aquarium's {sensor_name.lower()} conditions (under 200 characters).",
511-
"required": True,
512-
"selector": {"text": None}
513-
}
514-
# Add to AI analysis structure for notifications (detailed)
515-
notification_key = sensor_name.lower().replace(" ", "_") + "_notification_analysis"
516-
analysis_structure_notification[notification_key] = {
517-
"description": f"Detailed analysis of the aquarium's {sensor_name.lower()} conditions. Provide comprehensive explanation including current status, potential issues, trends, and detailed recommendations if needed.",
518-
"required": True,
519-
"selector": {"text": None}
520-
}
501+
for sensor_entity, sensor_name, analyze_conf, default_analyze in sensor_mappings:
502+
# Check if analysis is enabled for this parameter
503+
analyze_enabled = entry.data.get(analyze_conf, default_analyze)
504+
505+
# Only process sensor if it's configured AND analysis is enabled
506+
if sensor_entity and analyze_enabled:
507+
sensor_info = get_sensor_info(hass, sensor_entity, sensor_name)
508+
if sensor_info:
509+
sensor_data.append(sensor_info)
510+
# Add to AI analysis structure for sensors (brief)
511+
structure_key = sensor_name.lower().replace(" ", "_") + "_analysis"
512+
analysis_structure_sensors[structure_key] = {
513+
"description": f"Brief 1-2 sentence analysis of the aquarium's {sensor_name.lower()} conditions (under 200 characters).",
514+
"required": True,
515+
"selector": {"text": None}
516+
}
517+
# Add to AI analysis structure for notifications (detailed)
518+
notification_key = sensor_name.lower().replace(" ", "_") + "_notification_analysis"
519+
analysis_structure_notification[notification_key] = {
520+
"description": f"Detailed analysis of the aquarium's {sensor_name.lower()} conditions. Provide comprehensive explanation including current status, potential issues, trends, and detailed recommendations if needed.",
521+
"required": True,
522+
"selector": {"text": None}
523+
}
524+
elif sensor_entity and not analyze_enabled:
525+
_LOGGER.debug("Skipping %s analysis for %s (toggle disabled)", sensor_name, tank_name)
521526

522527
if not sensor_data:
523528
_LOGGER.warning("No valid sensor data available for analysis")
@@ -578,7 +583,9 @@ async def send_ai_aquarium_analysis(now, override_notification=None):
578583

579584
# Prepare camera instructions if camera is configured
580585
camera_instructions = ""
581-
if camera:
586+
# Check if camera is configured AND camera analysis is enabled
587+
analyze_camera = entry.data.get(CONF_ANALYZE_CAMERA, DEFAULT_ANALYZE_CAMERA)
588+
if camera and analyze_camera:
582589
# Add camera analysis fields to the structure
583590
combined_analysis_structure["camera_visual_analysis"] = {
584591
"description": "Brief 1-2 sentence visual analysis of the aquarium from the camera image (under 200 characters). Focus on water clarity, fish/plant health, and any maintenance needs visible.",
@@ -593,6 +600,8 @@ async def send_ai_aquarium_analysis(now, override_notification=None):
593600

594601
# Use custom camera instructions
595602
camera_instructions = f"\n\n{prompt_camera_instructions}"
603+
elif camera and not analyze_camera:
604+
_LOGGER.debug("Skipping camera analysis for %s (toggle disabled)", tank_name)
596605

597606
# Build AI instructions from custom prompts
598607
instructions_parts = [
@@ -612,8 +621,8 @@ async def send_ai_aquarium_analysis(now, override_notification=None):
612621
"structure": combined_analysis_structure
613622
}
614623

615-
# Add camera attachment if configured
616-
if camera:
624+
# Add camera attachment if configured and analysis enabled
625+
if camera and analyze_camera:
617626
ai_task_data["attachments"] = {
618627
"media_content_id": f"media-source://camera/{camera}",
619628
"media_content_type": "application/vnd.apple.mpegurl",
@@ -705,12 +714,15 @@ async def send_ai_aquarium_analysis(now, override_notification=None):
705714
fallback_message_parts = []
706715
fallback_sensor_data = []
707716

708-
for sensor_entity, sensor_name in sensor_mappings:
709-
sensor_info = get_sensor_info(hass, sensor_entity, sensor_name)
710-
if sensor_info:
711-
fallback_sensor_data.append(sensor_info)
712-
icon = get_sensor_icon(sensor_info['name'])
713-
fallback_message_parts.append(f"{icon} {sensor_info['name']}: {sensor_info['value']}")
717+
for sensor_entity, sensor_name, analyze_conf, default_analyze in sensor_mappings:
718+
# Only process sensor if analysis is enabled
719+
analyze_enabled = entry.data.get(analyze_conf, default_analyze)
720+
if sensor_entity and analyze_enabled:
721+
sensor_info = get_sensor_info(hass, sensor_entity, sensor_name)
722+
if sensor_info:
723+
fallback_sensor_data.append(sensor_info)
724+
icon = get_sensor_icon(sensor_info['name'])
725+
fallback_message_parts.append(f"{icon} {sensor_info['name']}: {sensor_info['value']}")
714726

715727
if fallback_message_parts:
716728
# Add overall status at the top of fallback message too

custom_components/aquarium_ai/const.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525
CONF_MISC_INFO: Final = "misc_info"
2626
CONF_RUN_ANALYSIS_ON_STARTUP: Final = "run_analysis_on_startup"
2727

28+
# Parameter analysis toggle configuration constants
29+
CONF_ANALYZE_TEMPERATURE: Final = "analyze_temperature"
30+
CONF_ANALYZE_PH: Final = "analyze_ph"
31+
CONF_ANALYZE_SALINITY: Final = "analyze_salinity"
32+
CONF_ANALYZE_DISSOLVED_OXYGEN: Final = "analyze_dissolved_oxygen"
33+
CONF_ANALYZE_WATER_LEVEL: Final = "analyze_water_level"
34+
CONF_ANALYZE_ORP: Final = "analyze_orp"
35+
CONF_ANALYZE_CAMERA: Final = "analyze_camera"
36+
2837
# AI Prompt Configuration constants
2938
CONF_PROMPT_MAIN_INSTRUCTIONS: Final = "prompt_main_instructions"
3039
CONF_PROMPT_PARAMETER_GUIDELINES: Final = "prompt_parameter_guidelines"
@@ -47,6 +56,15 @@
4756
DEFAULT_MISC_INFO: Final = ""
4857
DEFAULT_RUN_ANALYSIS_ON_STARTUP: Final = False
4958

59+
# Default values for parameter analysis toggles (all enabled by default)
60+
DEFAULT_ANALYZE_TEMPERATURE: Final = True
61+
DEFAULT_ANALYZE_PH: Final = True
62+
DEFAULT_ANALYZE_SALINITY: Final = True
63+
DEFAULT_ANALYZE_DISSOLVED_OXYGEN: Final = True
64+
DEFAULT_ANALYZE_WATER_LEVEL: Final = True
65+
DEFAULT_ANALYZE_ORP: Final = True
66+
DEFAULT_ANALYZE_CAMERA: Final = True
67+
5068
# Update frequency options (in minutes)
5169
UPDATE_FREQUENCIES: Final = {
5270
"1_hour": 60,

custom_components/aquarium_ai/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
"iot_class": "cloud_polling",
1010
"issue_tracker": "https://github.com/TheRealFalseReality/Aquarium-AI-Homeassistant/issues",
1111
"requirements": [],
12-
"version": "1.0.9"
12+
"version": "1.2.0"
1313
}

custom_components/aquarium_ai/services.yaml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
run_analysis:
22
name: Run AI Analysis
3-
description: Trigger an immediate AI analysis for all aquariums configured by this integration
3+
description: >
4+
Trigger an immediate AI analysis for all aquariums configured by this integration.
5+
Only parameters with their analysis toggle switches enabled will be included in the analysis.
6+
Use the parameter analysis toggle switches (e.g., "Analyze Temperature", "Analyze pH") to control which parameters are analyzed.
47
fields:
58
send_notification:
69
name: Send Notification
@@ -12,7 +15,10 @@ run_analysis:
1215

1316
run_analysis_for_aquarium:
1417
name: Run AI Analysis for Specific Aquarium
15-
description: Trigger an immediate AI analysis for a specific aquarium configured by this integration
18+
description: >
19+
Trigger an immediate AI analysis for a specific aquarium configured by this integration.
20+
Only parameters with their analysis toggle switches enabled will be included in the analysis.
21+
Use the parameter analysis toggle switches (e.g., "Analyze Temperature", "Analyze pH") to control which parameters are analyzed.
1622
fields:
1723
config_entry:
1824
name: Aquarium

0 commit comments

Comments
 (0)