App Updates
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+52
@@ -0,0 +1,52 @@
|
||||
/** #106 follow-up: reproduce with the EXACT _build_task_summary shape. */
|
||||
import { expect, fixture, html } from "@open-wc/testing";
|
||||
import "../components/task-dialog.js";
|
||||
import type { MaintenanceTaskDialog } from "../components/task-dialog";
|
||||
import { createMockHass } from "./_test-utils.js";
|
||||
|
||||
it("fleet-shaped task (nested manual schedule + plural-only threshold) keeps its trigger", async () => {
|
||||
const { hass, sent } = createMockHass({
|
||||
states: { "sensor.maintenance_supporter_batteries_to_replace": { state: "8" } },
|
||||
handlers: { "maintenance_supporter/task/update": () => ({ success: true }) },
|
||||
});
|
||||
const el = await fixture<MaintenanceTaskDialog>(html`
|
||||
<maintenance-task-dialog .hass=${hass}></maintenance-task-dialog>
|
||||
`);
|
||||
await el.updateComplete;
|
||||
await el.openEdit("entry_x", {
|
||||
id: "t1",
|
||||
name: "Replace low batteries",
|
||||
type: "inspection",
|
||||
enabled: true,
|
||||
schedule_type: "sensor_based",
|
||||
interval_days: null,
|
||||
interval_unit: "days",
|
||||
due_date: null,
|
||||
interval_anchor: "completion",
|
||||
schedule: { kind: "manual" },
|
||||
warning_days: 7,
|
||||
battery_fleet_task: true,
|
||||
trigger_config: {
|
||||
type: "threshold",
|
||||
entity_ids: ["sensor.maintenance_supporter_batteries_to_replace"],
|
||||
trigger_above: 0,
|
||||
entity_logic: "any",
|
||||
auto_complete_on_recovery: true,
|
||||
},
|
||||
trigger_entity_info: {
|
||||
entity_id: "sensor.maintenance_supporter_batteries_to_replace",
|
||||
friendly_name: "Batteries to replace",
|
||||
unit_of_measurement: null,
|
||||
},
|
||||
} as any);
|
||||
await el.updateComplete;
|
||||
// the user's edit: rename + warning days
|
||||
(el as any)._name = "Remplacer les piles";
|
||||
(el as any)._warningDays = "1";
|
||||
await (el as any)._save();
|
||||
const update = sent.find((m: any) => m.type === "maintenance_supporter/task/update") as any;
|
||||
expect(update, "update sent").to.exist;
|
||||
expect(update.trigger_config, "trigger_config must survive").to.exist;
|
||||
expect(update.trigger_config.type).to.equal("threshold");
|
||||
expect(update.trigger_config.trigger_above).to.equal(0);
|
||||
});
|
||||
+1
@@ -84,6 +84,7 @@ export class MaintenanceBatteryFleetSection extends LitElement {
|
||||
try {
|
||||
await this.hass.connection.sendMessagePromise({
|
||||
type: "maintenance_supporter/battery_fleet/setup",
|
||||
language: this._lang,
|
||||
});
|
||||
await this._load();
|
||||
} catch (e) {
|
||||
|
||||
@@ -1045,6 +1045,7 @@ export class MaintenanceSupporterPanel extends LitElement {
|
||||
try {
|
||||
const res = await this.hass.connection.sendMessagePromise<{ entry_id: string; task_id?: string }>({
|
||||
type: "maintenance_supporter/battery_fleet/setup",
|
||||
language: this.hass.language || "en",
|
||||
});
|
||||
this._batteryFleetSetupAvailable = false;
|
||||
await this._loadData();
|
||||
|
||||
@@ -1235,6 +1235,10 @@ export const panelStyles = css`
|
||||
:host([narrow]) .task-header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
/* Longer labels (de "Überspringen", fr "Archiver", …) overflow a phone
|
||||
viewport in a nowrap row — the ⋮ menu then needs a horizontal scroll
|
||||
to reach. Wrap instead; language-independent. */
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
:host([narrow]) .filter-bar {
|
||||
@@ -1433,7 +1437,7 @@ export const panelStyles = css`
|
||||
.two-column-layout { grid-template-columns: 1fr; }
|
||||
.tab { padding: 12px 16px; font-size: 14px; }
|
||||
.task-header { flex-direction: column; align-items: flex-start; }
|
||||
.task-header-actions { width: 100%; justify-content: flex-start; }
|
||||
.task-header-actions { width: 100%; justify-content: flex-start; flex-wrap: wrap; }
|
||||
.filter-bar { flex-wrap: wrap; }
|
||||
.filter-bar select { flex: 1; min-width: 0; }
|
||||
/* Mirror the :host([narrow]) grid layout for narrow desktop windows */
|
||||
|
||||
Binary file not shown.
@@ -2528,6 +2528,10 @@ ${H(n.notes)}</div>`:""}
|
||||
:host([narrow]) .task-header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
/* Longer labels (de "Überspringen", fr "Archiver", …) overflow a phone
|
||||
viewport in a nowrap row — the ⋮ menu then needs a horizontal scroll
|
||||
to reach. Wrap instead; language-independent. */
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
:host([narrow]) .filter-bar {
|
||||
@@ -2726,7 +2730,7 @@ ${H(n.notes)}</div>`:""}
|
||||
.two-column-layout { grid-template-columns: 1fr; }
|
||||
.tab { padding: 12px 16px; font-size: 14px; }
|
||||
.task-header { flex-direction: column; align-items: flex-start; }
|
||||
.task-header-actions { width: 100%; justify-content: flex-start; }
|
||||
.task-header-actions { width: 100%; justify-content: flex-start; flex-wrap: wrap; }
|
||||
.filter-bar { flex-wrap: wrap; }
|
||||
.filter-bar select { flex: 1; min-width: 0; }
|
||||
/* Mirror the :host([narrow]) grid layout for narrow desktop windows */
|
||||
@@ -5354,7 +5358,7 @@ ${p?`<div class="sub">${p}</div>`:""}
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 8px; }
|
||||
`,d([y({attribute:!1})],X.prototype,"hass",2),d([_()],X.prototype,"_open",2),d([_()],X.prototype,"_loading",2),d([_()],X.prototype,"_adopting",2),d([_()],X.prototype,"_error",2),d([_()],X.prototype,"_setups",2),d([_()],X.prototype,"_selected",2),d([_()],X.prototype,"_baselines",2),d([_()],X.prototype,"_targets",2),d([_()],X.prototype,"_objects",2);customElements.get("maintenance-suggested-setups-dialog")||customElements.define("maintenance-suggested-setups-dialog",X);var ve=class extends S{constructor(){super(...arguments);this._ov=null;this._loading=!1;this._marking=!1;this._error="";this._localeReady=!1;this._markAll=async()=>{await this._mark(void 0)};this._repair=async()=>{if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/setup"}),await this._load()}catch(e){this._error=j(e,this._lang)}finally{this._marking=!1}}}}get _lang(){return this.hass?.language||"en"}connectedCallback(){super.connectedCallback(),this.hass&&this._load()}updated(e){e.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,V(this._lang).then(()=>this.requestUpdate()),this._ov===null&&!this._loading&&this._load())}async _load(){this._loading=!0,this._error="";try{this._ov=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/overview"})}catch(e){this._error=j(e,this._lang)}finally{this._loading=!1}}async _mark(e){if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/mark_replaced",...e?{entity_ids:e}:{}}),await this._load()}catch(t){this._error=j(t,this._lang)}finally{this._marking=!1}}}_shoppingLine(e){return Object.entries(e).map(([t,i])=>`${i}\xD7 ${t}`).join(" \xB7 ")}render(){let e=this._lang;if(this._loading&&this._ov===null)return o`<div class="bf-card"><div class="bf-loading">…</div></div>`;let t=this._ov;if(!t)return this._error?o`<div class="bf-card"><div class="bf-error">${this._error}</div></div>`:h;let i=t.low.length;return o`
|
||||
`,d([y({attribute:!1})],X.prototype,"hass",2),d([_()],X.prototype,"_open",2),d([_()],X.prototype,"_loading",2),d([_()],X.prototype,"_adopting",2),d([_()],X.prototype,"_error",2),d([_()],X.prototype,"_setups",2),d([_()],X.prototype,"_selected",2),d([_()],X.prototype,"_baselines",2),d([_()],X.prototype,"_targets",2),d([_()],X.prototype,"_objects",2);customElements.get("maintenance-suggested-setups-dialog")||customElements.define("maintenance-suggested-setups-dialog",X);var ve=class extends S{constructor(){super(...arguments);this._ov=null;this._loading=!1;this._marking=!1;this._error="";this._localeReady=!1;this._markAll=async()=>{await this._mark(void 0)};this._repair=async()=>{if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/setup",language:this._lang}),await this._load()}catch(e){this._error=j(e,this._lang)}finally{this._marking=!1}}}}get _lang(){return this.hass?.language||"en"}connectedCallback(){super.connectedCallback(),this.hass&&this._load()}updated(e){e.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,V(this._lang).then(()=>this.requestUpdate()),this._ov===null&&!this._loading&&this._load())}async _load(){this._loading=!0,this._error="";try{this._ov=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/overview"})}catch(e){this._error=j(e,this._lang)}finally{this._loading=!1}}async _mark(e){if(!this._marking){this._marking=!0,this._error="";try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/mark_replaced",...e?{entity_ids:e}:{}}),await this._load()}catch(t){this._error=j(t,this._lang)}finally{this._marking=!1}}}_shoppingLine(e){return Object.entries(e).map(([t,i])=>`${i}\xD7 ${t}`).join(" \xB7 ")}render(){let e=this._lang;if(this._loading&&this._ov===null)return o`<div class="bf-card"><div class="bf-loading">…</div></div>`;let t=this._ov;if(!t)return this._error?o`<div class="bf-card"><div class="bf-error">${this._error}</div></div>`:h;let i=t.low.length;return o`
|
||||
<div class="bf-card">
|
||||
<div class="bf-head">
|
||||
<ha-icon icon="mdi:battery-alert"></ha-icon>
|
||||
@@ -8389,7 +8393,7 @@ ${p?`<div class="sub">${p}</div>`:""}
|
||||
<div class="palette-hint">${s("palette_hint",e)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`}_openAdoptProblemSensors(){this.shadowRoot.querySelector("maintenance-adopt-problem-sensors-dialog")?.open()}async _onProblemSensorsAdopted(e){let t=e.detail?.tasks_created??0,i=e.detail?.created??[];await this._loadData();let a=s("adopt_problem_done",this._lang).replace("{tasks}",String(t));i.length>0?this._showActionToast(a,s("adopt_problem_configure",this._lang),()=>{let l=i[0],c=this._objects.find(u=>u.entry_id===l.entry_id),p=c?.tasks.find(u=>u.id===l.task_id);c&&p&&this.shadowRoot.querySelector("maintenance-task-dialog")?.openEdit(l.entry_id,p)}):this._showToast(a)}async _setupBatteryFleet(){try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/setup"});this._batteryFleetSetupAvailable=!1,await this._loadData();let t=this._objects.find(a=>a.entry_id===e.entry_id),i=t?.tasks.find(a=>a.id===e.task_id)||t?.tasks[0];t&&i&&this._showTask(t.entry_id,i.id),this._showToast(s("battery_fleet_setup_done",this._lang))}catch(e){this._showToast(j(e,this._lang))}}_openSuggestedSetups(){this.shadowRoot.querySelector("maintenance-suggested-setups-dialog")?.open()}_onSetupsAdopted(e){let t=e.detail?.tasks_created??0;this._showToast(s("setups_done",this._lang).replace("{tasks}",String(t))),this._loadData()}async _openTemplateGallery(){if(this._templateGalleryOpen=!0,!(this._templates.length>0))try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/templates",language:this._lang});this._templateCategories=e.categories||{},this._templates=(e.templates||[]).filter(t=>!t.disabled)}catch{this._showToast(s("action_error",this._lang))}}async _createFromTemplate(e){this._templateBusy=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/from_template",language:this._lang,template_id:e});this._templateGalleryOpen=!1,await this._loadData(),this._showToast(s("template_created",this._lang)),t?.entry_id&&this._showObject(t.entry_id)}catch{this._showToast(s("action_error",this._lang))}finally{this._templateBusy=!1}}_categoryName(e){let t=this._templateCategories[e];return t&&(t[`name_${this._lang}`]||t.name_en)||e}_renderTemplateGallery(){if(!this._templateGalleryOpen)return h;let e=this._lang,t=new Map;for(let i of this._templates)t.has(i.category)||t.set(i.category,[]),t.get(i.category).push(i);return o`
|
||||
`}_openAdoptProblemSensors(){this.shadowRoot.querySelector("maintenance-adopt-problem-sensors-dialog")?.open()}async _onProblemSensorsAdopted(e){let t=e.detail?.tasks_created??0,i=e.detail?.created??[];await this._loadData();let a=s("adopt_problem_done",this._lang).replace("{tasks}",String(t));i.length>0?this._showActionToast(a,s("adopt_problem_configure",this._lang),()=>{let l=i[0],c=this._objects.find(u=>u.entry_id===l.entry_id),p=c?.tasks.find(u=>u.id===l.task_id);c&&p&&this.shadowRoot.querySelector("maintenance-task-dialog")?.openEdit(l.entry_id,p)}):this._showToast(a)}async _setupBatteryFleet(){try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/battery_fleet/setup",language:this.hass.language||"en"});this._batteryFleetSetupAvailable=!1,await this._loadData();let t=this._objects.find(a=>a.entry_id===e.entry_id),i=t?.tasks.find(a=>a.id===e.task_id)||t?.tasks[0];t&&i&&this._showTask(t.entry_id,i.id),this._showToast(s("battery_fleet_setup_done",this._lang))}catch(e){this._showToast(j(e,this._lang))}}_openSuggestedSetups(){this.shadowRoot.querySelector("maintenance-suggested-setups-dialog")?.open()}_onSetupsAdopted(e){let t=e.detail?.tasks_created??0;this._showToast(s("setups_done",this._lang).replace("{tasks}",String(t))),this._loadData()}async _openTemplateGallery(){if(this._templateGalleryOpen=!0,!(this._templates.length>0))try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/templates",language:this._lang});this._templateCategories=e.categories||{},this._templates=(e.templates||[]).filter(t=>!t.disabled)}catch{this._showToast(s("action_error",this._lang))}}async _createFromTemplate(e){this._templateBusy=!0;try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/from_template",language:this._lang,template_id:e});this._templateGalleryOpen=!1,await this._loadData(),this._showToast(s("template_created",this._lang)),t?.entry_id&&this._showObject(t.entry_id)}catch{this._showToast(s("action_error",this._lang))}finally{this._templateBusy=!1}}_categoryName(e){let t=this._templateCategories[e];return t&&(t[`name_${this._lang}`]||t.name_en)||e}_renderTemplateGallery(){if(!this._templateGalleryOpen)return h;let e=this._lang,t=new Map;for(let i of this._templates)t.has(i.category)||t.set(i.category,[]),t.get(i.category).push(i);return o`
|
||||
<div class="palette-backdrop" @click=${()=>{this._templateGalleryOpen=!1}}>
|
||||
<div class="template-gallery" @click=${i=>i.stopPropagation()}>
|
||||
<div class="template-gallery-head">
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -38,22 +38,27 @@ def find_fleet_entry(hass: HomeAssistant) -> ConfigEntry | None:
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup_battery_fleet(hass: HomeAssistant) -> dict[str, Any]:
|
||||
async def async_setup_battery_fleet(hass: HomeAssistant, language: str | None = None) -> dict[str, Any]:
|
||||
"""Create (or return) the Battery Fleet object with type-parts + the task.
|
||||
|
||||
Idempotent: a second call reconciles the type-parts against the current
|
||||
fleet (adds parts for newly-seen types) and returns the existing entry.
|
||||
``language`` is the caller's UI language (same contract as the template
|
||||
WS): the created object/task/part names and notes are localized through
|
||||
the ``_T`` table, falling back to the server language, then English.
|
||||
"""
|
||||
from ..websocket.objects import async_create_object
|
||||
from ..websocket.tasks_persist import async_persist_task
|
||||
from .i18n import normalize_language
|
||||
from .parts import normalize_part
|
||||
|
||||
lang = (language or normalize_language(hass))[:2].lower()
|
||||
types = discover_battery_types(hass) # {TYPE: total_qty}
|
||||
|
||||
existing = find_fleet_entry(hass)
|
||||
if existing is not None:
|
||||
added = _reconcile_type_parts(hass, existing, types)
|
||||
repaired = await _reconcile_fleet_task(hass, existing)
|
||||
added = _reconcile_type_parts(hass, existing, types, lang)
|
||||
repaired = await _reconcile_fleet_task(hass, existing, lang)
|
||||
return {
|
||||
"entry_id": existing.entry_id,
|
||||
"created": False,
|
||||
@@ -62,7 +67,9 @@ async def async_setup_battery_fleet(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"task_repaired": repaired,
|
||||
}
|
||||
|
||||
entry_id = await async_create_object(hass, name="Battery Fleet")
|
||||
from ..templates import localize_template_text
|
||||
|
||||
entry_id = await async_create_object(hass, name=localize_template_text("Battery Fleet", lang) or "Battery Fleet")
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if entry is None: # pragma: no cover — just created above
|
||||
raise HomeAssistantError("Battery Fleet object entry vanished after creation")
|
||||
@@ -74,7 +81,7 @@ async def async_setup_battery_fleet(hass: HomeAssistant) -> dict[str, Any]:
|
||||
new_data[CONF_OBJECT] = obj
|
||||
parts: dict[str, dict[str, Any]] = {}
|
||||
for btype, total_qty in types.items():
|
||||
part = normalize_part(_type_part(btype, total_qty))
|
||||
part = normalize_part(_type_part(btype, total_qty, lang))
|
||||
parts[part["id"]] = part
|
||||
new_data[CONF_PARTS] = parts
|
||||
hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
@@ -88,19 +95,7 @@ async def async_setup_battery_fleet(hass: HomeAssistant) -> dict[str, Any]:
|
||||
await store.async_save()
|
||||
|
||||
# The single aggregate task, triggered by the global low-count sensor.
|
||||
obj_id = obj.get("id", "")
|
||||
task = {
|
||||
"id": uuid4().hex,
|
||||
"object_id": obj_id,
|
||||
"name": "Replace low batteries",
|
||||
"type": "inspection",
|
||||
"enabled": True,
|
||||
TASK_FLAG: True,
|
||||
"schedule": {"kind": "manual"},
|
||||
"trigger_config": _fleet_trigger_config(),
|
||||
"created_at": dt_util.now().date().isoformat(),
|
||||
"notes": ("Aggregate battery check. The detail view lists which devices are low and which battery types to buy."),
|
||||
}
|
||||
task = _fleet_task(obj.get("id", ""), lang)
|
||||
await async_persist_task(hass, entry, task)
|
||||
|
||||
return {
|
||||
@@ -130,23 +125,48 @@ def _fleet_trigger_config() -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _type_part(btype: str, total_qty: int) -> dict[str, Any]:
|
||||
"""A spare-part definition for one battery type.
|
||||
def _fleet_task(obj_id: str, lang: str) -> dict[str, Any]:
|
||||
"""The single aggregate fleet task, with localized name + notes."""
|
||||
from ..templates import localize_template_text
|
||||
|
||||
return {
|
||||
"id": uuid4().hex,
|
||||
"object_id": obj_id,
|
||||
"name": localize_template_text("Replace low batteries", lang) or "Replace low batteries",
|
||||
"type": "inspection",
|
||||
"enabled": True,
|
||||
TASK_FLAG: True,
|
||||
"schedule": {"kind": "manual"},
|
||||
"trigger_config": _fleet_trigger_config(),
|
||||
"created_at": dt_util.now().date().isoformat(),
|
||||
"notes": localize_template_text(
|
||||
"Aggregate battery check. The detail view lists which devices are low and which battery types to buy.",
|
||||
lang,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _type_part(btype: str, total_qty: int, lang: str) -> dict[str, Any]:
|
||||
"""A spare-part definition for one battery type (localized name/notes).
|
||||
|
||||
reorder_threshold defaults to keeping a spare set roughly the size of the
|
||||
fleet's need for that type (min 2); restock is double that. auto_buy_task
|
||||
stays off so setup never spawns extra buy-tasks — the fleet task's detail
|
||||
is the shopping surface; the user can enable auto-buy per type later.
|
||||
"""
|
||||
from ..templates import localize_template_text
|
||||
|
||||
threshold = max(2, total_qty)
|
||||
name_tpl = localize_template_text("{type} battery", lang) or "{type} battery"
|
||||
notes_tpl = localize_template_text("Typical service life ~{months} months.", lang) or "Typical service life ~{months} months."
|
||||
return {
|
||||
"id": f"batt_{btype.lower()}",
|
||||
"name": f"{btype} battery",
|
||||
"name": name_tpl.format(type=btype),
|
||||
"unit": "pcs",
|
||||
"reorder_threshold": threshold,
|
||||
"restock_quantity": threshold * 2,
|
||||
"auto_buy_task": False,
|
||||
"notes": f"Typical service life ~{lifetime_months(btype)} months (editorial).",
|
||||
"notes": notes_tpl.format(months=lifetime_months(btype)),
|
||||
}
|
||||
|
||||
|
||||
@@ -223,12 +243,12 @@ def fleet_task_trigger_ok(entry: ConfigEntry) -> bool:
|
||||
return tc.get("type") == "threshold" and LOW_COUNT_ENTITY_ID in eids
|
||||
|
||||
|
||||
async def _reconcile_fleet_task(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def _reconcile_fleet_task(hass: HomeAssistant, entry: ConfigEntry, lang: str) -> bool:
|
||||
"""Repair the fleet task if broken. Returns True when something was fixed.
|
||||
|
||||
* Trigger lost (issue #106) → restore the canonical threshold trigger,
|
||||
keeping the user's name/type/translations untouched.
|
||||
* Task deleted entirely → recreate it fresh.
|
||||
* Task deleted entirely → recreate it fresh (localized).
|
||||
"""
|
||||
from ..websocket.tasks_persist import async_persist_task
|
||||
|
||||
@@ -249,23 +269,11 @@ async def _reconcile_fleet_task(hass: HomeAssistant, entry: ConfigEntry) -> bool
|
||||
return True
|
||||
|
||||
obj = entry.data.get(CONF_OBJECT, {})
|
||||
task = {
|
||||
"id": uuid4().hex,
|
||||
"object_id": obj.get("id", ""),
|
||||
"name": "Replace low batteries",
|
||||
"type": "inspection",
|
||||
"enabled": True,
|
||||
TASK_FLAG: True,
|
||||
"schedule": {"kind": "manual"},
|
||||
"trigger_config": _fleet_trigger_config(),
|
||||
"created_at": dt_util.now().date().isoformat(),
|
||||
"notes": ("Aggregate battery check. The detail view lists which devices are low and which battery types to buy."),
|
||||
}
|
||||
await async_persist_task(hass, entry, task)
|
||||
await async_persist_task(hass, entry, _fleet_task(obj.get("id", ""), lang))
|
||||
return True
|
||||
|
||||
|
||||
def _reconcile_type_parts(hass: HomeAssistant, entry: ConfigEntry, types: dict[str, int]) -> int:
|
||||
def _reconcile_type_parts(hass: HomeAssistant, entry: ConfigEntry, types: dict[str, int], lang: str) -> int:
|
||||
"""Add parts for battery types newly seen since setup. Returns count added."""
|
||||
from .parts import normalize_part
|
||||
|
||||
@@ -275,7 +283,7 @@ def _reconcile_type_parts(hass: HomeAssistant, entry: ConfigEntry, types: dict[s
|
||||
for btype, total_qty in types.items():
|
||||
pid = f"batt_{btype.lower()}"
|
||||
if pid not in existing_ids:
|
||||
parts[pid] = normalize_part(_type_part(btype, total_qty))
|
||||
parts[pid] = normalize_part(_type_part(btype, total_qty, lang))
|
||||
added += 1
|
||||
if added:
|
||||
new_data = dict(entry.data)
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -22,5 +22,5 @@
|
||||
"requirements": [
|
||||
"pypdf>=4.3.0"
|
||||
],
|
||||
"version": "2.38.1"
|
||||
"version": "2.38.3"
|
||||
}
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -4329,6 +4329,105 @@ _T: dict[str, dict[str, str]] = {
|
||||
"hi": "CO2 बोतल बदलें",
|
||||
"zh": "更换CO2气瓶",
|
||||
},
|
||||
# ── Battery Fleet seeding (helpers/battery_fleet_setup.py) ──────────────
|
||||
# The object/task/part names + notes the one-click fleet setup creates.
|
||||
# Object name mirrors the panel's battery_fleet_title locale values so the
|
||||
# created object matches the detail section's heading.
|
||||
"Battery Fleet": {
|
||||
"de": "Batterie-Flotte",
|
||||
"es": "Flota de baterías",
|
||||
"fr": "Parc de piles",
|
||||
"it": "Parco batterie",
|
||||
"nl": "Batterijvloot",
|
||||
"pt": "Frota de baterias",
|
||||
"ru": "Парк батарей",
|
||||
"uk": "Парк батарей",
|
||||
"pl": "Flota baterii",
|
||||
"cs": "Flotila baterií",
|
||||
"sv": "Batteriflotta",
|
||||
"da": "Batteriflåde",
|
||||
"nb": "Batteriflåte",
|
||||
"fi": "Paristokanta",
|
||||
"ja": "電池フリート",
|
||||
"hi": "बैटरी फ्लीट",
|
||||
"zh": "电池群",
|
||||
},
|
||||
"Replace low batteries": {
|
||||
"de": "Schwache Batterien ersetzen",
|
||||
"es": "Reemplazar baterías bajas",
|
||||
"fr": "Remplacer les piles faibles",
|
||||
"it": "Sostituire le batterie scariche",
|
||||
"nl": "Zwakke batterijen vervangen",
|
||||
"pt": "Substituir baterias fracas",
|
||||
"ru": "Заменить разряженные батареи",
|
||||
"uk": "Замінити розряджені батареї",
|
||||
"pl": "Wymień słabe baterie",
|
||||
"cs": "Vyměnit slabé baterie",
|
||||
"sv": "Byt svaga batterier",
|
||||
"da": "Udskift svage batterier",
|
||||
"nb": "Bytt svake batterier",
|
||||
"fi": "Vaihda heikot paristot",
|
||||
"ja": "残量低下の電池を交換",
|
||||
"hi": "कम बैटरी बदलें",
|
||||
"zh": "更换低电量电池",
|
||||
},
|
||||
"Aggregate battery check. The detail view lists which devices are low and which battery types to buy.": {
|
||||
"de": "Gesammelte Batterie-Prüfung. Die Detailansicht zeigt, welche Geräte schwach sind und welche Batterietypen zu kaufen sind.",
|
||||
"es": "Comprobación agregada de baterías. La vista de detalle muestra qué dispositivos están bajos y qué tipos de batería comprar.",
|
||||
"fr": "Contrôle groupé des piles. La vue détaillée liste les appareils faibles et les types de piles à acheter.",
|
||||
"it": "Controllo aggregato delle batterie. La vista di dettaglio elenca i dispositivi scarichi e i tipi di batteria da acquistare.",
|
||||
"nl": "Gebundelde batterijcontrole. De detailweergave toont welke apparaten zwak zijn en welke batterijtypes te kopen zijn.",
|
||||
"pt": "Verificação agregada de baterias. A vista de detalhe lista os dispositivos fracos e os tipos de bateria a comprar.",
|
||||
"ru": "Сводная проверка батарей. В подробном виде показано, какие устройства разряжены и какие типы батарей купить.",
|
||||
"uk": "Зведена перевірка батарей. У детальному перегляді показано, які пристрої розряджені та які типи батарей купити.",
|
||||
"pl": "Zbiorcza kontrola baterii. Widok szczegółów pokazuje, które urządzenia są słabe i jakie typy baterii kupić.",
|
||||
"cs": "Souhrnná kontrola baterií. Detailní pohled ukazuje, která zařízení jsou slabá a jaké typy baterií koupit.",
|
||||
"sv": "Samlad batterikontroll. Detaljvyn visar vilka enheter som är svaga och vilka batterityper som ska köpas.",
|
||||
"da": "Samlet batteritjek. Detaljevisningen viser, hvilke enheder der er svage, og hvilke batterityper der skal købes.",
|
||||
"nb": "Samlet batterisjekk. Detaljvisningen viser hvilke enheter som er svake og hvilke batterityper som må kjøpes.",
|
||||
"fi": "Koottu paristotarkistus. Yksityiskohtanäkymä näyttää, mitkä laitteet ovat heikkoja ja mitä paristotyyppejä ostaa.",
|
||||
"ja": "電池のまとめて点検。詳細ビューには残量低下の機器と購入する電池の種類が表示されます。",
|
||||
"hi": "समग्र बैटरी जाँच। विवरण दृश्य दिखाता है कि कौन से उपकरण कम हैं और कौन से बैटरी प्रकार खरीदने हैं।",
|
||||
"zh": "电池汇总检查。详情视图列出哪些设备电量低以及需要购买哪些电池类型。",
|
||||
},
|
||||
"{type} battery": {
|
||||
"de": "{type}-Batterie",
|
||||
"es": "Batería {type}",
|
||||
"fr": "Pile {type}",
|
||||
"it": "Batteria {type}",
|
||||
"nl": "{type}-batterij",
|
||||
"pt": "Bateria {type}",
|
||||
"ru": "Батарейка {type}",
|
||||
"uk": "Батарейка {type}",
|
||||
"pl": "Bateria {type}",
|
||||
"cs": "Baterie {type}",
|
||||
"sv": "{type}-batteri",
|
||||
"da": "{type}-batteri",
|
||||
"nb": "{type}-batteri",
|
||||
"fi": "{type}-paristo",
|
||||
"ja": "{type}電池",
|
||||
"hi": "{type} बैटरी",
|
||||
"zh": "{type}电池",
|
||||
},
|
||||
"Typical service life ~{months} months.": {
|
||||
"de": "Typische Lebensdauer ~{months} Monate.",
|
||||
"es": "Vida útil típica ~{months} meses.",
|
||||
"fr": "Durée de vie typique ~{months} mois.",
|
||||
"it": "Durata tipica ~{months} mesi.",
|
||||
"nl": "Typische levensduur ~{months} maanden.",
|
||||
"pt": "Vida útil típica ~{months} meses.",
|
||||
"ru": "Типичный срок службы ~{months} мес.",
|
||||
"uk": "Типовий термін служби ~{months} міс.",
|
||||
"pl": "Typowa żywotność ~{months} mies.",
|
||||
"cs": "Typická životnost ~{months} měsíců.",
|
||||
"sv": "Typisk livslängd ~{months} månader.",
|
||||
"da": "Typisk levetid ~{months} måneder.",
|
||||
"nb": "Typisk levetid ~{months} måneder.",
|
||||
"fi": "Tyypillinen käyttöikä ~{months} kk.",
|
||||
"ja": "標準的な寿命 約{months}か月。",
|
||||
"hi": "सामान्य आयु ~{months} महीने।",
|
||||
"zh": "典型使用寿命约{months}个月。",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1605,7 +1605,7 @@
|
||||
},
|
||||
"interval_unit": {
|
||||
"name": "間隔の単位",
|
||||
"description": "間隔の単位: days、weeks、months、years。既定値: days。"
|
||||
"description": "間隔の単位: days、weeks、months、years のいずれか。既定値: days。"
|
||||
},
|
||||
"due_date": {
|
||||
"name": "期限日",
|
||||
@@ -1657,7 +1657,7 @@
|
||||
},
|
||||
"interval_unit": {
|
||||
"name": "間隔の単位",
|
||||
"description": "days、weeks、months、years。"
|
||||
"description": "days、weeks、months、years のいずれか。"
|
||||
},
|
||||
"due_date": {
|
||||
"name": "期限日",
|
||||
@@ -1681,7 +1681,7 @@
|
||||
},
|
||||
"priority": {
|
||||
"name": "優先度",
|
||||
"description": "low、normal、high。"
|
||||
"description": "low、normal、high のいずれか。"
|
||||
},
|
||||
"labels": {
|
||||
"name": "ラベル",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user