From 269c82542de1eb095cb84b6a95c1e06aebea6441 Mon Sep 17 00:00:00 2001
From: Magnus Larsson <maglars2@gmail.com>
Date: Mon, 7 Oct 2024 17:57:30 +0200
Subject: [PATCH 1/8] Add templated attributes

---
 .gitignore                              |  3 ++-
 custom_components/birthdays/__init__.py | 33 +++++++++++++++----------
 2 files changed, 22 insertions(+), 14 deletions(-)

diff --git a/.gitignore b/.gitignore
index bcc0968..5c77619 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 **/.DS_Store
-.history
\ No newline at end of file
+.history
+.idea
\ No newline at end of file
diff --git a/custom_components/birthdays/__init__.py b/custom_components/birthdays/__init__.py
index 530d8c3..4a95924 100644
--- a/custom_components/birthdays/__init__.py
+++ b/custom_components/birthdays/__init__.py
@@ -1,13 +1,13 @@
 import asyncio
 import logging
 
-import async_timeout
 import voluptuous as vol
 
 import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers.entity import Entity
 from homeassistant.helpers.entity_component import EntityComponent
 from homeassistant.helpers.event import async_call_later
+from homeassistant.helpers.template import is_template_string, Template, render_complex
 from homeassistant.util import dt as dt_util
 from homeassistant.util import slugify
 
@@ -23,18 +23,18 @@
 
 BIRTHDAY_CONFIG_SCHEMA = vol.Schema({
     vol.Optional(CONF_UNIQUE_ID): cv.string,
-    vol.Required(CONF_NAME) : cv.string,
-    vol.Required(CONF_DATE_OF_BIRTH) : cv.date,
-    vol.Optional(CONF_ICON, default = 'mdi:cake'): cv.string,
-    vol.Optional(CONF_ATTRIBUTES, default = {}) : vol.Schema({cv.string: cv.string}),
+    vol.Required(CONF_NAME): cv.string,
+    vol.Required(CONF_DATE_OF_BIRTH): cv.date,
+    vol.Optional(CONF_ICON, default='mdi:cake'): cv.string,
+    vol.Optional(CONF_ATTRIBUTES, default={}): vol.Schema({cv.string: cv.string}),
 })
 
 CONFIG_SCHEMA = vol.Schema({
     DOMAIN: vol.All(cv.ensure_list, [BIRTHDAY_CONFIG_SCHEMA])
 }, extra=vol.ALLOW_EXTRA)
 
-async def async_setup(hass, config):
 
+async def async_setup(hass, config):
     devices = []
 
     for birthday_data in config[DOMAIN]:
@@ -48,7 +48,6 @@ async def async_setup(hass, config):
     component = EntityComponent(_LOGGER, DOMAIN, hass)
     await component.async_add_entities(devices)
 
-
     tasks = [asyncio.create_task(device.update_data()) for device in devices]
     await asyncio.wait(tasks)
 
@@ -64,7 +63,7 @@ def __init__(self, unique_id, name, date_of_birth, icon, attributes, hass):
 
         if unique_id is not None:
             self._unique_id = slugify(unique_id)
-        else: 
+        else:
             self._unique_id = slugify(name)
 
         self._state = None
@@ -75,10 +74,15 @@ def __init__(self, unique_id, name, date_of_birth, icon, attributes, hass):
         self._extra_state_attributes = {
             CONF_DATE_OF_BIRTH: str(self._date_of_birth),
         }
+        self._templated_attributes = {}
 
         if len(attributes) > 0 and attributes is not None:
-            for k,v in attributes.items():
-                self._extra_state_attributes[k] = v
+            for k, v in attributes.items():
+                if is_template_string(v):
+                    _LOGGER.info(f'{v} is a template and will be evaluated at runtime')
+                    self._templated_attributes[k] = Template(template=v, hass=hass)
+                else:
+                    self._extra_state_attributes[k] = v
 
     @property
     def name(self):
@@ -103,6 +107,10 @@ def icon(self):
 
     @property
     def extra_state_attributes(self):
+        for key, templated_value in self._templated_attributes.items():
+            value = render_complex(templated_value, variables={"this": self})
+            self._extra_state_attributes[key] = value
+
         return self._extra_state_attributes
 
     @property
@@ -117,7 +125,7 @@ def _get_seconds_until_midnight(self):
         one_day_in_seconds = 24 * 60 * 60
 
         now = dt_util.now()
