def _update_cost(self) -> None:
        """Update incurred costs."""
        energy_state = self.hass.states.get(
            cast(str, self._config[self._adapter.entity_energy_key]))

        if energy_state is None:
            return

        state_class = energy_state.attributes.get(ATTR_STATE_CLASS)
        if state_class not in SUPPORTED_STATE_CLASSES:
            if not self._wrong_state_class_reported:
                self._wrong_state_class_reported = True
                _LOGGER.warning(
                    "Found unexpected state_class %s for %s",
                    state_class,
                    energy_state.entity_id,
                )
            return

        # last_reset must be set if the sensor is STATE_CLASS_MEASUREMENT
        if (state_class == STATE_CLASS_MEASUREMENT
                and ATTR_LAST_RESET not in energy_state.attributes):
            return

        try:
            energy = float(energy_state.state)
        except ValueError:
            return

        # Determine energy price
        if self._config["entity_energy_price"] is not None:
            energy_price_state = self.hass.states.get(
                self._config["entity_energy_price"])

            if energy_price_state is None:
                return

            try:
                energy_price = float(energy_price_state.state)
            except ValueError:
                return

            if energy_price_state.attributes.get(
                    ATTR_UNIT_OF_MEASUREMENT,
                    "").endswith(f"/{ENERGY_WATT_HOUR}"):
                energy_price *= 1000.0

            if energy_price_state.attributes.get(
                    ATTR_UNIT_OF_MEASUREMENT,
                    "").endswith(f"/{ENERGY_MEGA_WATT_HOUR}"):
                energy_price /= 1000.0

        else:
            energy_price_state = None
            energy_price = cast(float, self._config["number_energy_price"])

        if self._last_energy_sensor_state is None:
            # Initialize as it's the first time all required entities are in place.
            self._reset(energy_state)
            return

        energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)

        if self._adapter.source_type == "grid":
            if energy_unit not in VALID_ENERGY_UNITS:
                energy_unit = None

        elif self._adapter.source_type == "gas":
            if energy_unit not in VALID_ENERGY_UNITS_GAS:
                energy_unit = None

        if energy_unit == ENERGY_WATT_HOUR:
            energy_price /= 1000
        elif energy_unit == ENERGY_MEGA_WATT_HOUR:
            energy_price *= 1000

        if energy_unit is None:
            if not self._wrong_unit_reported:
                self._wrong_unit_reported = True
                _LOGGER.warning(
                    "Found unexpected unit %s for %s",
                    energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT),
                    energy_state.entity_id,
                )
            return

        if state_class != STATE_CLASS_TOTAL_INCREASING and energy_state.attributes.get(
                ATTR_LAST_RESET
        ) != self._last_energy_sensor_state.attributes.get(ATTR_LAST_RESET):
            # Energy meter was reset, reset cost sensor too
            energy_state_copy = copy.copy(energy_state)
            energy_state_copy.state = "0.0"
            self._reset(energy_state_copy)
        elif state_class == STATE_CLASS_TOTAL_INCREASING and reset_detected(
                self.hass,
                cast(str, self._config[self._adapter.entity_energy_key]),
                energy,
                float(self._last_energy_sensor_state.state),
                self._last_energy_sensor_state,
        ):
            # Energy meter was reset, reset cost sensor too
            energy_state_copy = copy.copy(energy_state)
            energy_state_copy.state = "0.0"
            self._reset(energy_state_copy)
        # Update with newly incurred cost
        old_energy_value = float(self._last_energy_sensor_state.state)
        cur_value = cast(float, self._attr_native_value)
        self._attr_native_value = cur_value + (energy -
                                               old_energy_value) * energy_price

        self._last_energy_sensor_state = energy_state
Example #2
0
class EnergyCostSensor(SensorEntity):
    """Calculate costs incurred by consuming energy.

    This is intended as a fallback for when no specific cost sensor is available for the
    utility.
    """

    _wrong_state_class_reported = False
    _wrong_unit_reported = False

    def __init__(
        self,
        adapter: SourceAdapter,
        config: dict,
    ) -> None:
        """Initialize the sensor."""
        super().__init__()

        self._adapter = adapter
        self.entity_id = (
            f"{config[adapter.entity_energy_key]}_{adapter.entity_id_suffix}")
        self._attr_device_class = DEVICE_CLASS_MONETARY
        self._attr_state_class = STATE_CLASS_TOTAL_INCREASING
        self._config = config
        self._last_energy_sensor_state: StateType | None = None
        self._cur_value = 0.0

    def _reset(self, energy_state: StateType) -> None:
        """Reset the cost sensor."""
        self._attr_native_value = 0.0
        self._cur_value = 0.0
        self._last_energy_sensor_state = energy_state
        self.async_write_ha_state()

    @callback
    def _update_cost(self) -> None:
        """Update incurred costs."""
        energy_state = self.hass.states.get(
            cast(str, self._config[self._adapter.entity_energy_key]))

        if energy_state is None:
            return

        if (state_class := energy_state.attributes.get(ATTR_STATE_CLASS)
            ) != STATE_CLASS_TOTAL_INCREASING:
            if not self._wrong_state_class_reported:
                self._wrong_state_class_reported = True
                _LOGGER.warning(
                    "Found unexpected state_class %s for %s",
                    state_class,
                    energy_state.entity_id,
                )
            return

        try:
            energy = float(energy_state.state)
        except ValueError:
            return

        # Determine energy price
        if self._config["entity_energy_price"] is not None:
            energy_price_state = self.hass.states.get(
                self._config["entity_energy_price"])

            if energy_price_state is None:
                return

            try:
                energy_price = float(energy_price_state.state)
            except ValueError:
                return

            if (self._adapter.source_type == "grid"
                    and energy_price_state.attributes.get(
                        ATTR_UNIT_OF_MEASUREMENT,
                        "").endswith(f"/{ENERGY_WATT_HOUR}")):
                energy_price *= 1000.0

        else:
            energy_price_state = None
            energy_price = cast(float, self._config["number_energy_price"])

        if self._last_energy_sensor_state is None:
            # Initialize as it's the first time all required entities are in place.
            self._reset(energy_state.state)
            return

        energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)

        if self._adapter.source_type == "grid":
            if energy_unit == ENERGY_WATT_HOUR:
                energy_price /= 1000
            elif energy_unit != ENERGY_KILO_WATT_HOUR:
                energy_unit = None

        elif self._adapter.source_type == "gas":
            if energy_unit != VOLUME_CUBIC_METERS:
                energy_unit = None

        if energy_unit is None:
            if not self._wrong_unit_reported:
                self._wrong_unit_reported = True
                _LOGGER.warning(
                    "Found unexpected unit %s for %s",
                    energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT),
                    energy_state.entity_id,
                )
            return

        if reset_detected(
                self.hass,
                cast(str, self._config[self._adapter.entity_energy_key]),
                energy,
                float(self._last_energy_sensor_state),
        ):
            # Energy meter was reset, reset cost sensor too
            self._reset(0)
        # Update with newly incurred cost
        old_energy_value = float(self._last_energy_sensor_state)
        self._cur_value += (energy - old_energy_value) * energy_price
        self._attr_native_value = round(self._cur_value, 2)

        self._last_energy_sensor_state = energy_state.state