Skip to content

Commit 6280bd2

Browse files
committed
Home Assistant 2026.3 compatibility
Quality of life improvements: - venv - python 3.14 - devcontainer node feature Resolves #118 Resolves #111
1 parent e0bd8c0 commit 6280bd2

11 files changed

Lines changed: 204 additions & 138 deletions

File tree

.devcontainer.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "TJPoorman/home_maintenance",
3-
"image": "mcr.microsoft.com/devcontainers/python:3.13",
3+
"image": "mcr.microsoft.com/devcontainers/python:3.14",
44
"postCreateCommand": "scripts/setup",
55
"forwardPorts": [
66
8123
@@ -29,7 +29,7 @@
2929
"files.trimTrailingWhitespace": true,
3030
"python.analysis.typeCheckingMode": "basic",
3131
"python.analysis.autoImportCompletions": true,
32-
"python.defaultInterpreterPath": "/usr/local/bin/python",
32+
"python.defaultInterpreterPath": "/workspaces/home_maintenance/.venv/bin/python",
3333
"[python]": {
3434
"editor.defaultFormatter": "charliermarsh.ruff"
3535
}
@@ -38,6 +38,9 @@
3838
},
3939
"remoteUser": "vscode",
4040
"features": {
41+
"ghcr.io/devcontainers/features/node:1": {
42+
"version": "20"
43+
},
4144
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
4245
"packages": [
4346
"ffmpeg",

custom_components/home_maintenance/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from homeassistant.config_entries import ConfigEntry
55
from homeassistant.helpers import config_validation as cv
66

7-
VERSION = "1.5.2"
7+
VERSION = "1.5.5"
88
NAME = "Home Maintenance"
99
MANUFACTURER = "@TJPoorman"
1010

custom_components/home_maintenance/panel/dist/main.js

Lines changed: 53 additions & 41 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { LitElement, css, html } from 'lit';
2+
import type { HomeAssistant } from "custom-card-helpers";
3+
import { property } from "lit/decorators.js";
4+
5+
export interface MenuItem {
6+
value: string;
7+
label: string;
8+
icon: string;
9+
}
10+
11+
/**
12+
* A simple menu component for task actions.
13+
* Borrowed from https://codeberg.org/dan-danache/ha-zigbee-map/src/branch/master/custom_components/zigbee_map/panel/src/zigbee-map-panel.js
14+
*/
15+
class HMTaskMenu extends LitElement {
16+
@property() hass?: HomeAssistant;
17+
@property() items: MenuItem[] = [];
18+
19+
constructor() {
20+
super();
21+
}
22+
23+
render() {
24+
return html`
25+
<ha-dropdown @wa-select=${this._handleMenuAction}>
26+
<ha-icon-button slot="trigger">
27+
<ha-icon icon="mdi:dots-vertical"></ha-icon>
28+
</ha-icon-button>
29+
${this.items.map(item => html`
30+
<ha-dropdown-item value="${item.value}">
31+
<span>${item.label}</span>
32+
<ha-icon slot="icon" icon="${item.icon}"></ha-icon>
33+
</ha-dropdown-item>
34+
`)}
35+
</ha-dropdown>
36+
`
37+
}
38+
39+
_handleMenuAction(event: CustomEvent) {
40+
const action = event.detail.item.value;
41+
this.dispatchEvent(new CustomEvent('menu-action', {
42+
detail: { action },
43+
bubbles: true,
44+
composed: true
45+
}));
46+
}
47+
48+
static get styles() {
49+
return css`
50+
ha-icon-button ha-icon {
51+
display: flex;
52+
}
53+
span {
54+
white-space: nowrap;
55+
padding-right: 1em;
56+
}
57+
`
58+
}
59+
}
60+
61+
if (!customElements.get('hm-task-menu')) customElements.define('hm-task-menu', HMTaskMenu)

custom_components/home_maintenance/panel/src/main.ts

Lines changed: 43 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import {
22
mdiCheckCircleOutline,
3-
mdiDelete,
4-
mdiPencil,
5-
mdiDotsHorizontal,
63
} from "@mdi/js";
74
import { LitElement, html, nothing } from "lit";
85
import { property, state, query } from "lit/decorators.js";
@@ -15,6 +12,7 @@ import { loadConfigDashboard } from "./helpers";
1512
import { commonStyle } from './styles'
1613
import { EntityRegistryEntry, IntegrationConfig, IntervalType, INTERVAL_TYPES, getIntervalTypeLabels, Label, Task, Tag } from './types';
1714
import { completeTask, getConfig, loadLabelRegistry, loadRegistryEntries, loadTags, loadTask, loadTasks, removeTask, saveTask, updateTask } from './data/websockets';
15+
import './components/hm-task-menu'
1816

1917
interface TaskFormData {
2018
title: string;
@@ -60,10 +58,6 @@ export class HomeMaintenancePanel extends LitElement {
6058
tag: "",
6159
};
6260

63-
// Shared overflow menu state
64-
@state() private _selectedTaskId: string | null = null;
65-
@query("#actions-menu") private _actionsMenu?: any;
66-
6761
private get _columns() {
6862
return {
6963
icon: {
@@ -172,12 +166,22 @@ export class HomeMaintenancePanel extends LitElement {
172166
hideable: false,
173167
type: "overflow-menu",
174168
template: (task: Task) => html`
175-
<ha-icon-button
176-
@click=${(e: Event) => this._handleShowMenu(task.id, e)}
177-
.label="Actions"
178-
title="Actions"
179-
.path=${mdiDotsHorizontal}
180-
></ha-icon-button>
169+
<hm-task-menu
170+
.hass=${this.hass}
171+
.items=${[
172+
{
173+
value: 'edit',
174+
label: localize('panel.cards.current.actions.edit', this.hass!.language),
175+
icon: 'mdi:pencil'
176+
},
177+
{
178+
value: 'delete',
179+
label: localize('panel.cards.current.actions.remove', this.hass!.language),
180+
icon: 'mdi:delete'
181+
}
182+
]}
183+
@menu-action=${(e: CustomEvent) => this._handleMenuAction(e, task.id)}
184+
></hm-task-menu>
181185
`,
182186
},
183187
}
@@ -427,7 +431,6 @@ export class HomeMaintenancePanel extends LitElement {
427431
</div>
428432
429433
${this.renderEditDialog()}
430-
${this.renderActionsMenu()}
431434
`;
432435
}
433436

@@ -448,6 +451,7 @@ export class HomeMaintenancePanel extends LitElement {
448451
header="${localize('panel.cards.new.sections.optional', this.hass.language)}"
449452
.opened=${this._advancedOpen}
450453
@opened-changed=${(e: CustomEvent) => (this._advancedOpen = e.detail.value)}
454+
class="extras-panel"
451455
>
452456
<ha-form
453457
.hass=${this.hass}
@@ -460,9 +464,9 @@ export class HomeMaintenancePanel extends LitElement {
460464
</ha-expansion-panel>
461465
462466
<div class="form-field">
463-
<mwc-button @click=${this._handleAddTaskClick}>
464-
${localize('panel.cards.new.actions.add_task', this.hass.language)}
465-
</mwc-button>
467+
<ha-button size="small" class="add-button"
468+
@click=${this._handleAddTaskClick}>${localize('panel.cards.new.actions.add_task', this.hass.language)}
469+
</ha-button>
466470
</div>
467471
`;
468472
}
@@ -500,9 +504,11 @@ export class HomeMaintenancePanel extends LitElement {
500504
<ha-dialog
501505
open
502506
heading="${localize('panel.dialog.edit_task.title', this.hass.language)}: ${this._editFormData.title}"
507+
prevent-scrim-close
503508
@closed=${this._handleDialogClosed}
504509
>
505510
<ha-form
511+
autofocus
506512
.hass=${this.hass}
507513
.schema=${this._editSchema}
508514
.computeLabel=${this._computeEditLabel.bind(this)}
@@ -511,45 +517,17 @@ export class HomeMaintenancePanel extends LitElement {
511517
@value-changed=${(e: CustomEvent) => this._handleEditFormValueChanged(e)}
512518
></ha-form>
513519
514-
<mwc-button slot="secondaryAction" @click=${() => (this._editingTaskId = null)}>
515-
${localize('panel.dialog.edit_task.actions.cancel', this.hass.language)}
516-
</mwc-button>
517-
<mwc-button slot="primaryAction" @click=${this._handleSaveEditClick}>
518-
${localize('panel.dialog.edit_task.actions.save', this.hass.language)}
519-
</mwc-button>
520+
<ha-dialog-footer slot="footer">
521+
<ha-button data-dialog="close" appearance="plain" slot="secondaryAction">
522+
${localize('panel.dialog.edit_task.actions.cancel', this.hass.language)}
523+
</ha-button>
524+
<ha-button slot="primaryAction" @click=${this._handleSaveEditClick}>
525+
${localize('panel.dialog.edit_task.actions.save', this.hass.language)}
526+
</ha-button>
520527
</ha-dialog>
521528
`;
522529
}
523530

524-
renderActionsMenu() {
525-
if (!this.hass) return html``;
526-
527-
return html`
528-
<ha-md-menu id="actions-menu" positioning="fixed">
529-
<ha-md-menu-item
530-
@click=${() => {
531-
if (this._selectedTaskId) {
532-
this._handleOpenEditDialogClick(this._selectedTaskId);
533-
}
534-
}}
535-
>
536-
<ha-svg-icon slot="start" path=${mdiPencil}></ha-svg-icon>
537-
${localize('panel.cards.current.actions.edit', this.hass!.language)}
538-
</ha-md-menu-item>
539-
<ha-md-menu-item
540-
@click=${() => {
541-
if (this._selectedTaskId) {
542-
this._handleRemoveTaskClick(this._selectedTaskId);
543-
}
544-
}}
545-
>
546-
<ha-svg-icon slot="start" path=${mdiDelete}></ha-svg-icon>
547-
${localize('panel.cards.current.actions.remove', this.hass!.language)}
548-
</ha-md-menu-item>
549-
</ha-md-menu>
550-
`;
551-
}
552-
553531
private async _handleAddTaskClick() {
554532
const { title, interval_value, interval_type, last_performed, tag, icon, label } = this._formData;
555533

@@ -661,9 +639,18 @@ export class HomeMaintenancePanel extends LitElement {
661639
}
662640

663641
private _handleDialogClosed(e: CustomEvent) {
664-
const action = e.detail?.action;
665-
if (action === "close" || action === "cancel") {
666-
this._editingTaskId = null;
642+
this._editingTaskId = null;
643+
}
644+
645+
private _handleMenuAction(e: CustomEvent, taskId: string) {
646+
const action = e.detail.action;
647+
switch (action) {
648+
case 'edit':
649+
this._handleOpenEditDialogClick(taskId);
650+
break;
651+
case 'delete':
652+
this._handleRemoveTaskClick(taskId);
653+
break;
667654
}
668655
}
669656

@@ -675,18 +662,7 @@ export class HomeMaintenancePanel extends LitElement {
675662
this._editFormData = { ...this._editFormData, ...ev.detail.value };
676663
}
677664

678-
private _handleShowMenu(taskId: string, ev: Event) {
679-
this._selectedTaskId = taskId;
680-
681-
if (!this._actionsMenu) {
682-
return;
683-
}
684-
685-
this._actionsMenu.anchorElement = ev.currentTarget as HTMLElement;
686-
this._actionsMenu.show();
687-
688-
ev.stopPropagation();
689-
} static styles = commonStyle;
665+
static styles = commonStyle;
690666
}
691667

692668
customElements.define("home-maintenance-panel", HomeMaintenancePanel);

custom_components/home_maintenance/panel/src/styles.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,14 @@ export const commonStyle = css`
7373
ha-textfield,
7474
ha-select,
7575
ha-icon-picker {
76+
text-align: right;
7677
min-width: 265px;
7778
}
7879
80+
.extras-panel{
81+
margin-bottom: 14px;
82+
}
83+
7984
.filler {
8085
flex-grow: 1;
8186
}

hacs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Home Maintenance",
3-
"homeassistant": "2025.5.0",
3+
"homeassistant": "2026.3.2",
44
"hacs": "2.0.5",
55
"render_readme": true,
66
"zip_release": true,

requirements.txt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
colorlog==6.9.0
2-
homeassistant==2025.5.0
3-
pip>=21.3.1
4-
ruff==0.11.13
1+
homeassistant==2026.3
2+
colorlog==6.10.1
3+
ruff==0.15.7

scripts/develop

100644100755
Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
#!/usr/bin/env bash
22

3-
set -e
4-
53
cd "$(dirname "$0")/.."
64

7-
# Create config dir if not present
8-
if [[ ! -d "${PWD}/config" ]]; then
9-
mkdir -p "${PWD}/config"
10-
hass --config "${PWD}/config" --script ensure_config
5+
CONFIG_DIR="${PWD}/config"
6+
CC_SRC="${PWD}/custom_components"
7+
CC_DST="${CONFIG_DIR}/custom_components"
8+
VENV_DIR="${PWD}/.venv"
9+
10+
if [[ -f "${VENV_DIR}/bin/activate" ]]; then
11+
source "${VENV_DIR}/bin/activate"
12+
else
13+
echo "ERROR: venv not found at ${VENV_DIR}"
14+
exit 1
15+
fi
16+
17+
if [[ ! -d "${CONFIG_DIR}" ]]; then
18+
mkdir -p "${CONFIG_DIR}"
19+
hass --config "${CONFIG_DIR}" --script ensure_config
20+
fi
21+
22+
if [[ -L "${CC_DST}" ]]; then
23+
echo "custom_components symlink already exists"
24+
elif [[ -d "${CC_DST}" ]]; then
25+
echo "WARNING: ${CC_DST} exists and is not a symlink. Skipping link."
26+
else
27+
ln -s "${CC_SRC}" "${CC_DST}"
28+
echo "Created symlink: ${CC_DST} -> ${CC_SRC}"
1129
fi
1230

13-
# Set the path to custom_components
14-
## This let's us have the structure we want <root>/custom_components/integration_blueprint
15-
## while at the same time have Home Assistant configuration inside <root>/config
16-
## without resulting to symlinks.
17-
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
31+
export PYTHONPATH="${PYTHONPATH}:${CC_SRC}"
1832

19-
# Start Home Assistant
20-
hass --config "${PWD}/config" --debug
33+
hass --config "${CONFIG_DIR}" --debug

scripts/lint

100644100755
File mode changed.

0 commit comments

Comments
 (0)