diff --git a/src/panels/lovelace/cards/clock/hui-clock-card-analog.ts b/src/panels/lovelace/cards/clock/hui-clock-card-analog.ts
new file mode 100644
index 0000000000..9abad06c37
--- /dev/null
+++ b/src/panels/lovelace/cards/clock/hui-clock-card-analog.ts
@@ -0,0 +1,300 @@
+import { css, html, LitElement, nothing } from "lit";
+import type { PropertyValues } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { classMap } from "lit/directives/class-map";
+import type { ClockCardConfig } from "../types";
+import type { HomeAssistant } from "../../../../types";
+import { INTERVAL } from "../hui-clock-card";
+import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
+
+@customElement("hui-clock-card-analog")
+export class HuiClockCardAnalog extends LitElement {
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @property({ attribute: false }) public config?: ClockCardConfig;
+
+ @state() private _dateTimeFormat?: Intl.DateTimeFormat;
+
+ @state() private _hourDeg?: number;
+
+ @state() private _minuteDeg?: number;
+
+ @state() private _secondDeg?: number;
+
+ private _tickInterval?: undefined | number;
+
+ private _initDate() {
+ if (!this.config || !this.hass) {
+ return;
+ }
+
+ let locale = this.hass.locale;
+ if (this.config.time_format) {
+ locale = { ...locale, time_format: this.config.time_format };
+ }
+
+ this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hourCycle: "h12",
+ timeZone:
+ this.config.time_zone ||
+ resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
+ });
+
+ this._tick();
+ }
+
+ protected updated(changedProps: PropertyValues) {
+ if (changedProps.has("hass")) {
+ const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
+ if (!oldHass || oldHass.locale !== this.hass?.locale) {
+ this._initDate();
+ }
+ }
+ }
+
+ public connectedCallback() {
+ super.connectedCallback();
+ this._startTick();
+ }
+
+ public disconnectedCallback() {
+ super.disconnectedCallback();
+ this._stopTick();
+ }
+
+ private _startTick() {
+ this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
+ this._tick();
+ }
+
+ private _stopTick() {
+ if (this._tickInterval) {
+ clearInterval(this._tickInterval);
+ this._tickInterval = undefined;
+ }
+ }
+
+ private _tick() {
+ if (!this._dateTimeFormat) return;
+
+ const parts = this._dateTimeFormat.formatToParts();
+ const hourStr = parts.find((p) => p.type === "hour")?.value;
+ const minuteStr = parts.find((p) => p.type === "minute")?.value;
+ const secondStr = parts.find((p) => p.type === "second")?.value;
+
+ const hour = hourStr ? parseInt(hourStr, 10) : 0;
+ const minute = minuteStr ? parseInt(minuteStr, 10) : 0;
+ const second = secondStr ? parseInt(secondStr, 10) : 0;
+
+ this._hourDeg = hour * 30 + minute * 0.5; // 30deg per hour + 0.5deg per minute
+ this._minuteDeg = minute * 6 + second * 0.1; // 6deg per minute + 0.1deg per second
+ this._secondDeg = this.config?.show_seconds ? second * 6 : undefined; // 6deg per second
+ }
+
+ render() {
+ if (!this.config) return nothing;
+
+ const sizeClass = this.config.clock_size
+ ? `size-${this.config.clock_size}`
+ : "";
+
+ return html`
+
+
+ ${this.config.ticks === "quarter"
+ ? Array.from({ length: 4 }, (_, i) => i).map(
+ (i) =>
+ // 4 ticks
+ html`
+
+ `
+ )
+ : !this.config.ticks || // Default to hour ticks
+ this.config.ticks === "hour"
+ ? Array.from({ length: 12 }, (_, i) => i).map(
+ (i) =>
+ // 12 ticks
+ html`
+
+ `
+ )
+ : this.config.ticks === "minute"
+ ? Array.from({ length: 60 }, (_, i) => i).map(
+ (i) =>
+ // 60 ticks
+ html`
+
+ `
+ )
+ : nothing}
+
+
+
+ ${this.config.show_seconds
+ ? html`
`
+ : nothing}
+
+
+ `;
+ }
+
+ static styles = css`
+ :host {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .analog-clock {
+ --clock-size: 100px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: var(--clock-size);
+ height: var(--clock-size);
+ }
+
+ .analog-clock.size-medium {
+ --clock-size: 160px;
+ }
+
+ .analog-clock.size-large {
+ --clock-size: 220px;
+ }
+
+ .dial {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ }
+
+ .dial-border {
+ border: 2px solid var(--divider-color);
+ border-radius: 50%;
+ }
+
+ .tick {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform: rotate(var(--tick-rotation));
+ pointer-events: none;
+ z-index: 0;
+ }
+
+ .tick .line {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 1px;
+ height: calc(var(--clock-size) * 0.04);
+ background: var(--primary-text-color);
+ opacity: 0.5;
+ border-radius: 1px;
+ }
+
+ .tick.hour .line {
+ width: 2px;
+ height: calc(var(--clock-size) * 0.07);
+ opacity: 0.8;
+ }
+
+ .center-dot {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--primary-text-color);
+ transform: translate(-50%, -50%);
+ z-index: 3;
+ }
+
+ .hand {
+ position: absolute;
+ left: 50%;
+ bottom: 50%;
+ transform-origin: 50% 100%;
+ transform: translate(-50%, 0) rotate(var(--hand-rotation, 0deg));
+ background: var(--primary-text-color);
+ border-radius: 2px;
+ will-change: transform;
+ }
+
+ .hand.hour {
+ width: 4px;
+ height: calc(var(--clock-size) * 0.25); /* 25% of the clock size */
+ background: var(--primary-text-color);
+ box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.2);
+ z-index: 1;
+ }
+
+ .hand.minute {
+ width: 3px;
+ height: calc(var(--clock-size) * 0.35); /* 35% of the clock size */
+ background: var(--primary-text-color);
+ box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.2);
+ opacity: 0.9;
+ z-index: 3;
+ }
+
+ .hand.second {
+ width: 2px;
+ height: calc(var(--clock-size) * 0.42); /* 42% of the clock size */
+ background: var(--ha-color-border-danger-normal);
+ box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2);
+ opacity: 0.8;
+ z-index: 2;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-clock-card-analog": HuiClockCardAnalog;
+ }
+}
diff --git a/src/panels/lovelace/cards/clock/hui-clock-card-digital.ts b/src/panels/lovelace/cards/clock/hui-clock-card-digital.ts
new file mode 100644
index 0000000000..3d171f20e7
--- /dev/null
+++ b/src/panels/lovelace/cards/clock/hui-clock-card-digital.ts
@@ -0,0 +1,196 @@
+import { css, html, LitElement, nothing } from "lit";
+import type { PropertyValues } from "lit";
+import { customElement, property, state } from "lit/decorators";
+import type { ClockCardConfig } from "../types";
+import type { HomeAssistant } from "../../../../types";
+import { INTERVAL } from "../hui-clock-card";
+import { useAmPm } from "../../../../common/datetime/use_am_pm";
+import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
+
+@customElement("hui-clock-card-digital")
+export class HuiClockCardDigital extends LitElement {
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @property({ attribute: false }) public config?: ClockCardConfig;
+
+ @state() private _dateTimeFormat?: Intl.DateTimeFormat;
+
+ @state() private _timeHour?: string;
+
+ @state() private _timeMinute?: string;
+
+ @state() private _timeSecond?: string;
+
+ @state() private _timeAmPm?: string;
+
+ private _tickInterval?: undefined | number;
+
+ private _initDate() {
+ if (!this.config || !this.hass) {
+ return;
+ }
+
+ let locale = this.hass?.locale;
+
+ if (this.config?.time_format) {
+ locale = { ...locale, time_format: this.config.time_format };
+ }
+
+ this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hourCycle: useAmPm(locale) ? "h12" : "h23",
+ timeZone:
+ this.config?.time_zone ||
+ resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
+ });
+
+ this._tick();
+ }
+
+ protected updated(changedProps: PropertyValues) {
+ if (changedProps.has("hass")) {
+ const oldHass = changedProps.get("hass");
+ if (!oldHass || oldHass.locale !== this.hass?.locale) {
+ this._initDate();
+ }
+ }
+ }
+
+ public connectedCallback() {
+ super.connectedCallback();
+ this._startTick();
+ }
+
+ public disconnectedCallback() {
+ super.disconnectedCallback();
+ this._stopTick();
+ }
+
+ private _startTick() {
+ this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
+ this._tick();
+ }
+
+ private _stopTick() {
+ if (this._tickInterval) {
+ clearInterval(this._tickInterval);
+ this._tickInterval = undefined;
+ }
+ }
+
+ private _tick() {
+ if (!this._dateTimeFormat) return;
+
+ const parts = this._dateTimeFormat.formatToParts();
+
+ this._timeHour = parts.find((part) => part.type === "hour")?.value;
+ this._timeMinute = parts.find((part) => part.type === "minute")?.value;
+ this._timeSecond = this.config?.show_seconds
+ ? parts.find((part) => part.type === "second")?.value
+ : undefined;
+ this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
+ }
+
+ render() {
+ if (!this.config) return nothing;
+
+ const sizeClass = this.config.clock_size
+ ? `size-${this.config.clock_size}`
+ : "";
+
+ return html`
+
+
${this._timeHour}
+
${this._timeMinute}
+ ${this._timeSecond !== undefined
+ ? html`
${this._timeSecond}
`
+ : nothing}
+ ${this._timeAmPm !== undefined
+ ? html`
${this._timeAmPm}
`
+ : nothing}
+
+ `;
+ }
+
+ static styles = css`
+ :host {
+ display: block;
+ }
+
+ .time-parts {
+ align-items: center;
+ display: grid;
+ grid-template-areas:
+ "hour minute second"
+ "hour minute am-pm";
+
+ font-size: 1.5rem;
+ font-weight: var(--ha-font-weight-medium);
+ line-height: 0.8;
+ direction: ltr;
+ }
+
+ .time-title + .time-parts {
+ font-size: 1.5rem;
+ }
+
+ .time-parts.size-medium {
+ font-size: 3rem;
+ }
+
+ .time-parts.size-large {
+ font-size: 4rem;
+ }
+
+ .time-parts.size-medium .time-part.second,
+ .time-parts.size-medium .time-part.am-pm {
+ font-size: var(--ha-font-size-l);
+ margin-left: 6px;
+ }
+
+ .time-parts.size-large .time-part.second,
+ .time-parts.size-large .time-part.am-pm {
+ font-size: var(--ha-font-size-2xl);
+ margin-left: 8px;
+ }
+
+ .time-parts .time-part.hour {
+ grid-area: hour;
+ }
+
+ .time-parts .time-part.minute {
+ grid-area: minute;
+ }
+
+ .time-parts .time-part.second {
+ grid-area: second;
+ line-height: 0.9;
+ opacity: 0.4;
+ }
+
+ .time-parts .time-part.am-pm {
+ grid-area: am-pm;
+ line-height: 0.9;
+ opacity: 0.6;
+ }
+
+ .time-parts .time-part.second,
+ .time-parts .time-part.am-pm {
+ font-size: var(--ha-font-size-xs);
+ margin-left: 4px;
+ }
+
+ .time-parts .time-part.hour:after {
+ content: ":";
+ margin: 0 2px;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-clock-card-digital": HuiClockCardDigital;
+ }
+}
diff --git a/src/panels/lovelace/cards/hui-clock-card.ts b/src/panels/lovelace/cards/hui-clock-card.ts
index 7d3a69cb84..0287faf42a 100644
--- a/src/panels/lovelace/cards/hui-clock-card.ts
+++ b/src/panels/lovelace/cards/hui-clock-card.ts
@@ -1,4 +1,3 @@
-import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -11,10 +10,8 @@ import type {
LovelaceGridOptions,
} from "../types";
import type { ClockCardConfig } from "./types";
-import { useAmPm } from "../../../common/datetime/use_am_pm";
-import { resolveTimeZone } from "../../../common/datetime/resolve-time-zone";
-const INTERVAL = 1000;
+export const INTERVAL = 1000;
@customElement("hui-clock-card")
export class HuiClockCard extends LitElement implements LovelaceCard {
@@ -33,45 +30,14 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
@state() private _config?: ClockCardConfig;
- @state() private _dateTimeFormat?: Intl.DateTimeFormat;
-
- @state() private _timeHour?: string;
-
- @state() private _timeMinute?: string;
-
- @state() private _timeSecond?: string;
-
- @state() private _timeAmPm?: string;
-
- private _tickInterval?: undefined | number;
-
public setConfig(config: ClockCardConfig): void {
this._config = config;
- this._initDate();
- }
-
- private _initDate() {
- if (!this._config || !this.hass) {
- return;
+ // Dynamically import the clock type based on the configuration
+ if (config.clock_style === "analog") {
+ import("./clock/hui-clock-card-analog");
+ } else {
+ import("./clock/hui-clock-card-digital");
}
-
- let locale = this.hass?.locale;
-
- if (this._config?.time_format) {
- locale = { ...locale, time_format: this._config.time_format };
- }
-
- this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- hourCycle: useAmPm(locale) ? "h12" : "h23",
- timeZone:
- this._config?.time_zone ||
- resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
- });
-
- this._tick();
}
public getCardSize(): number {
@@ -80,77 +46,59 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
}
public getGridOptions(): LovelaceGridOptions {
- if (this._config?.clock_size === "medium") {
- return {
- min_rows: this._config?.title ? 2 : 1,
- rows: 2,
- max_rows: 4,
- min_columns: 4,
- columns: 6,
- };
+ switch (this._config?.clock_style) {
+ case "analog":
+ switch (this._config?.clock_size) {
+ case "medium":
+ return {
+ min_rows: this._config?.title ? 4 : 3,
+ rows: 3,
+ min_columns: 5,
+ columns: 6,
+ };
+ case "large":
+ return {
+ min_rows: this._config?.title ? 5 : 4,
+ rows: 4,
+ min_columns: 6,
+ columns: 6,
+ };
+ default:
+ return {
+ min_rows: this._config?.title ? 3 : 2,
+ rows: 2,
+ min_columns: 2,
+ columns: 6,
+ };
+ }
+ default:
+ switch (this._config?.clock_size) {
+ case "medium":
+ return {
+ min_rows: this._config?.title ? 2 : 1,
+ rows: 2,
+ max_rows: 4,
+ min_columns: 4,
+ columns: 6,
+ };
+ case "large":
+ return {
+ min_rows: 2,
+ rows: 2,
+ max_rows: 4,
+ min_columns: 6,
+ columns: 6,
+ };
+ default:
+ return {
+ min_rows: 1,
+ rows: 1,
+ max_rows: 4,
+ min_columns: 3,
+ columns: 6,
+ };
+ }
}
-
- if (this._config?.clock_size === "large") {
- return {
- min_rows: 2,
- rows: 2,
- max_rows: 4,
- min_columns: 6,
- columns: 6,
- };
- }
-
- return {
- min_rows: 1,
- rows: 1,
- max_rows: 4,
- min_columns: 3,
- columns: 6,
- };
- }
-
- protected updated(changedProps: PropertyValues) {
- if (changedProps.has("hass")) {
- const oldHass = changedProps.get("hass");
- if (!oldHass || oldHass.locale !== this.hass?.locale) {
- this._initDate();
- }
- }
- }
-
- public connectedCallback() {
- super.connectedCallback();
- this._startTick();
- }
-
- public disconnectedCallback() {
- super.disconnectedCallback();
- this._stopTick();
- }
-
- private _startTick() {
- this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
- this._tick();
- }
-
- private _stopTick() {
- if (this._tickInterval) {
- clearInterval(this._tickInterval);
- this._tickInterval = undefined;
- }
- }
-
- private _tick() {
- if (!this._dateTimeFormat) return;
-
- const parts = this._dateTimeFormat.formatToParts();
-
- this._timeHour = parts.find((part) => part.type === "hour")?.value;
- this._timeMinute = parts.find((part) => part.type === "minute")?.value;
- this._timeSecond = this._config?.show_seconds
- ? parts.find((part) => part.type === "second")?.value
- : undefined;
- this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
}
protected render() {
@@ -170,16 +118,19 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
${this._config.title !== undefined
? html`${this._config.title}
`
: nothing}
-
-
${this._timeHour}
-
${this._timeMinute}
- ${this._timeSecond !== undefined
- ? html`
${this._timeSecond}
`
- : nothing}
- ${this._timeAmPm !== undefined
- ? html`
${this._timeAmPm}
`
- : nothing}
-
+ ${this._config.clock_style === "analog"
+ ? html`
+
+ `
+ : html`
+
+ `}
`;
@@ -234,74 +185,6 @@ export class HuiClockCard extends LitElement implements LovelaceCard {
font-size: var(--ha-font-size-2xl);
line-height: var(--ha-line-height-condensed);
}
-
- .time-parts {
- align-items: center;
- display: grid;
- grid-template-areas:
- "hour minute second"
- "hour minute am-pm";
-
- font-size: 2rem;
- font-weight: var(--ha-font-weight-medium);
- line-height: 0.8;
- direction: ltr;
- }
-
- .time-title + .time-parts {
- font-size: 1.5rem;
- }
-
- .time-wrapper.size-medium .time-parts {
- font-size: 3rem;
- }
-
- .time-wrapper.size-large .time-parts {
- font-size: 4rem;
- }
-
- .time-wrapper.size-medium .time-parts .time-part.second,
- .time-wrapper.size-medium .time-parts .time-part.am-pm {
- font-size: var(--ha-font-size-l);
- margin-left: 6px;
- }
-
- .time-wrapper.size-large .time-parts .time-part.second,
- .time-wrapper.size-large .time-parts .time-part.am-pm {
- font-size: var(--ha-font-size-2xl);
- margin-left: 8px;
- }
-
- .time-parts .time-part.hour {
- grid-area: hour;
- }
-
- .time-parts .time-part.minute {
- grid-area: minute;
- }
-
- .time-parts .time-part.second {
- grid-area: second;
- line-height: 0.9;
- opacity: 0.4;
- }
-
- .time-parts .time-part.am-pm {
- grid-area: am-pm;
- line-height: 0.9;
- opacity: 0.6;
- }
-
- .time-parts .time-part.second,
- .time-parts .time-part.am-pm {
- font-size: var(--ha-font-size-xs);
- margin-left: 4px;
- }
-
- .time-parts .time-part.hour:after {
- content: ":";
- margin: 0 2px;
- }
`;
}
diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts
index bc28fa3bac..99bb70734e 100644
--- a/src/panels/lovelace/cards/types.ts
+++ b/src/panels/lovelace/cards/types.ts
@@ -371,11 +371,15 @@ export interface MarkdownCardConfig extends LovelaceCardConfig {
export interface ClockCardConfig extends LovelaceCardConfig {
type: "clock";
title?: string;
+ clock_style?: "digital" | "analog";
clock_size?: "small" | "medium" | "large";
show_seconds?: boolean | undefined;
time_format?: TimeFormat;
time_zone?: string;
no_background?: boolean;
+ // Analog clock options
+ border?: boolean;
+ ticks?: "none" | "quarter" | "hour" | "minute";
}
export interface MediaControlCardConfig extends LovelaceCardConfig {
diff --git a/src/panels/lovelace/editor/config-elements/hui-clock-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-clock-card-editor.ts
index c1819febf3..8f791e992a 100644
--- a/src/panels/lovelace/editor/config-elements/hui-clock-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-clock-card-editor.ts
@@ -6,6 +6,7 @@ import {
assert,
assign,
boolean,
+ defaulted,
enums,
literal,
object,
@@ -30,6 +31,7 @@ const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
title: optional(string()),
+ clock_style: optional(union([literal("digital"), literal("analog")])),
clock_size: optional(
union([literal("small"), literal("medium"), literal("large")])
),
@@ -37,6 +39,19 @@ const cardConfigStruct = assign(
time_zone: optional(enums(Object.keys(timezones))),
show_seconds: optional(boolean()),
no_background: optional(boolean()),
+ // Analog clock options
+ border: optional(defaulted(boolean(), false)),
+ ticks: optional(
+ defaulted(
+ union([
+ literal("none"),
+ literal("quarter"),
+ literal("hour"),
+ literal("minute"),
+ ]),
+ literal("hour")
+ )
+ ),
})
);
@@ -50,9 +65,23 @@ export class HuiClockCardEditor
@state() private _config?: ClockCardConfig;
private _schema = memoizeOne(
- (localize: LocalizeFunc) =>
+ (localize: LocalizeFunc, clockStyle: "digital" | "analog") =>
[
{ name: "title", selector: { text: {} } },
+ {
+ name: "clock_style",
+ selector: {
+ select: {
+ mode: "dropdown",
+ options: ["digital", "analog"].map((value) => ({
+ value,
+ label: localize(
+ `ui.panel.lovelace.editor.card.clock.clock_styles.${value}`
+ ),
+ })),
+ },
+ },
+ },
{
name: "clock_size",
selector: {
@@ -69,20 +98,66 @@ export class HuiClockCardEditor
},
{ name: "show_seconds", selector: { boolean: {} } },
{ name: "no_background", selector: { boolean: {} } },
- {
- name: "time_format",
- selector: {
- select: {
- mode: "dropdown",
- options: ["auto", ...Object.values(TimeFormat)].map((value) => ({
- value,
- label: localize(
- `ui.panel.lovelace.editor.card.clock.time_formats.${value}`
- ),
- })),
- },
- },
- },
+ ...(clockStyle === "digital"
+ ? ([
+ {
+ name: "time_format",
+ selector: {
+ select: {
+ mode: "dropdown",
+ options: ["auto", ...Object.values(TimeFormat)].map(
+ (value) => ({
+ value,
+ label: localize(
+ `ui.panel.lovelace.editor.card.clock.time_formats.${value}`
+ ),
+ })
+ ),
+ },
+ },
+ },
+ ] as const satisfies readonly HaFormSchema[])
+ : clockStyle === "analog"
+ ? ([
+ {
+ name: "border",
+ description: {
+ suffix: localize(
+ `ui.panel.lovelace.editor.card.clock.border.description`
+ ),
+ },
+ default: false,
+ selector: {
+ boolean: {},
+ },
+ },
+ {
+ name: "ticks",
+ description: {
+ suffix: localize(
+ `ui.panel.lovelace.editor.card.clock.ticks.description`
+ ),
+ },
+ default: "hour",
+ selector: {
+ select: {
+ mode: "dropdown",
+ options: ["none", "quarter", "hour", "minute"].map(
+ (value) => ({
+ value,
+ label: localize(
+ `ui.panel.lovelace.editor.card.clock.ticks.${value}.label`
+ ),
+ description: localize(
+ `ui.panel.lovelace.editor.card.clock.ticks.${value}.description`
+ ),
+ })
+ ),
+ },
+ },
+ },
+ ] as const satisfies readonly HaFormSchema[])
+ : []),
{
name: "time_zone",
selector: {
@@ -107,10 +182,15 @@ export class HuiClockCardEditor
);
private _data = memoizeOne((config) => ({
+ clock_style: "digital",
clock_size: "small",
time_zone: "auto",
time_format: "auto",
show_seconds: false,
+ no_background: false,
+ // Analog clock options
+ border: false,
+ ticks: "hour",
...config,
}));
@@ -128,8 +208,13 @@ export class HuiClockCardEditor
`;
@@ -143,6 +228,14 @@ export class HuiClockCardEditor
delete ev.detail.value.time_format;
}
+ if (ev.detail.value.clock_style === "analog") {
+ ev.detail.value.border = ev.detail.value.border ?? false;
+ ev.detail.value.ticks = ev.detail.value.ticks ?? "hour";
+ } else {
+ delete ev.detail.value.border;
+ delete ev.detail.value.ticks;
+ }
+
fireEvent(this, "config-changed", { config: ev.detail.value });
}
@@ -154,6 +247,10 @@ export class HuiClockCardEditor
return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.title"
);
+ case "clock_style":
+ return this.hass!.localize(
+ `ui.panel.lovelace.editor.card.clock.clock_style`
+ );
case "clock_size":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.clock_size`
@@ -174,6 +271,31 @@ export class HuiClockCardEditor
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.no_background`
);
+ case "border":
+ return this.hass!.localize(
+ `ui.panel.lovelace.editor.card.clock.border.label`
+ );
+ case "ticks":
+ return this.hass!.localize(
+ `ui.panel.lovelace.editor.card.clock.ticks.label`
+ );
+ default:
+ return undefined;
+ }
+ };
+
+ private _computeHelperCallback = (
+ schema: SchemaUnion>
+ ) => {
+ switch (schema.name) {
+ case "border":
+ return this.hass!.localize(
+ `ui.panel.lovelace.editor.card.clock.border.description`
+ );
+ case "ticks":
+ return this.hass!.localize(
+ `ui.panel.lovelace.editor.card.clock.ticks.description`
+ );
default:
return undefined;
}
diff --git a/src/translations/en.json b/src/translations/en.json
index d1c72df25e..84a3c086ab 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -7704,6 +7704,11 @@
"clock": {
"name": "Clock",
"description": "The Clock card displays the current time using your desired size and format.",
+ "clock_style": "Clock style",
+ "clock_styles": {
+ "digital": "Digital",
+ "analog": "Analog"
+ },
"clock_size": "Clock size",
"clock_sizes": {
"small": "Small",
@@ -7723,7 +7728,31 @@
"time_zones": {
"auto": "Use user settings"
},
- "no_background": "No background"
+ "no_background": "No background",
+ "border": {
+ "label": "Border",
+ "description": "Whether to show a border around the clock"
+ },
+ "ticks": {
+ "label": "Ticks",
+ "description": "Whether to show ticks (indices) on the outside of the clock",
+ "none": {
+ "label": "None",
+ "description": "No ticks (Hands only)"
+ },
+ "quarter": {
+ "label": "Quarter",
+ "description": "4 ticks (Every 15 minutes)"
+ },
+ "hour": {
+ "label": "Hour",
+ "description": "12 ticks (Every hour)"
+ },
+ "minute": {
+ "label": "Minute",
+ "description": "60 ticks (Every minute)"
+ }
+ }
},
"media-control": {
"name": "Media control",