├── hacs.json ├── src ├── types │ ├── custom │ │ ├── global.d.ts │ │ └── custom-card-helpers.d.ts │ └── index.ts ├── .babelrc ├── assets │ └── localization │ │ └── languages │ │ ├── ru.json │ │ ├── da.json │ │ ├── en.json │ │ ├── hu.json │ │ ├── pl.json │ │ ├── et.json │ │ ├── sl.json │ │ ├── sv.json │ │ ├── it.json │ │ ├── nl.json │ │ ├── es.json │ │ ├── fi.json │ │ ├── fr.json │ │ ├── pt-BR.json │ │ └── de.json ├── constants.ts ├── cardStyles.ts ├── cardContent.ts └── index.ts ├── .github └── workflows │ └── validate.yaml ├── rollup.config.js ├── .eslintrc ├── tsconfig.json ├── rollup.config.dev.js ├── LICENSE ├── package.json ├── dev └── index.html ├── .gitignore └── README.md /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sun Card", 3 | "render_readme": true, 4 | "filename": "home-assistant-sun-card.js" 5 | } 6 | -------------------------------------------------------------------------------- /src/types/custom/global.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | declare global { 4 | interface Window { customCards: { name: string, type: string, description: string }[] } 5 | } 6 | -------------------------------------------------------------------------------- /src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env", 4 | "@babel/typescript" 5 | ], 6 | "plugins": [ 7 | ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }], 8 | "@babel/proposal-class-properties" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/localization/languages/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Азимут", 3 | "Dawn": "Рассвет", 4 | "Dusk": "Сумерки", 5 | "Elevation": "Высота", 6 | "Noon": "Зенит", 7 | "Sunrise": "Восход", 8 | "Sunset": "Закат", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/da.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azimut", 3 | "Dawn": "Daggry", 4 | "Dusk": "Tusmørke", 5 | "Elevation": "Højde", 6 | "Noon": "Middag", 7 | "Sunrise": "Solopgang", 8 | "Sunset": "Solnedgang", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azimuth", 3 | "Dawn": "Dawn", 4 | "Dusk": "Dusk", 5 | "Elevation": "Elevation", 6 | "Noon": "Solar noon", 7 | "Sunrise": "Sunrise", 8 | "Sunset": "Sunset", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azimut", 3 | "Dawn": "Hajnal", 4 | "Dusk": "Szürkület", 5 | "Elevation": "Magasság", 6 | "Noon": "Dél", 7 | "Sunrise": "Napkelte", 8 | "Sunset": "Napnyugta", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azymut", 3 | "Dawn": "Świt", 4 | "Dusk": "Zmierzch", 5 | "Elevation": "Wysokość", 6 | "Noon": "Górowanie", 7 | "Sunrise": "Wschód", 8 | "Sunset": "Zachód", 9 | "errors": { 10 | "SunIntegrationNotFound": "Nie odnaleziono integracji sun" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/et.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Asimuut", 3 | "Dawn": "Koidik", 4 | "Dusk": "Hämarik", 5 | "Elevation": "Kõrgus", 6 | "Noon": "Keskpäev", 7 | "Sunrise": "Päikesetõus", 8 | "Sunset": "Päikeseloojang", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/sl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azimut", 3 | "Dawn": "Zora", 4 | "Dusk": "Mrak", 5 | "Elevation": "Višina", 6 | "Noon": "Sončno poldne", 7 | "Sunrise": "Sončni vzhod", 8 | "Sunset": "Sončni zahod", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azimut", 3 | "Dawn": "Gryning", 4 | "Dusk": "Skymning", 5 | "Elevation": "Elevation", 6 | "Noon": "Middag", 7 | "Sunrise": "Soluppgång", 8 | "Sunset": "Solnedgång", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azimuth", 3 | "Dawn": "Alba", 4 | "Dusk": "Crepuscolo", 5 | "Elevation": "Elevazione", 6 | "Noon": "Mezzogiorno solare", 7 | "Sunrise": "Alba", 8 | "Sunset": "Tramonto", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azimut", 3 | "Dawn": "Dageraad", 4 | "Dusk": "Schemer", 5 | "Elevation": "Hoogte", 6 | "Noon": "Zonne-middag", 7 | "Sunrise": "Zonsopkomst", 8 | "Sunset": "Zonsondergang", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azimut", 3 | "Dawn": "Amanecer", 4 | "Dusk": "Anochecer", 5 | "Elevation": "Elevación", 6 | "Noon": "Mediodía solar", 7 | "Sunrise": "Salida del sol", 8 | "Sunset": "Atardecer", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/fi.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Atsimuutti", 3 | "Dawn": "Sarastus", 4 | "Dusk": "Hämärä", 5 | "Elevation": "Korkeus", 6 | "Noon": "Keskipäivä", 7 | "Sunrise": "Auringonnousu", 8 | "Sunset": "Auringonlasku", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azimut", 3 | "Dawn": "Aube", 4 | "Dusk": "Crépuscule", 5 | "Elevation": "Élévation", 6 | "Noon": "Midi solaire", 7 | "Sunrise": "Lever du soleil", 8 | "Sunset": "Coucher du soleil", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azimute", 3 | "Dawn": "Amanhecer", 4 | "Dusk": "Anoitecer", 5 | "Elevation": "Elevação", 6 | "Noon": "Meio dia solar", 7 | "Sunrise": "Nascer do sol", 8 | "Sunset": "Pôr do sol", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/localization/languages/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Azimuth": "Azimut", 3 | "Dawn": "Morgendämmerung", 4 | "Dusk": "Abenddämmerung", 5 | "Elevation": "Zenitwinkel", 6 | "Noon": "Zenit", 7 | "Sunrise": "Sonnenaufgang", 8 | "Sunset": "Sonnenuntergang", 9 | "errors": { 10 | "SunIntegrationNotFound": "Sun integration not found" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - name: HACS validation 15 | uses: "hacs/action@main" 16 | with: 17 | category: "plugin" -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import json from '@rollup/plugin-json' 4 | import resolve from '@rollup/plugin-node-resolve' 5 | 6 | const extensions = ['.ts'] 7 | 8 | export default { 9 | input: './src/index.ts', 10 | output: { 11 | file: './dist/home-assistant-sun-card.js', 12 | format: 'cjs' 13 | }, 14 | plugins: [ 15 | resolve({ extensions }), 16 | commonjs(), 17 | json(), 18 | babel({extensions, include: ['src/**/*'], babelHelpers: 'bundled'}) 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "eslint-plugin-import" 5 | ], 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:import/recommended", 9 | "plugin:import/typescript", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "rules": { 14 | "@typescript-eslint/no-non-null-assertion": "off", 15 | "@typescript-eslint/member-ordering": "error", 16 | "eol-last": "error", 17 | "object-curly-spacing": ["error", "always"], 18 | "semi": ["error", "never"], 19 | "space-before-function-paren": ["error", "always"] 20 | } 21 | } -------------------------------------------------------------------------------- /src/types/custom/custom-card-helpers.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'custom-card-helpers' { 2 | export interface HomeAssistant { 3 | themes: Themes & { darkMode: boolean } 4 | language: string, 5 | locale: { language: string } 6 | states: { 7 | 'sun.sun': { 8 | state: string 9 | attributes: { 10 | azimuth: number 11 | elevation: number 12 | next_dawn: string 13 | next_dusk: string 14 | next_midnight: string 15 | next_noon: string 16 | next_rising: string 17 | next_setting: string 18 | rising: boolean 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es2017", 8 | "dom", 9 | "dom.iterable" 10 | ], 11 | "noEmit": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "strict": true, 16 | "noImplicitAny": false, 17 | "skipLibCheck": true, 18 | "resolveJsonModule": true, 19 | "experimentalDecorators": true, 20 | "allowSyntheticDefaultImports": true, 21 | "typeRoots": ["./src/types/custom", "node_modules/@types"] 22 | }, 23 | "include": ["./src"] 24 | } -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import json from '@rollup/plugin-json' 4 | import resolve from '@rollup/plugin-node-resolve' 5 | import serve from 'rollup-plugin-serve' 6 | 7 | const extensions = ['.ts'] 8 | 9 | export default { 10 | input: './src/index.ts', 11 | output: { 12 | file: './dist/home-assistant-sun-card.js', 13 | format: 'cjs' 14 | }, 15 | plugins: [ 16 | resolve({ extensions }), 17 | commonjs(), 18 | json(), 19 | babel({extensions, include: ['src/**/*'], babelHelpers: 'bundled'}), 20 | serve({ 21 | contentBase: ['dev/', 'dist/'], 22 | host: '0.0.0.0', 23 | port: 5000, 24 | allowCrossOrigin: true, 25 | headers: { 26 | 'Access-Control-Allow-Origin': '*', 27 | }, 28 | }) 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 AitorDB 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type TSunCardConfig = { 2 | darkMode?: boolean 3 | language?: string 4 | showAzimuth?: boolean 5 | showElevation?: boolean 6 | timeFormat?: '12h' | '24h' 7 | title?: string 8 | } 9 | 10 | export type TSunCardTime = { 11 | time: string, 12 | period?: 'AM' | 'PM' 13 | } 14 | 15 | export type TSunCardData = { 16 | azimuth: number 17 | dawnProgressPercent: number 18 | dayProgressPercent: number 19 | duskProgressPercent: number 20 | elevation: number 21 | error?: string 22 | sunPercentOverHorizon: number 23 | sunPosition: { 24 | x: number 25 | y: number 26 | } 27 | times: { 28 | dawn: TSunCardTime 29 | dusk: TSunCardTime 30 | noon: TSunCardTime 31 | sunrise: TSunCardTime 32 | sunset: TSunCardTime 33 | } 34 | } 35 | 36 | export type TSunCardTexts = { 37 | Azimuth: string 38 | Dawn: string 39 | Dusk: string 40 | Elevation: string 41 | Noon: string 42 | Sunrise: string 43 | Sunset: string 44 | 45 | errors: { 46 | [key in ESunCardErrors]: string 47 | } 48 | } 49 | 50 | export enum ESunCardErrors { 51 | 'SunIntegrationNotFound' = 'SunIntegrationNotFound' 52 | } 53 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import da from './assets/localization/languages/da.json' 2 | import de from './assets/localization/languages/de.json' 3 | import en from './assets/localization/languages/en.json' 4 | import es from './assets/localization/languages/es.json' 5 | import et from './assets/localization/languages/et.json' 6 | import fi from './assets/localization/languages/fi.json' 7 | import fr from './assets/localization/languages/fr.json' 8 | import hu from './assets/localization/languages/hu.json' 9 | import it from './assets/localization/languages/it.json' 10 | import nl from './assets/localization/languages/nl.json' 11 | import pl from './assets/localization/languages/pl.json' 12 | import ptBR from './assets/localization/languages/pt-BR.json' 13 | import ru from './assets/localization/languages/ru.json' 14 | import sl from './assets/localization/languages/sl.json' 15 | import sv from './assets/localization/languages/sv.json' 16 | import { TSunCardConfig, TSunCardTexts } from './types' 17 | 18 | export class Constants { 19 | static readonly DEFAULT_CONFIG: TSunCardConfig = { 20 | darkMode: true, 21 | language: 'en', 22 | showAzimuth: false, 23 | showElevation: false, 24 | timeFormat: '24h' 25 | } 26 | 27 | static readonly EVENT_X_POSITIONS = { 28 | dayStart: 5, 29 | sunrise: 101, 30 | sunset: 449, 31 | dayEnd: 545 32 | } 33 | 34 | static readonly HORIZON_Y = 108 35 | static readonly LOCALIZATION_LANGUAGES: Record = { 36 | da, de, en, es, et, fi, fr, hu, it, nl, pl, 'pt-BR': ptBR, ru, sl, sv 37 | } 38 | static readonly SUN_RADIUS = 17 39 | } 40 | -------------------------------------------------------------------------------- /src/cardStyles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit-element' 2 | 3 | export default css` 4 | .sun-card { 5 | --sun-card-lines: #464646; 6 | --sun-card-text-color: #fff; 7 | --sun-card-subtitle-color: #fff; 8 | 9 | color: var(--sun-card-text-color); 10 | padding: 1rem; 11 | } 12 | 13 | .sun-card-body { 14 | padding-top: 0.5rem; 15 | } 16 | 17 | .sun-card.sun-card-light { 18 | --sun-card-lines: #ececec; 19 | --sun-card-text-color: #000; 20 | --sun-card-subtitle-color: #828282; 21 | } 22 | 23 | .sun-card-header { 24 | display: flex; 25 | justify-content: space-between; 26 | } 27 | 28 | .sun-card-footer .sun-card-footer-row { 29 | display: flex; 30 | justify-content: space-around; 31 | padding-top: 1.5rem; 32 | } 33 | 34 | .sun-card-title { 35 | font-size: 1.5rem; 36 | font-weight: 500; 37 | padding-bottom: 2rem; 38 | margin: 0; 39 | } 40 | 41 | .sun-card-text-container { 42 | display: flex; 43 | flex-direction: column; 44 | align-items: center; 45 | } 46 | 47 | .sun-card-header .sun-card-text-subtitle { 48 | font-size: 1.15rem; 49 | font-weight: 400; 50 | padding-bottom: 0.25rem; 51 | color: var(--sun-card-subtitle-color); 52 | } 53 | 54 | .sun-card-header .sun-card-text-time { 55 | font-size: 1.85rem; 56 | font-weight: 400; 57 | } 58 | 59 | .sun-card-footer .sun-card-text-subtitle { 60 | font-size: 1.25rem; 61 | font-weight: 400; 62 | padding-bottom: 0.5rem; 63 | color: var(--sun-card-subtitle-color); 64 | } 65 | 66 | .sun-card-footer .sun-card-text-time { 67 | font-size: 1.25rem; 68 | font-weight: 500; 69 | } 70 | 71 | .sun-card-text-time-period { 72 | font-size: 0.75rem; 73 | } 74 | ` 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homeassistant-sun-card", 3 | "version": "1.0.0", 4 | "description": "Home assistant sun card based on Google weather design", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "rollup -c rollup.config.dev.js --watch", 8 | "build": "rimraf dist && npm run lint && npm run rollup", 9 | "lint": "eslint src/*.ts", 10 | "rollup": "rollup -c" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/AitorDB/homeassistant-sun-card.git" 15 | }, 16 | "author": "AitorDB ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/AitorDB/homeassistant-sun-card/issues" 20 | }, 21 | "homepage": "https://github.com/AitorDB/homeassistant-sun-card#readme", 22 | "devDependencies": { 23 | "@babel/core": "^7.12.3", 24 | "@babel/plugin-proposal-class-properties": "^7.12.1", 25 | "@babel/plugin-proposal-decorators": "^7.14.2", 26 | "@babel/plugin-proposal-object-rest-spread": "^7.14.2", 27 | "@babel/plugin-syntax-decorators": "^7.12.13", 28 | "@babel/preset-env": "^7.14.2", 29 | "@babel/preset-typescript": "^7.13.0", 30 | "@rollup/plugin-babel": "^5.3.0", 31 | "@rollup/plugin-commonjs": "^19.0.0", 32 | "@rollup/plugin-json": "^4.1.0", 33 | "@rollup/plugin-node-resolve": "^13.0.0", 34 | "@typescript-eslint/eslint-plugin": "^4.23.0", 35 | "@typescript-eslint/parser": "^4.23.0", 36 | "eslint": "^7.26.0", 37 | "eslint-plugin-import": "^2.23.2", 38 | "rollup": "^2.47.0", 39 | "rollup-plugin-serve": "^1.1.0", 40 | "rollup-plugin-typescript2": "^0.30.0", 41 | "typescript": "^4.2.4" 42 | }, 43 | "dependencies": { 44 | "custom-card-helpers": "^1.6.6", 45 | "lit-element": "^2.4.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sun card development preview 8 | 31 | 32 | 33 |
34 | 35 |
36 | 37 | 71 | 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Home assistant Sun card 2 | Home assistant Sun card based on Google weather design 3 | 4 | ## Preview 5 | ![Light mode preview](https://user-images.githubusercontent.com/6829526/118412152-54d93900-b690-11eb-8b2b-e87b4cbcca7f.png) 6 | ![Dark mode preview](https://user-images.githubusercontent.com/6829526/118412162-64f11880-b690-11eb-9bd7-b8c6c7d8efd8.png) 7 | 8 | ## Requirements 9 | - This card uses [Sun integration](https://www.home-assistant.io/integrations/sun/) so it needs to be enabled 10 | 11 | ## Install 12 | ### HACS 13 | Home assistant Sun card is available by default on HACS directory. More info [here](https://hacs.xyz/). 14 | 15 | ### Manually 16 | 1. Download the `home-assistant-sun-card.js` file from the [latest release available](https://github.com/AitorDB/home-assistant-sun-card/releases) and save it in your `configuration/www` folder. 17 | 1. Go to `Configuration > Lovelace dashboard > Resources` in Home Assistant and click on `Add resource`. 18 | 1. Add `/local/community/home-assistant-sun-card.js` to the URL. 19 | 1. Choose `Javascript Module` as Resource type. 20 | 21 | ## Set up 22 | ### Using UI 23 | 1. Go to your dashboard, enter in edit mode and click on `Add card`, you should be able to find `Custom: Sun card` in the list. 24 | 1. Once in the UI editor you can modify the card behavior by adding some of the config that you will find below 25 | 26 | Note: If `Custom: Sun card` doesn't appear you will have to reload cleaning the cache. 27 | 28 | ### Using YAML 29 | 1. You just need to add a new card with `type: 'custom:sun-card'` to your cards list and any of the config that you will find below if you want to customize more your card. 30 | 31 | Note: If you get an error similar to this `Custom element doesn't exist` you will have to reload cleaning the cache. 32 | 33 | ## Config 34 | | Name | Accepted values | Description | Default | 35 | |---------------|----------------------|--------------------------------------|-----------------------------------------------------| 36 | | darkMode | `boolean` | Changes card colors to dark or light | Home assistant dark mode state | 37 | | language | `string`1 | Changes card language | Home assistant language or english if not supported | 38 | | showAzimuth | `boolean` | Displays azimuth in the footer | `false` | 39 | | showElevation | `boolean` | Displays elevation in the footer | `false` | 40 | | timeFormat | `'12h'`/`'24h'` | Displayed time format | Locale based on Home assistant language | 41 | | title | `string` | Card title | Doesn't display a title by default | | 42 | 43 | (1) Supported languages: `da`, `de`, `en`, `es`, `et`, `fi`, `fr`, `hu`, `it`, `nl`, `pl`, `pt-BR`, `ru`, `sl`, `sv` 44 | 45 | ## Known issues 46 | - Home assistant seems to provide next events instead today's one 47 | -------------------------------------------------------------------------------- /src/cardContent.ts: -------------------------------------------------------------------------------- 1 | import { html, TemplateResult } from 'lit-element' 2 | import { TSunCardConfig, TSunCardData, TSunCardTexts, TSunCardTime } from './types' 3 | 4 | export class SunCardContent { 5 | static generate (data: TSunCardData, localization: TSunCardTexts, config: TSunCardConfig): TemplateResult { 6 | if (data?.error) { 7 | return html` 8 | 9 | ${this.generateError()} 10 | 11 | ` 12 | } 13 | 14 | return html` 15 | 16 |
17 | ${this.generateHeader(data, localization, config)} 18 | ${this.generateBody(data)} 19 | ${this.generateFooter(data, localization, config)} 20 |
21 |
22 | ` 23 | } 24 | 25 | private static generateHeader (data: TSunCardData, localization: TSunCardTexts, config: TSunCardConfig): TemplateResult { 26 | const title = config.title !== undefined ? html` 27 |

${config.title}

28 | ` : html`` 29 | 30 | return html` 31 | ${title} 32 |
33 |
34 | ${localization.Sunrise} 35 | ${data?.times.sunrise ? this.generateTime(data.times.sunrise) : ''} 36 | 37 |
38 |
39 | ${localization.Sunset} 40 | ${data?.times.sunset ? this.generateTime(data.times.sunset) : ''} 41 |
42 |
43 | ` 44 | } 45 | 46 | private static generateBody (data: TSunCardData): TemplateResult { 47 | const sunID = Math.random().toString(36).replace('0.', '') 48 | const dawnID = Math.random().toString(36).replace('0.', '') 49 | const dayID = Math.random().toString(36).replace('0.', '') 50 | const duskID = Math.random().toString(36).replace('0.', '') 51 | 52 | return html` 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
90 | ` 91 | } 92 | 93 | private static generateError (): TemplateResult { 94 | return html` 95 | 96 | ` 97 | } 98 | 99 | private static generateFooter (data: TSunCardData, localization: TSunCardTexts, config: TSunCardConfig): TemplateResult { 100 | const upperRow = html` 101 | 115 | ` 116 | 117 | let bottomRow = html`` 118 | if (config.showAzimuth || config.showElevation) { 119 | const azimuth = config.showAzimuth ? html` 120 |
121 | ${localization.Azimuth} 122 | ${data?.azimuth ?? ''} 123 |
124 | ` : html`` 125 | 126 | const elevation = config.showElevation ? html` 127 |
128 | ${localization.Elevation} 129 | ${data?.elevation ?? ''} 130 |
131 | ` : html`` 132 | 133 | bottomRow = html` 134 | 138 | ` 139 | } 140 | 141 | return html` 142 | 146 | ` 147 | } 148 | 149 | private static generateTime (time: TSunCardTime) { 150 | if (time.period) { 151 | return html` 152 | 153 | ${time.time} ${time.period} 154 | 155 | ` 156 | } 157 | 158 | return html` 159 | ${time.time} 160 | ` 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { HomeAssistant } from 'custom-card-helpers' 2 | import { customElement, LitElement, state } from 'lit-element' 3 | 4 | import cardStyles from './cardStyles' 5 | import { Constants } from './constants' 6 | import { SunCardContent } from './cardContent' 7 | import { ESunCardErrors, TSunCardConfig, TSunCardData } from './types' 8 | 9 | @customElement('sun-card') 10 | class SunCard extends LitElement { 11 | static readonly cardType = 'sun-card' 12 | static readonly cardName = 'Sun Card' 13 | static readonly cardDescription = 'Custom card that display a graph to track the sun position and related events' 14 | 15 | @state() 16 | private config: TSunCardConfig = {} 17 | 18 | @state() 19 | private data!: TSunCardData 20 | 21 | private hasRendered = false 22 | private lastHass!: HomeAssistant 23 | 24 | set hass (hass: HomeAssistant) { 25 | this.lastHass = hass 26 | 27 | if (!this.hasRendered) { 28 | return 29 | } 30 | 31 | this.processLastHass() 32 | } 33 | 34 | calculatePositionAndProgressesByTime (hass: HomeAssistant) { 35 | const sunLine = this.shadowRoot?.querySelector('path') as SVGPathElement 36 | const sunrise = new Date(hass.states['sun.sun'].attributes.next_rising) 37 | const sunset = new Date(hass.states['sun.sun'].attributes.next_setting) 38 | const eventsAt = { 39 | dayStart: 0, 40 | sunrise: this.convertDateToMinutesSinceDayStarted(sunrise), 41 | sunset: this.convertDateToMinutesSinceDayStarted(sunset), 42 | dayEnd: (23 * 60) + 59 43 | } 44 | 45 | const now = new Date() 46 | const minutesSinceTodayStarted = this.convertDateToMinutesSinceDayStarted(now) 47 | 48 | // Dawn section position [0 - 105] 49 | const dawnSectionPosition = (Math.min(minutesSinceTodayStarted, eventsAt.sunrise) * 105) / eventsAt.sunrise 50 | 51 | // Day section position [106 - 499] 52 | const minutesSinceDayStarted = Math.max(minutesSinceTodayStarted - eventsAt.sunrise, 0) 53 | const daySectionPosition = (Math.min(minutesSinceDayStarted, eventsAt.sunset - eventsAt.sunrise) * (499 - 106)) / (eventsAt.sunset - eventsAt.sunrise) 54 | 55 | // Dusk section position [500 - 605] 56 | const minutesSinceDuskStarted = Math.max(minutesSinceTodayStarted - eventsAt.sunset, 0) 57 | const duskSectionPosition = (minutesSinceDuskStarted * (605 - 500)) / (eventsAt.dayEnd - eventsAt.sunset) 58 | 59 | const position = dawnSectionPosition + daySectionPosition + duskSectionPosition 60 | const sunPosition = sunLine.getPointAtLength(position) 61 | 62 | const dawnProgressPercent = (100 * (sunPosition.x - Constants.EVENT_X_POSITIONS.dayStart)) / (Constants.EVENT_X_POSITIONS.sunrise - Constants.EVENT_X_POSITIONS.dayStart) 63 | const dayProgressPercent = (100 * (sunPosition.x - Constants.EVENT_X_POSITIONS.sunrise)) / (Constants.EVENT_X_POSITIONS.sunset - Constants.EVENT_X_POSITIONS.sunrise) 64 | const duskProgressPercent = (100 * (sunPosition.x - Constants.EVENT_X_POSITIONS.sunset)) / (Constants.EVENT_X_POSITIONS.dayEnd - Constants.EVENT_X_POSITIONS.sunset) 65 | 66 | const sunYTop = sunPosition.y - Constants.SUN_RADIUS 67 | const yOver = Constants.HORIZON_Y - sunYTop 68 | let sunPercentOverHorizon = 0 69 | if (yOver > 0) { 70 | sunPercentOverHorizon = Math.min((100 * yOver) / (2 * Constants.SUN_RADIUS), 100) 71 | } 72 | 73 | return { 74 | dawnProgressPercent, 75 | dayProgressPercent, 76 | duskProgressPercent, 77 | sunPercentOverHorizon, 78 | sunPosition: { x: sunPosition.x, y: sunPosition.y } 79 | } 80 | } 81 | 82 | convertDateToMinutesSinceDayStarted (date: Date) { 83 | return (date.getHours() * 60) + date.getMinutes() 84 | } 85 | 86 | parseTime (timeText: string, locale?: string) { 87 | const regex = /\d{1,2}[:.]\d{1,2}|[AMP]+/g 88 | const date = new Date(timeText) 89 | const { language, timeFormat } = this.getConfig() 90 | const result = date.toLocaleTimeString(locale ?? language, { hour12: timeFormat === '12h' }).match(regex) as [string, ('AM' | 'PM')?] 91 | 92 | if (!result && !locale) { 93 | return this.parseTime(timeText, Constants.DEFAULT_CONFIG.language) 94 | } 95 | 96 | const [time, period] = result 97 | return { time, period } 98 | } 99 | 100 | processLastHass () { 101 | if (!this.lastHass) { 102 | return 103 | } 104 | 105 | if (!this.lastHass.states['sun.sun']) { 106 | return this.showError(ESunCardErrors.SunIntegrationNotFound) 107 | } 108 | 109 | this.config.darkMode = this.config.darkMode ?? this.lastHass.themes.darkMode 110 | this.config.language = this.config.language ?? this.lastHass.locale?.language ?? this.lastHass.language 111 | this.config.timeFormat = this.config.timeFormat ?? this.getTimeFormatByLanguage(this.config.language) 112 | 113 | const times = { 114 | dawn: this.parseTime(this.lastHass.states['sun.sun'].attributes.next_dawn), 115 | dusk: this.parseTime(this.lastHass.states['sun.sun'].attributes.next_dusk), 116 | noon: this.parseTime(this.lastHass.states['sun.sun'].attributes.next_noon), 117 | sunrise: this.parseTime(this.lastHass.states['sun.sun'].attributes.next_rising), 118 | sunset: this.parseTime(this.lastHass.states['sun.sun'].attributes.next_setting) 119 | } 120 | 121 | const { 122 | dawnProgressPercent, 123 | dayProgressPercent, 124 | duskProgressPercent, 125 | sunPercentOverHorizon, 126 | sunPosition 127 | } = this.calculatePositionAndProgressesByTime(this.lastHass) 128 | 129 | const data: TSunCardData = { 130 | azimuth: this.lastHass.states['sun.sun'].attributes.azimuth, 131 | dawnProgressPercent, 132 | dayProgressPercent, 133 | duskProgressPercent, 134 | elevation: this.lastHass.states['sun.sun'].attributes.elevation, 135 | sunPercentOverHorizon, 136 | sunPosition, 137 | times 138 | } 139 | 140 | this.data = data 141 | } 142 | 143 | getConfig () { 144 | const config: TSunCardConfig = {} 145 | config.darkMode = this.config.darkMode ?? Constants.DEFAULT_CONFIG.darkMode 146 | config.language = this.config.language ?? Constants.DEFAULT_CONFIG.language 147 | config.showAzimuth = this.config.showAzimuth ?? Constants.DEFAULT_CONFIG.showAzimuth 148 | config.showElevation = this.config.showElevation ?? Constants.DEFAULT_CONFIG.showElevation 149 | config.timeFormat = this.config.timeFormat ?? Constants.DEFAULT_CONFIG.timeFormat 150 | config.title = this.config.title 151 | 152 | if (!Object.keys(Constants.LOCALIZATION_LANGUAGES).includes(config.language!)) { 153 | config.language = Constants.DEFAULT_CONFIG.language 154 | } 155 | 156 | return config 157 | } 158 | 159 | getTimeFormatByLanguage (language: string) { 160 | const date = new Date() 161 | const time = date.toLocaleTimeString(language).toLocaleLowerCase() 162 | return time.includes('pm') || time.includes('am') ? '12h' : '24h' 163 | } 164 | 165 | setConfig (config: TSunCardConfig) { 166 | this.config = { ...config } 167 | } 168 | 169 | showError (error: ESunCardErrors) { 170 | this.data = { error } as TSunCardData 171 | } 172 | 173 | protected render () { 174 | const config = this.getConfig() 175 | const language = config.language! 176 | const localization = Constants.LOCALIZATION_LANGUAGES[language] 177 | return SunCardContent.generate(this.data, localization, config) 178 | } 179 | 180 | protected updated (changedProperties) { 181 | super.updated(changedProperties) 182 | 183 | if (!this.hasRendered) { 184 | this.hasRendered = true 185 | this.processLastHass() 186 | return 187 | } 188 | 189 | if (this.data.error) { 190 | const errorElement = this.shadowRoot?.querySelector('hui-error-card') as HTMLElement & { setConfig (config: { error: string }): void } 191 | if (errorElement) { 192 | const config = this.getConfig() 193 | const language = config.language! 194 | const localization = Constants.LOCALIZATION_LANGUAGES[language] 195 | const error = localization.errors[this.data.error] 196 | errorElement.setConfig?.({ error }) 197 | console.error(error) 198 | } 199 | } 200 | } 201 | 202 | static get styles () { 203 | return cardStyles 204 | } 205 | } 206 | 207 | window.customCards = window.customCards || [] 208 | window.customCards.push({ 209 | type: SunCard.cardType, 210 | name: SunCard.cardName, 211 | description: SunCard.cardDescription 212 | }) 213 | --------------------------------------------------------------------------------