barleybobs / homeassistant-ecowater-softener

A Homeassistant custom component to integrate Ecowater water softeners
https://github.com/barleybobs/homeassistant-ecowater-softener
MIT License
34 stars 10 forks source link

"Status" sensor and "Last API call Successful" sensor #76

Open figorr opened 1 week ago

figorr commented 1 week ago

In the previous code there was a "status" sensor that was showing if the device was "online" or "offline"

At ecowater.py there was this part of code:

def getData(self):
        try:
            data = self._get()
            new_data = {}

            nextRecharge_re = "device-info-nextRecharge'\)\.html\('(?P<nextRecharge>.*)'"

            new_data['daysUntilOutOfSalt'] = int(data['out_of_salt_days'])
            new_data['outOfSaltOn'] = data['out_of_salt']
            new_data['saltLevel'] = data['salt_level']
            new_data['saltLevelPercent'] = data['salt_level_percent']
            new_data['waterUsageToday'] = data['water_today']
            new_data['waterUsageDailyAverage'] = data['water_avg']
            new_data['waterAvailable'] = data['water_avail']
            new_data['waterFlow'] = data['water_flow']
            new_data['waterUnits'] = data['water_units']
            new_data['rechargeEnabled'] = data['rechargeEnabled']
            new_data['rechargeScheduled'] = False if (re.search(nextRecharge_re, data['recharge'])).group('nextRecharge') == 'Not Scheduled' else True
            new_data['deviceStatus'] = data['online']

            return new_data
        except Exception as e:
            logging.error(f'Error with data: {e}')
            return ''

Was the API sending the data for the status sensor? I don't know if it could be easily implemented in the new code? Or maybe it could be added in another way.

Then the integration, at sensor.py was adding the status sensor:

