Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
d48aa41
Reuse shared handle in sensors
bvweerd Jul 18, 2025
fec9190
Remove stubs and fix unload listeners test
bvweerd Jul 18, 2025
37b351a
Merge pull request #64 from bvweerd/codex/refactor-basepidentity-hand…
bvweerd Jul 20, 2025
25494b6
Merge pull request #65 from bvweerd/codex/modify-async_setup_entry-fo…
bvweerd Jul 20, 2025
8d000e7
Improve issue labeling workflow
bvweerd Jul 20, 2025
3fbb88d
Merge pull request #66 from bvweerd/codex/improve-automatic-labeling-…
bvweerd Jul 20, 2025
aa3c0fd
Add test for invalid last state handling in select
bvweerd Jul 20, 2025
abf492d
Add update_interval change test
bvweerd Jul 20, 2025
ebbb6d0
Add test for options update listener
bvweerd Jul 20, 2025
11f3a18
Add test for ConfigEntryNotReady when sensor unavailable
bvweerd Jul 20, 2025
7d04229
Merge pull request #67 from bvweerd/codex/add-test-for-pidstartmodese…
bvweerd Jul 20, 2025
3660b3a
Merge pull request #68 from bvweerd/codex/write-test-for-piddatacoord…
bvweerd Jul 20, 2025
c125e41
Merge pull request #69 from bvweerd/codex/add-new-test-in-tests/-dire…
bvweerd Jul 20, 2025
a398e5b
Merge pull request #70 from bvweerd/codex/add-tests-for-async_setup_e…
bvweerd Jul 20, 2025
d766079
Update precommit.yml
bvweerd Jul 20, 2025
7e16784
Update workflow naming
Aug 6, 2025
d0fc27e
Update requirements.txt
Aug 6, 2025
4fdb63c
chore: apply pre-commit fixes
github-actions[bot] Aug 6, 2025
2a13a93
refactor: reuse dummy pid fixture
bvweerd Aug 12, 2025
68221bc
refactor tests integration setup
bvweerd Aug 12, 2025
3e9eb30
Merge pull request #75 from bvweerd/add-dummy_pid_class-fixture-in-te…
bvweerd Aug 12, 2025
0297c1f
Merge pull request #76 from bvweerd/update-pytest-fixtures-for-integr…
bvweerd Aug 12, 2025
f95c20f
Increase range of coefficients
anssih Aug 13, 2025
beb73ce
Increase maximum sample time to 600 seconds
anssih Aug 13, 2025
a636a23
Increase resolution of diagnostics sensors
anssih Aug 13, 2025
96cc3c3
chore: fix issue labeler workflow
bvweerd Aug 13, 2025
9ff9f02
Merge pull request #79 from bvweerd/fix-action-version-for-issue-labeler
bvweerd Aug 13, 2025
85edb17
Register PIDContributionSensor in test
bvweerd Aug 13, 2025
456c9e7
Merge branch 'dev' into fix-entity-unknown-in-test
bvweerd Aug 13, 2025
57ab9ba
Merge pull request #80 from bvweerd/fix-entity-unknown-in-test
bvweerd Aug 13, 2025
8ca9463
Merge branch 'dev' into increase-ranges
bvweerd Aug 13, 2025
1820732
Fix test to match precision
bvweerd Aug 13, 2025
6349a04
Merge pull request #78 from anssih/increase-ranges
bvweerd Aug 13, 2025
1ab424c
test: cover last known value start mode
bvweerd Aug 13, 2025
0797a00
Merge pull request #81 from bvweerd/increase-test-coverage-to-100%
bvweerd Aug 13, 2025
4bbeedc
Add service to set PID output
bvweerd Aug 14, 2025
3db0e57
Merge 4bbeedc646df03989d78a8cd60cb827d7d6bc422 into 0797a0017fc17547b…
bvweerd Aug 14, 2025
21ff632
chore: apply pre-commit fixes
github-actions[bot] Aug 14, 2025
0e7ad29
Fix output service schema and validation
bvweerd Aug 14, 2025
383f421
Toggle auto mode off before re-enabling
bvweerd Aug 14, 2025
fb1ccc9
Merge pull request #100 from bvweerd/fix-auto-mode-toggle-functionality
bvweerd Aug 14, 2025
41084c5
Merge branch 'add-ui-service-for-pid-output-control' into fix-pytest
bvweerd Aug 14, 2025
ef22f4d
Merge pull request #99 from bvweerd/fix-pytest
bvweerd Aug 14, 2025
511b5a0
Move entity to target in services
bvweerd Aug 14, 2025
1f19c87
Merge pull request #101 from bvweerd/fix-services.yaml-error
bvweerd Aug 14, 2025
d25003f
fix services yaml description
bvweerd Aug 14, 2025
d48a9c6
Merge pull request #102 from bvweerd/fix-mapping-values-error-in-serv…
bvweerd Aug 14, 2025
67a2d5f
Update __init__.py
bvweerd Aug 15, 2025
1651e08
Allow set_output service to use target
bvweerd Aug 15, 2025
8c0aa77
Merge pull request #104 from bvweerd/fix-simple_pid_controller.set_ou…
bvweerd Aug 15, 2025
57a0d32
Handle target entity IDs correctly
bvweerd Aug 15, 2025
daed667
Merge branch 'add-ui-service-for-pid-output-control' into fix-simple_…
bvweerd Aug 15, 2025
a52410f
Merge pull request #105 from bvweerd/fix-simple_pid_controller.set_ou…
bvweerd Aug 15, 2025
5ee0daa
Update PID output when auto mode disabled
bvweerd Aug 15, 2025
339c3ce
Merge pull request #106 from bvweerd/fix-output-not-set-when-auto_mod…
bvweerd Aug 15, 2025
3de0642
Merge pull request #98 from bvweerd/add-ui-service-for-pid-output-con…
bvweerd Aug 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ bug:
feature:
- "feature request"
- "enhancement"
documentation:
- "docs"
- "documentation"
maintenance:
- "dependabot"