-        total_seconds_passed_today = (now.hour*60*60) + (now.minute*60) + now.second
+        total_seconds_passed_today = (now.hour * 60 * 60) + (now.minute * 60) + now.second
 
         return one_day_in_seconds - total_seconds_passed_today
 
@@ -130,12 +138,11 @@ async def update_data(self, *_):
         if next_birthday < today:
             next_birthday = next_birthday.replace(year=today.year + 1)
 
-        days_until_next_birthday = (next_birthday-today).days
+        days_until_next_birthday = (next_birthday - today).days
 
         age = next_birthday.year - self._date_of_birth.year
         self._extra_state_attributes[CONF_AGE_AT_NEXT_BIRTHDAY] = age
 
-        
         self._state = days_until_next_birthday
 
         if days_until_next_birthday == 0:

From 2e04770c3836c5c20e7b7891fb8e604cc12e8dbb Mon Sep 17 00:00:00 2001
From: Magnus Larsson <maglars2@gmail.com>
Date: Mon, 7 Oct 2024 18:18:43 +0200
Subject: [PATCH 2/8] Expose date_of_birth as property

---
 custom_components/birthdays/__init__.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/custom_components/birthdays/__init__.py b/custom_components/birthdays/__init__.py
index 4a95924..16045f1 100644
--- a/custom_components/birthdays/__init__.py
+++ b/custom_components/birthdays/__init__.py
@@ -113,6 +113,10 @@ def extra_state_attributes(self):
 
         return self._extra_state_attributes
 
+    @property
+    def date_of_birth(self):
+        return self._date_of_birth
+
     @property
     def unit_of_measurement(self):
         return 'days'

From eef057674adeb92343c2cc568a7e24ade7d35cbf Mon Sep 17 00:00:00 2001
From: Magnus Larsson <maglars2@gmail.com>
Date: Tue, 8 Oct 2024 18:28:51 +0200
Subject: [PATCH 3/8] Add globally defined attributes

---
 custom_components/birthdays/__init__.py | 33 +++++++++++++++++++++++--
 1 file changed, 31 insertions(+), 2 deletions(-)

diff --git a/custom_components/birthdays/__init__.py b/custom_components/birthdays/__init__.py
index 16045f1..89d6761 100644
--- a/custom_components/birthdays/__init__.py
+++ b/custom_components/birthdays/__init__.py
@@ -18,6 +18,8 @@
 CONF_DATE_OF_BIRTH = 'date_of_birth'
 CONF_ICON = 'icon'
 CONF_ATTRIBUTES = 'attributes'
+CONF_GLOBAL_CONFIG = 'config'
+CONF_BIRTHDAYS = 'birthdays'
 CONF_AGE_AT_NEXT_BIRTHDAY = 'age_at_next_birthday'
 DOMAIN = 'birthdays'
 
@@ -29,20 +31,47 @@
     vol.Optional(CONF_ATTRIBUTES, default={}): vol.Schema({cv.string: cv.string}),
 })
 
