Skip to content

Commit

Permalink
feat: add sensors, throttle updates to 5s
Browse files Browse the repository at this point in the history
  • Loading branch information
vermut committed May 21, 2024
1 parent 9d41131 commit e75c5b3
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 69 deletions.
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1 change: 1 addition & 0 deletions custom_components/openmower/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
PLATFORMS: list[Platform] = [
Platform.LAWN_MOWER,
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.DEVICE_TRACKER,
]
_LOGGER = logging.getLogger(__name__)
Expand Down
44 changes: 44 additions & 0 deletions custom_components/openmower/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations

import logging

from homeassistant.components import mqtt
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorDeviceClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PREFIX, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import OpenMowerMqttEntity

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
# Make sure MQTT integration is enabled and the client is available
if not await mqtt.async_wait_for_mqtt_client(hass):
_LOGGER.error("MQTT integration is not available")
return

prefix = entry.data[CONF_PREFIX]
async_add_entities(
[
OpenMowerMqttBinarySensorEntity(
"Emergency", prefix, "robot_state/json", "emergency"
),
OpenMowerMqttBinarySensorEntity(
"Is charging", prefix, "robot_state/json", "is_charging"
),
]
)


class OpenMowerMqttBinarySensorEntity(OpenMowerMqttEntity, BinarySensorEntity):
def _process_update(self, value):
self._attr_is_on = bool(value)
44 changes: 10 additions & 34 deletions custom_components/openmower/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@
from homeassistant.components.device_tracker import TrackerEntity, SourceType
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PREFIX, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.json import json_loads_object

from .const import DOMAIN
from .entity import OpenMowerMqttEntity

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -42,21 +39,13 @@ async def async_setup_entry(
)


class OpenMowerPosition(TrackerEntity):
_attr_name = "OpenMower"
_attr_unique_id = "openmower_position"
_attr_device_info = DeviceInfo(
identifiers={(DOMAIN, "openmower")}, manufacturer="OpenMower"
)

class OpenMowerPosition(OpenMowerMqttEntity, TrackerEntity):
# Constants
_EARTH = 6371008.8
_M = 1 / ((2 * math.pi / 360) * 6371008.8)

def __init__(self, prefix: str, datum_lat: float, datum_lon: float) -> None:
self._mqtt_topic_prefix = prefix
if self._mqtt_topic_prefix and self._mqtt_topic_prefix[-1] != "/":
self._mqtt_topic_prefix = self._mqtt_topic_prefix + "/"
super().__init__("Position", prefix, "robot_state/json", "pose")

self._datum_lat = datum_lat
self._datum_lon = datum_lon
Expand All @@ -65,28 +54,15 @@ def __init__(self, prefix: str, datum_lat: float, datum_lon: float) -> None:
self._attr_longitude = datum_lon
self._attr_latitude = datum_lat

async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
await mqtt.async_subscribe(
self.hass,
self._mqtt_topic_prefix + "robot_state/json",
self.async_robot_state_received,
0,
)

@callback
def async_robot_state_received(self, msg: mqtt.ReceiveMessage) -> None:
value_json = json_loads_object(msg.payload)

def _process_update(self, value):
# https://stackoverflow.com/a/50506609
# https://github.com/Turfjs/turf/issues/635#issuecomment-292011500
# Calculate new longitude and latitude
self._attr_latitude = self._datum_lat + (value_json["pose"]["y"] * self._M)
self._attr_longitude = self._datum_lon + (
value_json["pose"]["x"] * self._M
) / math.cos(self._datum_lat * (math.pi / 180))

self.async_write_ha_state()
# Calculate new latitude and longitude
self._attr_latitude = self._datum_lat + (value["y"] * self._M)
self._attr_longitude = self._datum_lon + (value["x"] * self._M) / math.cos(
self._datum_lat * (math.pi / 180)
)

@property
def latitude(self) -> float | None:
Expand Down
59 changes: 59 additions & 0 deletions custom_components/openmower/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from datetime import timedelta
from typing import Optional

from homeassistant.components import mqtt
from homeassistant.components.openmower import DOMAIN
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify, Throttle
from homeassistant.util.json import json_loads_object

MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)


class OpenMowerMqttEntity(Entity):
_attr_has_entity_name = True

def __init__(self, name: str, prefix: str, topic: str, key: Optional[str]) -> None:
self._attr_name = name
self._attr_unique_id = slugify(f"{prefix}_{name}").lower()

self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, slugify(prefix))},
manufacturer="OpenMower",
name=slugify(prefix).capitalize(),
)

self._mqtt_topic = topic
self._mqtt_topic_prefix = prefix
self._mqtt_payload_json_key = key

if self._mqtt_topic_prefix and self._mqtt_topic_prefix[-1] != "/":
self._mqtt_topic_prefix = self._mqtt_topic_prefix + "/"

async def async_added_to_hass(self) -> None:
await mqtt.async_subscribe(
self.hass,
self._mqtt_topic_prefix + self._mqtt_topic,
self._async_robot_state_received,
0,
)

@callback
def _async_robot_state_received(self, msg: mqtt.ReceiveMessage) -> None:
self._update_state(msg)

