Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace deprecated constants #36

Closed
andersevenrud opened this issue Mar 30, 2024 · 12 comments · Fixed by #39
Closed

Replace deprecated constants #36

andersevenrud opened this issue Mar 30, 2024 · 12 comments · Fixed by #39
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed

Comments

@andersevenrud
Copy link
Owner

I saw this when a user reported issues in #34. These need to be changed ASAP

2024-03-30 00:08:35.421 WARNING (MainThread) [homeassistant.const] ELECTRIC_POTENTIAL_VOLT was used from nexa_bridge_x, this is a deprecated constant which will be removed in HA Core 2025.1. Use UnitOfElectricPotential.VOLT instead, please create a bug report at https://github.com/andersevenrud/ha-nexa-bridge-x/issues
2024-03-30 00:08:35.437 WARNING (MainThread) [homeassistant.const] POWER_WATT was used from nexa_bridge_x, this is a deprecated constant which will be removed in HA Core 2025.1. Use UnitOfPower.WATT instead, please create a bug report at https://github.com/andersevenrud/ha-nexa-bridge-x/issues
2024-03-30 00:08:35.453 WARNING (MainThread) [homeassistant.const] ELECTRIC_CURRENT_AMPERE was used from nexa_bridge_x, this is a deprecated constant which will be removed in HA Core 2025.1. Use UnitOfElectricCurrent.AMPERE instead, please create a bug report at https://github.com/andersevenrud/ha-nexa-bridge-x/issues
2024-03-30 00:08:35.469 WARNING (MainThread) [homeassistant.const] ENERGY_KILO_WATT_HOUR was used from nexa_bridge_x, this is a deprecated constant which will be removed in HA Core 2025.1. Use UnitOfEnergy.KILO_WATT_HOUR instead, please create a bug report at https://github.com/andersevenrud/ha-nexa-bridge-x/issues
2024-03-30 00:08:35.484 WARNING (MainThread) [homeassistant.const] POWER_KILO_WATT was used from nexa_bridge_x, this is a deprecated constant which will be removed in HA Core 2025.1. Use UnitOfPower.KILO_WATT instead, please create a bug report at https://github.com/andersevenrud/ha-nexa-bridge-x/issues
2024-03-30 00:08:35.501 WARNING (MainThread) [homeassistant.const] TEMP_CELSIUS was used from nexa_bridge_x, this is a deprecated constant which will be removed in HA Core 2025.1. Use UnitOfTemperature.CELSIUS instead, please create a bug report at https://github.com/andersevenrud/ha-nexa-bridge-x/issues
@andersevenrud andersevenrud added enhancement New feature or request help wanted Extra attention is needed good first issue Good for newcomers labels Mar 30, 2024
@sonite
Copy link
Contributor

sonite commented Mar 30, 2024

This solves it in const.py:

"""
Home Assistant - Nexa Bridge X Integration

Author: Anders Evenrud <andersevenrud@gmail.com>
Homepage: https://github.com/andersevenrud/ha-nexa-bridge-x
License: MIT
"""
from homeassistant.components.sensor import (
    SensorDeviceClass,
    SensorStateClass,
)
from homeassistant.const import (
    UnitOfPower,
    UnitOfEnergy,
    UnitOfElectricPotential,
    UnitOfElectricCurrent,
    UnitOfTemperature,
    PERCENTAGE,
    LIGHT_LUX
)

DOMAIN = "nexa_bridge_x"

POLL_INTERVAL = 30

POLL_TIMEOUT = 25

CALL_TIMEOUT = 30

RECONNECT_SLEEP = 5

DEFAULT_USERNAME = "nexa"

DEFAULT_PASSWORD = "nexa"

WS_PORT = 8887

HTTP_BASIC_AUTH = False

SWITCH_LEVEL_SENSOR = False

SWITCH_BINARY_SENSOR = False

FORCE_NODE_ENUM = False

