Protect home charging with fresh telemetry and verified stops

codex/public-repo-cleanup
Carlo Costanzo 1 month ago
parent 3d28eeffbf
commit 55fc28277c

3
.gitignore vendored

@ -76,6 +76,9 @@ config/custom_components/*
!config/custom_components/alexa_camera_compat/
!config/custom_components/alexa_camera_compat/**
config/custom_components/alexa_camera_compat/__pycache__/
!config/custom_components/tesla_charge_guard/
!config/custom_components/tesla_charge_guard/**
config/custom_components/tesla_charge_guard/__pycache__/
config/www/community
config/www/ookla_speedtest/
config/.cache/

@ -0,0 +1,51 @@
# Tesla home charge verification
This small read-only adapter exposes Tesla Fleet's **source sample timestamps**
and observes live JuiceBox MQTT current/status reports. It reuses the existing
Tesla Fleet connection and credentials. It sends no vehicle or charger commands.
Native Home Assistant scripts in the vehicle package own protective stops,
notifications, investigation dispatch, and restart review.
Before a protective command, the package requests fresh Tesla data to check
vehicle scope. A new home charger session with missing Tesla data gets an
incident refresh within a minute. A missing vehicle identity still prompts
investigation/critical home-charger checks, but does not authorize stopping a
vehicle elsewhere. A fresh away or DC report vetoes commands even when the
current field is missing. A fresh zero-current home-charger report can clear
the home incident while the vehicle is charging elsewhere.
`tesla_charge_guard.snapshot` returns an assessment and fires
`tesla_charge_guard_report`. `refresh: true` requests one refresh with a 25-second
timeout; it does not wake the car. A sleeping/rate-limited vehicle can return
cached data, which remains unverified. Tesla samples expire after 120 seconds,
JuiceBox current/status reports after 60 seconds. Retained MQTT messages are
ignored. Fresh away/DC reports veto home stop commands. Source data with missing
current, invalid current, unknown charging state, or a future timestamp is not
accepted as evidence of stopped charging.
The operational guardrail is 18 A around the intended 16 A home rate. This is
not a replacement for electrical protection. A successful stop command is not
proof of a stopped vehicle; the package requires a fresh zero-current report.
The adapter uses the installed Tesla Fleet runtime coordinator, so its contract
must be checked after Home Assistant upgrades. If the vehicle cannot be found,
the result becomes unverified. It retains no credentials, VIN, or coordinates
in the returned snapshot. MQTT acquisition timestamps are intentionally kept
in memory; after a restart, a new non-retained report is required.
The package's restoring session helper remembers a verified home connection
across power loss or HA restarts. It conveys last-known scope, never freshness;
only a fresh unplugged/away report ends that scope. This allows a five-minute
protective stop even while the Tesla integration is still reconnecting.
The Car view exposes the restart-review helper. Clearing it acknowledges review
and permits subsequent home charging; it does not start charging. Confirm the
home current limit is 16 A and review the incident before clearing it. The monitoring
warning and restart-review warning remain separate from an unresolved critical
stop. Source snapshot sensors/events are excluded from recorder noise; aggregate
incident sensors and curated JUICEBOX log entries remain recorded.
Validation: `python tools/test_tesla_charge_guard.py`; run the adapter contract
test in an environment with Home Assistant installed. Check the full HA config
before loading the adapter. Loading a new custom integration requires an HA
restart; subsequent package-only edits can use normal scripts/templates reloads.

@ -0,0 +1,116 @@
"""Read-only source verification for the package's native HA charging actions."""
import asyncio
from time import time
import voluptuous as vol
from homeassistant.components import mqtt, zone
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.typing import ConfigType
from .policy import classify, number
DOMAIN = "tesla_charge_guard"
EVENT = "tesla_charge_guard_report"
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
vol.Required("entity_id"): cv.entity_id,
vol.Required("mqtt_prefix"): mqtt.valid_subscribe_topic,
vol.Optional("current_limit", default=18): vol.All(vol.Coerce(float), vol.Range(min=1, max=48)),
})}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Expose coordinator sample times without adding credentials or controls."""
settings = config[DOMAIN]
juicebox = {}
refresh_lock = asyncio.Lock()
def vehicle():
registry_entry = er.async_get(hass).async_get(settings["entity_id"])
if registry_entry is None or registry_entry.platform != "tesla_fleet":
raise ServiceValidationError("Configured entity must belong to Tesla Fleet")
entry = hass.config_entries.async_get_entry(registry_entry.config_entry_id)
runtime = getattr(entry, "runtime_data", None)
for item in getattr(runtime, "vehicles", []):
if registry_entry.unique_id.startswith(f"{item.vin}-"):
return item
raise ServiceValidationError("Tesla Fleet vehicle is not loaded")
def assessment():
tesla = {}
try:
data = vehicle().coordinator.data or {}
latitude = data.get("drive_state_latitude")
longitude = data.get("drive_state_longitude")
home = hass.states.get("zone.home")
at_home = (zone.in_zone(home, latitude, longitude)
if home and latitude is not None and longitude is not None else None)
charging = str(data.get("charge_state_charging_state", "")).lower()
tesla = {
"current": data.get("charge_state_charger_actual_current"),
"charging": charging,
"sample_time": (number(data.get("charge_state_timestamp")) or 0) / 1000,
"location_time": (number(data.get("drive_state_timestamp")) or 0) / 1000,
"at_home": at_home,
"dc": data.get("charge_state_fast_charger_present"),
"plugged": charging in {"charging", "starting", "stopped", "complete", "no_power"},
}
except (ServiceValidationError, AttributeError, TypeError, ValueError):
pass
session = hass.states.is_state("input_boolean.tesla_home_charge_session_active", "on")
return classify(tesla, juicebox, time(), settings["current_limit"], session)
@callback
def publish():
hass.bus.async_fire(EVENT, assessment())
@callback
def receive(message):
# Retained startup/reconnect values cannot establish current draw now.
if message.retain:
return
field = "current" if "/Current/" in message.topic else "status"
value = number(message.payload) if field == "current" else str(message.payload)
if value is None:
# Invalid reports revoke the old field instead of extending its life.
juicebox.pop(field, None)
juicebox.pop(f"{field}_time", None)
else:
juicebox[field] = value
juicebox[f"{field}_time"] = time()
publish()
removers = []
for field in ("Current", "Status"):
removers.append(await mqtt.async_subscribe(
hass, f"{settings['mqtt_prefix']}/{field}/state", receive))
async def snapshot(call: ServiceCall):
error = None
if call.data["refresh"]:
try:
async with refresh_lock, asyncio.timeout(25):
await vehicle().coordinator.async_refresh()
except (ServiceValidationError, TimeoutError) as err:
error = type(err).__name__
result = assessment()
if error:
result["refresh_error"] = error
hass.bus.async_fire(EVENT, result)
return result
hass.services.async_register(DOMAIN, "snapshot", snapshot,
schema=vol.Schema({vol.Optional("refresh", default=False): cv.boolean}),
supports_response=SupportsResponse.OPTIONAL)
@callback
def cleanup(_event):
for remove in removers:
remove()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)
return True

@ -0,0 +1,10 @@
{
"domain": "tesla_charge_guard",
"name": "Tesla Home Charge Verification",
"codeowners": ["@CCOSTAN"],
"dependencies": ["mqtt"],
"after_dependencies": ["tesla_fleet"],
"documentation": "https://github.com/CCOSTAN/Home-AssistantConfig",
"integration_type": "service",
"version": "1.0.0"
}

@ -0,0 +1,103 @@
"""Classify measured charging data without treating cached values as fresh."""
from math import isfinite
def number(value):
"""Reject missing, boolean, negative, and non-finite measurements."""
try:
result = float(value)
except (TypeError, ValueError):
return None
return result if not isinstance(value, bool) and isfinite(result) and result >= 0 else None
def fresh(timestamp, now, max_age):
"""Accept source timestamps with at most five seconds of clock skew."""
timestamp = number(timestamp)
return timestamp is not None and -5 <= now - timestamp <= max_age
def classify(tesla, juicebox, now, limit=18, session_active=False):
"""Return a conservative, privacy-safe home charging assessment."""
tc = number(tesla.get("current"))
jc = number(juicebox.get("current"))
ts = str(tesla.get("charging", "")).lower()
js = str(juicebox.get("status", "")).lower()
tesla_sample_fresh = fresh(tesla.get("sample_time"), now, 120)
tf = tesla_sample_fresh and tc is not None
jf = (fresh(juicebox.get("current_time"), now, 60)
and fresh(juicebox.get("status_time"), now, 60) and jc is not None)
location_fresh = fresh(tesla.get("location_time"), now, 120)
at_home = tesla.get("at_home")
dc = tesla.get("dc")
# A fresh away/DC report vetoes vehicle commands, including cached home hints.
away = (location_fresh and at_home is False) or (tesla_sample_fresh and dc is True)
home = not away and ((at_home is True and dc is False) or session_active)
# The newest known source owns session context. Old Tesla charging data must
# not resurrect a session after a newer JuiceBox report confirmed unplugged.
jb_latest = (number(juicebox.get("current_time")) or 0) >= (number(tesla.get("sample_time")) or 0)
active = jc is not None and jc > 0 if jb_latest else ts in {"charging", "starting"}
connected = js not in {"", "unknown", "unavailable", "unplugged"}
context = active or connected if jb_latest else active or tesla.get("plugged") is True
home_session = not away and ((home and (session_active or context)) or (jb_latest and jc is not None and context))
result = {
"status": "unverified", "actual_current": None, "source": "none",
"tesla_fresh": tf, "juicebox_fresh": jf,
"home_session": home_session, "stop_allowed": home and home_session,
"session_observed": None,
"checked_at": now, "sample_time": 0,
"tesla_sample_time": tesla.get("sample_time", 0),
"juicebox_sample_time": juicebox.get("current_time", 0),
"reason": "No fresh charging report; cached values cannot verify safety.",
}
if away and not (jf and jc > limit):
result.update(status="away", stop_allowed=False, home_session=False, session_observed=False,
reason="Fresh away or DC charging report; home stop commands blocked.")
# Charging elsewhere must not prevent a recovered home charger from
# clearing its own incident. This still never permits a vehicle command.
if jf and jc == 0 and js in {"unplugged", "plugged in", "plugged", "connected", "standby", "ready"}:
result.update(status="stopped", actual_current=0, source="juicebox",
sample_time=juicebox["current_time"],
reason="Fresh home charger zero current confirms the home incident is stopped.")
return result
# Either live source can expose excessive draw; a lower reading cannot mask it.
excessive = []
if jf and jc > limit:
excessive.append((jc, "juicebox", juicebox["current_time"]))
if tf and home and ts in {"charging", "starting"} and tc > limit:
excessive.append((tc, "tesla", tesla["sample_time"]))
if excessive:
current, source, timestamp = max(excessive)
result.update(status="excessive", actual_current=current, source=source,
sample_time=timestamp, stop_allowed=home, home_session=True,
session_observed=True if home else None,
reason=f"Fresh measured home current exceeds {limit:g} A.")
return result
if jf and jc == 0 and tf and ts in {"charging", "starting"} and tc > 0:
result.update(home_session=home, stop_allowed=home,
reason="Fresh sources disagree about whether charging is active.")
return result
# A fresh active report prevents an older/fresh conflicting zero from clearing.
if (jf and jc > 0) or (tf and ts in {"charging", "starting"}):
if home and ((jf and 0 < jc <= limit) or
(tf and ts in {"charging", "starting"} and 0 < tc <= limit)):
source = "juicebox" if jf and jc > 0 else "tesla"
result.update(status="charging_safe", source=source,
home_session=True, stop_allowed=True, session_observed=True,
actual_current=jc if source == "juicebox" else tc,
sample_time=juicebox["current_time"] if source == "juicebox" else tesla["sample_time"],
reason="Fresh home charging current is within the guardrail.")
return result
if ((jf and jc == 0 and js in {"unplugged", "plugged in", "plugged", "connected", "standby", "ready"}) or
(tf and tc == 0 and ts in {"stopped", "complete", "disconnected", "no_power"})):
source = "juicebox" if jf and jc == 0 else "tesla"
result.update(status="stopped", actual_current=0, source=source,
sample_time=juicebox["current_time"] if source == "juicebox" else tesla["sample_time"],
stop_allowed=False, reason="Fresh measured zero current confirms charging is stopped.")
# Keep a connected stopped session in scope: charging can resume after
# power returns. Only a fresh unplugged/away report ends the durable lease.
unplugged = js == "unplugged" if source == "juicebox" else ts == "disconnected"
result.update(home_session=home and not unplugged,
session_observed=False if unplugged else True if home else None)
return result

@ -0,0 +1,18 @@
######################################################################
# @CCOSTAN - Follow Me on X
# For more info visit https://www.vcloudinfo.com/click-here
# Original Repo : https://github.com/CCOSTAN/Home-AssistantConfig
# -------------------------------------------------------------------
# Tesla Home Charge Verification - Read-only source snapshot
# -------------------------------------------------------------------
######################################################################
snapshot:
name: Verify home charging
description: Check source timestamps and measured current. Does not wake the car or change charging.
fields:
refresh:
name: Refresh Tesla data
description: Request a bounded Tesla Fleet refresh; cached responses remain unverified.
default: false
selector:
boolean:

@ -147,7 +147,8 @@
variables:
status_severity: >
[[[
if (states['binary_sensor.juicepassproxy_monitoring_degraded']?.state === 'on') return 'critical';
if (states['binary_sensor.tesla_home_charge_critical']?.state === 'on') return 'critical';
if (states['binary_sensor.juicepassproxy_monitoring_degraded']?.state === 'on') return 'warning';
return entity?.state === 'online' ? 'healthy' : 'warning';
]]]
state_display: >
@ -157,6 +158,22 @@
? 'NO REPORTS' : 'VERIFYING';
]]]
- type: custom:button-card
template: bearstone_infra_status_kpi
entity: input_boolean.tesla_home_charge_restart_required
name: Charging review
icon: mdi:shield-car
variables:
status_severity: >
[[[
if (states['binary_sensor.tesla_home_charge_critical']?.state === 'on') return 'critical';
return entity?.state === 'on' ? 'warning' : 'healthy';
]]]
state_display: >
[[[ return entity?.state === 'on' ? 'REVIEW BEFORE RESUME' : 'CLEAR'; ]]]
tap_action:
action: more-info
# -------------------------------------------------------------------
# Current vehicle, tires, and charging telemetry
# -------------------------------------------------------------------

@ -70,6 +70,7 @@
]]]
raw_suffix: ACTIVE
warning_entity_ids:
- binary_sensor.tesla_home_charge_attention
- binary_sensor.infra_wan_sustained_degradation
- binary_sensor.rheem_wh_telemetry_stale
- binary_sensor.docker_host_maintenance_attention
@ -81,7 +82,7 @@
- binary_sensor.infra_compute_degraded
- binary_sensor.infra_storage_degraded
- binary_sensor.infra_external_monitoring_degraded
- binary_sensor.juicepassproxy_monitoring_degraded
- binary_sensor.tesla_home_charge_critical
- binary_sensor.rheem_wh_active_alert
- binary_sensor.garage_phyn_leak_alert
@ -147,12 +148,15 @@
];
const battery = Number(entity?.state);
const tires = tireIds.map((id) => Number(states[id]?.state)).filter(Number.isFinite);
if (states['binary_sensor.juicepassproxy_monitoring_degraded']?.state === 'on') return 'critical';
if (states['binary_sensor.tesla_home_charge_critical']?.state === 'on') return 'critical';
if (states['binary_sensor.tesla_home_charge_attention']?.state === 'on') return 'warning';
if (!Number.isFinite(battery) || tires.length !== tireIds.length) return 'unknown';
return Math.min(...tires) < 38.5 ? 'critical' : 'healthy';
]]]
state_display: >
[[[
if (states['binary_sensor.tesla_home_charge_critical']?.state === 'on') return 'CHECK CHARGING';
if (states['input_boolean.tesla_home_charge_restart_required']?.state === 'on') return 'RESTART REVIEW';
if (states['binary_sensor.juicepassproxy_monitoring_degraded']?.state === 'on') return 'MONITOR CHECK';
const battery = Number(entity?.state);
if (!Number.isFinite(battery)) return 'NO DATA';
@ -523,13 +527,24 @@
- type: custom:button-card
template: bearstone_infra_alert_row
entity: binary_sensor.juicepassproxy_monitoring_degraded
name: JuiceBox monitoring unavailable
entity: binary_sensor.tesla_home_charge_attention
name: Home charging needs attention
icon: mdi:ev-station-off
variables:
alert_kind: binary_on
exception_detail: NO LIVE REPORTS - CHECK THE TESLA AND CHARGER
state_display: '[[[ return "NO DATA"; ]]]'
warning_only: true
critical_entity_ids:
- binary_sensor.tesla_home_charge_critical
exception_detail: >
[[[
if (states['binary_sensor.tesla_home_charge_critical']?.state === 'on')
return 'CHECK THE TESLA - CHARGING STOP NOT YET VERIFIED';
if (states['input_boolean.tesla_home_charge_restart_required']?.state === 'on')
return 'CHARGING PAUSED - REVIEW BEFORE RESUMING';
return 'MONITORING REPORTS MISSING - INVESTIGATION REQUESTED';
]]]
state_display: >
[[[ return states['binary_sensor.tesla_home_charge_critical']?.state === 'on' ? 'CHECK' : 'REVIEW'; ]]]
tap_action:
action: navigate
navigation_path: /dashboard-infrastructure/car

@ -11,6 +11,7 @@
# Notes: Filters vcloudinfo availability chatter plus location/weather noise and raw Glances host telemetry.
# Notes: L10s status/room telemetry is hidden; curated VACUUM logbook entries remain visible.
# Notes: Tesla Fleet Spaceship telemetry is hidden; TeslaMate keeps the historical car record.
# Notes: Charging source snapshots stay hidden; curated JUICEBOX incident events remain visible.
# Notes: Person/location changes and Life360 online status stay out of Activity; recorder keeps location history.
# Notes: Raw Portainer container/stack status telemetry is hidden; Docker aggregate outage sensors and curated DOCKER events remain.
# Notes: Mobile app phone telemetry stays out of Activity; phone notify services are unaffected.
@ -86,6 +87,7 @@ exclude:
- "*alarm_panel_2*"
entities:
- automation.cuckoo_clock
- sensor.tesla_home_charge_guard
- automation.detect_lights_and_adjust_the_brightness_when_turned_on_based_on_time
- automation.docker_state_sync_repairs_dynamic
- automation.docker_telemetry_template_refresh

@ -78,7 +78,7 @@ After Home Assistant starts, Kitchen Show waits three minutes for the initial st
| [![YAML source: maintenance_log](https://img.shields.io/static/v1?label=YAML&message=maintenance_log&color=lightgrey&logo=github&logoColor=181717)](maintenance_log.yaml) | Webhook-based water softener salt maintenance logging with idempotent event handling, Activity Feed logging, parser-safe refill cadence history, and dashboard-ready template sensors. [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/_OqTv9i-jAs) [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/07/home-assistant-water-softener-salt-automation.html) | `automation.maintenance_log_joanna_webhook_ingest`, `sensor.water_softener_salt_average_days_between_refills`, `sensor.water_softener_salt_refill_count`, `sensor.water_softener_salt_total_added` |
| [![YAML source: kiosk_tablet](https://img.shields.io/static/v1?label=YAML&message=kiosk_tablet&color=lightgrey&logo=github&logoColor=181717)](kiosk_tablet.yaml) | Shares Garage-then-Front-Door priority plus reusable Bedroom header/alert state while keeping each display purpose-built. The Garage view now remains active for three minutes after both doors close before either kiosk returns to its default dashboard. Bedroom retains Fully wake/drift recovery and SleepIQ screen behavior around a clock-free night-watch display; Kitchen retains its clock/forecast, rotating household context, native camera overlays, Kiosk Satellite idle dimming, and an event-driven household policy that corrects master volume to 13% at 08:00 and startup, and invokes the Dim screensaver while `group.bed` is on or the family is away. Nearby rain or lightning starts one five-minute NOAA radar takeover while the sun is above the horizon; afterward, the rotating Storms Approaching banner can replay the latest 20-minute loop while the event remains active. Actionable banner slides reuse one transient detail timer for NWS instructions, the front-door package view, a person-centered family map, open-entry details, Phyn water status with a confirmed valve-restore action, and hot-water recovery status. Sunset restores the ambient wall immediately. True idle panel-off remains disabled on the Crown LineageOS device until it has a reliable motion-wake input. Fully availability/page/app/power checks plus Kitchen connectivity, current-page, and screenshot freshness keep the Systems Kiosks tab truthful; a reachable Kitchen display remains online while stale session timestamps remain visible only in the telemetry panel. Available Kitchen updates remain advisory telemetry, while stable attention attributes preserve useful outage transitions without recording every heartbeat. At 07:00, Home Assistant requests the pending signed installer and waits two minutes for staging before dispatching Joanna to finish the Android confirmation and verify the Kitchen Show returns to its dashboard; an unsuccessful installation is retried at 07:00 the following day until the update entity confirms success. [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/ChgEu0IDWzc) | `binary_sensor.kiosk_garage_camera_active`, `sensor.kiosk_active_camera_view`, `sensor.bedroom_kiosk_header`, `sensor.kiosk_household_alert`, `binary_sensor.kiosk_rain_radar_weather_event`, `binary_sensor.kiosk_rain_radar_active`, `timer.kitchen_kiosk_radar_display`, `input_text.kitchen_kiosk_detail_view`, `timer.kitchen_kiosk_detail_display`, `script.kitchen_kiosk_show_event`, `script.kitchen_kiosk_show_detail`, `script.kitchen_kiosk_restore_phyn_water`, `binary_sensor.bedroom_kiosk_needs_attention`, `binary_sensor.kiosk_devices_need_attention`, `sensor.alarm_panel_1_current_page`, `button.alarm_panel_1_load_start_url`, `binary_sensor.kitchen_kiosk_should_be_on`, `number.kitchen_show_volume_2`, `light.kitchen_show_screen_2`, `button.kitchen_show_postpone_screensaver_2`, `button.kitchen_show_take_screenshot_2`, `sensor.kitchen_show_active_camera_view_2`, `automation.kitchen_kiosk_satellite_update_joanna_dispatch` |
| [![YAML source: powerwall](https://img.shields.io/static/v1?label=YAML&message=powerwall&color=lightgrey&logo=github&logoColor=181717)](powerwall.yaml) | Track Tesla Powerwall grid status, keep a restart-safe outage anchor, publish monitor-only staged resilience recommendations from Home Assistant, handle immediate household load shedding, and alert if the Powerwall stays low after grid power returns. [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/hR_0lFEE2bA) [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/06/tesla-powerwall-home-assistant-watchdog.html) | `binary_sensor.powerwall_grid_status`, `sensor.powerwall_*`, `input_datetime.power_resilience_outage_started`, `sensor.power_resilience_recommended_stage`, `script.notify_live_activity`, `repairs.create` |
| [![YAML source: tesla_model_y](https://img.shields.io/static/v1?label=YAML&message=tesla_model_y&color=lightgrey&logo=github&logoColor=181717)](tesla_model_y.yaml) | Remind the garage and parents to plug in the Model Y after a low-battery arrival, alert both parents when a reported tire drops below 38.5 PSI, and issue one critical parents push plus a garage announcement when persisted JuiceBox current reaches 20 A. A restoring latch prevents repeat current alarms. A missing JuiceBox MQTT heartbeat dispatches Joanna after three minutes and raises a critical Systems alert after five minutes if reports remain absent; fresh reports clear the alert. Portainer status remains diagnostic. Plug-in alerts snooze for 24 hours; tire alerts and Joanna's morning-briefing action item share 1-day and 3-day snoozes. [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/hDNyZfTfTtE) [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/09/home-assistant-tesla-tire-pressure-alerts.html) | `sensor.spaceship_battery_level`, `sensor.tire_pressure_*`, `sensor.juicebox_current_last_known`, `sensor.juicepassproxy_mqtt_heartbeat`, `binary_sensor.juicepassproxy_investigation_needed`, `binary_sensor.juicepassproxy_monitoring_degraded`, `switch.spaceship_charge`, `cover.large_garage_door`, `input_boolean.tesla_high_charge_rate_alert_active`, `input_datetime.tesla_model_y_*`, `input_datetime.tesla_tire_pressure_snooze_until`, `notify.alexa_media_garage`, `script.notify_engine`, `script.joanna_dispatch` |
| [![YAML source: tesla_model_y](https://img.shields.io/static/v1?label=YAML&message=tesla_model_y&color=lightgrey&logo=github&logoColor=181717)](tesla_model_y.yaml) | Keeps garage-driven plug-in/tire reminders and durable dashboard snapshots. Fresh actual home draw above 18 A requests a native Tesla stop, critical push, and investigation; the intended 16 A limit stays unchanged. Source timestamps reject cached Tesla data and retained MQTT. Missing reports dispatch investigation at 3 minutes. Unverified home sessions pause at 5 minutes; safe/stopped sessions receive a quiet monitoring warning at 15 minutes. Fresh post-stop zero current clears the critical incident. A restoring review latch requires human acknowledgment before home charging resumes; no start commands are issued. Monitoring recovery clears its own tagged alert. Plug-in and tire snoozes remain available. [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/hDNyZfTfTtE) [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/09/home-assistant-tesla-tire-pressure-alerts.html) | `sensor.tesla_home_charge_guard`, `binary_sensor.tesla_home_charge_critical`, `binary_sensor.tesla_home_charge_attention`, `sensor.juicepassproxy_mqtt_heartbeat`, `binary_sensor.juicepassproxy_investigation_needed`, `binary_sensor.juicepassproxy_monitoring_degraded`, `switch.spaceship_charge`, `input_boolean.tesla_home_charge_restart_required`, `input_boolean.tesla_high_charge_rate_alert_active`, `script.tesla_home_charge_protect`, `script.notify_engine`, `script.joanna_dispatch` |
| [![YAML source: vacuum](https://img.shields.io/static/v1?label=YAML&message=vacuum&color=lightgrey&logo=github&logoColor=181717)](vacuum.yaml) | Dreame away-only vacuum orchestration with room tracking, sweep/mop phases, a four-day start guard, family-return task cancellation, push alerts, and Alexa one-off room cleans. | `input_select.l10s_vacuum_phase`, `sensor.l10s_vacuum_error`, `repairs.create` |
| [![YAML source: hass_agent_homepc](https://img.shields.io/static/v1?label=YAML&message=hass_agent_homepc&color=lightgrey&logo=github&logoColor=181717)](hass_agent_homepc.yaml) | Mirrors PC lock/unlock state to the office lamp and wakes `CARLO-HOMEPC` on workday morning bed exits. | `sensor.carlo_homepc_carlo_homepc_sessionstate`, `button.carlo_home`, `switch.office_lamp_switch` |
| [![YAML source: sleepiq](https://img.shields.io/static/v1?label=YAML&message=sleepiq&color=lightgrey&logo=github&logoColor=181717)](sleepiq.yaml) | Sleep Number presence/snore automations plus a zero-filtered 14-day score summary; Overview Health keeps the detailed scores, vitals, pressure, and bed controls. | `group.bed`, `binary_sensor.sleepnumber_carlo_carlo_is_in_bed`, `binary_sensor.sleepnumber_carlo_stacey_is_in_bed`, `sensor.sleepnumber_carlo_carlo_sleep_score`, `sensor.sleepnumber_carlo_stacey_sleep_score`, `sensor.sleep_score_14_day_averages`, `number.sleepnumber_carlo_carlo_firmness`, `select.sleepnumber_carlo_foundation_preset_right` |
@ -116,7 +116,7 @@ When a package has a dedicated blog post or video, I link it right inside the YA
| [![YAML source: garadget](https://img.shields.io/static/v1?label=YAML&message=garadget&color=lightgrey&logo=github&logoColor=181717)](garadget.yaml) | Garage arrival hub with auto-open guardrails, entry prompts, nighttime reminders, camera context, and the older MQTT/token setup notes. | [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/wFO0H5zgnzk) · [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/05/home-assistant-garage-automations-arrival-alerts-tesla.html) · [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2019/03/how-to-add-garadget-to-home-assistant.html) · [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2020/05/fixing-garadget-in-home-assistant.html) |
| [![YAML source: garage_entry_light](https://img.shields.io/static/v1?label=YAML&message=garage_entry_light&color=lightgrey&logo=github&logoColor=181717)](../automation/garage_entry_light.yaml) | Simple door-open and door-closed hallway lighting pattern from the garage walkthrough. | [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/wFO0H5zgnzk) · [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/05/home-assistant-garage-automations-arrival-alerts-tesla.html) |
| [![YAML source: kiosk_tablet](https://img.shields.io/static/v1?label=YAML&message=kiosk_tablet&color=lightgrey&logo=github&logoColor=181717)](kiosk_tablet.yaml) + [Bedroom dashboard](../dashboards/bedroom) + [Kiosk Satellite preview](../dashboards/kiosk_satellite_staging) | Shared event priority with purpose-built display surfaces: clock-free Bedroom night-watch context and cameras through Fully, plus Kitchen clock/forecast, reminders, native camera overlays, motion-aware dimming, and household-aware screen power through Kiosk Satellite. | [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/ChgEu0IDWzc) · [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2017/08/project-update-visualization-of-home.html) |
| [![YAML source: tesla_model_y](https://img.shields.io/static/v1?label=YAML&message=tesla_model_y&color=lightgrey&logo=github&logoColor=181717)](tesla_model_y.yaml) | Tesla Fleet tire-pressure alerts with a shared briefing snooze, garage-close arrival signals for low-battery plug-in reminders, a JuiceBox-only 20 A current alarm, and a three-minute Joanna investigation followed by a five-minute critical alert for missing JuiceBox MQTT reports, with automatic clearing when reports resume. | [![Watch tire-pressure video](https://img.shields.io/badge/Watch-Tire%20Pressure-FF0000?logo=youtube&logoColor=white)](https://youtu.be/hDNyZfTfTtE) · [![Tire-pressure blog post](https://img.shields.io/static/v1?label=vCloudInfo&message=Tire%20Pressure&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/09/home-assistant-tesla-tire-pressure-alerts.html) · [![Watch garage video](https://img.shields.io/badge/Watch-Garage%20Automations-FF0000?logo=youtube&logoColor=white)](https://youtu.be/wFO0H5zgnzk) · [![Garage automations blog post](https://img.shields.io/static/v1?label=vCloudInfo&message=Garage%20Automations&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/05/home-assistant-garage-automations-arrival-alerts-tesla.html) |
| [![YAML source: tesla_model_y](https://img.shields.io/static/v1?label=YAML&message=tesla_model_y&color=lightgrey&logo=github&logoColor=181717)](tesla_model_y.yaml) | Tesla tire-pressure alerts and garage-driven plug-in reminders, plus charging-aware protection: fresh home draw above 18 A requests a verified stop; missing reports dispatch investigation at 3 minutes, protect an unverified home session at 5 minutes, and quietly warn about persistent monitoring at 15 minutes. Tagged incident cleanup and a restoring human restart-review latch prevent false recovery and unattended resumption. | [![Watch tire-pressure video](https://img.shields.io/badge/Watch-Tire%20Pressure-FF0000?logo=youtube&logoColor=white)](https://youtu.be/hDNyZfTfTtE) · [![Tire-pressure blog post](https://img.shields.io/static/v1?label=vCloudInfo&message=Tire%20Pressure&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/09/home-assistant-tesla-tire-pressure-alerts.html) · [![Watch garage video](https://img.shields.io/badge/Watch-Garage%20Automations-FF0000?logo=youtube&logoColor=white)](https://youtu.be/wFO0H5zgnzk) · [![Garage automations blog post](https://img.shields.io/static/v1?label=vCloudInfo&message=Garage%20Automations&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/05/home-assistant-garage-automations-arrival-alerts-tesla.html) |
| [![YAML source: llmvision](https://img.shields.io/static/v1?label=YAML&message=llmvision&color=lightgrey&logo=github&logoColor=181717)](llmvision.yaml) | LLM Vision package reminder flow that turns a front-door camera event into a stored package-present state and phone alert. | [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/nAhCezFetvI) |
| [![YAML source: maintenance_log](https://img.shields.io/static/v1?label=YAML&message=maintenance_log&color=lightgrey&logo=github&logoColor=181717)](maintenance_log.yaml) + [![YAML source: 09_salt_trends](https://img.shields.io/static/v1?label=YAML&message=09_salt_trends&color=lightgrey&logo=github&logoColor=181717)](../dashboards/infrastructure/views/09_salt_trends.yaml) + [![YAML source: home_water_sections](https://img.shields.io/static/v1?label=YAML&message=home_water_sections&color=lightgrey&logo=github&logoColor=181717)](../dashboards/infrastructure/partials/home_water_sections.yaml) | Water softener salt maintenance flow with a reusable webhook, duplicate protection, helper-backed history, template sensors, and Salt Trends dashboard views. | [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/_OqTv9i-jAs) · [![vCloudInfo Blog Post](https://img.shields.io/static/v1?label=vCloudInfo&message=Blog%20Post&color=21759B&logo=wordpress&logoColor=white)](https://www.vcloudinfo.com/2026/07/home-assistant-water-softener-salt-automation.html) · [Issue #1816](https://github.com/CCOSTAN/Home-AssistantConfig/issues/1816) |
| [![YAML source: climate](https://img.shields.io/static/v1?label=YAML&message=climate&color=lightgrey&logo=github&logoColor=181717)](climate.yaml) + [![YAML source: home_water_sections](https://img.shields.io/static/v1?label=YAML&message=home_water_sections&color=lightgrey&logo=github&logoColor=181717)](../dashboards/infrastructure/partials/home_water_sections.yaml) | HVAC blower-failure evidence, runtime-based AC filter reminders, two-unit native NFC tag change capture, per-unit change counts/cadence, reset actions, and 360-day condenser-line cleanout reminders. | [![Watch on YouTube](https://img.shields.io/badge/Watch-YouTube-FF0000?logo=youtube&logoColor=white)](https://youtu.be/nScl2JxYSB8) · [Issue #1817](https://github.com/CCOSTAN/Home-AssistantConfig/issues/1817) · [Issue #273](https://github.com/CCOSTAN/Home-AssistantConfig/issues/273) |

@ -246,7 +246,7 @@ template:
'binary_sensor.infra_compute_degraded',
'binary_sensor.infra_storage_degraded',
'binary_sensor.infra_external_monitoring_degraded',
'binary_sensor.juicepassproxy_monitoring_degraded',
'binary_sensor.tesla_home_charge_attention',
'binary_sensor.infra_ups_on_battery',
'binary_sensor.docker_host_maintenance_attention',
'binary_sensor.hvac_maintenance_due',
@ -264,7 +264,7 @@ template:
'binary_sensor.infra_compute_degraded',
'binary_sensor.infra_storage_degraded',
'binary_sensor.infra_external_monitoring_degraded',
'binary_sensor.juicepassproxy_monitoring_degraded',
'binary_sensor.tesla_home_charge_attention',
'binary_sensor.infra_ups_on_battery',
'binary_sensor.docker_host_maintenance_attention',
'binary_sensor.hvac_maintenance_due',

@ -28,16 +28,21 @@
# live Tesla Fleet and JuiceBox reports remain the normal update path.
# Notes: Snapshot sensors intentionally omit state_class; TeslaMate remains the
# long-term vehicle source while raw JuiceBox energy owns charge statistics.
# Notes: Home charging is intentionally limited to 16 A in the Tesla; this
# package observes JuiceBox current but never changes the vehicle or EVSE limit.
# Notes: Home charging is intentionally limited to 16 A in the Tesla. Fresh
# actual draw above 18 A requests a native Tesla stop; current limits are unchanged.
# Notes: The 30 A home EVSE circuit's EEPROM-backed offline wire rating was set
# to 24 A during unplugged commissioning. JuicePassProxy is a passive sensor-only
# monitor that sends no charger or former-cloud UDP traffic and exposes no
# limit controls. Transient online/offline max-current fields are ignored.
# Notes: Persisted JuiceBox current reaching 20 A sends one critical parents
# push plus an exact garage announcement; a restoring latch prevents repeats.
# Notes: Missing proxy MQTT reports dispatch Joanna after three minutes and
# alert the household after five; fresh reports clear the Systems problem.
# Notes: Source verification ignores retained MQTT and cached Tesla timestamps.
# Tesla samples expire after 120 seconds; live JuiceBox reports after 60 seconds.
# Notes: Missing reports dispatch Joanna at three minutes. At five minutes,
# unverified home sessions request a protective stop; verified safe/stopped
# sessions get a quiet monitoring warning only if the outage persists 15 minutes.
# Notes: Stops require home/AC context and fresh post-command zero-current proof.
# A restoring review latch blocks home charging resumption until manually cleared.
# No automatic start commands are issued. Tagged incident pushes clear only after
# their own recovery checks. Older untagged alerts require dismissal on the phone.
# Notes: Portainer state remains diagnostic and cannot by itself mark the
# charger monitor offline.
# Tire-pressure video: https://youtu.be/hDNyZfTfTtE
@ -48,8 +53,19 @@
input_boolean:
tesla_high_charge_rate_alert_active:
name: Tesla High Charge Rate Alert Active
name: Tesla Home Charging Incident Active
icon: mdi:car-electric-alert
tesla_home_charge_restart_required:
name: Tesla Home Charging Restart Review Required
icon: mdi:car-electric-outline
tesla_home_charge_session_active:
name: Tesla Home Charging Session Active
icon: mdi:ev-plug-type1
tesla_charge_guard:
entity_id: sensor.spaceship_charger_current
mqtt_prefix: hmd/sensor/JuiceBox
current_limit: 18
mqtt:
sensor:
@ -91,7 +107,50 @@ template:
mqtt_heartbeat: "{{ states('sensor.juicepassproxy_mqtt_heartbeat') }}"
source_state: "{{ states('sensor.juicepassproxy_state') }}"
source_health: "{{ states('sensor.juicepassproxy_health') }}"
guidance: "Verify charger monitoring before relying on the high-current alert."
guidance: "{{ state_attr('sensor.tesla_home_charge_guard', 'reason') }}"
- name: "JuicePassProxy Monitoring Persistent"
unique_id: juicepassproxy_monitoring_persistent
device_class: problem
delay_on: "00:14:00"
state: "{{ not is_state('sensor.juicepassproxy_mqtt_heartbeat', 'online') }}"
- name: "Tesla Home Charge Critical"
unique_id: tesla_home_charge_critical
device_class: problem
state: >-
{{ is_state('input_boolean.tesla_high_charge_rate_alert_active', 'on')
or (is_state('binary_sensor.juicepassproxy_monitoring_degraded', 'on')
and is_state('sensor.tesla_home_charge_guard', 'unverified')
and state_attr('sensor.tesla_home_charge_guard', 'home_session') == true) }}
- name: "Tesla Home Charge Attention"
unique_id: tesla_home_charge_attention
device_class: problem
state: >-
{{ is_state('binary_sensor.juicepassproxy_monitoring_degraded', 'on')
or is_state('binary_sensor.tesla_home_charge_critical', 'on')
or is_state('input_boolean.tesla_home_charge_restart_required', 'on') }}
- triggers:
- trigger: event
event_type: tesla_charge_guard_report
sensor:
- name: "Tesla Home Charge Guard"
unique_id: tesla_home_charge_guard
icon: mdi:shield-car
state: "{{ trigger.event.data.status }}"
attributes:
actual_current: "{{ trigger.event.data.actual_current }}"
source: "{{ trigger.event.data.source }}"
sample_time: "{{ trigger.event.data.sample_time }}"
checked_at: "{{ trigger.event.data.checked_at }}"
tesla_fresh: "{{ trigger.event.data.tesla_fresh }}"
juicebox_fresh: "{{ trigger.event.data.juicebox_fresh }}"
home_session: "{{ trigger.event.data.home_session }}"
session_observed: "{{ trigger.event.data.session_observed }}"
stop_allowed: "{{ trigger.event.data.stop_allowed }}"
reason: "{{ trigger.event.data.reason }}"
- triggers:
- trigger: state
@ -277,89 +336,277 @@ template:
- "{{ snapshot_source_entity == 'sensor.juicebox_power_factor' }}"
state: "{{ snapshot_state }}"
automation:
- alias: "Tesla Model Y - High Home Charge Rate Alert"
id: f4f013f9-74e7-492e-8400-ae84897086ef
script:
tesla_home_charge_dispatch:
alias: Tesla Home Charge - Joanna Investigation
mode: queued
max: 3
fields:
summary:
description: Verified incident summary
request:
description: Investigation scope
sequence:
- service: script.joanna_dispatch
data:
trigger_context: "HA Tesla home charging protection"
source: "home_assistant_automation.juicepassproxy_monitoring_alert"
summary: "{{ summary }}"
entity_ids:
- sensor.tesla_home_charge_guard
- sensor.juicepassproxy_mqtt_heartbeat
- binary_sensor.tesla_home_charge_critical
- sensor.juicepassproxy_state
- sensor.juicepassproxy_health
- sensor.spaceship_charger_current
- sensor.spaceship_charging
- switch.spaceship_charge
- input_boolean.tesla_home_charge_restart_required
diagnostics: >-
assessment={{ states('sensor.tesla_home_charge_guard') }},
actual_current={{ state_attr('sensor.tesla_home_charge_guard', 'actual_current') }},
source={{ state_attr('sensor.tesla_home_charge_guard', 'source') }},
sample_time={{ state_attr('sensor.tesla_home_charge_guard', 'sample_time') }},
home_session={{ state_attr('sensor.tesla_home_charge_guard', 'home_session') }},
mqtt_heartbeat={{ states('sensor.juicepassproxy_mqtt_heartbeat') }}
request: "{{ request }}"
tesla_home_charge_protect:
alias: Tesla Home Charge - Protective Stop
mode: single
max_exceeded: silent
trigger:
- platform: numeric_state
entity_id: sensor.juicebox_current_last_known
above: 19.99
id: juicebox_current
- platform: homeassistant
event: start
id: startup
condition:
- condition: state
entity_id: input_boolean.tesla_high_charge_rate_alert_active
state: 'off'
- condition: numeric_state
entity_id: sensor.juicebox_current_last_known
above: 19.99
action:
fields:
reason:
description: Why home charging needs to be paused
sequence:
- service: tesla_charge_guard.snapshot
data:
refresh: true
response_variable: guard
- condition: template
value_template: >-
{{ guard.status == 'excessive'
or (guard.status == 'unverified' and guard.home_session)
or (guard.status == 'charging_safe'
and is_state('input_boolean.tesla_home_charge_restart_required', 'on')) }}
- variables:
stop_requested_at: "{{ as_timestamp(now()) }}"
incident_reason: "{{ reason | default(guard.reason, true) }}"
- service: input_boolean.turn_on
target:
entity_id: input_boolean.tesla_high_charge_rate_alert_active
- variables:
observed_current: >-
{{ states('sensor.juicebox_current_last_known') | float(0) | round(1) }}
entity_id:
- input_boolean.tesla_high_charge_rate_alert_active
- input_boolean.tesla_home_charge_restart_required
- parallel:
- sequence:
- condition: template
value_template: "{{ guard.stop_allowed }}"
- service: switch.turn_off
continue_on_error: true
target:
entity_id: switch.spaceship_charge
- service: script.notify_engine
continue_on_error: true
data:
title: "URGENT: Tesla Home Charging"
value1: >-
{{ incident_reason }}
{{ 'A Tesla stop was requested; verification is pending.' if guard.stop_allowed
else 'A vehicle stop is blocked because home AC charging cannot be established. Check the charger.' }}
who: parents
group: Tesla_Charge_Rate_Alarm
tag: Tesla_Charge_Rate_Alarm
level: critical
- service: notify.alexa_media_garage
continue_on_error: true
data:
message: >-
Warning. The JuiceBox reports the Tesla charging at
{{ observed_current }} amps, at or above the twenty amp safety
threshold. Unplug the Tesla now.
Warning. {{ incident_reason }}
{{ 'A charging stop was requested. Check the Tesla if charging continues.' if guard.stop_allowed
else 'Check the charger. The vehicle location could not be verified for a stop.' }}
data:
type: announce
- service: script.tesla_home_charge_dispatch
continue_on_error: true
data:
summary: "{{ incident_reason }}"
request: >-
Home Assistant owns the protective stop and restart-review latch.
Verify fresh actual-current reports and the owning monitoring path.
Repair monitoring when safe; check for an active deployment before
restarting a container. Do not change current limits, clear the review
latch, or start charging. Report verified results and any stop failure.
- repeat:
count: 2
sequence:
- if:
- condition: template
value_template: >-
{{ not (is_state('sensor.tesla_home_charge_guard', 'stopped')
and state_attr('sensor.tesla_home_charge_guard', 'sample_time') | float(0) > stop_requested_at | float(0)) }}
then:
- delay: "00:00:05"
- service: tesla_charge_guard.snapshot
continue_on_error: true
data:
refresh: true
response_variable: verification
- wait_template: >-
{{ is_state('sensor.tesla_home_charge_guard', 'stopped')
and state_attr('sensor.tesla_home_charge_guard', 'sample_time') | float(0) > stop_requested_at | float(0) }}
timeout: "00:00:25"
continue_on_timeout: true
- if:
- condition: template
value_template: >-
{{ not wait.completed and repeat.index == 1
and state_attr('sensor.tesla_home_charge_guard', 'stop_allowed') == true }}
then:
- service: switch.turn_off
continue_on_error: true
target:
entity_id: switch.spaceship_charge
- variables:
stop_verified: >-
{{ is_state('sensor.tesla_home_charge_guard', 'stopped')
and state_attr('sensor.tesla_home_charge_guard', 'sample_time') | float(0) > stop_requested_at | float(0) }}
- if:
- condition: template
value_template: "{{ not stop_verified }}"
then:
- service: script.notify_engine
continue_on_error: true
data:
title: "URGENT: Unplug Tesla"
title: "URGENT: Tesla Stop Unverified"
value1: >-
The JuiceBox reports {{ observed_current }} A, at or above the
20 A safety threshold. Unplug the Tesla now.
who: 'parents'
group: 'Tesla_Charge_Rate_Alarm'
level: 'critical'
{{ incident_reason }} Fresh zero-current confirmation did not arrive.
Check the Tesla and unplug it if charging continues. Home charging
requires your review before resuming.
who: parents
group: Tesla_Charge_Rate_Alarm
tag: Tesla_Charge_Rate_Alarm
level: critical
- service: script.send_to_logbook
data:
topic: "JUICEBOX"
topic: JUICEBOX
message: >-
High charge-rate alarm sent at {{ observed_current }} A; garage and
parents were told to unplug the Tesla.
{{ incident_reason }} Stop {{ 'verified by a fresh zero-current report' if stop_verified else 'unverified after bounded attempts' }}.
Home charging restart review remains required; no start command was issued.
- alias: "Tesla Model Y - Reset High Charge Rate Alert"
id: ccb67406-4ac1-41de-8f47-a3a76e6d8762
automation:
- alias: "Tesla Model Y - Refresh Home Charge Verification"
id: 7602df3d-20b6-448d-bbcd-b00a774e6dd9
mode: single
max_exceeded: silent
trigger:
- platform: state
entity_id: sensor.juicebox_current_last_known
- platform: homeassistant
event: start
id: startup
- platform: time_pattern
minutes: "/1"
id: periodic
- platform: state
entity_id:
- sensor.spaceship_charger_current
- sensor.spaceship_charging
- device_tracker.spaceship_location
- input_boolean.tesla_home_charge_restart_required
id: vehicle
action:
- service: tesla_charge_guard.snapshot
continue_on_error: true
data:
refresh: >-
{{ trigger.id == 'periodic'
and (is_state('binary_sensor.juicepassproxy_investigation_needed', 'on')
or (is_state('sensor.tesla_home_charge_guard', 'unverified')
and state_attr('sensor.tesla_home_charge_guard', 'juicebox_fresh') == true)) }}
- alias: "Tesla Model Y - High Home Charge Rate Alert"
id: f4f013f9-74e7-492e-8400-ae84897086ef
mode: queued
max: 5
max_exceeded: silent
trigger:
- platform: event
event_type: tesla_charge_guard_report
action:
- if:
- condition: template
value_template: >-
{{ trigger.event.data.session_observed is not none
and is_state('input_boolean.tesla_home_charge_session_active', 'on') != trigger.event.data.session_observed }}
then:
- service: >-
{{ 'input_boolean.turn_on' if trigger.event.data.session_observed else 'input_boolean.turn_off' }}
target:
entity_id: input_boolean.tesla_home_charge_session_active
- if:
- condition: state
entity_id: input_boolean.tesla_high_charge_rate_alert_active
state: 'off'
- condition: template
value_template: >-
{{ trigger.event.data.status == 'excessive'
or (trigger.event.data.status == 'charging_safe'
and is_state('input_boolean.tesla_home_charge_restart_required', 'on')) }}
then:
- service: script.turn_on
target:
entity_id: script.tesla_home_charge_protect
data:
variables:
reason: >-
{{ 'Home charging resumed while restart review was still required.' if trigger.event.data.status == 'charging_safe'
else 'Fresh home charging draw is ' ~ trigger.event.data.actual_current ~ ' A, above the 18 A guardrail.' }}
- alias: "Tesla Model Y - Reset High Charge Rate Alert"
id: ccb67406-4ac1-41de-8f47-a3a76e6d8762
mode: queued
max: 3
trigger:
- platform: state
entity_id: sensor.tesla_home_charge_guard
to: stopped
- platform: state
entity_id: script.tesla_home_charge_protect
to: 'off'
condition:
- condition: state
entity_id: input_boolean.tesla_high_charge_rate_alert_active
state: 'on'
- condition: numeric_state
entity_id: sensor.juicebox_current_last_known
below: 20
- condition: state
entity_id: script.tesla_home_charge_protect
state: 'off'
- condition: state
entity_id: sensor.tesla_home_charge_guard
state: stopped
- condition: template
value_template: >-
{% set sample = state_attr('sensor.tesla_home_charge_guard', 'sample_time') | float(0) %}
{{ sample > as_timestamp(states.input_boolean.tesla_high_charge_rate_alert_active.last_changed)
and -5 <= as_timestamp(now()) - sample <= 60 }}
action:
- service: input_boolean.turn_off
target:
entity_id: input_boolean.tesla_high_charge_rate_alert_active
- service: notify.ios_parents
continue_on_error: true
data:
message: clear_notification
data:
tag: Tesla_Charge_Rate_Alarm
- service: script.send_to_logbook
data:
topic: "JUICEBOX"
topic: JUICEBOX
message: >-
High charge-rate alarm reset; JuiceBox current is below 20 A.
Fresh post-incident zero current verified; the critical charging incident
cleared. Restart review stays enabled until a person clears it.
- alias: "Tesla Model Y - JuicePassProxy Monitoring Alert"
id: e4b6bf07-4760-4e7c-b2da-6db234a46df8
mode: parallel
max: 3
mode: queued
max: 5
trigger:
- platform: state
entity_id: binary_sensor.juicepassproxy_investigation_needed
@ -368,100 +615,122 @@ automation:
- platform: state
entity_id: binary_sensor.juicepassproxy_monitoring_degraded
to: 'on'
id: critical
id: assess
- platform: state
entity_id: binary_sensor.juicepassproxy_monitoring_degraded
from: 'on'
to: 'off'
entity_id: binary_sensor.tesla_home_charge_critical
to: 'on'
id: risk
- platform: state
entity_id: binary_sensor.juicepassproxy_monitoring_persistent
to: 'on'
id: persistent
- platform: state
entity_id: sensor.juicepassproxy_mqtt_heartbeat
to: online
id: restored
- platform: state
entity_id: sensor.tesla_home_charge_guard
attribute: juicebox_fresh
to: true
id: restored
action:
- choose:
- conditions:
- condition: trigger
id: investigate
- condition: state
entity_id: binary_sensor.juicepassproxy_investigation_needed
state: 'on'
sequence:
- service: script.joanna_dispatch
- service: tesla_charge_guard.snapshot
continue_on_error: true
data:
refresh: true
- service: script.tesla_home_charge_dispatch
continue_on_error: true
data:
trigger_context: "HA automation e4b6bf07-4760-4e7c-b2da-6db234a46df8 (JuicePassProxy Monitoring Alert)"
source: "home_assistant_automation.juicepassproxy_monitoring_alert"
summary: "JuiceBox MQTT reports have been absent for three minutes"
entity_ids:
- sensor.juicepassproxy_mqtt_heartbeat
- binary_sensor.juicepassproxy_investigation_needed
- binary_sensor.juicepassproxy_monitoring_degraded
- sensor.juicepassproxy_state
- sensor.juicepassproxy_health
- sensor.juicebox_status
- sensor.juicebox_current
- sensor.spaceship_charging
diagnostics: >-
mqtt_heartbeat={{ states('sensor.juicepassproxy_mqtt_heartbeat') }},
proxy_state={{ states('sensor.juicepassproxy_state') }},
proxy_health={{ states('sensor.juicepassproxy_health') }},
juicebox_status={{ states('sensor.juicebox_status') }},
juicebox_current={{ states('sensor.juicebox_current') }} A,
tesla_charging={{ states('sensor.spaceship_charging') }}
request: >-
Verify the owning JuicePassProxy container and recent MQTT reports.
Restore the failed monitoring path when safe; check for an active
deployment before restarting a container. Do not alter charger
limits or interrupt vehicle charging. Report verified recovery.
Verify the owning JuicePassProxy container and recent live MQTT
reports. Restore the failed path when safe; check for an active
deployment before restarting a container. Home Assistant owns
charging protection. Do not change limits, start charging, or
clear the restart-review latch. Report verified recovery.
- service: script.send_to_logbook
data:
topic: "JUICEBOX"
message: >-
JuiceBox MQTT reports were missing for three minutes; Joanna
investigation was requested before alerting the household.
topic: JUICEBOX
message: "Missing reports for three minutes; Joanna dispatched with source verification."
- conditions:
- condition: trigger
id: critical
id:
- assess
- risk
- condition: state
entity_id: binary_sensor.juicepassproxy_monitoring_degraded
state: 'on'
- condition: state
entity_id: input_boolean.tesla_high_charge_rate_alert_active
state: 'off'
sequence:
- parallel:
- service: notify.alexa_media_garage
continue_on_error: true
- service: tesla_charge_guard.snapshot
data:
refresh: true
response_variable: guard
- if:
- condition: template
value_template: "{{ guard.status == 'unverified' and guard.home_session }}"
then:
- service: script.tesla_home_charge_protect
data:
message: >-
Warning. JuiceBox monitoring reports are still missing
after five minutes. Check the Tesla and charger before
relying on the high-current alert.
data:
type: announce
- service: script.notify_engine
continue_on_error: true
reason: "Both charging sources are unverified during a home charging session after five minutes without JuiceBox reports."
else:
- service: script.send_to_logbook
data:
title: "URGENT: Check JuiceBox Monitoring"
value1: >-
JuiceBox MQTT reports are still missing after five
minutes. Joanna was dispatched to investigate. Check the
Tesla and charger; do not rely on the high-current alert
until monitoring returns.
who: 'parents'
group: 'JuiceBox_Proxy_Offline'
level: 'critical'
- service: script.send_to_logbook
data:
topic: "JUICEBOX"
message: >-
JuiceBox MQTT reports were still missing after five minutes;
the garage and parents were alerted after Joanna dispatch.
topic: JUICEBOX
message: >-
Five-minute monitoring assessment: {{ guard.status }}.
No charging critical alert was sent; persistent monitoring
faults receive a quiet warning at 15 minutes.
- conditions:
- condition: trigger
id: restored
id: persistent
- condition: state
entity_id: binary_sensor.juicepassproxy_monitoring_persistent
state: 'on'
- condition: state
entity_id: binary_sensor.tesla_home_charge_critical
state: 'off'
sequence:
- service: script.notify_engine
continue_on_error: true
data:
title: "JuiceBox Monitor Restored"
title: JuiceBox Monitoring Needs Review
value1: >-
Fresh JuiceBox reports have resumed. Charging telemetry and
the 20 A high-current alert are available again.
who: 'parents'
group: 'JuiceBox_Proxy_Offline'
JuiceBox reports have been missing for 15 minutes. Joanna was
dispatched. Charging assessment: {{ states('sensor.tesla_home_charge_guard') }}.
who: parents
group: JuiceBox_Proxy_Offline
tag: JuiceBox_Proxy_Offline
level: passive
- conditions:
- condition: trigger
id: restored
- condition: state
entity_id: sensor.juicepassproxy_mqtt_heartbeat
state: online
- condition: template
value_template: "{{ state_attr('sensor.tesla_home_charge_guard', 'juicebox_fresh') == true }}"
sequence:
- service: notify.ios_parents
continue_on_error: true
data:
message: clear_notification
data:
tag: JuiceBox_Proxy_Offline
- service: script.send_to_logbook
data:
topic: "JUICEBOX"
message: "Fresh JuiceBox MQTT reports resumed; the monitoring alert cleared."
topic: JUICEBOX
message: "Live JuiceBox reports verified; matching monitoring notification cleared."
- alias: "Tesla Model Y - Plug In Reminder"
id: 6a873341-c823-4d5d-9d42-fc2df52b93db

@ -197,6 +197,8 @@ exclude:
entities:
- automation.update_garage_get_status
# Source verification is live-only; critical/attention sensors and curated incidents remain recorded.
- sensor.tesla_home_charge_guard
- automation.docker_telemetry_template_refresh
- sensor.bgw_probe_payload
- sensor.infra_dashboard_active_issue_count
@ -290,3 +292,4 @@ exclude:
event_types:
- alexa_smart_home
- tesla_charge_guard_report

@ -27,7 +27,7 @@ Reusable scripts that other automations call for notifications, lighting, safety
### Featured files to browse
| File | Why it matters |
| --- | --- |
| [![YAML source: notify_engine](https://img.shields.io/static/v1?label=YAML&message=notify_engine&color=lightgrey&logo=github&logoColor=181717)](notify_engine.yaml) | Single entrypoint for rich push notifications. |
| [![YAML source: notify_engine](https://img.shields.io/static/v1?label=YAML&message=notify_engine&color=lightgrey&logo=github&logoColor=181717)](notify_engine.yaml) | Single entrypoint for rich push notifications, with optional tags that let recovery actions target a specific notification for clearing. |
| [![YAML source: hvac_control](https://img.shields.io/static/v1?label=YAML&message=hvac_control&color=lightgrey&logo=github&logoColor=181717)](hvac_control.yaml) | Single reconciler for grid, entry-point, occupancy, and target-temperature HVAC policy. |
| [![YAML source: notify_live_activity](https://img.shields.io/static/v1?label=YAML&message=notify_live_activity&color=lightgrey&logo=github&logoColor=181717)](notify_live_activity.yaml) | Shared helper for tagged live activity/live update pushes and clear commands. |
| [![YAML source: kiosk_driveway_camera_takeover](https://img.shields.io/static/v1?label=YAML&message=kiosk_driveway_camera_takeover&color=lightgrey&logo=github&logoColor=181717)](kiosk_driveway_camera_takeover.yaml) | Coordinates the bounded Driveway camera takeover and wake behavior across both household kiosks. |

@ -6,6 +6,8 @@
# Notification Engine Scripts - Flexible push helpers for family devices.
# Compose messages from value1/value2/value3 with optional actions.
# -------------------------------------------------------------------
# Notes: Optional notification tags let recovery actions target a specific push.
# Untagged callers retain their existing payload; groups are only visual.
# Video Tutorial: https://youtu.be/mK1wdpxhLbM
######################################################################
### Script for Sending Notifications to Mobile Phones
@ -24,6 +26,7 @@
# - 'who': Recipient identifier (e.g., 'stacey', 'carlo', 'paige', 'family', 'parents').
# - 'camera_entity': Camera entity ID for snapshots (optional).
# - 'group': Notification group (optional, defaults to 'information').
# - 'tag': Stable notification identity for replacement or clearing (optional).
# - 'level': Interruption level for the notification (optional, defaults to 'active').
# The 'notify_engine_two_button' variant includes additional parameters for two interactive buttons:
@ -61,6 +64,14 @@ notify_engine:
has_actions: >-
{{ action1 | default('', true) | length > 0
or action2 | default('', true) | length > 0 }}
notify_data: >-
{% set payload = {
'group': group | default('information', true),
'entity_id': camera_entity | default('', true),
'push': {'interruption-level': level | default('active', true)}
} %}
{% set notification_tag = tag | default('', true) | string | trim %}
{{ dict(payload, tag=notification_tag) if notification_tag else payload }}
- choose:
- conditions: "{{ has_actions }}"
sequence:
@ -68,30 +79,27 @@ notify_engine:
data:
title: "{{ title | default('', true) }}"
message: "{{ notify_message }}"
data:
actions:
- title: "{{ title1 | default('', true) }}"
action: "{{ action1 | default('', true) }}"
icon: "{{ icon1 | default('sfsymbols:house.circle', true) }}"
destructive: "{{ destructive1 | default(false, true) }}"
- title: "{{ title2 | default('', true) }}"
action: "{{ action2 | default('', true) }}"
icon: "{{ icon2 | default('sfsymbols:house.circle', true) }}"
destructive: "{{ destructive2 | default(false, true) }}"
group: "{{ group | default('information', true) }}"
entity_id: "{{ camera_entity | default('', true) }}"
push:
interruption-level: "{{ level | default('active', true) }}"
data: >-
{{ dict(notify_data, actions=[
{
'title': title1 | default('', true),
'action': action1 | default('', true),
'icon': icon1 | default('sfsymbols:house.circle', true),
'destructive': destructive1 | default(false, true)
},
{
'title': title2 | default('', true),
'action': action2 | default('', true),
'icon': icon2 | default('sfsymbols:house.circle', true),
'destructive': destructive2 | default(false, true)
}
]) }}
default:
- service: "{{ notify_service }}"
data:
title: "{{ title | default('', true) }}"
message: "{{ notify_message }}"
data:
group: "{{ group | default('information', true) }}"
entity_id: "{{ camera_entity | default('', true) }}"
push:
interruption-level: "{{ level | default('active', true) }}"
data: "{{ notify_data }}"
notify_engine_two_button:
alias: Notify Engine Two Button
@ -106,6 +114,7 @@ notify_engine_two_button:
value3: "{{ value3 | default('', true) }}"
who: "{{ who | default('family', true) }}"
group: "{{ group | default('information', true) }}"
tag: "{{ tag | default('', true) }}"
level: "{{ level | default('active', true) }}"
camera_entity: "{{ camera_entity | default('', true) }}"
title1: "{{ title1 | default('', true) }}"

@ -0,0 +1,113 @@
"""Safety policy contracts; no live services, credentials, or vehicle commands."""
import importlib.util
from pathlib import Path
import unittest
spec = importlib.util.spec_from_file_location(
"charge_policy", Path(__file__).resolve().parents[1]
/ "config/custom_components/tesla_charge_guard/policy.py")
policy = importlib.util.module_from_spec(spec)
spec.loader.exec_module(policy)
class ChargePolicyTests(unittest.TestCase):
def setUp(self):
self.now = 10_000
self.tesla = dict(current=16, charging="charging", sample_time=self.now,
location_time=self.now, at_home=True, dc=False, plugged=True)
self.juicebox = dict(current=16, status="Charging", current_time=self.now,
status_time=self.now)
def assess(self):
return policy.classify(self.tesla, self.juicebox, self.now)
def test_actual_current_guardrail_and_no_masking_by_lower_source(self):
for source in (self.tesla, self.juicebox):
with self.subTest(source=source):
source["current"] = 18.1
self.assertEqual(self.assess()["status"], "excessive")
self.assertTrue(self.assess()["stop_allowed"])
source["current"] = 18
self.assertEqual(self.assess()["status"], "charging_safe")
def test_cached_zero_and_missing_values_never_prove_stopped(self):
self.tesla.update(current=0, charging="stopped", sample_time=self.now - 121)
self.juicebox.update(current=0, status="Unplugged", current_time=self.now - 61)
self.assertEqual(self.assess()["status"], "unverified")
for invalid in (None, "unknown", "nan", "inf", -1, True):
with self.subTest(invalid=invalid):
self.tesla.update(current=invalid, sample_time=self.now)
self.juicebox.update(current=invalid, current_time=self.now)
self.assertEqual(self.assess()["status"], "unverified")
def test_fresh_away_and_dc_reports_veto_all_vehicle_stops(self):
self.juicebox["current"] = 24
self.tesla["at_home"] = False
self.assertFalse(self.assess()["stop_allowed"])
self.tesla.update(at_home=True, dc=True)
self.assertFalse(self.assess()["stop_allowed"])
self.tesla["current"] = None
result = policy.classify(self.tesla, self.juicebox, self.now, session_active=True)
self.assertFalse(result["stop_allowed"])
def test_home_charger_activity_without_vehicle_scope_requires_investigation(self):
result = policy.classify({}, self.juicebox, self.now)
self.assertEqual(result["status"], "unverified")
self.assertTrue(result["home_session"])
self.assertFalse(result["stop_allowed"])
self.assertIsNone(result["session_observed"])
def test_home_zero_clears_home_incident_without_stopping_vehicle_away(self):
self.tesla["at_home"] = False
self.juicebox.update(current=0, status="Unplugged")
result = self.assess()
self.assertEqual(result["status"], "stopped")
self.assertFalse(result["stop_allowed"])
self.assertFalse(result["home_session"])
def test_unknown_home_session_keeps_the_protective_stop_scope(self):
self.tesla["sample_time"] = self.now - 121
self.juicebox["current_time"] = self.now - 61
result = self.assess()
self.assertEqual(result["status"], "unverified")
self.assertTrue(result["home_session"])
self.assertTrue(result["stop_allowed"])
def test_newer_unplugged_report_does_not_resurrect_old_charging_session(self):
self.tesla["sample_time"] = self.now - 300
self.juicebox.update(current=0, status="Unplugged", current_time=self.now - 100,
status_time=self.now - 100)
result = self.assess()
self.assertEqual(result["status"], "unverified")
self.assertFalse(result["home_session"])
self.assertFalse(result["stop_allowed"])
def test_restored_home_session_remains_unverified_until_new_source_data(self):
result = policy.classify({}, {}, self.now, session_active=True)
self.assertEqual(result["status"], "unverified")
self.assertTrue(result["stop_allowed"])
self.assertIsNone(result["session_observed"])
self.tesla["at_home"] = False
result = policy.classify(self.tesla, {}, self.now, session_active=True)
self.assertFalse(result["stop_allowed"])
self.assertFalse(result["session_observed"])
def test_fresh_zero_requires_a_stopped_state_and_no_active_conflict(self):
self.tesla.update(current=0, charging="stopped")
self.juicebox.update(current=0, status="Unplugged")
self.assertEqual(self.assess()["status"], "stopped")
self.tesla.update(current=16, charging="charging")
self.assertNotEqual(self.assess()["status"], "stopped")
self.tesla.update(current=0, charging="unknown")
self.juicebox["status"] = "unknown"
self.assertEqual(self.assess()["status"], "unverified")
def test_future_samples_and_old_status_cannot_prove_safe(self):
self.tesla["sample_time"] = self.now + 6
self.juicebox["status_time"] = self.now - 61
self.assertEqual(self.assess()["status"], "unverified")
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,66 @@
"""Adapter contracts, run inside an HA environment with upstream I/O isolated."""
import asyncio
from pathlib import Path
import sys
from tempfile import TemporaryDirectory
from time import time
from types import SimpleNamespace
import unittest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "config"))
from custom_components.tesla_charge_guard import async_setup
from homeassistant.core import HomeAssistant
class AdapterContractTests(unittest.IsolatedAsyncioTestCase):
async def test_retained_reports_and_successful_cached_refresh_do_not_prove_safety(self):
with TemporaryDirectory() as directory:
hass = HomeAssistant(directory)
hass.states.async_set("zone.home", "0", {"latitude": 0, "longitude": 0, "radius": 100})
old = (time() - 600) * 1000
data = dict(charge_state_charger_actual_current=0,
charge_state_charging_state="Disconnected",
charge_state_timestamp=old, drive_state_timestamp=old,
drive_state_latitude=0, drive_state_longitude=0,
charge_state_fast_charger_present=False)
async def refresh():
# A successful refresh with a sleeping vehicle returns cached data.
pass
vehicle = SimpleNamespace(vin="fixture", coordinator=SimpleNamespace(data=data, async_refresh=refresh))
registry_entry = SimpleNamespace(platform="tesla_fleet", config_entry_id="fixture", unique_id="fixture-current")
registry = SimpleNamespace(async_get=lambda entity_id: registry_entry)
hass.config_entries = SimpleNamespace(async_get_entry=lambda entry_id: SimpleNamespace(runtime_data=SimpleNamespace(vehicles=[vehicle])))
callbacks = {}
async def subscribe(_hass, topic, callback):
callbacks[topic] = callback
return lambda: None
with patch("custom_components.tesla_charge_guard.er.async_get", return_value=registry), patch("custom_components.tesla_charge_guard.mqtt.async_subscribe", side_effect=subscribe):
await async_setup(hass, {"tesla_charge_guard": dict(entity_id="sensor.fixture", mqtt_prefix="fixture", current_limit=18)})
callbacks["fixture/Current/state"](SimpleNamespace(topic="fixture/Current/state", payload="24", retain=True))
callbacks["fixture/Status/state"](SimpleNamespace(topic="fixture/Status/state", payload="Charging", retain=True))
result = await hass.services.async_call("tesla_charge_guard", "snapshot", {"refresh": True}, blocking=True, return_response=True)
self.assertEqual(result["status"], "unverified")
self.assertFalse(result["tesla_fresh"])
self.assertFalse(result["juicebox_fresh"])
callbacks["fixture/Current/state"](SimpleNamespace(topic="fixture/Current/state", payload="24", retain=False))
callbacks["fixture/Status/state"](SimpleNamespace(topic="fixture/Status/state", payload="Charging", retain=False))
result = await hass.services.async_call("tesla_charge_guard", "snapshot", {}, blocking=True, return_response=True)
self.assertEqual(result["status"], "excessive")
self.assertEqual(result["actual_current"], 24)
self.assertTrue(result["stop_allowed"])
self.assertFalse({"vin", "latitude", "longitude", "access_token"} & result.keys())
callbacks["fixture/Current/state"](SimpleNamespace(topic="fixture/Current/state", payload="nan", retain=False))
result = await hass.services.async_call("tesla_charge_guard", "snapshot", {}, blocking=True, return_response=True)
self.assertEqual(result["status"], "unverified")
self.assertFalse(result["juicebox_fresh"])
await hass.async_stop(force=True)
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save

Powered by TurnKey Linux.