SENSOR_TYPES: tuple[EcowaterSensorEntityDescription, ...] = (
    EcowaterSensorEntityDescription(
        key=STATUS,
        name="status",
        icon="mdi:power",
    ),

It is a sensor I was using to manage an automation that sends a notification when the device became "offline". It is not a huge problem ... I can modify the automation just to check if any other sensor (like "water used today") became "unavailable" or "unknown".

Thank you for all the work. The integration looks great.

figorr commented 6 days ago

I think maybe I found how to include the "status" sensor and the "last_api_call_successful" sensor to show which was the last time and date from a successful connection to API and therefore if the API is "online" or "offline".

The solution requires to edit ecowater.py in order to create a couple more properties. And the we can add theses properties as sensors in the sensor.py and const.py files.

ecowater.py code:

import ayla_iot_unofficial, datetime, time
import logging

from .const import (
    APP_ID,
    APP_SECRET,
    UPDATE_PROPERTY,
    SALT_TENTHS_MAX
)

# Setup the logger
_LOGGER = logging.getLogger(__name__)

class EcowaterDevice(ayla_iot_unofficial.device.Device):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._last_api_call_successful = None  # Store date and time from the last successful connection to API

    def update(self, property_list=None):
        try:
            data = super(EcowaterDevice, self).update(property_list)
            # If API call was successful, we update time and date from the last successful connection
            self._last_api_call_successful = datetime.datetime.now(datetime.timezone.utc)
            _LOGGER.info("Successful API connection. Last successful call was registered.")
        except Exception as e:
            # If API call was failed, we kept the last successful connection date and time and we register the error
            _LOGGER.error(f"Error calling to API: {e}")
            return None

        # If data hasn't been updated in the last 5 mins then tell the device to update the data and wait 30 secs for the device to update the data
        last_updated = datetime.datetime.strptime(
            self.properties_full["gallons_used_today"]["data_updated_at"], "%Y-%m-%dT%H:%M:%SZ"
        )
        last_updated = last_updated.replace(tzinfo=datetime.timezone.utc)
        current_time = datetime.datetime.now(datetime.timezone.utc)
        five_minutes_ago = current_time - datetime.timedelta(minutes=5)

        if last_updated < five_minutes_ago:
            self.ayla_api.self_request(
                'post', self.set_property_endpoint(UPDATE_PROPERTY), json={'datapoint': {'value': 1}}
            )
            _LOGGER.info("Asking for data device update.")
            time.sleep(30)
            data = super(EcowaterDevice, self).update(property_list)

        return data

    # API Info
    @property
    def last_api_call_successful(self) -> datetime.datetime:
        """It returns the date and time from the last successful connection to API or None if never there was a successful connection."""
        return self._last_api_call_successful

    @property
    def status(self) -> str:
        """It returns 'online' if last call to API was successful, 'offline' if not."""
        if self._last_api_call_successful:
            return "online"
        else:
            return "offline"

    # Device Info
    @property
    def model(self) -> str:
        return self.get_property_value('model_description')

    @property
    def software_version(self) -> str:
        return self.get_property_value("base_software_version")

    @property
    def ip_address(self) -> str:
        return self._device_ip_address

    @property
    def rssi(self) -> int:
        return self.get_property_value("rf_signal_strength_dbm")

    # Water

    @property
    def water_use_avg_daily(self) -> int:
        return self.get_property_value("avg_daily_use_gals")

    @property
    def water_use_today(self) -> int:
        return self.get_property_value("gallons_used_today")

    @property
    def water_available(self) -> int:
        return self.get_property_value("treated_water_avail_gals")

    # Water flow

    @property
    def current_water_flow(self) -> float:
        return self.get_property_value("current_water_flow_gpm") / 10

    # Salt

    @property
    def salt_level_percentage(self) -> float:
        return (self.get_property_value("salt_level_tenths") * 100) / SALT_TENTHS_MAX[str(self.get_property_value("model_id"))]

    @property
    def out_of_salt_days(self) -> int:
        return self.get_property_value("out_of_salt_estimate_days")

    @property
    def out_of_salt_date(self) -> datetime.date:
        return datetime.datetime.now().date() + datetime.timedelta(days = self.get_property_value("out_of_salt_estimate_days"))

    @property
    def salt_type(self) -> str:
        if self.get_property_value("salt_type_enum") == 0:
            return "NaCl"
        else:
            return "KCl"

    # Rock

    @property
    def rock_removed_avg_daily(self) -> float:
        return self.get_property_value("daily_avg_rock_removed_lbs") /10000

    @property
    def rock_removed(self) -> float:
        return self.get_property_value("total_rock_removed_lbs") / 10

    # Recharge
    @property
    def recharge_status(self) -> str:
        if self.get_property_value("regen_status_enum") == 0:
            return "None"
        elif self.get_property_value("regen_status_enum") == 1:
            return "Scheduled"
        else:
            return "Recharging"

    @property
    def recharge_enabled(self) -> bool:
        return self.get_property_value("regen_enable_enum") == 1

    @property
    def recharge_scheduled(self) -> bool:
        return self.get_property_value("regen_status_enum") == 1

    @property
    def recharge_recharging(self) -> bool:
        return self.get_property_value("regen_status_enum") == 2

    @property
    def last_recharge_days(self) -> int:
        return self.get_property_value("days_since_last_regen")

    @property
    def last_recharge_date(self) -> datetime.date:
        return datetime.datetime.now().date() - datetime.timedelta(days = self.get_property_value("days_since_last_regen"))

class EcowaterAccount:
    def __init__(self, username: str, password: str) -> None:
        self.ayla_api = ayla_iot_unofficial.new_ayla_api(username, password, APP_ID, APP_SECRET)
        self.ayla_api.sign_in()

    def get_devices(self) -> list:
        devices = self.ayla_api.get_devices()

        # Filter for Ecowater devices
        devices = list(filter(lambda device: device._oem_model_number.startswith("EWS"), devices))

        # Convert devices to EcowaterDevice Class
        for device in devices:
            setattr(device, "__class__", EcowaterDevice)

        return devices

Then we should add at sensor.py the new sensors:

...
from .const import (
    DOMAIN,
    MODEL,
    SOFTWARE_VERSION,
    WATER_AVAILABLE,
    WATER_USAGE_TODAY,
    WATER_USAGE_DAILY_AVERAGE,
    CURRENT_WATER_FLOW,
    SALT_LEVEL_PERCENTAGE,
    OUT_OF_SALT_ON,
    DAYS_UNTIL_OUT_OF_SALT,
    SALT_TYPE,
    LAST_RECHARGE,
    DAYS_SINCE_RECHARGE,
    RECHARGE_ENABLED,
    RECHARGE_STATUS,
    ROCK_REMOVED,
    ROCK_REMOVED_DAILY_AVERAGE,
    LAST_API_CALL_SUCCESSFUL,   # New line
    STATUS   # New line
)
...
    ),
    EcowaterSensorEntityDescription(
        key=LAST_API_CALL_SUCCESSFUL,
        name="Last Update",
        icon="mdi:calendar-clock",
        device_class=SensorDeviceClass.TIMESTAMP
    ),
    EcowaterSensorEntityDescription(
        key=STATUS,
        name="Status",
        icon="mdi:lan-connect"
    )
...

And we should edit const.py to add a couple of lines:

LAST_API_CALL_SUCCESSFUL = "last_api_call_successful"
STATUS = "status"

@barleybobs, do you think this could work and wouldn't break the code?

figorr commented 4 days ago

Tested and it works OK. No warnings at logs.