NODE_MEDIA_CAPABILITIES = [
    "mediaVolume",
    "mediaPlayPause",
    "mediaMute"
]

NODE_BINARY_CAPABILITIES = [
    "notificationContact",
    "notificationMotion",
    "notificationSmoke",
    "notificationWater",
    "notificationTwilight",
    "notificationTamper",
    "notificationButton",
    "notification"
] + (SWITCH_BINARY_SENSOR and ["switchBinary"] or [])

NODE_SENSOR_CAPABILITIES = [
    "meter",
    "power",
    "electric_voltage",
    "electric_ampere",
    "temperature",
    "humidity",
    "luminance",
    "battery",
    "customEvent"
] + (SWITCH_LEVEL_SENSOR and ["switchLevel"] or [])

ENERGY_ATTRS = [
    "total_kilowatt_hours",
    "current_wattage",
    "current_kilowatt_hours",
    "today_kilowatt_hours",
    "yesterday_kilowatt_hours",
    "month_kilowatt_hours"
]

# TODO: Add support for legacy energy metering
LEGACY_ENERGY_ATTRS = [
    #"total_kilowatt_hours",
    #"current_wattage",
]

BINARY_MAP = {
    "notificationContact": {
        "name": "Contact"
    },
    "notificationMotion": {
        "name": "Motion"
    },
    "notificationSmoke": {
        "name": "Smoke"
    },
    "notificationWater": {
        "name": "Water"
    },
    "notificationTwilight": {
        "name": "Twilight"
    },
    "notificationTamper": {
        "name": "Tamper"
    },
    "notificationButton": {
        "name": "Button"
    },
    "notification": {
        "name": "Value"
    },
    "switchBinary": {
        "name": "Switch"
    },
}

SENSOR_MAP = {
    "switchLevel": {
        "name": "Level",
        "unit": PERCENTAGE,
        "device": None,
        "class": SensorStateClass.MEASUREMENT
    },
    "meter": {
        "name": "Energy",
        "unit": UnitOfEnergy.KILO_WATT_HOUR,
        "device": SensorDeviceClass.ENERGY,
        "class": SensorStateClass.TOTAL_INCREASING
    },
    "power": {
        "name": "Wattage",
        "unit": UnitOfPower.WATT,
        "device": SensorDeviceClass.POWER,
        "class": SensorStateClass.MEASUREMENT
    },
    "electric_voltage": {
        "name": "Voltage",
        "unit": UnitOfElectricPotential.VOLT,
        "device": SensorDeviceClass.VOLTAGE,
        "class": SensorStateClass.MEASUREMENT
    },
    "electric_ampere": {
        "name": "Amperage",
        "unit": UnitOfElectricCurrent.AMPERE,
        "device": SensorDeviceClass.CURRENT,
        "class": SensorStateClass.MEASUREMENT
    },
    "temperature": {
        "name": "Temperature",
        "unit": UnitOfTemperature.CELSIUS,
        "device": SensorDeviceClass.TEMPERATURE,
        "class": SensorStateClass.MEASUREMENT
    },
    "humidity": {
        "name": "Humidity",
        "unit": PERCENTAGE,
        "device": SensorDeviceClass.HUMIDITY,
        "class": SensorStateClass.MEASUREMENT
    },
    "luminance": {
        "name": "Luminance",
        "unit": LIGHT_LUX,
        "device": SensorDeviceClass.ILLUMINANCE,
        "class": SensorStateClass.MEASUREMENT
    },
    "battery": {
        "name": "Battery",
        "unit": PERCENTAGE,
        "device": SensorDeviceClass.BATTERY,
        "class": SensorStateClass.MEASUREMENT
    },
    "customEvent": {
        "name": "Trigger",
        "unit": None,
        "device": SensorDeviceClass.ENUM,
        "class": None
    },
}

