diff --git a/Manifest.in b/Manifest.in
new file mode 100644
index 0000000..b5034bd
--- /dev/null
+++ b/Manifest.in
@@ -0,0 +1 @@
+include habapp_rules/energy/monthly_report_template.html
\ No newline at end of file
diff --git a/changelog.md b/changelog.md
index c53c0cf..89d8ac1 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,11 @@
+# Version 5.6.0 - 24.03.2024
+
+# Features
+
+- added ``habapp_rules.energy.montly_report.MonthlyReport`` for generating a monthly energy report mail
+- bumped holidays to 0.45
+- bumped multi-notifier to 0.4.0
+
# Version 5.5.1 - 07.03.2024
# Bugfix
@@ -19,9 +27,9 @@
# Bugfix
-- fixed bug in ``habapp_rules.actors.state_observer.StateObserverNumber`` which triggered the manual-detected-callback if the received number deviates only a little bit because of data types. (e.g.: 1.000001 != 1.0)
+- fixed bug in ``habapp_rules.actors.state_observer.StateObserverNumber`` which triggered the manual-detected-callback if the received number deviates only a little bit because of data types. (e.g.: 1.000001 != 1.0)
- fixed bug for dimmer lights in ``habapp_rules.actors.light`` which did not set the correct brightness if light was switched on.
-- fixed bug in ``habapp_rules.common.hysteresis.HysteresisSwitch.get_output`` resulted in a wrong switch state if the value was 0.
+- fixed bug in ``habapp_rules.common.hysteresis.HysteresisSwitch.get_output`` resulted in a wrong switch state if the value was 0.
- added missing state transition to ``habapp_rules.sensors.motion.Motion``. When state was ``PostSleepLocked`` and sleep started there was no change to ``SleepLocked``
- fixed strange behavior of ``habapp_rules.system.presence.Presence`` which did not abort leaving when the first phone appeared. This let to absence state if someone returned when leaving was active.
diff --git a/custom_pylint_dictionary.txt b/custom_pylint_dictionary.txt
index bcab0e8..ccebff6 100644
--- a/custom_pylint_dictionary.txt
+++ b/custom_pylint_dictionary.txt
@@ -3,11 +3,13 @@ autoupdate
bool
config
dataclass
+donut
dwd
DWD
enums
ga
HCL
+html
init
KNX
kwargs
diff --git a/habapp_rules/__version__.py b/habapp_rules/__version__.py
index 5a62936..50cf21e 100644
--- a/habapp_rules/__version__.py
+++ b/habapp_rules/__version__.py
@@ -1,2 +1,2 @@
"""Set version for the package."""
-__version__ = "5.5.1"
+__version__ = "5.6.0"
diff --git a/habapp_rules/energy/__init__.py b/habapp_rules/energy/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/habapp_rules/energy/donut_chart.py b/habapp_rules/energy/donut_chart.py
new file mode 100644
index 0000000..f59230c
--- /dev/null
+++ b/habapp_rules/energy/donut_chart.py
@@ -0,0 +1,39 @@
+"""Module to create donut charts."""
+import collections.abc
+import pathlib
+
+import matplotlib.pyplot
+
+
+def _auto_percent_format(values: list[float]) -> collections.abc.Callable:
+ """Get labels for representing the absolute value.
+
+ :param values: list of all values
+ :return: function which returns the formatted string if called
+ """
+
+ def my_format(pct: float) -> str:
+ """get formatted value.
+
+ :param pct: percent value
+ :return: formatted value
+ """
+ total = sum(values)
+ return f"{(pct * total / 100.0):.1f} kWh"
+
+ return my_format
+
+
+def create_chart(labels: list[str], values: list[float], chart_path: pathlib.Path) -> None:
+ """Create the donut chart.
+
+ :param labels: labels for the donut chart
+ :param values: values of the donut chart
+ :param chart_path: target path for the chart
+ """
+ _, ax = matplotlib.pyplot.subplots()
+ _, texts, _ = ax.pie(values, labels=labels, autopct=_auto_percent_format(values), pctdistance=0.7, textprops={"fontsize": 10})
+ for text in texts:
+ text.set_backgroundcolor("white")
+
+ matplotlib.pyplot.savefig(str(chart_path), bbox_inches="tight", transparent=True)
diff --git a/habapp_rules/energy/monthly_report.py b/habapp_rules/energy/monthly_report.py
new file mode 100644
index 0000000..778ee06
--- /dev/null
+++ b/habapp_rules/energy/monthly_report.py
@@ -0,0 +1,231 @@
+"""Module for sending the monthly energy consumption."""
+import dataclasses
+import datetime
+import logging
+import pathlib
+import tempfile
+
+import HABApp
+import HABApp.core.internals
+import dateutil.relativedelta
+import jinja2
+import multi_notifier.connectors.connector_mail
+
+import habapp_rules.__version__
+import habapp_rules.core.exceptions
+import habapp_rules.core.logger
+import habapp_rules.energy.donut_chart
+
+LOGGER = logging.getLogger(__name__)
+
+MONTH_MAPPING = {
+ 1: "Januar",
+ 2: "Februar",
+ 3: "März",
+ 4: "April",
+ 5: "Mai",
+ 6: "Juni",
+ 7: "Juli",
+ 8: "August",
+ 9: "September",
+ 10: "Oktober",
+ 11: "November",
+ 12: "Dezember"
+}
+
+
+def _get_previous_month_name() -> str:
+ """Get name of the previous month
+
+ :return: name of current month
+
+ if other languages are required, the global dict must be replaced
+ """
+ today = datetime.date.today()
+ last_month = today.replace(day=1) - datetime.timedelta(days=1)
+
+ return MONTH_MAPPING[last_month.month]
+
+
+def _get_next_trigger() -> datetime.datetime:
+ """Get next trigger time (always first day of month at midnight)
+
+ :return: next trigger time
+ """
+ return datetime.datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0) + dateutil.relativedelta.relativedelta(months=1)
+
+
+@dataclasses.dataclass
+class EnergyShare:
+ """Dataclass for defining energy share objects."""
+ openhab_name: str
+ chart_name: str
+ monthly_power: float = 0
+
+ _openhab_item = None
+
+ def __post_init__(self) -> None:
+ """This is triggered after init.
+
+ :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if the number item could not be found
+ """
+ try:
+ self._openhab_item = HABApp.openhab.items.NumberItem.get_item(self.openhab_name)
+ except AssertionError:
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationException(f"The given item name is not a number item. '{self.openhab_name}'")
+ except HABApp.core.errors.ItemNotFoundException:
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationException(f"Could not find any item for given name '{self.openhab_name}'")
+
+ @property
+ def openhab_item(self) -> HABApp.openhab.items.NumberItem:
+ """Get OpenHAB item.
+
+ :return: OpenHAB item
+ """
+ return self._openhab_item
+
+
+class MonthlyReport(HABApp.Rule):
+ """Rule for sending the monthly energy consumption.
+
+ Example:
+ known_energy_share = [
+ habapp_rules.energy.monthly_report.EnergyShare("Dishwasher_Energy", "Dishwasher"),
+ habapp_rules.energy.monthly_report.EnergyShare("Light", "Light")
+ ]
+
+ config_mail = multi_notifier.connectors.connector_mail.MailConfig(
+ user="sender@test.de",
+ password="fancy_password",
+ smtp_host="smtp.test.de",
+ smtp_port=587,
+ )
+
+ habapp_rules.energy.monthly_report.MonthlyReport("Total_Energy", known_energy_share, "Group_RRD4J", config_mail, "test@test.de")
+ """
+
+ def __init__(
+ self,
+ name_energy_sum: str,
+ known_energy_share: list[EnergyShare],
+ persistence_group_name: str | None,
+ config_mail: multi_notifier.connectors.connector_mail.MailConfig | None,
+ recipients: str | list[str]) -> None:
+ """Initialize the rule.
+
+ :param name_energy_sum: name of OpenHAB Number item, which holds the total energy consumption (NumberItem)
+ :param known_energy_share: list of EnergyShare objects
+ :param persistence_group_name: OpenHAB group name which holds all items which are persisted. If the group name is given it will be checked if all energy items are in the group
+ :param config_mail: config for sending mails
+ :param recipients: list of recipients who get the mail
+ :raises habapp_rules.core.exceptions.HabAppRulesConfigurationException: if config is not valid
+ """
+ HABApp.Rule.__init__(self)
+ self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, name_energy_sum)
+ self._recipients = recipients
+
+ self._item_energy_sum = HABApp.openhab.items.NumberItem.get_item(name_energy_sum)
+ self._known_energy_share = known_energy_share
+ self._mail = multi_notifier.connectors.connector_mail.Mail(config_mail)
+
+ if persistence_group_name is not None:
+ # check if all energy items are in the given persistence group
+ items_to_check = [self._item_energy_sum] + [share.openhab_item for share in self._known_energy_share]
+ not_in_persistence_group = [item.name for item in items_to_check if persistence_group_name not in item.groups]
+ if not_in_persistence_group:
+ raise habapp_rules.core.exceptions.HabAppRulesConfigurationException(f"The following OpenHAB items are not in the persistence group '{persistence_group_name}': {not_in_persistence_group}")
+
+ self.run.at(next_trigger_time := _get_next_trigger(), self._cb_send_energy)
+ self._instance_logger.info(f"Successfully initiated monthly consumption rule for {name_energy_sum}. Triggered first execution to {next_trigger_time.isoformat()}")
+
+ def _get_historic_value(self, item: HABApp.openhab.items.NumberItem, start_time: datetime.datetime) -> float:
+ """Get historic value of given Number item
+
+ :param item: item instance
+ :param start_time: start time to search for the interested value
+ :return: historic value of the item
+ """
+ historic = item.get_persistence_data(start_time=start_time, end_time=start_time + datetime.timedelta(hours=1)).data
+ if not historic:
+ self._instance_logger.warning(f"Could not get value of item '{item.name}' of time = {start_time}")
+ return 0
+
+ return next(iter(historic.values()))
+
+ # pylint: disable=wrong-spelling-in-docstring
+ def _create_html(self, energy_sum_month: float) -> str:
+ """Create html which will be sent by the mail
+
+ :param energy_sum_month: sum value for the current month
+ :return: html with replaced values
+
+ The template was created by https://app.bootstrapemail.com/editor/documents with the following input:
+
+
+
+
+
+
+
+
+
+
+
Strom Verbrauch
+
von Februar
+
+
+
Aktueller Zählerstand: 7000 kWh.
+
Hier die Details:
+
+
+
+
+
Generated with habapp_rules version = 20.0.3
+
+
+
+
+
+ """
+ html_template_path = pathlib.Path(__file__).parent / "monthly_report_template.html"
+
+ with html_template_path.open() as html_template_file:
+ html_template = html_template_file.read()
+
+ return jinja2.Template(html_template).render(
+ month=_get_previous_month_name(),
+ energy_now=f"{self._item_energy_sum.value:.1f}",
+ energy_last_month=f"{energy_sum_month:.1f}",
+ habapp_version=habapp_rules.__version__.__version__,
+ chart="{{ chart }}" # this is needed to not replace the chart from the mail-template
+ )
+
+ def _cb_send_energy(self) -> None:
+ """Send the mail with the energy consumption of the last month"""
+ self._instance_logger.debug("Send energy consumption was triggered.")
+ # get values
+ now = datetime.datetime.now()
+ last_month = now - dateutil.relativedelta.relativedelta(months=1)
+
+ energy_sum_month = self._item_energy_sum.value - self._get_historic_value(self._item_energy_sum, last_month)
+ for share in self._known_energy_share:
+ share.monthly_power = share.openhab_item.value - self._get_historic_value(share.openhab_item, last_month)
+
+ energy_unknown = energy_sum_month - sum(share.monthly_power for share in self._known_energy_share)
+
+ with tempfile.TemporaryDirectory() as temp_dir_name:
+ # create plot
+ labels = [share.chart_name for share in self._known_energy_share] + ["Rest"]
+ values = [share.monthly_power for share in self._known_energy_share] + [energy_unknown]
+ chart_path = pathlib.Path(temp_dir_name) / "chart.png"
+ habapp_rules.energy.donut_chart.create_chart(labels, values, chart_path)
+
+ # get html
+ html = self._create_html(energy_sum_month)
+
+ # send mail
+ self._mail.send_message(self._recipients, html, f"Stromverbrauch {_get_previous_month_name()}", images={"chart": str(chart_path)})
+
+ self.run.at(next_trigger_time := _get_next_trigger(), self._cb_send_energy)
+ self._instance_logger.info(f"Successfully sent energy consumption mail to {self._recipients}. Scheduled the next trigger time to {next_trigger_time.isoformat()}")
diff --git a/habapp_rules/energy/monthly_report_template.html b/habapp_rules/energy/monthly_report_template.html
new file mode 100644
index 0000000..5719a2c
--- /dev/null
+++ b/habapp_rules/energy/monthly_report_template.html
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Strom Verbrauch
+
+
+ von {{ month }}
+
+
+
+
+ Aktueller Zählerstand: {{ energy_now }} kWh. Verbrauch dieser Monat: {{ energy_last_month }} kWh.
+
+ Aufteilung:
+
+ {{ chart }}
+
+
+
+
+ Generated with habapp_rules version = {{ habapp_version }}
+ |
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
diff --git a/requirements.txt b/requirements.txt
index d7e0a64..d8d95f0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
HABApp==24.02.0
-multi-notifier~=0.2.2
+matplotlib~=3.8.3
+multi-notifier~=0.4.0
transitions~=0.9
-holidays==0.44
\ No newline at end of file
+holidays==0.45
\ No newline at end of file
diff --git a/requirements_dev.txt b/requirements_dev.txt
index a35a742..e89da9f 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,2 +1,2 @@
graphviz~=0.20
-nose_helper~=1.2.0
\ No newline at end of file
+nose_helper~=1.3.0
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 19ef258..6a445a7 100644
--- a/setup.py
+++ b/setup.py
@@ -21,5 +21,6 @@ def load_req() -> typing.List[str]:
packages=setuptools.find_packages(exclude=["tests*", "rules*"]),
install_requires=load_req(),
python_requires=">=3.10",
- license="Apache License 2.0"
+ license="Apache License 2.0",
+ include_package_data=True
)
diff --git a/tests/energy/donut_chart.py b/tests/energy/donut_chart.py
new file mode 100644
index 0000000..c5a9303
--- /dev/null
+++ b/tests/energy/donut_chart.py
@@ -0,0 +1,43 @@
+"""Tests for donut chart."""
+import unittest
+import unittest.mock
+
+import habapp_rules.energy.donut_chart
+
+
+# pylint: disable=protected-access
+class TestDonutFunctions(unittest.TestCase):
+ """Test all donut plot functions."""
+
+ def test_auto_percent_format(self):
+ """Test _auto_percent_format."""
+ values = [100, 20, 80]
+ percent_values = [val / sum(values) * 100 for val in values]
+
+ label_function = habapp_rules.energy.donut_chart._auto_percent_format(values)
+
+ for idx, percent_value in enumerate(percent_values):
+ self.assertEqual(f"{values[idx]:.1f} kWh", label_function(percent_value))
+
+ def test_create_chart(self):
+ """Test create_chart."""
+ labels = ["one", "two", "three"]
+ values = [1, 2, 3.0]
+ path = unittest.mock.MagicMock()
+
+ with unittest.mock.patch("matplotlib.pyplot") as pyplot_mock:
+ ax_mock = unittest.mock.MagicMock()
+ pyplot_mock.subplots.return_value = None, ax_mock
+ text_mock_1 = unittest.mock.MagicMock()
+ text_mock_2 = unittest.mock.MagicMock()
+ ax_mock.pie.return_value = None, [text_mock_1, text_mock_2], None
+
+ habapp_rules.energy.donut_chart.create_chart(labels, values, path)
+
+ pyplot_mock.subplots.assert_called_once()
+ ax_mock.pie.assert_called_once_with(values, labels=labels, autopct=unittest.mock.ANY, pctdistance=0.7, textprops={"fontsize": 10})
+
+ text_mock_1.set_backgroundcolor.assert_called_once_with("white")
+ text_mock_2.set_backgroundcolor.assert_called_once_with("white")
+
+ pyplot_mock.savefig.assert_called_once_with(str(path), bbox_inches="tight", transparent=True)
diff --git a/tests/energy/monthly_report.py b/tests/energy/monthly_report.py
new file mode 100644
index 0000000..046c20b
--- /dev/null
+++ b/tests/energy/monthly_report.py
@@ -0,0 +1,188 @@
+"""Tests for monthly energy report."""
+import collections
+import datetime
+import unittest
+import unittest.mock
+
+import HABApp.openhab.definitions.helpers.persistence_data
+import HABApp.openhab.items
+
+import habapp_rules.__version__
+import habapp_rules.core.exceptions
+import habapp_rules.energy.monthly_report
+import tests.helper.oh_item
+import tests.helper.test_case_base
+
+# pylint: disable=protected-access
+class TestFunctions(unittest.TestCase):
+ """Test all global functions."""
+
+ def test_get_last_month_name(self):
+ """Test _get_last_month_name."""
+ TestCase = collections.namedtuple("TestCase", ["month_number", "expected_name"])
+
+ test_cases = [
+ TestCase(1, "Dezember"),
+ TestCase(2, "Januar"),
+ TestCase(3, "Februar"),
+ TestCase(4, "März"),
+ TestCase(5, "April"),
+ TestCase(6, "Mai"),
+ TestCase(7, "Juni"),
+ TestCase(8, "Juli"),
+ TestCase(9, "August"),
+ TestCase(10, "September"),
+ TestCase(11, "Oktober"),
+ TestCase(12, "November"),
+ ]
+
+ today = datetime.datetime.today()
+ with unittest.mock.patch("datetime.date") as mock_date:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ mock_date.today.return_value = today.replace(month=test_case.month_number)
+ self.assertEqual(test_case.expected_name, habapp_rules.energy.monthly_report._get_previous_month_name())
+
+ def test_get_next_trigger(self):
+ """Test _get_next_trigger."""
+ TestCase = collections.namedtuple("TestCase", ["current_datetime", "expected_trigger"])
+
+ test_cases = [
+ TestCase(datetime.datetime(2022, 1, 1, 0, 0, 0), datetime.datetime(2022, 2, 1, 0, 0, 0)),
+ TestCase(datetime.datetime(2023, 12, 17, 2, 42, 55), datetime.datetime(2024, 1, 1, 0, 0, 0)),
+ TestCase(datetime.datetime(2024, 2, 29, 23, 59, 59), datetime.datetime(2024, 3, 1, 0, 0, 0))
+ ]
+
+ with unittest.mock.patch("datetime.datetime") as mock_datetime:
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ mock_datetime.now.return_value = test_case.current_datetime
+ self.assertEqual(test_case.expected_trigger, habapp_rules.energy.monthly_report._get_next_trigger())
+
+
+class TestEnergyShare(tests.helper.test_case_base.TestCaseBase):
+ """Test EnergyShare dataclass."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Number_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.SwitchItem, "Switch_1", None)
+
+ def test_init(self):
+ """Test init."""
+ # valid init
+ energy_share = habapp_rules.energy.monthly_report.EnergyShare("Number_1", "First Number")
+ self.assertEqual("Number_1", energy_share.openhab_name)
+ self.assertEqual("First Number", energy_share.chart_name)
+ self.assertEqual(0, energy_share.monthly_power)
+ self.assertEqual("Number_1", energy_share.openhab_item.name)
+
+ expected_item = HABApp.openhab.items.NumberItem("Number_1")
+ self.assertEqual(expected_item, energy_share.openhab_item)
+
+ # invalid init (Item not found)
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
+ habapp_rules.energy.monthly_report.EnergyShare("Number_2", "Second Number")
+
+ # invalid init (Item is not a number)
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
+ habapp_rules.energy.monthly_report.EnergyShare("Switch_1", "Second Number")
+
+
+class TestMonthlyReport(tests.helper.test_case_base.TestCaseBase):
+ """Test MonthlyReport rule."""
+
+ def setUp(self) -> None:
+ """Setup test case."""
+ tests.helper.test_case_base.TestCaseBase.setUp(self)
+
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Energy_Sum", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Energy_1", None)
+ tests.helper.oh_item.add_mock_item(HABApp.openhab.items.NumberItem, "Energy_2", None)
+
+ self._energy_1 = habapp_rules.energy.monthly_report.EnergyShare("Energy_1", "Energy 1")
+ self._energy_2 = habapp_rules.energy.monthly_report.EnergyShare("Energy_2", "Energy 2")
+ self._mail_config = unittest.mock.MagicMock()
+
+ self._rule = habapp_rules.energy.monthly_report.MonthlyReport("Energy_Sum", [self._energy_1, self._energy_2], None, self._mail_config, "test@test.de")
+
+ def test_init(self):
+ """Test init."""
+ TestCase = collections.namedtuple("TestCase", ["sum_in_group", "item_1_in_group", "item_2_in_group", "raises_exception"])
+
+ test_cases = [
+ TestCase(True, True, True, False),
+ TestCase(True, True, False, True),
+ TestCase(True, False, True, True),
+ TestCase(True, False, False, True),
+ TestCase(False, True, True, True),
+ TestCase(False, True, False, True),
+ TestCase(False, False, True, True),
+ TestCase(False, False, False, True),
+ ]
+
+ for test_case in test_cases:
+ with self.subTest(test_case=test_case):
+ self._energy_1.openhab_item.groups = {"PersistenceGroup"} if test_case.item_1_in_group else set()
+ self._energy_2.openhab_item.groups = {"PersistenceGroup"} if test_case.item_2_in_group else set()
+ self._rule._item_energy_sum.groups = {"PersistenceGroup"} if test_case.sum_in_group else set()
+
+ if test_case.raises_exception:
+ with self.assertRaises(habapp_rules.core.exceptions.HabAppRulesConfigurationException):
+ habapp_rules.energy.monthly_report.MonthlyReport("Energy_Sum", [self._energy_1, self._energy_2], "PersistenceGroup", self._mail_config, "test@test.de")
+ else:
+ habapp_rules.energy.monthly_report.MonthlyReport("Energy_Sum", [self._energy_1, self._energy_2], "PersistenceGroup", self._mail_config, "test@test.de")
+
+ def test_get_historic_value(self):
+ """Test _get_historic_value."""
+ mock_item = unittest.mock.MagicMock()
+ fake_persistence_data = HABApp.openhab.definitions.helpers.persistence_data.OpenhabPersistenceData()
+ mock_item.get_persistence_data.return_value = fake_persistence_data
+
+ start_time = datetime.datetime.now()
+ end_time = start_time + datetime.timedelta(hours=1)
+
+ # no data
+ self.assertEqual(0, self._rule._get_historic_value(mock_item, start_time))
+ mock_item.get_persistence_data.assert_called_once_with(start_time=start_time, end_time=end_time)
+
+ # data
+ fake_persistence_data.data = {"0.0": 42, "1.0": 1337}
+ self.assertEqual(42, self._rule._get_historic_value(mock_item, start_time))
+
+ def test_create_html(self):
+ """Test create_html."""
+ self._rule._item_energy_sum.value = 20_123.5489135
+
+ template_mock = unittest.mock.MagicMock()
+ with (unittest.mock.patch("pathlib.Path.open"),
+ unittest.mock.patch("jinja2.Template", return_value=template_mock),
+ unittest.mock.patch("habapp_rules.energy.monthly_report._get_previous_month_name", return_value="MonthName")):
+ self._rule._create_html(10_042.123456)
+
+ template_mock.render.assert_called_once_with(
+ month="MonthName",
+ energy_now="20123.5",
+ energy_last_month="10042.1",
+ habapp_version=habapp_rules.__version__.__version__,
+ chart="{{ chart }}"
+ )
+
+ def test_cb_send_energy(self):
+ """Test cb_send_energy."""
+ self._rule._item_energy_sum.value = 1000
+ self._energy_1.openhab_item.value = 100
+ self._energy_2.openhab_item.value = 50
+
+ with (unittest.mock.patch.object(self._rule, "_get_historic_value", side_effect=[800, 90, 45]),
+ unittest.mock.patch("habapp_rules.energy.donut_chart.create_chart", return_value="html text result") as create_chart_mock,
+ unittest.mock.patch.object(self._rule, "_create_html") as create_html_mock,
+ unittest.mock.patch("habapp_rules.energy.monthly_report._get_previous_month_name", return_value="MonthName"),
+ unittest.mock.patch.object(self._rule, "_mail") as mail_mock):
+ self._rule._cb_send_energy()
+
+ create_chart_mock.assert_called_once_with(["Energy 1", "Energy 2", "Rest"], [10, 5, 185], unittest.mock.ANY)
+ create_html_mock.assert_called_once_with(200)
+ mail_mock.send_message("test@test.de", "html text result", "Stromverbrauch MonthName", images={"chart": unittest.mock.ANY})
diff --git a/tests/run_unittest.py b/tests/run_unittest.py
index 655594f..33a488c 100644
--- a/tests/run_unittest.py
+++ b/tests/run_unittest.py
@@ -9,5 +9,8 @@
EXCLUDED_PY_FILES = ["run_unittest.py", "__init__.py", "rule_runner.py"]
INPUT_MODULES = [f"tests.{'.'.join(f.parts)[:-3]}" for f in pathlib.Path(".").rglob("*.py") if f.name not in EXCLUDED_PY_FILES]
-with unittest.mock.patch("logging.getLogger", spec=logging.getLogger):
+logger_mock = unittest.mock.MagicMock()
+logger_mock.level = logging.WARNING
+
+with unittest.mock.patch("logging.getLogger", return_value=logger_mock):
result = unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromNames(INPUT_MODULES))