@Throttle(MIN_TIME_BETWEEN_UPDATES)
def _update_state(self, msg):
if self._mqtt_payload_json_key:
value_json = json_loads_object(msg.payload)
self._process_update(value_json[self._mqtt_payload_json_key])
else:
self._process_update(msg.payload)
self.async_write_ha_state()

def _process_update(self, value):
raise NotImplementedError("Subclasses should implement this!")
1 change: 0 additions & 1 deletion custom_components/openmower/lawn_mower.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.json import json_loads_object

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)
Expand Down
125 changes: 91 additions & 34 deletions custom_components/openmower/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
from homeassistant.components import mqtt
from homeassistant.components.sensor import SensorEntity, SensorDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, CONF_PREFIX
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.const import (
PERCENTAGE,
CONF_PREFIX,
EntityCategory,
UnitOfTemperature,
UnitOfElectricPotential,
UnitOfElectricCurrent,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.json import json_loads_object

from .const import DOMAIN
from .entity import OpenMowerMqttEntity

_LOGGER = logging.getLogger(__name__)

Expand All @@ -26,37 +30,90 @@ async def async_setup_entry(
_LOGGER.error("MQTT integration is not available")
return

async_add_entities([OpenMowerBatterySensor(entry.data[CONF_PREFIX])])
prefix = entry.data[CONF_PREFIX]
async_add_entities(
[
OpenMowerBatterySensor(
"Battery", prefix, "robot_state/json", "battery_percentage"
),
OpenMowerDiagnosticSensor(
"Current action progress",
prefix,
"robot_state/json",
"current_action_progress",
),
OpenMowerDiagnosticSensor(
"GPS Percentage", prefix, "robot_state/json", "gps_percentage"
),
OpenMowerMqttSensorEntity(
"Current State", prefix, "robot_state/json", "current_state"
),
OpenMowerCurrentSensor(
"Charge Current", prefix, "sensors/om_charge_current/data", None
),
OpenMowerRawDiagnosticSensor(
"GPS Accuracy", prefix, "sensors/om_gps_accuracy/data", None
),
OpenMowerTemperatureSensor(
"Left ESC Temperature", prefix, "sensors/om_left_esc_temp/data", None
),
OpenMowerTemperatureSensor(
"Mow ESC Temperature", prefix, "sensors/om_mow_esc_temp/data", None
),
OpenMowerCurrentSensor(
"Mow Motor Current", prefix, "sensors/om_mow_motor_current/data", None
),
OpenMowerTemperatureSensor(
"Mow Motor Temperature", prefix, "sensors/om_mow_motor_temp/data", None
),
OpenMowerTemperatureSensor(
"Right ESC Temperature", prefix, "sensors/om_right_esc_temp/data", None
),
OpenMowerVoltageSensor(
"Battery Voltage", prefix, "sensors/om_v_battery/data", None
),
OpenMowerVoltageSensor(
"Charge Voltage", prefix, "sensors/om_v_charge/data", None
),
]
)


class OpenMowerMqttSensorEntity(OpenMowerMqttEntity, SensorEntity):
def _process_update(self, value):
self._attr_native_value = value


class OpenMowerBatterySensor(SensorEntity):
class OpenMowerBatterySensor(OpenMowerMqttSensorEntity):
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE

_attr_name = "Battery"
_attr_unique_id = "openmower_battery"
_attr_device_info = DeviceInfo(
identifiers={(DOMAIN, "openmower")}, manufacturer="OpenMower"
)
def _process_update(self, value):
self._attr_native_value = int(float(value) * 100)


class OpenMowerDiagnosticSensor(OpenMowerMqttSensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC


class OpenMowerRawDiagnosticSensor(OpenMowerDiagnosticSensor):
def _process_update(self, value):
self._attr_native_value = float(value)


class OpenMowerCurrentSensor(OpenMowerRawDiagnosticSensor):
_attr_device_class = SensorDeviceClass.CURRENT
_attr_unit_of_measurement = UnitOfElectricCurrent.AMPERE
_attr_suggested_display_precision = 1


class OpenMowerTemperatureSensor(OpenMowerRawDiagnosticSensor):
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_unit_of_measurement = UnitOfTemperature.CELSIUS
_attr_suggested_display_precision = 0


def __init__(self, prefix: str) -> None:
self._mqtt_topic_prefix = prefix
if self._mqtt_topic_prefix and self._mqtt_topic_prefix[-1] != "/":
self._mqtt_topic_prefix = self._mqtt_topic_prefix + "/"

async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
await mqtt.async_subscribe(
self.hass,
self._mqtt_topic_prefix + "robot_state/json",
self.async_robot_state_received,
0,
)
_LOGGER.info("Added to Hass, subscribing to topics")

@callback
def async_robot_state_received(self, msg: mqtt.ReceiveMessage) -> None:
value_json = json_loads_object(msg.payload)

self._attr_native_value = int(float(value_json["battery_percentage"]) * 100)
self.async_write_ha_state()
class OpenMowerVoltageSensor(OpenMowerRawDiagnosticSensor):
_attr_device_class = SensorDeviceClass.VOLTAGE
_attr_unit_of_measurement = UnitOfElectricPotential.VOLT
_attr_suggested_display_precision = 1

0 comments on commit e75c5b3

Please sign in to comment.