Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions .devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "TJPoorman/home_maintenance",
"image": "mcr.microsoft.com/devcontainers/python:3.13",
"image": "mcr.microsoft.com/devcontainers/python:3.14",
"postCreateCommand": "scripts/setup",
"forwardPorts": [
8123
Expand Down Expand Up @@ -29,7 +29,7 @@
"files.trimTrailingWhitespace": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true,
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.defaultInterpreterPath": "/workspaces/home_maintenance/.venv/bin/python",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
}
Expand All @@ -38,6 +38,9 @@
},
"remoteUser": "vscode",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"packages": [
"ffmpeg",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"
python-version: "3.14"
cache: "pip"

- name: Install requirements
Expand Down
2 changes: 1 addition & 1 deletion custom_components/home_maintenance/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers import config_validation as cv

VERSION = "1.5.2"
VERSION = "1.5.5"
NAME = "Home Maintenance"
MANUFACTURER = "@TJPoorman"

Expand Down
94 changes: 53 additions & 41 deletions custom_components/home_maintenance/panel/dist/main.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { LitElement, css, html } from 'lit';
import type { HomeAssistant } from "custom-card-helpers";
import { property } from "lit/decorators.js";

export interface MenuItem {
value: string;
label: string;
icon: string;
}

/**
* A simple menu component for task actions.
* Borrowed from https://codeberg.org/dan-danache/ha-zigbee-map/src/branch/master/custom_components/zigbee_map/panel/src/zigbee-map-panel.js
*/
class HMTaskMenu extends LitElement {
@property() hass?: HomeAssistant;
@property() items: MenuItem[] = [];

constructor() {
super();
}

render() {
return html`
<ha-dropdown @wa-select=${this._handleMenuAction}>
<ha-icon-button slot="trigger">
<ha-icon icon="mdi:dots-vertical"></ha-icon>
</ha-icon-button>
${this.items.map(item => html`
<ha-dropdown-item value="${item.value}">
<span>${item.label}</span>
<ha-icon slot="icon" icon="${item.icon}"></ha-icon>
</ha-dropdown-item>
`)}
</ha-dropdown>
`
}

_handleMenuAction(event: CustomEvent) {
const action = event.detail.item.value;
this.dispatchEvent(new CustomEvent('menu-action', {
detail: { action },
bubbles: true,
composed: true
}));
}

static get styles() {
return css`
ha-icon-button ha-icon {
display: flex;
}
span {
white-space: nowrap;
padding-right: 1em;
}
`
}
}

if (!customElements.get('hm-task-menu')) customElements.define('hm-task-menu', HMTaskMenu)
110 changes: 43 additions & 67 deletions custom_components/home_maintenance/panel/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import {
mdiCheckCircleOutline,
mdiDelete,
mdiPencil,
mdiDotsHorizontal,
} from "@mdi/js";
import { LitElement, html, nothing } from "lit";
import { property, state, query } from "lit/decorators.js";
Expand All @@ -15,6 +12,7 @@ import { loadConfigDashboard } from "./helpers";
import { commonStyle } from './styles'
import { EntityRegistryEntry, IntegrationConfig, IntervalType, INTERVAL_TYPES, getIntervalTypeLabels, Label, Task, Tag } from './types';
import { completeTask, getConfig, loadLabelRegistry, loadRegistryEntries, loadTags, loadTask, loadTasks, removeTask, saveTask, updateTask } from './data/websockets';
import './components/hm-task-menu'

interface TaskFormData {
title: string;
Expand Down Expand Up @@ -60,10 +58,6 @@ export class HomeMaintenancePanel extends LitElement {
tag: "",
};

// Shared overflow menu state
@state() private _selectedTaskId: string | null = null;
@query("#actions-menu") private _actionsMenu?: any;

private get _columns() {
return {
icon: {
Expand Down Expand Up @@ -172,12 +166,22 @@ export class HomeMaintenancePanel extends LitElement {
hideable: false,
type: "overflow-menu",
template: (task: Task) => html`
<ha-icon-button
@click=${(e: Event) => this._handleShowMenu(task.id, e)}
.label="Actions"
title="Actions"
.path=${mdiDotsHorizontal}
></ha-icon-button>
<hm-task-menu
.hass=${this.hass}
.items=${[
{
value: 'edit',
label: localize('panel.cards.current.actions.edit', this.hass!.language),
icon: 'mdi:pencil'
},
{
value: 'delete',
label: localize('panel.cards.current.actions.remove', this.hass!.language),
icon: 'mdi:delete'
}
]}
@menu-action=${(e: CustomEvent) => this._handleMenuAction(e, task.id)}
></hm-task-menu>
`,
},
}
Expand Down Expand Up @@ -427,7 +431,6 @@ export class HomeMaintenancePanel extends LitElement {
</div>

${this.renderEditDialog()}
${this.renderActionsMenu()}
`;
}

Expand All @@ -448,6 +451,7 @@ export class HomeMaintenancePanel extends LitElement {
header="${localize('panel.cards.new.sections.optional', this.hass.language)}"
.opened=${this._advancedOpen}
@opened-changed=${(e: CustomEvent) => (this._advancedOpen = e.detail.value)}
class="extras-panel"
>
<ha-form
.hass=${this.hass}
Expand All @@ -460,9 +464,9 @@ export class HomeMaintenancePanel extends LitElement {
</ha-expansion-panel>

<div class="form-field">
<mwc-button @click=${this._handleAddTaskClick}>
${localize('panel.cards.new.actions.add_task', this.hass.language)}
</mwc-button>
<ha-button size="small" class="add-button"
@click=${this._handleAddTaskClick}>${localize('panel.cards.new.actions.add_task', this.hass.language)}
</ha-button>
</div>
`;
}
Expand Down Expand Up @@ -500,9 +504,11 @@ export class HomeMaintenancePanel extends LitElement {
<ha-dialog
open
heading="${localize('panel.dialog.edit_task.title', this.hass.language)}: ${this._editFormData.title}"
prevent-scrim-close
@closed=${this._handleDialogClosed}
>
<ha-form
autofocus
.hass=${this.hass}
.schema=${this._editSchema}
.computeLabel=${this._computeEditLabel.bind(this)}
Expand All @@ -511,45 +517,17 @@ export class HomeMaintenancePanel extends LitElement {
@value-changed=${(e: CustomEvent) => this._handleEditFormValueChanged(e)}
></ha-form>

<mwc-button slot="secondaryAction" @click=${() => (this._editingTaskId = null)}>
${localize('panel.dialog.edit_task.actions.cancel', this.hass.language)}
</mwc-button>
<mwc-button slot="primaryAction" @click=${this._handleSaveEditClick}>
${localize('panel.dialog.edit_task.actions.save', this.hass.language)}
</mwc-button>
<ha-dialog-footer slot="footer">
<ha-button data-dialog="close" appearance="plain" slot="secondaryAction">
${localize('panel.dialog.edit_task.actions.cancel', this.hass.language)}
</ha-button>
<ha-button slot="primaryAction" @click=${this._handleSaveEditClick}>
${localize('panel.dialog.edit_task.actions.save', this.hass.language)}
</ha-button>
</ha-dialog>
`;
}

renderActionsMenu() {
if (!this.hass) return html``;

return html`
<ha-md-menu id="actions-menu" positioning="fixed">
<ha-md-menu-item
@click=${() => {
if (this._selectedTaskId) {
this._handleOpenEditDialogClick(this._selectedTaskId);
}
}}
>
<ha-svg-icon slot="start" path=${mdiPencil}></ha-svg-icon>
${localize('panel.cards.current.actions.edit', this.hass!.language)}
</ha-md-menu-item>
<ha-md-menu-item
@click=${() => {
if (this._selectedTaskId) {
this._handleRemoveTaskClick(this._selectedTaskId);
}
}}
>
<ha-svg-icon slot="start" path=${mdiDelete}></ha-svg-icon>
${localize('panel.cards.current.actions.remove', this.hass!.language)}
</ha-md-menu-item>
</ha-md-menu>
`;
}

private async _handleAddTaskClick() {
const { title, interval_value, interval_type, last_performed, tag, icon, label } = this._formData;

Expand Down Expand Up @@ -661,9 +639,18 @@ export class HomeMaintenancePanel extends LitElement {
}

private _handleDialogClosed(e: CustomEvent) {
const action = e.detail?.action;
if (action === "close" || action === "cancel") {
this._editingTaskId = null;
this._editingTaskId = null;
}

private _handleMenuAction(e: CustomEvent, taskId: string) {
const action = e.detail.action;
switch (action) {
case 'edit':
this._handleOpenEditDialogClick(taskId);
break;
case 'delete':
this._handleRemoveTaskClick(taskId);
break;
}
}

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

private _handleShowMenu(taskId: string, ev: Event) {
this._selectedTaskId = taskId;

if (!this._actionsMenu) {
return;
}

this._actionsMenu.anchorElement = ev.currentTarget as HTMLElement;
this._actionsMenu.show();

ev.stopPropagation();
} static styles = commonStyle;
static styles = commonStyle;
}

customElements.define("home-maintenance-panel", HomeMaintenancePanel);
5 changes: 5 additions & 0 deletions custom_components/home_maintenance/panel/src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,14 @@ export const commonStyle = css`
ha-textfield,
ha-select,
ha-icon-picker {
text-align: right;
min-width: 265px;
}

.extras-panel{
margin-bottom: 14px;
}

.filler {
flex-grow: 1;
}
Expand Down
4 changes: 2 additions & 2 deletions custom_components/home_maintenance/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ def update_task(self, task_id: str, updated: dict) -> None:

if "tag_id" in updated:
tag_id = updated["tag_id"]
task.tag_id = tag_id if tag_id else None
entity.task["tag_id"] = tag_id if tag_id else None
task.tag_id = tag_id or None
entity.task["tag_id"] = tag_id or None

if "labels" in updated:
registry = entity_registry.async_get(self.hass)
Expand Down
2 changes: 1 addition & 1 deletion hacs.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Home Maintenance",
"homeassistant": "2025.5.0",
"homeassistant": "2026.3.2",
"hacs": "2.0.5",
"render_readme": true,
"zip_release": true,
Expand Down
7 changes: 3 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
colorlog==6.9.0
homeassistant==2025.5.0
pip>=21.3.1
ruff==0.11.13
homeassistant==2026.3
colorlog==6.10.1
ruff==0.15.7
39 changes: 26 additions & 13 deletions scripts/develop
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
#!/usr/bin/env bash

set -e

cd "$(dirname "$0")/.."

# Create config dir if not present
if [[ ! -d "${PWD}/config" ]]; then
mkdir -p "${PWD}/config"
hass --config "${PWD}/config" --script ensure_config
CONFIG_DIR="${PWD}/config"
CC_SRC="${PWD}/custom_components"
CC_DST="${CONFIG_DIR}/custom_components"
VENV_DIR="${PWD}/.venv"

if [[ -f "${VENV_DIR}/bin/activate" ]]; then
source "${VENV_DIR}/bin/activate"
else
echo "ERROR: venv not found at ${VENV_DIR}"
exit 1
fi

if [[ ! -d "${CONFIG_DIR}" ]]; then
mkdir -p "${CONFIG_DIR}"
hass --config "${CONFIG_DIR}" --script ensure_config
fi

if [[ -L "${CC_DST}" ]]; then
echo "custom_components symlink already exists"
elif [[ -d "${CC_DST}" ]]; then
echo "WARNING: ${CC_DST} exists and is not a symlink. Skipping link."
else
ln -s "${CC_SRC}" "${CC_DST}"
echo "Created symlink: ${CC_DST} -> ${CC_SRC}"
fi

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

# Start Home Assistant
hass --config "${PWD}/config" --debug
hass --config "${CONFIG_DIR}" --debug
Empty file modified scripts/lint
100644 → 100755
Empty file.
Loading
Loading