2 changes: 1 addition & 1 deletion .github/workflows/hacs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ on:
schedule:
- cron: "0 0 * * *"


permissions:
contents: read

jobs:
validate-hacs:
name: "HACS"
runs-on: ubuntu-latest
steps:
- name: HACS Validation
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/hassfest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ permissions:

jobs:
hassfest:
name: "Hassfest"
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/label-issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ on:
- edited
jobs:
label_issues:
name: "Automatically Label Issues"
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: peaceiris/actions-issue-labeler@v1
- uses: github/issue-labeler@v3.4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler.yml

31 changes: 25 additions & 6 deletions .github/workflows/precommit.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: Run Pre-Commit
name: Run Code Quality Check

permissions:
contents: write

on:
push:
branches:
Expand All @@ -11,11 +14,9 @@ on:
- main
workflow_dispatch:

permissions:
contents: write

jobs:
test:
name: "Code Quality Check"
env:
PYTHONPATH: .
runs-on: ubuntu-latest
Expand All @@ -28,7 +29,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
python-version: "3.12"
cache: 'pip' # caching pip dependencies

- name: Install dependencies
Expand All @@ -39,4 +40,22 @@ jobs:

- name: Run pre-commit hooks
run: |
pre-commit run --all-files
# Pre-commit exits with code 1 when it makes changes. We don't want
# the workflow to fail in that case, so ignore the exit code.
pre-commit run --all-files || true

- name: Commit and push changes
if: success()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore: apply pre-commit fixes"
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
git push origin "HEAD:${BRANCH}"
fi
5 changes: 3 additions & 2 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ permissions:

jobs:
test:
name: "Pytest"
env:
PYTHONPATH: .
runs-on: ubuntu-latest
Expand All @@ -28,7 +29,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
python-version: "3.12"
cache: 'pip' # caching pip dependencies

- name: Install dependencies
Expand All @@ -37,4 +38,4 @@ jobs:
pip install -r requirements.txt

- name: Run tests
run: pytest --maxfail=1 --disable-warnings -q
run: pytest --maxfail=1 --disable-warnings -q
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,39 @@ Here's an example output showing the controller responding to a setpoint:

---

## 🔧 Service Actions
This Integration does **not** expose any custom services. All interactions are performed via UI-based entities.
## 🔧 Service Actions
The integration provides a `simple_pid_controller.set_output` service to adjust the controller output directly.

### `simple_pid_controller.set_output`
| Field | Description |
|-------|-------------|
| `entity_id` | PID output sensor entity to control (may be provided via `target`) |
| `preset` | Optional preset: `zero_start`, `last_known_value`, or `startup_value` |
| `value` | Optional manual value between configured `Output Min` and `Output Max` |

- When **Auto Mode** is off, the last output value is updated to the chosen value.
- When **Auto Mode** is on, the PID restarts from the new value and the coordinator refreshes.
- Exactly one PID output sensor entity must be targeted.

Example using `target`:

```yaml
service: simple_pid_controller.set_output
target:
entity_id: sensor.spid_x_pid_output
data:
value: 200
```

Example using `target`:

```yaml
action: simple_pid_controller.set_output
target:
entity_id: sensor.spid_x_pid_output
data:
value: 200
```



89 changes: 86 additions & 3 deletions custom_components/simple_pid_controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.const import Platform, ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import entity_registry as er
from dataclasses import dataclass
import voluptuous as vol
import homeassistant.helpers.config_validation as cv

from .coordinator import PIDDataCoordinator

from .const import (
Expand All @@ -34,6 +37,19 @@
Platform.SELECT,
]

SERVICE_SET_OUTPUT = "set_output"
ATTR_VALUE = "value"
ATTR_PRESET = "preset"
PRESET_OPTIONS = ["zero_start", "last_known_value", "startup_value"]

