Skip to content

Commit

Permalink
Remove date-fns in favor of luxon
Browse files Browse the repository at this point in the history
  • Loading branch information
Patrick Kissling committed Oct 7, 2023
1 parent ad30aca commit e47838c
Show file tree
Hide file tree
Showing 35 changed files with 167 additions and 206 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Credits go to [basmilius](https://github.com/basmilius) for the awesome [weather
## Migrating from v1 to v2

* Configuration property `forecast_days` was renamed to `forecast_rows` to indicate that this attribute does not only work for daily, but also for hourly forecasts.
* `date-fns` has been replaced by `luxon` for date/time formatting. If you configure `date_pattern`, make sure to migrate your pattern to comply with [luxon](https://moment.github.io/luxon/#/formatting?id=table-of-tokens). Additionally, the weekday is now [_not_ hardcoded](https://github.com/pkissling/clock-weather-card/issues/89) anymore.

## FAQ

Expand Down Expand Up @@ -124,9 +125,9 @@ time_zone: null
| weather_icon_type | `line` \| `fill` | **Optional** | Appearance of the large weather icon | `line` |
| animated_icon | boolean | **Optional** | Whether the large weather icon should be animated | `true` |
| forecast_rows | number | **Optional** | The amount of weather forecast rows to show. Depending on `hourly_forecast` each row either corresponds to a day or an hour | `5` |
| locale | string[^2] | **Optional** | Language to use for language specific text. If not provided, falls back to the locale set in HA | `en-GB` |
| time_format | `24` \| `12` | **Optional** | Format used to display the time. If not provided, falls back to the time format set in HA | `24` |
| date_pattern | string | **Optional** | Pattern to use for time formatting. If not provided, falls back to the default date formatting of the configured language. See [date-fns](https://date-fns.org/v2.29.3/docs/format) for valid patterns | `P` |
| locale | string[^2] | **Optional** | Language to use for language specific text and date/time formatting. If not provided, falls back to the locale set in HA or, if not set in HA, to `en-GB` | `en-GB` |
| time_format | `24` \| `12` | **Optional** | Format used to display the time. If not provided, falls back to the default time format of the configured `locale` | `24` |
| date_pattern | string | **Optional** | Pattern to use for date formatting. If not provided, falls back to a localized default date formatting. See [luxon](https://moment.github.io/luxon/#/formatting?id=table-of-tokens) for valid tokens | `D` |
| hide_today_section | boolean | **Optional** | Hides the cards today section (upper section), containing the large weather icon, clock and current date | `false` |
| hide_forecast_section | boolean | **Optional** | Hides the cards forecast section (lower section),containing the weather forecast | `false` |
| hide_clock | boolean | **Optional** | Hides the clock from the today section and prominently displays the current temperature instead | `false` |
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,17 @@
"dependencies": {
"@lit-labs/scoped-registry-mixin": "^1.0.0",
"custom-card-helpers": "^1.8.0",
"date-fns": "^2.29.3",
"home-assistant-js-websocket": "^8.0.0",
"lit": "^2.0.0",
"luxon": "^3.3.0"
"luxon": "^3.4.3"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-proposal-decorators": "^7.14.5",
"@rollup/plugin-image": "^2.1.1",
"@rollup/plugin-json": "^5.0.0",
"@types/luxon": "^3.3.0",
"@types/luxon": "^3.3.2",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^8.0.1",
Expand Down
114 changes: 47 additions & 67 deletions src/clock-weather-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ import { extractMostOccuring, max, min, round, roundDown, roundIfNotNull, roundU
import { svg, png } from './images'
import { version } from '../package.json'
import { safeRender } from './helpers'
import { format, type Locale } from 'date-fns'
import * as locales from 'date-fns/locale'
import { DateTime } from 'luxon'

console.info(
Expand Down Expand Up @@ -63,16 +61,16 @@ export class ClockWeatherCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant

@state() private config!: MergedClockWeatherCardConfig
@state() private currentDate!: Date
@state() private currentDate!: DateTime
@state() private forecastSubscriber?: Promise<() => void>
@state() private forecasts?: WeatherForecast[]

constructor () {
super()
this.currentDate = new Date()
const msToNextMinute = (60 - this.currentDate.getSeconds()) * 1000
setTimeout(() => setInterval(() => { this.currentDate = new Date() }, 1000 * 60), msToNextMinute)
setTimeout(() => { this.currentDate = new Date() }, msToNextMinute)
this.currentDate = DateTime.now()
const msToNextMinute = (60 - this.currentDate.second) * 1000
setTimeout(() => setInterval(() => { this.currentDate = DateTime.now() }, 1000 * 60), msToNextMinute)
setTimeout(() => { this.currentDate = DateTime.now() }, msToNextMinute)
}

public static getStubConfig (_hass: HomeAssistant, entities: string[], entitiesFallback: string[]): Record<string, unknown> {
Expand All @@ -85,7 +83,7 @@ export class ClockWeatherCard extends LitElement {
}

public getCardSize (): number {
return 3 + roundUp(this.config.forecast_days / 2)
return 3 + roundUp(this.config.forecast_rows / 2)
}

// https://lit.dev/docs/components/properties/#accessors-custom
Expand All @@ -98,8 +96,8 @@ export class ClockWeatherCard extends LitElement {
throw new Error('Attribute "entity" must be present.')
}

if (config.forecast_days && config.forecast_days < 1) {
throw new Error('Attribute "forecast_days" must be greater than 0.')
if (config.forecast_rows && config.forecast_rows < 1) {
throw new Error('Attribute "forecast_rows" mst be greater than 0.')
}

if (config.time_format && config.time_format.toString() !== '24' && config.time_format.toString() !== '12') {
Expand Down Expand Up @@ -240,22 +238,25 @@ export class ClockWeatherCard extends LitElement {
const maxTemp = Math.round(max(maxTemps))

const gradientRange = this.gradientRange(minTemp, maxTemp, temperatureUnit)
return forecasts.map((forecast) => safeRender(() => this.renderForecastItem(forecast, gradientRange, minTemp, maxTemp, currentTemp, hourly)))

const displayTexts = forecasts
.map(f => f.datetime)
.map(d => hourly ? this.time(d) : this.localize(`day.${d.weekday}`))
const maxColOneChars = displayTexts.length ? max(displayTexts.map(t => t.length)) : 0

return forecasts.map((forecast, i) => safeRender(() => this.renderForecastItem(forecast, gradientRange, minTemp, maxTemp, currentTemp, hourly, displayTexts[i], maxColOneChars)))
}

private renderForecastItem (forecast: MergedWeatherForecast, gradientRange: Rgb[], minTemp: number, maxTemp: number, currentTemp: number | null, hourly: boolean): TemplateResult {
const twelveHour = this.getTimeFormat() === '12'
const displayText = !hourly ? this.localize('day.' + forecast.datetime.getDay()) : this.time(forecast.datetime)
private renderForecastItem (forecast: MergedWeatherForecast, gradientRange: Rgb[], minTemp: number, maxTemp: number, currentTemp: number | null, hourly: boolean, displayText: string, maxColOneChars: number): TemplateResult {
const weatherState = forecast.condition === 'pouring' ? 'raindrops' : forecast.condition === 'rainy' ? 'raindrop' : forecast.condition
const weatherIcon = this.toIcon(weatherState, 'fill', true, 'static')
const tempUnit = this.getWeather().attributes.temperature_unit
const isNow = !hourly ? new Date().getDate() === forecast.datetime.getDate() : new Date().getHours() === forecast.datetime.getHours()
const isNow = hourly ? DateTime.now().hour === forecast.datetime.hour : DateTime.now().day === forecast.datetime.day
const minTempDay = Math.round(isNow && currentTemp !== null ? Math.min(currentTemp, forecast.templow) : forecast.templow)
const maxTempDay = Math.round(isNow && currentTemp !== null ? Math.max(currentTemp, forecast.temperature) : forecast.temperature)
const colOneSize = hourly && twelveHour ? '3rem' : hourly ? '2.5rem' : '2rem'

return html`
<clock-weather-card-forecast-row style="--col-one-size: ${colOneSize};">
<clock-weather-card-forecast-row style="--col-one-size: ${(maxColOneChars * 0.5)}rem;">
${this.renderText(displayText)}
${this.renderIcon(weatherIcon)}
${this.renderText(this.toConfiguredTempWithUnit(tempUnit, minTempDay), 'right')}
Expand Down Expand Up @@ -393,7 +394,7 @@ export class ClockWeatherCard extends LitElement {
hide_today_section: config.hide_today_section ?? false,
hide_clock: config.hide_clock ?? false,
hide_date: config.hide_date ?? false,
date_pattern: config.date_pattern ?? 'P',
date_pattern: config.date_pattern ?? 'D',
use_browser_time: config.use_browser_time ?? true,
time_zone: config.time_zone ?? undefined
}
Expand Down Expand Up @@ -431,39 +432,27 @@ export class ClockWeatherCard extends LitElement {
}

private getLocale (): string {
return this.config.locale ?? this.hass.locale?.language ?? 'en-GB'
}

private getDateFnsLocale (): Locale {
const locale = this.getLocale()
const localeParts = locale
.replace('_', '-')
.split('-')
const localeOne = localeParts[0].toLowerCase()
const localeTwo = localeParts[1]?.toUpperCase() || ''
const dateFnsLocale = localeOne + localeTwo
// HA provides en-US as en
if (dateFnsLocale === 'en') {
return locales.enUS
}
const importedLocale = locales[dateFnsLocale]
if (!importedLocale) {
console.error('clock-weather-card - Locale not supported: ' + dateFnsLocale)
return locales.enGB
}
return importedLocale
return this.config.locale ?? this.hass.locale.language ?? 'en-GB'
}

private date (): string {
const zonedDate = this.toZonedDate(this.currentDate)
const weekday = this.localize(`day.${zonedDate.getDay()}`)
const date = format(zonedDate, this.config.date_pattern, { locale: this.getDateFnsLocale() })
return `${weekday}, ${date}`
return this.toZonedDate(this.currentDate).toFormat(this.config.date_pattern)
}

private time (date: Date = this.currentDate): string {
const withTimeZone = this.toZonedDate(date)
return format(withTimeZone, this.getTimeFormat() === '24' ? 'HH:mm' : 'h:mm aa')
private time (date: DateTime = this.currentDate): string {
if (this.config.time_format) {
return this.toZonedDate(date)
.toFormat(this.config.time_format === '24' ? 'HH:mm' : 'h:mm a')
}
if (this.hass.locale.time_format === TimeFormat.am_pm) {
return this.toZonedDate(date).toFormat('h:mm a')
}

if (this.hass.locale.time_format === TimeFormat.twenty_four) {
return this.toZonedDate(date).toFormat('HH:mm')
}

return this.toZonedDate(date).toFormat('t')
}

private getIconAnimationKind (): 'static' | 'animated' {
Expand Down Expand Up @@ -498,16 +487,6 @@ export class ClockWeatherCard extends LitElement {
: this.toCelsius(unit, temp)
}

private getTimeFormat (): '12' | '24' {
if (this.config.time_format) {
return this.config.time_format
}

if (this.hass.locale?.time_format === TimeFormat.twenty_four) return '24'
if (this.hass.locale?.time_format === TimeFormat.am_pm) return '12'
return '24'
}

private calculateBarRangePercents (minTemp: number, maxTemp: number, minTempDay: number, maxTempDay: number): { startPercent: number, endPercent: number } {
if (maxTemp === minTemp) {
// avoid division by 0
Expand All @@ -531,7 +510,7 @@ export class ClockWeatherCard extends LitElement {
const forecasts = this.getWeather().attributes.forecast ?? this.forecasts ?? []
const agg = forecasts.reduce<Record<number, WeatherForecast[]>>((forecasts, forecast) => {
const d = new Date(forecast.datetime)
const unit = !hourly ? d.getDate() : `${d.getMonth()}-${d.getDate()}-${+d.getHours()}`
const unit = hourly ? `${d.getMonth()}-${d.getDate()}-${+d.getHours()}` : d.getDate()
forecasts[unit] = forecasts[unit] || []
forecasts[unit].push(forecast)
return forecasts
Expand All @@ -544,19 +523,20 @@ export class ClockWeatherCard extends LitElement {
agg.push(avg)
return agg
}, [])
.sort((a, b) => a.datetime.getTime() - b.datetime.getTime())
.sort((a, b) => a.datetime.toMillis() - b.datetime.toMillis())
.slice(0, maxRowsCount)
}

private toZonedDate (date: Date): Date {
if (this.config.use_browser_time) return date
private toZonedDate (date: DateTime): DateTime {
const localizedDate = date.setLocale(this.getLocale())
if (this.config.use_browser_time) return localizedDate
const timeZone = this.config.time_zone ?? this.hass?.config?.time_zone
const withTimeZone = DateTime.fromJSDate(date).setZone(timeZone)
if (!withTimeZone.isValid) {
console.error(`clock-weather-card - Time Zone [${timeZone}] not supported. Falling back to browser time.`)
return date
const withTimeZone = localizedDate.setZone(timeZone)
if (withTimeZone.isValid) {
return withTimeZone
}
return new Date(withTimeZone.year, withTimeZone.month - 1, withTimeZone.day, withTimeZone.hour, withTimeZone.minute, withTimeZone.second, withTimeZone.millisecond)
console.error(`clock-weather-card - Time Zone [${timeZone}] not supported. Falling back to browser time.`)
return localizedDate
}

private calculateAverageForecast (forecasts: WeatherForecast[]): MergedWeatherForecast {
Expand All @@ -578,7 +558,7 @@ export class ClockWeatherCard extends LitElement {
return {
temperature: maxTemp,
templow: minTemp,
datetime: new Date(forecasts[0].datetime),
datetime: DateTime.fromISO(forecasts[0].datetime),
condition,
precipitation_probability: precipitationProbability,
precipitation
Expand Down Expand Up @@ -621,7 +601,7 @@ export class ClockWeatherCard extends LitElement {
return (this.getWeather().attributes.forecast?.length ?? 0) > 0
}

private supportsFeature(feature: WeatherEntityFeature): boolean {
private supportsFeature (feature: WeatherEntityFeature): boolean {
return (this.getWeather().attributes.supported_features & feature) !== 0
}
}
4 changes: 2 additions & 2 deletions src/localize/languages/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
"exceptional": "Изключително"
},
"day": {
"0": "Нд",
"1": "Пн",
"2": "Вт",
"3": "Ср",
"4": "Чт",
"5": "Пт",
"6": "Сб"
"6": "Сб",
"7": "Нд"
}
}
4 changes: 2 additions & 2 deletions src/localize/languages/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
"exceptional": "Excepcional"
},
"day": {
"0": "Dg.",
"1": "Dl.",
"2": "Dt.",
"3": "Dc.",
"4": "Dj.",
"5": "Dv.",
"6": "Ds."
"6": "Ds.",
"7": "Dg."
}
}
4 changes: 2 additions & 2 deletions src/localize/languages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
"exceptional": "Chaos"
},
"day": {
"0": "Ne",
"1": "Po",
"2": "Út",
"3": "St",
"4": "Čt",
"5": "",
"6": "So"
"6": "So",
"7": "Ne"
}
}
4 changes: 2 additions & 2 deletions src/localize/languages/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
"exceptional": "Kaos"
},
"day": {
"0": "Søn",
"1": "Man",
"2": "Tir",
"3": "Ons",
"4": "Tor",
"5": "Fre",
"6": "Lør"
"6": "Lør",
"7": "Søn"
}
}
4 changes: 2 additions & 2 deletions src/localize/languages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
"exceptional": "Unwetter"
},
"day": {
"0": "So",
"1": "Mo",
"2": "Di",
"3": "Mi",
"4": "Do",
"5": "Fr",
"6": "Sa"
"6": "Sa",
"7": "So"
}
}
4 changes: 2 additions & 2 deletions src/localize/languages/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
"exceptional": "Εξαιρετικός"
},
"day": {
"0": "Κυρ",
"1": "Δευ",
"2": "Τρί",
"3": "Τετ",
"4": "Πεμ",
"5": "Παρ",
"6": "Σαβ"
"6": "Σαβ",
"7": "Κυρ"
}
}
4 changes: 2 additions & 2 deletions src/localize/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
"exceptional": "Exceptional"
},
"day": {
"0": "Sun",
"1": "Mon",
"2": "Tue",
"3": "Wed",
"4": "Thu",
"5": "Fri",
"6": "Sat"
"6": "Sat",
"7": "Sun"
}
}
4 changes: 2 additions & 2 deletions src/localize/languages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
"exceptional": "Caos"
},
"day": {
"0": "Dom",
"1": "Lun",
"2": "Mar",
"3": "Mie",
"4": "Jue",
"5": "Vie",
"6": "Sab"
"6": "Sab",
"7": "Dom"
}
}
Loading

0 comments on commit e47838c

Please sign in to comment.