Clean up stale directories and fix monorepo structure
- Remove remaining home-assistant-integration/ and dev-config/ - Move dashboard to repo root for proper monorepo layout - Remove crash dumps and symlinks - Clean structure: server/ and dashboard/ at root level
This commit is contained in:
parent
91559e7461
commit
9ae6a15970
|
Before Width: | Height: | Size: 152 B After Width: | Height: | Size: 152 B |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@ -1 +0,0 @@
|
|||||||
2025.7.4
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
# Development automations for testing Systant integration
|
|
||||||
[]
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
blueprint:
|
|
||||||
name: Motion-activated Light
|
|
||||||
description: Turn on a light when motion is detected.
|
|
||||||
domain: automation
|
|
||||||
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml
|
|
||||||
author: Home Assistant
|
|
||||||
input:
|
|
||||||
motion_entity:
|
|
||||||
name: Motion Sensor
|
|
||||||
selector:
|
|
||||||
entity:
|
|
||||||
filter:
|
|
||||||
- device_class: occupancy
|
|
||||||
domain: binary_sensor
|
|
||||||
- device_class: motion
|
|
||||||
domain: binary_sensor
|
|
||||||
light_target:
|
|
||||||
name: Light
|
|
||||||
selector:
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: light
|
|
||||||
no_motion_wait:
|
|
||||||
name: Wait time
|
|
||||||
description: Time to leave the light on after last motion is detected.
|
|
||||||
default: 120
|
|
||||||
selector:
|
|
||||||
number:
|
|
||||||
min: 0
|
|
||||||
max: 3600
|
|
||||||
unit_of_measurement: seconds
|
|
||||||
|
|
||||||
# If motion is detected within the delay,
|
|
||||||
# we restart the script.
|
|
||||||
mode: restart
|
|
||||||
max_exceeded: silent
|
|
||||||
|
|
||||||
triggers:
|
|
||||||
trigger: state
|
|
||||||
entity_id: !input motion_entity
|
|
||||||
from: "off"
|
|
||||||
to: "on"
|
|
||||||
|
|
||||||
actions:
|
|
||||||
- alias: "Turn on the light"
|
|
||||||
action: light.turn_on
|
|
||||||
target: !input light_target
|
|
||||||
- alias: "Wait until there is no motion from device"
|
|
||||||
wait_for_trigger:
|
|
||||||
trigger: state
|
|
||||||
entity_id: !input motion_entity
|
|
||||||
from: "on"
|
|
||||||
to: "off"
|
|
||||||
- alias: "Wait the number of seconds that has been set"
|
|
||||||
delay: !input no_motion_wait
|
|
||||||
- alias: "Turn off the light"
|
|
||||||
action: light.turn_off
|
|
||||||
target: !input light_target
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
blueprint:
|
|
||||||
name: Zone Notification
|
|
||||||
description: Send a notification to a device when a person leaves a specific zone.
|
|
||||||
domain: automation
|
|
||||||
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
|
|
||||||
author: Home Assistant
|
|
||||||
input:
|
|
||||||
person_entity:
|
|
||||||
name: Person
|
|
||||||
selector:
|
|
||||||
entity:
|
|
||||||
filter:
|
|
||||||
domain: person
|
|
||||||
zone_entity:
|
|
||||||
name: Zone
|
|
||||||
selector:
|
|
||||||
entity:
|
|
||||||
filter:
|
|
||||||
domain: zone
|
|
||||||
notify_device:
|
|
||||||
name: Device to notify
|
|
||||||
description: Device needs to run the official Home Assistant app to receive notifications.
|
|
||||||
selector:
|
|
||||||
device:
|
|
||||||
filter:
|
|
||||||
integration: mobile_app
|
|
||||||
|
|
||||||
triggers:
|
|
||||||
trigger: state
|
|
||||||
entity_id: !input person_entity
|
|
||||||
|
|
||||||
variables:
|
|
||||||
zone_entity: !input zone_entity
|
|
||||||
# This is the state of the person when it's in this zone.
|
|
||||||
zone_state: "{{ states[zone_entity].name }}"
|
|
||||||
person_entity: !input person_entity
|
|
||||||
person_name: "{{ states[person_entity].name }}"
|
|
||||||
|
|
||||||
conditions:
|
|
||||||
condition: template
|
|
||||||
# The first case handles leaving the Home zone which has a special state when zoning called 'home'.
|
|
||||||
# The second case handles leaving all other zones.
|
|
||||||
value_template: "{{ zone_entity == 'zone.home' and trigger.from_state.state == 'home' and trigger.to_state.state != 'home' or trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}"
|
|
||||||
|
|
||||||
actions:
|
|
||||||
- alias: "Notify that a person has left the zone"
|
|
||||||
domain: mobile_app
|
|
||||||
type: notify
|
|
||||||
device_id: !input notify_device
|
|
||||||
message: "{{ person_name }} has left {{ zone_state }}"
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
blueprint:
|
|
||||||
name: Confirmable Notification
|
|
||||||
description: >-
|
|
||||||
A script that sends an actionable notification with a confirmation before
|
|
||||||
running the specified action.
|
|
||||||
domain: script
|
|
||||||
source_url: https://github.com/home-assistant/core/blob/master/homeassistant/components/script/blueprints/confirmable_notification.yaml
|
|
||||||
author: Home Assistant
|
|
||||||
input:
|
|
||||||
notify_device:
|
|
||||||
name: Device to notify
|
|
||||||
description: Device needs to run the official Home Assistant app to receive notifications.
|
|
||||||
selector:
|
|
||||||
device:
|
|
||||||
filter:
|
|
||||||
integration: mobile_app
|
|
||||||
title:
|
|
||||||
name: "Title"
|
|
||||||
description: "The title of the button shown in the notification."
|
|
||||||
default: ""
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
message:
|
|
||||||
name: "Message"
|
|
||||||
description: "The message body"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
confirm_text:
|
|
||||||
name: "Confirmation Text"
|
|
||||||
description: "Text to show on the confirmation button"
|
|
||||||
default: "Confirm"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
confirm_action:
|
|
||||||
name: "Confirmation Action"
|
|
||||||
description: "Action to run when notification is confirmed"
|
|
||||||
default: []
|
|
||||||
selector:
|
|
||||||
action:
|
|
||||||
dismiss_text:
|
|
||||||
name: "Dismiss Text"
|
|
||||||
description: "Text to show on the dismiss button"
|
|
||||||
default: "Dismiss"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
dismiss_action:
|
|
||||||
name: "Dismiss Action"
|
|
||||||
description: "Action to run when notification is dismissed"
|
|
||||||
default: []
|
|
||||||
selector:
|
|
||||||
action:
|
|
||||||
|
|
||||||
mode: restart
|
|
||||||
|
|
||||||
sequence:
|
|
||||||
- alias: "Set up variables"
|
|
||||||
variables:
|
|
||||||
action_confirm: "{{ 'CONFIRM_' ~ context.id }}"
|
|
||||||
action_dismiss: "{{ 'DISMISS_' ~ context.id }}"
|
|
||||||
- alias: "Send notification"
|
|
||||||
domain: mobile_app
|
|
||||||
type: notify
|
|
||||||
device_id: !input notify_device
|
|
||||||
title: !input title
|
|
||||||
message: !input message
|
|
||||||
data:
|
|
||||||
actions:
|
|
||||||
- action: "{{ action_confirm }}"
|
|
||||||
title: !input confirm_text
|
|
||||||
- action: "{{ action_dismiss }}"
|
|
||||||
title: !input dismiss_text
|
|
||||||
- alias: "Awaiting response"
|
|
||||||
wait_for_trigger:
|
|
||||||
- trigger: event
|
|
||||||
event_type: mobile_app_notification_action
|
|
||||||
event_data:
|
|
||||||
action: "{{ action_confirm }}"
|
|
||||||
- trigger: event
|
|
||||||
event_type: mobile_app_notification_action
|
|
||||||
event_data:
|
|
||||||
action: "{{ action_dismiss }}"
|
|
||||||
- choose:
|
|
||||||
- conditions: "{{ wait.trigger.event.data.action == action_confirm }}"
|
|
||||||
sequence: !input confirm_action
|
|
||||||
- conditions: "{{ wait.trigger.event.data.action == action_dismiss }}"
|
|
||||||
sequence: !input dismiss_action
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
# Home Assistant Development Configuration for Systant Integration
|
|
||||||
|
|
||||||
homeassistant:
|
|
||||||
name: Systant Development
|
|
||||||
latitude: 0
|
|
||||||
longitude: 0
|
|
||||||
elevation: 0
|
|
||||||
unit_system: metric
|
|
||||||
time_zone: UTC
|
|
||||||
|
|
||||||
# Basic required components only
|
|
||||||
http:
|
|
||||||
server_host: 0.0.0.0
|
|
||||||
server_port: 8123
|
|
||||||
|
|
||||||
# Enable API
|
|
||||||
api:
|
|
||||||
|
|
||||||
# Enable logging
|
|
||||||
logger:
|
|
||||||
default: info
|
|
||||||
logs:
|
|
||||||
custom_components.systant: debug
|
|
||||||
|
|
||||||
# MQTT Configuration (for our integration testing)
|
|
||||||
mqtt:
|
|
||||||
- broker: mqtt.home
|
|
||||||
port: 1883
|
|
||||||
discovery: true
|
|
||||||
discovery_prefix: homeassistant
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
# Development scenes for testing Systant integration
|
|
||||||
[]
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
# Development scripts for testing Systant integration
|
|
||||||
{}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
"""The Systant System Monitor integration."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
PLATFORMS = ["sensor", "binary_sensor"]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
||||||
"""Set up the Systant integration."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Set up Systant from a config entry."""
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
|
|
||||||
# Store the config entry data
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = entry.data
|
|
||||||
|
|
||||||
# Forward the setup to the sensor platform
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Unload a config entry."""
|
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
||||||
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
"""Config flow for Systant integration."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant import config_entries
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required("mqtt_prefix", default="systant"): str,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|
||||||
"""Handle a config flow for Systant."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
|
|
||||||
async def async_step_user(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> FlowResult:
|
|
||||||
"""Handle the initial step."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
try:
|
|
||||||
await self.async_set_unique_id(user_input["mqtt_prefix"])
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=f"Systant ({user_input['mqtt_prefix']})",
|
|
||||||
data=user_input,
|
|
||||||
)
|
|
||||||
except CannotConnect:
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
except InvalidConfig:
|
|
||||||
errors["base"] = "invalid_config"
|
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
_LOGGER.exception("Unexpected exception")
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CannotConnect(HomeAssistantError):
|
|
||||||
"""Error to indicate we cannot connect."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidConfig(HomeAssistantError):
|
|
||||||
"""Error to indicate there is invalid configuration."""
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
"""Constants for the Systant integration."""
|
|
||||||
|
|
||||||
DOMAIN = "systant"
|
|
||||||
|
|
||||||
# MQTT topics
|
|
||||||
MQTT_STATS_TOPIC = "systant/+/stats"
|
|
||||||
MQTT_COMMANDS_TOPIC = "systant/+/commands"
|
|
||||||
|
|
||||||
# Default configuration
|
|
||||||
DEFAULT_NAME = "Systant"
|
|
||||||
DEFAULT_MQTT_PREFIX = "systant"
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "systant",
|
|
||||||
"name": "Systant System Monitor",
|
|
||||||
"codeowners": ["@ryanpandya"],
|
|
||||||
"config_flow": true,
|
|
||||||
"dependencies": ["mqtt"],
|
|
||||||
"documentation": "https://github.com/ryanpandya/systant",
|
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "local_push",
|
|
||||||
"mqtt": ["systant/+/stats", "systant/+/commands"],
|
|
||||||
"requirements": [],
|
|
||||||
"version": "0.1.0"
|
|
||||||
}
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
"""Support for Systant sensors."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components import mqtt
|
|
||||||
from homeassistant.components.sensor import SensorEntity
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
from homeassistant.util import dt as dt_util
|
|
||||||
|
|
||||||
from .const import DOMAIN, MQTT_STATS_TOPIC
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
config_entry: ConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up Systant sensors from config entry."""
|
|
||||||
mqtt_prefix = config_entry.data.get("mqtt_prefix", "systant")
|
|
||||||
|
|
||||||
# Create a coordinator that will manage discovered hosts
|
|
||||||
coordinator = SystantCoordinator(hass, mqtt_prefix)
|
|
||||||
await coordinator.async_setup()
|
|
||||||
|
|
||||||
# Store coordinator in hass data
|
|
||||||
hass.data[DOMAIN][config_entry.entry_id] = coordinator
|
|
||||||
|
|
||||||
|
|
||||||
class SystantCoordinator:
|
|
||||||
"""Coordinate discovery and management of Systant hosts."""
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, mqtt_prefix: str) -> None:
|
|
||||||
"""Initialize the coordinator."""
|
|
||||||
self.hass = hass
|
|
||||||
self.mqtt_prefix = mqtt_prefix
|
|
||||||
self.hosts: dict[str, SystantHost] = {}
|
|
||||||
self._unsubscribe_mqtt = None
|
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
|
||||||
"""Set up MQTT subscription for host discovery."""
|
|
||||||
topic = f"{self.mqtt_prefix}/+/stats"
|
|
||||||
|
|
||||||
self._unsubscribe_mqtt = await mqtt.async_subscribe(
|
|
||||||
self.hass, topic, self._message_received, 0
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER.info("Subscribed to %s for Systant host discovery", topic)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _message_received(self, message: mqtt.ReceiveMessage) -> None:
|
|
||||||
"""Handle received MQTT message."""
|
|
||||||
try:
|
|
||||||
# Extract hostname from topic: systant/hostname/stats
|
|
||||||
topic_parts = message.topic.split("/")
|
|
||||||
if len(topic_parts) != 3:
|
|
||||||
return
|
|
||||||
|
|
||||||
hostname = topic_parts[1]
|
|
||||||
|
|
||||||
# Parse message payload
|
|
||||||
payload = json.loads(message.payload)
|
|
||||||
|
|
||||||
# Create or update host
|
|
||||||
if hostname not in self.hosts:
|
|
||||||
_LOGGER.info("Discovered new Systant host: %s", hostname)
|
|
||||||
self.hosts[hostname] = SystantHost(self.hass, hostname, self.mqtt_prefix)
|
|
||||||
|
|
||||||
# Update host with new data
|
|
||||||
self.hosts[hostname].update_data(payload)
|
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
_LOGGER.warning("Invalid JSON received from %s", message.topic)
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error processing message from %s: %s", message.topic, err)
|
|
||||||
|
|
||||||
async def async_unload(self) -> None:
|
|
||||||
"""Unload the coordinator."""
|
|
||||||
if self._unsubscribe_mqtt:
|
|
||||||
self._unsubscribe_mqtt()
|
|
||||||
|
|
||||||
|
|
||||||
class SystantHost:
|
|
||||||
"""Represent a Systant host with its sensors."""
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, hostname: str, mqtt_prefix: str) -> None:
|
|
||||||
"""Initialize the host."""
|
|
||||||
self.hass = hass
|
|
||||||
self.hostname = hostname
|
|
||||||
self.mqtt_prefix = mqtt_prefix
|
|
||||||
self.sensors: list[SystantSensor] = []
|
|
||||||
self.last_seen = None
|
|
||||||
|
|
||||||
# Create sensors for this host
|
|
||||||
self._create_sensors()
|
|
||||||
|
|
||||||
def _create_sensors(self) -> None:
|
|
||||||
"""Create sensor entities for this host."""
|
|
||||||
# Last seen sensor
|
|
||||||
last_seen_sensor = SystantLastSeenSensor(self.hostname, self.mqtt_prefix)
|
|
||||||
self.sensors.append(last_seen_sensor)
|
|
||||||
|
|
||||||
# Add sensors to Home Assistant
|
|
||||||
self.hass.async_create_task(
|
|
||||||
self.hass.helpers.entity_platform.async_add_entities(
|
|
||||||
self.sensors, update_before_add=True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_data(self, data: dict[str, Any]) -> None:
|
|
||||||
"""Update sensors with new data."""
|
|
||||||
self.last_seen = dt_util.utcnow()
|
|
||||||
|
|
||||||
# Update all sensors
|
|
||||||
for sensor in self.sensors:
|
|
||||||
sensor.update_from_data(data)
|
|
||||||
|
|
||||||
|
|
||||||
class SystantSensor(SensorEntity):
|
|
||||||
"""Base class for Systant sensors."""
|
|
||||||
|
|
||||||
def __init__(self, hostname: str, mqtt_prefix: str, sensor_type: str) -> None:
|
|
||||||
"""Initialize the sensor."""
|
|
||||||
self._hostname = hostname
|
|
||||||
self._mqtt_prefix = mqtt_prefix
|
|
||||||
self._sensor_type = sensor_type
|
|
||||||
self._attr_unique_id = f"{mqtt_prefix}_{hostname}_{sensor_type}"
|
|
||||||
self._attr_name = f"Systant {hostname} {sensor_type.replace('_', ' ').title()}"
|
|
||||||
self._attr_device_info = {
|
|
||||||
"identifiers": {(DOMAIN, f"{mqtt_prefix}_{hostname}")},
|
|
||||||
"name": f"Systant {hostname}",
|
|
||||||
"manufacturer": "Systant",
|
|
||||||
"model": "System Monitor",
|
|
||||||
"via_device": (DOMAIN, mqtt_prefix),
|
|
||||||
}
|
|
||||||
|
|
||||||
def update_from_data(self, data: dict[str, Any]) -> None:
|
|
||||||
"""Update sensor from MQTT data."""
|
|
||||||
# To be implemented by subclasses
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SystantLastSeenSensor(SystantSensor):
|
|
||||||
"""Sensor for last seen timestamp."""
|
|
||||||
|
|
||||||
def __init__(self, hostname: str, mqtt_prefix: str) -> None:
|
|
||||||
"""Initialize the last seen sensor."""
|
|
||||||
super().__init__(hostname, mqtt_prefix, "last_seen")
|
|
||||||
self._attr_device_class = "timestamp"
|
|
||||||
self._attr_icon = "mdi:clock-outline"
|
|
||||||
|
|
||||||
def update_from_data(self, data: dict[str, Any]) -> None:
|
|
||||||
"""Update with current timestamp."""
|
|
||||||
self._attr_native_value = dt_util.utcnow()
|
|
||||||
self.async_write_ha_state()
|
|
||||||
Loading…
Reference in New Issue
Block a user