ENERGY_MAP = {
    "total_kilowatt_hours": {
        "name": "Total kWh",
        "unit": UnitOfEnergy.KILO_WATT_HOUR,
        "device": SensorDeviceClass.ENERGY,
        "class": SensorStateClass.TOTAL_INCREASING
    },
    "current_wattage": {
        "name": "Current W",
        "unit": UnitOfPower.WATT,
        "device": SensorDeviceClass.POWER,
        "class": SensorStateClass.MEASUREMENT
    },
    "current_kilowatt_hours": {
        "name": "Current kWh",
        "unit": UnitOfPower.KILO_WATT,
        "device": SensorDeviceClass.POWER,
        "class": SensorStateClass.MEASUREMENT
    },
    "today_kilowatt_hours": {
        "name": "Today kWh",
        "unit": UnitOfEnergy.KILO_WATT_HOUR,
        "device": SensorDeviceClass.ENERGY,
        "class": SensorStateClass.TOTAL_INCREASING
    },
    "yesterday_kilowatt_hours": {
        "name": "Yesterday kWh",
        "unit": UnitOfEnergy.KILO_WATT_HOUR,
        "device": SensorDeviceClass.ENERGY,
        "class": SensorStateClass.TOTAL_INCREASING
    },
    "month_kilowatt_hours": {
        "name": "Month kWh",
        "unit": UnitOfEnergy.KILO_WATT_HOUR,
        "device": SensorDeviceClass.ENERGY,
        "class": SensorStateClass.TOTAL_INCREASING
    },
}

@sonite
Copy link
Contributor

sonite commented Mar 30, 2024

2024-03-30 22:12:52.582 WARNING (MainThread) [homeassistant.components.light] None (<class 'custom_components.nexa_bridge_x.entities.NexaDimmerEntity'>) sets invalid supported color modes {<ColorMode.BRIGHTNESS: 'brightness'>, <ColorMode.ONOFF: 'onoff'>}, this will stop working in Home Assistant Core 2025.3, please create a bug report at https://github.com/andersevenrud/ha-nexa-bridge-x/issues

@sonite
Copy link
Contributor

sonite commented Mar 30, 2024

2024-03-30 22:12:52.582 WARNING (MainThread) [homeassistant.components.light] None (<class 'custom_components.nexa_bridge_x.entities.NexaDimmerEntity'>) sets invalid supported color modes {<ColorMode.BRIGHTNESS: 'brightness'>, <ColorMode.ONOFF: 'onoff'>}, this will stop working in Home Assistant Core 2025.3, please create a bug report at https://github.com/andersevenrud/ha-nexa-bridge-x/issues

I don't know fully what I'm doing, but this solved the issue in entities.py:

"""
Home Assistant - Nexa Bridge X Integration

Author: Anders Evenrud <andersevenrud@gmail.com>
Homepage: https://github.com/andersevenrud/ha-nexa-bridge-x
License: MIT
"""
from __future__ import annotations
from homeassistant.core import callback
from homeassistant.components.sensor import SensorEntity
from homeassistant.components.switch import SwitchEntity
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.components.media_player import (
    MediaPlayerDeviceClass,
    MediaPlayerEntity,
    MediaPlayerEntityFeature,
    MediaPlayerState,
    MediaType,
)
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.update_coordinator import (
    CoordinatorEntity,
    DataUpdateCoordinator
)
from homeassistant.components.light import (
    LightEntity,
    ColorMode,
    ATTR_BRIGHTNESS,
    ATTR_COLOR_MODE,
    LightEntityFeature,
    filter_supported_color_modes
)
from .const import (DOMAIN, SENSOR_MAP, ENERGY_MAP, BINARY_MAP)
from .nexa import (NexaNode, NexaNodeValueType)
import logging

_LOGGER = logging.getLogger(__name__)