-CONFIG_SCHEMA = vol.Schema({
+GLOBAL_CONFIG_SCHEMA = vol.Schema({
+    vol.Optional(CONF_ATTRIBUTES, default={}): vol.Schema({cv.string: cv.string}),
+})
+
+# Old schema (list of birthday configurations)
+OLD_CONFIG_SCHEMA = vol.Schema({
     DOMAIN: vol.All(cv.ensure_list, [BIRTHDAY_CONFIG_SCHEMA])
 }, extra=vol.ALLOW_EXTRA)
 
+# New schema (supports both global and birthday configs)
+NEW_CONFIG_SCHEMA = vol.Schema({
+    DOMAIN: {
+        CONF_BIRTHDAYS: vol.All(cv.ensure_list, [BIRTHDAY_CONFIG_SCHEMA]),
+        vol.Optional(CONF_GLOBAL_CONFIG, default={}): GLOBAL_CONFIG_SCHEMA
+    }
+}, extra=vol.ALLOW_EXTRA)
+
+# Use vol.Any() to support both old and new schemas
+CONFIG_SCHEMA = vol.Schema(vol.Any(
+    OLD_CONFIG_SCHEMA,
+    NEW_CONFIG_SCHEMA
+), extra=vol.ALLOW_EXTRA)
+
 
 async def async_setup(hass, config):
     devices = []
 
-    for birthday_data in config[DOMAIN]:
+    is_new_config = isinstance(config[DOMAIN], dict) and config[DOMAIN].get(CONF_BIRTHDAYS) is not None
+    birthdays = config[DOMAIN][CONF_BIRTHDAYS] if is_new_config else config[DOMAIN]
+
+    for birthday_data in birthdays:
         unique_id = birthday_data.get(CONF_UNIQUE_ID)
         name = birthday_data[CONF_NAME]
         date_of_birth = birthday_data[CONF_DATE_OF_BIRTH]
         icon = birthday_data[CONF_ICON]
         attributes = birthday_data[CONF_ATTRIBUTES]
+        if is_new_config:
+            global_config = config[DOMAIN][CONF_GLOBAL_CONFIG]  # Empty dict or has attributes
+            global_attributes = global_config.get(CONF_ATTRIBUTES) or {}
+            attributes = dict(global_attributes, **attributes)  # Add global_attributes but let local attributes be on top
+
         devices.append(BirthdayEntity(unique_id, name, date_of_birth, icon, attributes, hass))
 
     component = EntityComponent(_LOGGER, DOMAIN, hass)

From 2265185dd366cb8ad92a2f1e04b8a7dfd99c1335 Mon Sep 17 00:00:00 2001
From: Magnus Larsson <maglars2@gmail.com>
Date: Tue, 8 Oct 2024 18:41:31 +0200
Subject: [PATCH 4/8] Document templated and global attributes

---
 README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 47 insertions(+)

diff --git a/README.md b/README.md
index 5462159..1462870 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,53 @@ You can add a unique id and custom attributes to each birthday, for instance to
 To do this, add a dictionary under the `attributes` key in the configuration (see example above). The dictionary can contain any key-value pairs you want, and will be exposed as attributes on the entity.
 Fetching the attributes can be done using `state_attr` in a template, for instance `{{ state_attr('birthdays.einstein', 'occupation') }}` will return `Theoretical physicist`.
 
+### Templated attributes
+Attributes to an entity can also be a template. To do calculations based on data from the entity, use the `this`-keyword.
+Be aware that templates that cannot be correctly parsed can lead to the entity not being loaded, 
+so if your entity is suddenly gone after adding a templated attribute, please check the logs.
+
+Example calculating age in number of days:
+```yaml
+birthdays:
+  - name: 'Frodo Baggins'
+    date_of_birth: 1921-09-22
+    attributes:
+      days_since_birth: '{{ ((as_timestamp(now()) - as_timestamp(this.date_of_birth)) | int /60/1440) | int }}'
+```
+
+Properties of `this` that can be used:
+* name
+* unique_id
+* state
+* icon
+* date_of_birth
+* unit_of_measurement
+
+Note: Don't use `this.extra_state_attributes`, as that might trigger an infinite loop.
+
+### Global attributes:
+It is possible to add global attributes that will be added to all birthdays. Global attributes work just the same as other attributes,
+and can thus also be templated.
+
+This example will add the attribute `days_since_birth` on all entities:
+```yaml
+# Example configuration.yaml entry
+birthdays:
+  config:
+    attributes:
+      days_since_birth: '{{ ((as_timestamp(now()) - as_timestamp(this.date_of_birth)) | int /60/1440) | int }}'
+  birthdays:
+    - name: 'Frodo Baggins'
+      date_of_birth: 1921-09-22
+    - name: 'Bilbo Baggins'
+      date_of_birth: 1843-09-22
+    - name: Elvis
+      date_of_birth: 1935-01-08
+      icon: 'mdi:music'
+```
+
+Note that global attributes will be overridden by entity specific attributes.
+
 ## Automation
 All birthdays are updated at midnight, and when a birthday occurs an event is sent on the HA bus that can be used for automations. The event is called `birthday` and contains the data `name` and `age`. Note that there will be two events fired if two persons have the same birthday.
 

From 09203aecf6b2ebc4da5f1214bae497f40e504ce6 Mon Sep 17 00:00:00 2001
From: Magnus Larsson <maglars2@gmail.com>
Date: Tue, 8 Oct 2024 18:43:22 +0200
Subject: [PATCH 5/8] Update wording

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 1462870..6c24ace 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,7 @@ Fetching the attributes can be done using `state_attr` in a template, for instan
 
 ### Templated attributes
 Attributes to an entity can also be a template. To do calculations based on data from the entity, use the `this`-keyword.
-Be aware that templates that cannot be correctly parsed can lead to the entity not being loaded, 
+Be aware that if a template that cannot be correctly parse it can lead to the entity not being loaded, 
 so if your entity is suddenly gone after adding a templated attribute, please check the logs.
 
 Example calculating age in number of days:

From d9a4bdc5b2f1255d4d467a4081af420aed918e78 Mon Sep 17 00:00:00 2001
From: Magnus Larsson <maglars2@gmail.com>
Date: Tue, 8 Oct 2024 18:45:17 +0200
Subject: [PATCH 6/8] Update wording

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 6c24ace..1d73ecc 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,7 @@ Fetching the attributes can be done using `state_attr` in a template, for instan
 
 ### Templated attributes
 Attributes to an entity can also be a template. To do calculations based on data from the entity, use the `this`-keyword.
-Be aware that if a template that cannot be correctly parse it can lead to the entity not being loaded, 
+Be aware that if a template that cannot be correctly parsed it can lead to the entity not being loaded, 
 so if your entity is suddenly gone after adding a templated attribute, please check the logs.
 
 Example calculating age in number of days:

From 1a6f787b11cb269814dff22d1bc381e39b3b64aa Mon Sep 17 00:00:00 2001
From: Magnus Larsson <maglars2@gmail.com>
Date: Tue, 8 Oct 2024 22:52:14 +0200
Subject: [PATCH 7/8] Add precommit and tests

---
 .github/workflows/pre-commit.yml        | 22 +++++++++
 .gitignore                              |  5 +-
 .pre-commit-config.yaml                 | 49 ++++++++++++++++++++
 custom_components/__init__.py           |  1 +
 custom_components/birthdays/__init__.py |  7 ++-
 requirements.test.txt                   | 14 ++++++
 setup.cfg                               | 61 +++++++++++++++++++++++++
 tests/__init__.py                       |  1 +
 tests/conftest.py                       |  8 ++++
 tests/test_init.py                      | 53 +++++++++++++++++++++
 10 files changed, 216 insertions(+), 5 deletions(-)
 create mode 100644 .github/workflows/pre-commit.yml
 create mode 100644 .pre-commit-config.yaml
 create mode 100644 custom_components/__init__.py
 create mode 100644 requirements.test.txt
 create mode 100644 setup.cfg
 create mode 100644 tests/__init__.py
 create mode 100644 tests/conftest.py
 create mode 100644 tests/test_init.py

diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
new file mode 100644
index 0000000..0ece9c1
--- /dev/null
+++ b/.github/workflows/pre-commit.yml
@@ -0,0 +1,22 @@
+name: pre-commit
+
+on:
+  push:
+  pull_request:
+
+jobs:
+  pre-commit:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - uses: actions/setup-python@v4
+      with:
+        python-version: '3.10-dev'
+    - uses: pre-commit/action@v3.0.0
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install -r requirements.test.txt
+    - name: Run pytest
+      run: |
+        pytest
diff --git a/.gitignore b/.gitignore
index 5c77619..5d56f0b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
 **/.DS_Store
 .history
-.idea
\ No newline at end of file
+.idea
+.mypy_cache
+__pycache__/
+.coverage
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..e3d0634
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,49 @@
+repos:
+  - repo: https://github.com/asottile/pyupgrade
+    rev: v3.2.2
+    hooks:
+      - id: pyupgrade
+        args: [--py310-plus]
+  - repo: https://github.com/psf/black
+    rev: 22.10.0
+    hooks:
+      - id: black
+        args:
+          - --safe
+          - --quiet
+        files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
+  - repo: https://github.com/codespell-project/codespell
+    rev: v2.2.2
+    hooks:
+      - id: codespell
+        args:
+          - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
+          - --skip="./.*,*.csv,*.json"
+          - --quiet-level=2
+        exclude_types: [csv, json]
+  - repo: https://github.com/PyCQA/flake8
+    rev: 5.0.4
+    hooks:
+      - id: flake8
+        additional_dependencies:
+          - flake8-docstrings==1.5.0
+          - pydocstyle==5.0.2
+        files: ^(homeassistant|script|tests)/.+\.py$
+  - repo: https://github.com/PyCQA/isort
+    rev: 5.12.0
+    hooks:
+      - id: isort
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.3.0
+    hooks:
+      - id: check-executables-have-shebangs
+        stages: [manual]
+      - id: check-json
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: v0.991
+    hooks:
+      - id: mypy
+        args:
+          - --pretty
+          - --show-error-codes
+          - --show-error-context
diff --git a/custom_components/__init__.py b/custom_components/__init__.py
new file mode 100644
index 0000000..b30fdbe
--- /dev/null
+++ b/custom_components/__init__.py
@@ -0,0 +1 @@
+""" Custom components module"""
diff --git a/custom_components/birthdays/__init__.py b/custom_components/birthdays/__init__.py
index 89d6761..23fe187 100644
--- a/custom_components/birthdays/__init__.py
+++ b/custom_components/birthdays/__init__.py
@@ -7,9 +7,8 @@
 from homeassistant.helpers.entity import Entity
 from homeassistant.helpers.entity_component import EntityComponent
 from homeassistant.helpers.event import async_call_later
-from homeassistant.helpers.template import is_template_string, Template, render_complex
-from homeassistant.util import dt as dt_util
-from homeassistant.util import slugify
+from homeassistant.helpers.template import Template, is_template_string, render_complex
+from homeassistant.util import dt as dt_util, slugify
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -163,7 +162,7 @@ def _get_seconds_until_midnight(self):
         return one_day_in_seconds - total_seconds_passed_today
 
     async def update_data(self, *_):
-        from datetime import date, timedelta
+        from datetime import date
 
         today = dt_util.start_of_local_day().date()
         next_birthday = date(today.year, self._date_of_birth.month, self._date_of_birth.day)
diff --git a/requirements.test.txt b/requirements.test.txt
new file mode 100644
index 0000000..2e25a41
--- /dev/null
+++ b/requirements.test.txt
@@ -0,0 +1,14 @@
+# linters such as flake8 and pylint should be pinned, as new releases
+# make new things fail. Manually update these pins when pulling in a
+# new version
+
+aioresponses==0.7.2
+algoliasearch==2.6.2
+codecov==2.1.13
+coverage>=6.4.4
+mypy==0.991
+pytest>=7.1.0
+pytest-cov>=3.0.0
+pytest-mock>=3.10
+pytest-homeassistant-custom-component==0.12.20
+typing-extensions>=4.6.3
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..2812af5
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,61 @@
+[coverage:run]
+source =
+  custom_components
+
+[coverage:report]
+exclude_lines =
+    pragma: no cover
+    raise NotImplemented()
+    if __name__ == '__main__':
+    main()
+fail_under = 40
+show_missing = true
+
+[tool:pytest]
+testpaths = tests
+norecursedirs = .git
+addopts =
+    --strict
+    --cov=custom_components
+
+[flake8]
+# https://github.com/ambv/black#line-length
+max-line-length = 88
+# E501: line too long
+# W503: Line break occurred before a binary operator
+# E203: Whitespace before ':'
+# D202 No blank lines allowed after function docstring
+# W504 line break after binary operator
+ignore =
+    E501,
+    W503,
+    E203,
+    D202,
+    W504
+
+[isort]
+# https://github.com/timothycrosley/isort
+# https://github.com/timothycrosley/isort/wiki/isort-Settings
+# splits long import on multiple lines indented by 4 spaces
+multi_line_output = 3
+include_trailing_comma=True
+force_grid_wrap=0
+use_parentheses=True
+line_length=88
+indent = "    "
+# will group `import x` and `from x import` of the same module.
+force_sort_within_sections = true
+sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
+default_section = THIRDPARTY
+known_first_party = homeassistant,tests
+forced_separate = tests
+combine_as_imports = true
+
+[mypy]
+python_version = 3.10
+ignore_errors = true
+follow_imports = silent
+ignore_missing_imports = true
+warn_incomplete_stub = true
+warn_redundant_casts = true
+warn_unused_configs = true
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..95cde89
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+"""Tests for the birthdays component."""
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..1d99892
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,8 @@
+"""pytest fixtures."""
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def auto_enable_custom_integrations(enable_custom_integrations):
+    """Enable custom integrations defined in the test dir."""
+    yield
diff --git a/tests/test_init.py b/tests/test_init.py
new file mode 100644
index 0000000..c850ce7
--- /dev/null
+++ b/tests/test_init.py
@@ -0,0 +1,53 @@
+"""Test component setup."""
+from homeassistant.util import slugify
+
+from custom_components.birthdays import DOMAIN, CONF_BIRTHDAYS, CONF_GLOBAL_CONFIG, CONF_ATTRIBUTES
+
+from homeassistant.setup import async_setup_component
+
+
+async def test_async_setup__old_config_0_birthday_is_not_ok(hass):
+    """Cannot have 0 birthdays configured in old config."""
+    config = {DOMAIN: []}
+    await _test_setup(hass, config, False)
+
+
+async def test_async_setup__old_config_1_birthday_is_ok(hass):
+    """1 birthday is OK in old config."""
+    config = {DOMAIN: [{'name': 'HomeAssistant', 'date_of_birth': '2013-09-17'}]}
+    await _test_setup(hass, config, True)
+
+
+async def test_async_setup__new_config_0_birthday_is_not_ok(hass):
+    """Cannot have 0 birthdays configured in old config."""
+    config = {DOMAIN: {CONF_BIRTHDAYS: []}}
+    await _test_setup(hass, config, False)
+
+
+async def test_async_setup__new_config_1_birthday_is_ok(hass):
+    """1 birthday is OK in new config."""
+    config = {DOMAIN: {CONF_BIRTHDAYS: [{'name': 'HomeAssistant', 'date_of_birth': '2013-09-17'}]}}
+    await _test_setup(hass, config, True)
+
+
+async def test_async_setup__new_config_has_global_attributes(hass):
+    """Global attributes are allowed in schema"""
+    name = 'HomeAssistant'
+    config = {
+        DOMAIN: {
+            CONF_BIRTHDAYS: [
+                {'name': name, 'date_of_birth': '2013-09-17'}
+            ],
+            CONF_GLOBAL_CONFIG: {
+                CONF_ATTRIBUTES: {
+                    'message': 'Hello World!'
+                }
+            }
+        }
+    }
+
+    await _test_setup(hass, config, True)
+
+
+async def _test_setup(hass, config: dict, expected_result: bool):
+    assert await async_setup_component(hass, DOMAIN, config) is expected_result

From 0cac0d54b83953bc5ea3418b0885ae3e66fbcbf6 Mon Sep 17 00:00:00 2001
From: Magnus Larsson <maglars2@gmail.com>
Date: Tue, 8 Oct 2024 22:54:10 +0200
Subject: [PATCH 8/8] Fix linting problems

---
 tests/test_init.py | 32 ++++++++++++++++----------------
 1 file changed, 16 insertions(+), 16 deletions(-)

diff --git a/tests/test_init.py b/tests/test_init.py
index c850ce7..750bf36 100644
--- a/tests/test_init.py
+++ b/tests/test_init.py
@@ -1,8 +1,10 @@
 """Test component setup."""
-from homeassistant.util import slugify
-
-from custom_components.birthdays import DOMAIN, CONF_BIRTHDAYS, CONF_GLOBAL_CONFIG, CONF_ATTRIBUTES
-
+from custom_components.birthdays import (
+    CONF_ATTRIBUTES,
+    CONF_BIRTHDAYS,
+    CONF_GLOBAL_CONFIG,
+    DOMAIN,
+)
 from homeassistant.setup import async_setup_component
 
 
@@ -14,7 +16,7 @@ async def test_async_setup__old_config_0_birthday_is_not_ok(hass):
 
 async def test_async_setup__old_config_1_birthday_is_ok(hass):
     """1 birthday is OK in old config."""
-    config = {DOMAIN: [{'name': 'HomeAssistant', 'date_of_birth': '2013-09-17'}]}
+    config = {DOMAIN: [{"name": "HomeAssistant", "date_of_birth": "2013-09-17"}]}
     await _test_setup(hass, config, True)
 
 
@@ -26,23 +28,21 @@ async def test_async_setup__new_config_0_birthday_is_not_ok(hass):
 
 async def test_async_setup__new_config_1_birthday_is_ok(hass):
     """1 birthday is OK in new config."""
-    config = {DOMAIN: {CONF_BIRTHDAYS: [{'name': 'HomeAssistant', 'date_of_birth': '2013-09-17'}]}}
+    config = {
+        DOMAIN: {
+            CONF_BIRTHDAYS: [{"name": "HomeAssistant", "date_of_birth": "2013-09-17"}]
+        }
+    }
     await _test_setup(hass, config, True)
 
 
 async def test_async_setup__new_config_has_global_attributes(hass):
-    """Global attributes are allowed in schema"""
-    name = 'HomeAssistant'
+    """Global attributes are allowed in schema."""
+    name = "HomeAssistant"
     config = {
         DOMAIN: {
-            CONF_BIRTHDAYS: [
-                {'name': name, 'date_of_birth': '2013-09-17'}
-            ],
-            CONF_GLOBAL_CONFIG: {
-                CONF_ATTRIBUTES: {
-                    'message': 'Hello World!'
-                }
-            }
+            CONF_BIRTHDAYS: [{"name": name, "date_of_birth": "2013-09-17"}],
+            CONF_GLOBAL_CONFIG: {CONF_ATTRIBUTES: {"message": "Hello World!"}},
         }
     }