SET_OUTPUT_SCHEMA = cv.make_entity_service_schema(
{
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Optional(ATTR_VALUE): vol.Coerce(float),
vol.Optional(ATTR_PRESET): vol.In(PRESET_OPTIONS),
}
)


@dataclass
class MyData:
Expand Down Expand Up @@ -68,6 +84,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
CONF_SENSOR_ENTITY_ID, entry.data.get(CONF_SENSOR_ENTITY_ID)
)
self.last_contributions = (None, None, None) # (P, I, D)
self.last_known_output = None

def _get_entity_id(self, platform: str, key: str) -> str | None:
"""Lookup the real entity_id in the registry by unique_id == '<entry_id>_<key>'."""
Expand Down Expand Up @@ -145,6 +162,70 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
handle = PIDDeviceHandle(hass, entry)
entry.runtime_data = MyData(handle=handle)

if not hass.services.has_service(DOMAIN, SERVICE_SET_OUTPUT):

async def async_set_output(call: ServiceCall) -> None:
entity_id: str | list[str] | None = call.data.get(ATTR_ENTITY_ID)
if entity_id is None:
raise HomeAssistantError("entity_id is required")
if isinstance(entity_id, list):
if len(entity_id) != 1:
raise HomeAssistantError("Exactly one entity_id is required")
entity_id = entity_id[0]
preset: str | None = call.data.get(ATTR_PRESET)
value: float | None = call.data.get(ATTR_VALUE)

registry = er.async_get(hass)
ent = registry.async_get(entity_id)
if ent is None:
raise HomeAssistantError(f"Unknown entity {entity_id}")
config_entry = hass.config_entries.async_get_entry(ent.config_entry_id)
if config_entry is None or config_entry.runtime_data is None:
raise HomeAssistantError("PID controller not loaded")
dev_handle: PIDDeviceHandle = config_entry.runtime_data.handle
out_min = dev_handle.get_number("output_min") or 0.0
out_max = dev_handle.get_number("output_max") or 0.0

if (preset is None and value is None) or (
preset is not None and value is not None
):
raise HomeAssistantError("Either preset or value required")

if preset is not None:
if preset == "zero_start":
target = 0.0
elif preset == "last_known_value":
target = dev_handle.last_known_output or 0.0
elif preset == "startup_value":
target = dev_handle.get_number("starting_output") or 0.0
else:
raise HomeAssistantError("Invalid preset")
else:
target = value
if target is None:
raise HomeAssistantError("Value required")
if target < out_min or target > out_max:
raise HomeAssistantError(
f"Value {target} out of range {out_min}-{out_max}"
)

dev_handle.last_known_output = target
coordinator: PIDDataCoordinator = config_entry.runtime_data.coordinator
if dev_handle.pid.auto_mode:
dev_handle.pid.set_auto_mode(False)
dev_handle.pid.set_auto_mode(True, target)
coordinator.async_set_updated_data(target)
await coordinator.async_request_refresh()
else:
coordinator.async_set_updated_data(target)
# Update the internal PID output when in manual mode so that
# future calls to the controller return the newly set target.
dev_handle.pid._last_output = target

hass.services.async_register(
DOMAIN, SERVICE_SET_OUTPUT, async_set_output, schema=SET_OUTPUT_SCHEMA
)

# register updatelistener for optionsflow
entry.async_on_unload(entry.add_update_listener(_async_update_options_listener))

Expand All @@ -157,6 +238,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
# reset runtime_data zodat tests slagen
entry.runtime_data = None
if not hass.config_entries.async_entries(DOMAIN):
hass.services.async_remove(DOMAIN, SERVICE_SET_OUTPUT)
return unload_ok


Expand Down
67 changes: 33 additions & 34 deletions custom_components/simple_pid_controller/entity.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity, DeviceInfo

from .const import DOMAIN
from . import PIDDeviceHandle


class BasePIDEntity(Entity):
"""Base entity for Simple PID Controller integration."""

def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
key: str,
name: str,
) -> None:
"""Initialize the base PID entity."""
self.hass = hass
self._entry = entry
self._handle = PIDDeviceHandle(hass, entry)
self._key = key

# Common entity attributes
self._attr_name = f"{name}"
self._attr_has_entity_name = True
self._attr_unique_id = f"{entry.entry_id}_{key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name=self._handle.name,
)
from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity, DeviceInfo

from .const import DOMAIN


class BasePIDEntity(Entity):
"""Base entity for Simple PID Controller integration."""

def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
key: str,
name: str,
) -> None:
"""Initialize the base PID entity."""
self.hass = hass
self._entry = entry
self._handle = entry.runtime_data.handle
self._key = key

# Common entity attributes
self._attr_name = f"{name}"
self._attr_has_entity_name = True
self._attr_unique_id = f"{entry.entry_id}_{key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name=self._handle.name,
)
Loading
Loading