class NexaEntity(CoordinatorEntity):
    """Representation of a Nexa Bridge entity"""

    _attr_has_entity_name = True

    def __init__(self, coordinator: CoordinatorEntity) -> None:
        super().__init__(coordinator)
        self._server_unique_id = coordinator.config_entry.entry_id
        self._attr_device_info = DeviceInfo(
            manufacturer="Nexa",
            entry_type=DeviceEntryType.SERVICE,
            model=coordinator.data.info.model,
            name=coordinator.data.info.name,
            sw_version=coordinator.data.info.version,
            identifiers={
                (DOMAIN, coordinator.config_entry.entry_id)
            },
            configuration_url=(
                f"http://{coordinator.api.host}"
            )
        )


class NexaNodeEntity(NexaEntity):
    """Representation of a Nexa Device entity"""

    _attr_has_entity_name = True

    def __init__(self, node: NexaNode, coordinator: CoordinatorEntity) -> None:
        super().__init__(coordinator)

        self._attr_device_info = DeviceInfo(
            name=f"{coordinator.data.info.name} {node.name or node.id}",
            via_device=(DOMAIN, coordinator.config_entry.entry_id),
            identifiers={
                (DOMAIN, node.id)
            }
        )


class NexaDimmerEntity(NexaNodeEntity, LightEntity):
    """Entity for light"""
    supported_color_modes = {ColorMode.BRIGHTNESS}
    color_mode = ColorMode.BRIGHTNESS

    def __init__(self, coordinator: DataUpdateCoordinator, node: NexaNode):
        _LOGGER.info("Found light %s: %s", node.id, node.name)
        super().__init__(node, coordinator)
        self.id = node.id
        self._attr_name = "Light"
        self._attr_unique_id = f"dimmer_{node.id}"

    @callback
    def _handle_coordinator_update(self) -> None:
        node = self.coordinator.get_node_by_id(self.id)
        if node:
            value = node.get_value("switchLevel")
            value_percentage = int(value * 100)

            self._attr_is_on = value_percentage > 0
            self._attr_brightness = int(value * 255)
            self.async_write_ha_state()

    async def async_turn_on(self, **kwargs) -> None:
        """Turn the device on or Light command."""
        if ATTR_BRIGHTNESS in kwargs:
            value = kwargs.get(ATTR_BRIGHTNESS, 255) / 255
        else:
            value = 1.0

        await self._api_call(value)

    async def async_turn_off(self, **kwargs) -> None:
        """Turn device off."""
        await self._api_call(0.0)

    async def _api_call(self, value: float):
        await self.coordinator.api.node_call(self.id, "switchLevel", value)


class NexaSwitchEntity(NexaNodeEntity, SwitchEntity):
    """Entity for swtich"""

    def __init__(self, coordinator: DataUpdateCoordinator, node: NexaNode):
        _LOGGER.info("Found switch %s: %s", node.id, node.name)
        super().__init__(node, coordinator)
        self.id = node.id
        self._attr_name = "Switch"
        self._attr_unique_id = f"switch_{node.id}"

    @callback
    def _handle_coordinator_update(self) -> None:
        node = self.coordinator.get_node_by_id(self.id)
        if node:
            self._attr_is_on = node.get_value("switchBinary")
            self.async_write_ha_state()

    async def async_turn_on(self, **kwargs) -> None:
        """Send turn on command"""
        await self._api_call(True)

    async def async_turn_off(self, **kwargs) -> None:
        """Send turn off command"""
        await self._api_call(False)

    async def _api_call(self, value: bool):
        await self.coordinator.api.node_call(self.id, "switchBinary", value)


class NexaSensorEntity(NexaNodeEntity, SensorEntity):
    """Entity for sensor"""

    def __init__(
        self,
        coordinator: DataUpdateCoordinator,
        node: NexaNode,
        key: str
    ):
        _LOGGER.info("Found %s sensor %s: %s", key, node.id, node.name)
        super().__init__(node, coordinator)
        self.id = node.id
        self.key = key
        self._attr_native_value = None
        self._attr_name = "Sensor"
        self._attr_unique_id = f"sensor_{node.id}_{key}"

        if key in SENSOR_MAP:
            self._attr_name = f"{SENSOR_MAP[key]['name']} Sensor"
            self._attr_native_unit_of_measurement = SENSOR_MAP[key]["unit"]
            self._attr_device_class = SENSOR_MAP[key]["device"]
            self._attr_state_class = SENSOR_MAP[key]["class"]

        if key == "customEvent":
            self._attr_name = f"Last {SENSOR_MAP[key]['name']}"
            self._attr_options = node.custom_events

    @callback
    def _handle_coordinator_update(self) -> None:
        node = self.coordinator.get_node_by_id(self.id)
        if node:
            value = node.get_value(self.key)
            if self.key == "switchLevel":
                self._attr_native_value = int(value * 100)
            else:
                self._attr_native_value = value

            self.async_write_ha_state()


class NexaBinarySensorEntity(NexaNodeEntity, BinarySensorEntity):
    """Entity for binary sensor"""

    def __init__(
        self,
        coordinator: DataUpdateCoordinator,
        node: NexaNode,
        key: str
    ):
        _LOGGER.info("Found binary sensor %s: %s", node.id, node.name)
        super().__init__(node, coordinator)
        self.id = node.id
        self.key = key
        self._attr_is_on = None
        self._attr_unique_id = f"binary_sensor_{node.id}_{key}"

        if key in BINARY_MAP:
            self._attr_name = f"{BINARY_MAP[key]['name']} Sensor"
        else:
            self._attr_name = "Binary Sensor"

    @callback
    def _handle_coordinator_update(self) -> None:
        node = self.coordinator.get_node_by_id(self.id)
        if node:
            self._attr_is_on = node.get_value(self.key)
            self.async_write_ha_state()


class NexaEnergyEntity(NexaEntity, SensorEntity):
    """Entity for global energy usage"""

    def __init__(self, coordinator: DataUpdateCoordinator, attr: str):
        _LOGGER.info("Found energy information: %s", attr)
        super().__init__(coordinator)
        self.id = attr
        self._attr_native_value = None
        self._attr_unique_id = f"nexa_energy_{attr}"
        self._attr_name = ENERGY_MAP[attr]["name"]
        self._attr_native_unit_of_measurement = ENERGY_MAP[attr]["unit"]
        self._attr_device_class = ENERGY_MAP[attr]["device"]
        self._attr_state_class = ENERGY_MAP[attr]["class"]

    @callback
    def _handle_coordinator_update(self) -> None:
        self._attr_native_value = getattr(
            self.coordinator.data.energy,
            self.id
        )
        self.async_write_ha_state()


class NexaMediaPlayerEntity(NexaNodeEntity, MediaPlayerEntity):
    """Entity for media player"""

    _attr_device_class = MediaPlayerDeviceClass.SPEAKER
    _attr_media_content_type = MediaType.MUSIC
    _attr_is_volume_muted: bool | None = None
    _attr_state: MediaPlayerState | None = None
    _attr_volume_level: float | None = None
    _attr_supported_features: MediaPlayerEntityFeature = (
        MediaPlayerEntityFeature.PAUSE
        | MediaPlayerEntityFeature.VOLUME_SET
        | MediaPlayerEntityFeature.VOLUME_MUTE
        | MediaPlayerEntityFeature.PLAY
    )

    def __init__(
        self,
        coordinator: DataUpdateCoordinator,
        node: NexaNode
    ):
        _LOGGER.info("Found media player %s: %s", node.id, node.name)
        super().__init__(node, coordinator)
        self.id = node.id
        self._attr_native_value = None
        self._attr_unique_id = f"media_player_{node.id}"
        self._attr_name = node.name or node.id

    @callback
    def _handle_coordinator_update(self) -> None:
        node = self.coordinator.get_node_by_id(self.id)
        if node:
            if node.get_value("mediaPlayPause"):
                self._attr_state = MediaPlayerState.PLAYING
            else:
                self._attr_state = MediaPlayerState.PAUSED

            self._attr_volume_level = node.get_value("mediaVolume")
            self._attr_is_volume_muted = node.get_value("mediaMute")

            self.async_write_ha_state()

    async def async_media_play(self) -> None:
        """Send play command to media player"""
        await self._api_call("mediaPlayPause", True)

    async def async_media_pause(self) -> None:
        """Send pause command to media player"""
        await self._api_call("mediaPlayPause", False)

    async def async_set_volume_level(self, volume: float) -> None:
        """Send volume level to media player"""
        await self._api_call("mediaVolume", int(volume * 100))

    async def async_mute_volume(self, mute: bool) -> None:
        """Send mute command to media player."""
        await self._api_call("mediaMute", mute)

    async def _api_call(self, cap: str, value: NexaNodeValueType):
        await self.coordinator.api.node_call(self.id, cap, value)

@andersevenrud
Copy link
Owner Author

andersevenrud commented Mar 31, 2024

If you're up for a small quest:

You can create a fork of this repository, edit the changes on your own account (you can use the built-in Github file editor/IDE) and then submit a pull request to this repository.

Makes it much easier to review/test the changes.

@sonite
Copy link
Contributor

sonite commented Mar 31, 2024

If you're up for a small quest:

You can create a fork of this repository, edit the changes on your own account (you can use the built-in Github file editor/IDE) and then submit a pull request to this repository.

Makes it much easier to review/test the changes.

I'll give it a try tomorrow. It's a great way to learn GH

@andersevenrud
Copy link
Owner Author

andersevenrud commented Mar 31, 2024

It's a great way to learn GH

And a nice bonus is that you will get credited for the changes because you made a physical commit to this repository instead of me copy/pasting stuff. So you'll get a mention in the changelogs etc. atuomatically :)

You don't have to be credited if you don't want to of course (I can remove that in the pull request).

@sonite
Copy link
Contributor

sonite commented Apr 1, 2024

Should'nt this depend on the choices of the device setup in Nexa: e.g.

   """Entity for light"""
    if node:
        if capability = node.get_switchBinary(TRUE)
            Supported_color_modes = ColorMode.ONOFF, ColorMode.BRIGHTNESS
        else:
           Supported_color_modes = ColorMode.BRIGHTNESS

Nexa has dimmers and not lights. So it can have properties of on/off and/or only level. Whilst Home assistant Light entity assumes its a light and minimum property is always ONOFF?

https://developers.home-assistant.io/docs/core/entity/light/
The trade off have to be both ONOFF & BRIGHTNESS, but it creates warning message as well

But the code also declares """Must be the only supported mode"""?
https://github.com/home-assistant/core/blob/dev/homeassistant/components/light/init.py

@andersevenrud
Copy link
Owner Author

Nexa has dimmers and not lights.

Not sure I understand what you mean by this. If your device reports as a switch, you'll get a switch in HA, if it reports as a light/dimmer you'll get that. Or both.

@sonite
Copy link
Contributor

sonite commented Apr 1, 2024

I think I misunderstand the context between Nexas units and the information being sent, how HA's library works, and finally this module. Sorry for that. Fixed.

@andersevenrud
Copy link
Owner Author

I'm a little bit confused 😅 Open a new issue if there's some issue with your dimmers/lights so that this one does not go off-topic.

@andersevenrud
Copy link
Owner Author

Nexa has dimmers and not lights. So it can have properties of on/off and/or only level. Whilst Home assistant Light entity assumes its a light and minimum property is always ONOFF?

I'm a little bit confused


... Silly me. I understand now after looking up the documentation myself. The fix for this error is simply to remove the ColorMode.ONOFF from _attr_supported_color_modes. The ColorMode.ONOFF is not needed here because a binary swith will never be associated with a light entity and a physical on/off is a value of 0/100%.

Would you mind creating a PR for this as well ?

@andersevenrud
Copy link
Owner Author

Also related: #25

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants