├── hacs.json ├── images ├── example.PNG ├── add-card.png ├── card-editor.png └── airvisual_sensors.JPG ├── dist ├── ic-wind.svg ├── ic-humidity.svg ├── ic-w-scattered-clouds.svg ├── ic-w-rain.svg ├── ic-w-clear-sky.svg ├── ic-w-night-clear-sky.svg ├── ic-w-new-clouds.svg ├── ic-w-snow.svg ├── ic-face-2.svg ├── ic-face-4.svg ├── ic-face-1.svg ├── ic-face-3.svg ├── ic-face-6.svg ├── ic-face-5.svg ├── air-visual-card-editor.js └── air-visual-card.js ├── info.md ├── LICENSE └── README.md /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Air Visual Card" 3 | } 4 | -------------------------------------------------------------------------------- /images/example.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnguyen800/air-visual-card/HEAD/images/example.PNG -------------------------------------------------------------------------------- /images/add-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnguyen800/air-visual-card/HEAD/images/add-card.png -------------------------------------------------------------------------------- /images/card-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnguyen800/air-visual-card/HEAD/images/card-editor.png -------------------------------------------------------------------------------- /images/airvisual_sensors.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnguyen800/air-visual-card/HEAD/images/airvisual_sensors.JPG -------------------------------------------------------------------------------- /dist/ic-wind.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/ic-humidity.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/ic-w-scattered-clouds.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # Air Visual Card 2 | 3 | ![example](https://github.com/dnguyen800/air-visual-card/blob/master/images/example.PNG) 4 | 5 | This is a Home Assistant Lovelace card that uses the [AirVisual Sensor](https://www.home-assistant.io/components/sensor.airvisual/) to provide air quality index (AQI) data and creates a card like the ones found on [AirVisual website](https://www.airvisual.com). Requires the [AirVisual Sensor](https://www.home-assistant.io/components/sensor.airvisual/) to be setup. Tested with Yahoo and Darksky Weather component. 6 | 7 | ## Features 8 | - Card color and icons change depending on AQI level 9 | - Icons can be locally hosted or defaults to jsdelivr.net 10 | -------------------------------------------------------------------------------- /dist/ic-w-rain.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/ic-w-clear-sky.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/ic-w-night-clear-sky.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dan Nguyen 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 | -------------------------------------------------------------------------------- /dist/ic-w-new-clouds.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/ic-w-snow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/ic-face-2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/ic-face-4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/ic-face-1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/ic-face-3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/ic-face-6.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/ic-face-5.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Air Visual Card 2 | 3 | ![example](images/example.PNG) 4 | 5 | This is a Home Assistant Lovelace card that uses the [AirVisual component](https://www.home-assistant.io/integrations/airvisual/) or [World Air Quality Index (WAQI) component](https://www.home-assistant.io/integrations/waqi/) to provide air quality index (AQI) data and creates a card like the ones found on [AirVisual website](https://www.airvisual.com). Requires the [AirVisual component](https://www.home-assistant.io/integrations/airvisual/) or [World Air Quality Index (WAQI) component](https://www.home-assistant.io/integrations/waqi/). Tested with Yahoo and Darksky Weather component. 6 | 7 | ## Features 8 | - Card colors and icons change depending on AQI level 9 | 10 | 11 | ## Options 12 | 13 | ### Main Options 14 | 15 | | Name | Type | Default | Supported options | Description | 16 | | --------------------- | ------- | ---------------------------- | -------------------------------- | ------------------------------------------------------------ | 17 | | `type` | string | **Required** | `custom:air-visual-card` | Type of the card | 18 | | `air_pollution_level` | string | **Required** | `sensor.u_s_air_pollution_level` | Name of the Air Pollution Level sensor. | 19 | | `air_quality_index` | string | optional | `sensor.u_s_air_quality_index` | Name of the Air Quality Index sensor. If sensor does not exist, do not add this config value. | 20 | | `main_pollutant` | string | optional | `sensor.u_s_main_pollutant` | Name of the Main Pollutant sensor. If sensor does not exist, do not add this config value. | 21 | | `weather` | string | optional | `weather.dark_sky` | Name of the weather entity if you wish to display temperature, humidity and wind information on the card. | 22 | | `country` | string | `US` | `mdi:air-conditioner` | Name of the country that Airvisual is collecting AQI data from. | 23 | | `city` | string | optional | `San Francisco` | Name of the city that AirVisual is collecting AQI data from. | 24 | | `unit_of_measurement` | string | optional | `AQI` | Unit of measurement | 25 | | `icons` | string | `/hacsfiles/air-visual-card` | `/hacsfiles/air-visual-card` | The local directory where the .svg files are located. For example, 'icons: "/hacsfiles/air-visual-card"' is appropriate if this plugin is installed using HACS. If left blank, icons will be loaded from default location. | 26 | | `hide_title` | boolean | `true` | `true` | `false` | Set to `true` if you want to hide the title that includes the city name. Useful for minimalists or those using dark themes. | 27 | | `hide_face` | boolean | `false` | `true` | `false` | Set to `true` if you want to hide the face icon. | 28 | | `hide_weather` | boolean | `true` | `true` | `false` | Set to `false` if you want to show the weather information from the weather entity. | 29 | 30 | 31 | 32 | ## HACS Installation 33 | 1. Open the HACS on your Home Assistant instance. 34 | 2. Open the Plugins section and click on the Air Visual Card. 35 | 3. Click on Install, then click on "Add to Lovelace" 36 | 37 | ## Manual Installation 38 | 1. Download the [AirVisual Card](https://raw.githubusercontent.com/dnguyen800/air-visual-card/master/dist/air-visual-card.js) 39 | 2. Place the file in your `config/www` folder 40 | 3. Include the card code in the Resources section of your `ui-lovelace-card.yaml` like below: 41 | 42 | ```yaml 43 | resources: 44 | - url: /local/air-visual-card/air-visual-card.js 45 | type: js 46 | ``` 47 | 4. **Optional:** If you wish to store the Airvisual icons locally, then download the icons [here](https://github.com/dnguyen800/air-visual-card/tree/master/dist). 48 | 49 | 5. Save the icons in a directory in Home Assistant, such as `/local/air-visual-card` 50 | 51 | 6. Update the card configuration in `ui-lovelace.yaml` to include the following (use directory name in step #7): 52 | 53 | ```yaml 54 | icons: "/local/air-visual-card" 55 | ``` 56 | 57 | ## Instructions 58 | 1. Install the [AirVisual sensor](https://www.home-assistant.io/components/sensor.airvisual/) and confirm AQI, APL, and Main Pollutant sensors are created, like below. 59 | 60 | ![sensors](images/airvisual_sensors.JPG) 61 | 62 | 2. Add a card in the Lovelace UI. 63 | 3. Search for `air-visual-card` and click the search result. ![add-card](images/add-card.png) 64 | 4. Fill out the card editor. ![card-editor](images/card-editor.png) 65 | 66 | 67 | 68 | 69 | ## FAQ 70 | - The card doesn't show the temperature properly. 71 | 72 | Let me know which weather provider you are using and I'll try to fix the issue. I have only tested with the Yahoo! Weather component. Optionally, if you create a template sensor that reports the temperature as its state, you can use that sensor as for the temp config. 73 | 74 | - This card doesn't work in Fully Kiosk Browser on Amazon Fire tablets. Why? 75 | 76 | This card uses a new CSS function, CSS Grid Layout, which was implemented in October 2018, and isn't compatible with browsers using old versions of Android WebView. That's my guess anyways. 77 | 78 | - The card is showing the word 'unavailable' instead of the AQI data! 79 | 80 | Most likely your Airvisual key expired (it has a one-year expiration) and needs to be recreated. Delete and recreate a new key on airvisual.com and save the key in your HA config file. 81 | 82 | ## Support 83 | I am studying programming as a hobby and this is my first set of Home Assistant projects. Unfortunately, I know nothing about Javascript and relied on studying other Lovelace custom cards to write this. Suggestions are welcome but no promises if I can fix anything! If you're familiar with CSS, then you can edit the CSS style in the .js file directly. 84 | 85 | ## Credits 86 | - All the custom HA cards and components I studied from, including [@Arsaboo's Animated Weather card](https://github.com/arsaboo/homeassistant-config/blob/master/www/custom_ui/weather-card.js) and [Mini Media Player](https://github.com/kalkih/mini-media-player) 87 | - [airvisual.com](https://www.airvisual.com/) - For the card design and data 88 | - [Home Assistant Air Visual sensor](https://www.home-assistant.io/components/sensor.airvisual/) 89 | - [Weather Card](https://github.com/bramkragten/weather-card) by @bramkragten - for the visual card editor 90 | 91 | -------------------------------------------------------------------------------- /dist/air-visual-card-editor.js: -------------------------------------------------------------------------------- 1 | // I Used weather-card-editor.js from Weather Card as template 2 | // https://github.com/bramkragten/weather-card 3 | // 2023-02-25 card editor is likely broken as it doesn't show entities, 4 | 5 | const fireEvent = (node, type, detail, options) => { 6 | options = options || {}; 7 | detail = detail === null || detail === undefined ? {} : detail; 8 | const event = new Event(type, { 9 | bubbles: options.bubbles === undefined ? true : options.bubbles, 10 | cancelable: Boolean(options.cancelable), 11 | composed: options.composed === undefined ? true : options.composed, 12 | }); 13 | event.detail = detail; 14 | node.dispatchEvent(event); 15 | return event; 16 | }; 17 | 18 | if ( 19 | !customElements.get("ha-switch") && 20 | customElements.get("paper-toggle-button") 21 | ) { 22 | customElements.define("ha-switch", customElements.get("paper-toggle-button")); 23 | } 24 | 25 | const LitElement = customElements.get("hui-masonry-view") ? Object.getPrototypeOf(customElements.get("hui-masonry-view")) : Object.getPrototypeOf(customElements.get("hui-view")); 26 | const html = LitElement.prototype.html; 27 | const css = LitElement.prototype.css; 28 | 29 | const HELPERS = window.loadCardHelpers(); 30 | 31 | export class AirVisualCardEditor extends LitElement { 32 | setConfig(config) { 33 | this._config = { ...config }; 34 | } 35 | 36 | static get properties() { 37 | return { hass: {}, _config: {} }; 38 | } 39 | 40 | get _air_pollution_level() { 41 | return this._config.air_pollution_level || "sensor.u_s_air_pollution_level"; 42 | } 43 | 44 | get _air_quality_index() { 45 | return this._config.air_quality_index || "sensor.u_s_air_quality_index"; 46 | } 47 | 48 | get _main_pollutant() { 49 | return this._config.main_pollutant || "sensor.u_s_main_pollutant"; 50 | } 51 | 52 | get _country() { 53 | return this._config.country || ""; 54 | } 55 | 56 | get _city() { 57 | return this._config.city || ""; 58 | } 59 | 60 | get _icons() { 61 | return this._config.icons || "/hacsfiles/air-visual-card"; 62 | } 63 | 64 | get _weather() { 65 | return this._config.weather || "weather.home"; 66 | } 67 | 68 | get _speed_unit() { 69 | return this._config.speed_unit || "mp/h"; 70 | } 71 | get _unit_of_measurement() { 72 | return this._config.unit_of_measurement || "AQI"; 73 | } 74 | get _hide_title() { 75 | return this._config.hide_title !== false; 76 | } 77 | 78 | get _hide_face() { 79 | return this._config.hide_face !== true; 80 | } 81 | get _hide_weather() { 82 | return this._config.hide_weather !== false; 83 | } 84 | 85 | // WHAT DOES THIS DO? 86 | firstUpdated() { 87 | HELPERS.then(help => { 88 | if (help.importMoreInfoControl) { 89 | help.importMoreInfoControl("fan"); 90 | } 91 | }) 92 | } 93 | 94 | render() { 95 | if (!this.hass) { 96 | return html``; 97 | } 98 | 99 | // WHAT DOES THIS DO? 100 | const entities = Object.keys(this.hass.states).filter( 101 | (eid) => eid.substr(0, eid.indexOf(".")) === "sensor" 102 | ); 103 | 104 | return html` 105 |
106 |
107 | ${customElements.get("ha-entity-picker") 108 | ? html` 109 | 118 | ` 119 | : html``} 120 | 121 | 130 | 131 | 140 | 141 | 149 | 150 | 156 | 157 | 163 | 164 | 170 | 171 | 177 | 178 | 184 | 185 |
186 |
187 | Hide Title 193 |
194 |
195 | Hide Weather 201 |
202 |
203 | Hide Face 209 |
210 |
211 |
212 |
213 | `; 214 | } 215 | 216 | _valueChanged(ev) { 217 | if (!this._config || !this.hass) { 218 | return; 219 | } 220 | const target = ev.target; 221 | if (this[`_${target.configValue}`] === target.value) { 222 | return; 223 | } 224 | if (target.configValue) { 225 | if (target.value === "") { 226 | delete this._config[target.configValue]; 227 | } else { 228 | this._config = { 229 | ...this._config, 230 | [target.configValue]: 231 | target.checked !== undefined ? target.checked : target.value, 232 | }; 233 | } 234 | } 235 | fireEvent(this, "config-changed", { config: this._config }); 236 | } 237 | 238 | static get styles() { 239 | return css` 240 | .switches { 241 | margin: 8px 0; 242 | display: flex; 243 | justify-content: space-between; 244 | } 245 | .switch { 246 | display: flex; 247 | align-items: center; 248 | justify-items: center; 249 | } 250 | .switches span { 251 | padding: 0 16px; 252 | } 253 | `; 254 | } 255 | } 256 | 257 | customElements.define("air-visual-card-editor", AirVisualCardEditor); 258 | -------------------------------------------------------------------------------- /dist/air-visual-card.js: -------------------------------------------------------------------------------- 1 | // To study: 2 | // Plant Picture Card: https://github.com/badguy99/PlantPictureCard/blob/master/dist/PlantPictureCard.js 3 | // UPDATE FOR EACH RELEASE!!! From aftership-card. Version # is hard-coded for now. 4 | console.info( 5 | '%c AIR-VISUAL-CARD \n%c Version 2.0.5', 6 | 'color: orange; font-weight: bold; background: black', 7 | 'color: white; font-weight: bold; background: dimgray', 8 | ); 9 | 10 | // From weather-card 11 | const fireEvent = (node, type, detail, options) => { 12 | options = options || {}; 13 | detail = detail === null || detail === undefined ? {} : detail; 14 | const event = new Event(type, { 15 | bubbles: options.bubbles === undefined ? true : options.bubbles, 16 | cancelable: Boolean(options.cancelable), 17 | composed: options.composed === undefined ? true : options.composed 18 | }); 19 | event.detail = detail; 20 | node.dispatchEvent(event); 21 | return event; 22 | }; 23 | 24 | let oldStates = {} 25 | 26 | class AirVisualCard extends HTMLElement { 27 | // Placeholder for lovelace card editor 28 | // static getConfigElement() { 29 | // return document.createElement("air-visual-card-editor"); 30 | // } 31 | 32 | static async getConfigElement() { 33 | await import("./air-visual-card-editor.js"); 34 | return document.createElement("air-visual-card-editor"); 35 | } 36 | 37 | static getStubConfig() { 38 | return { air_pollution_level: "sensor.u_s_air_pollution_level", 39 | air_quality_index: "sensor.u_s_air_quality_index", 40 | main_pollutant: "sensor.u_s_main_pollutant", 41 | weather: "", 42 | hide_weather: 1, 43 | hide_title: 1, 44 | unit_of_measurement: "AQI", 45 | hide_face: 0 46 | } 47 | } 48 | 49 | constructor() { 50 | super(); 51 | this.attachShadow({ mode: 'open' }); 52 | } 53 | 54 | setConfig(config) { 55 | const root = this.shadowRoot; 56 | if (root.lastChild) root.removeChild(root.lastChild); 57 | 58 | const re = new RegExp("(sensor)"); 59 | if (!re.test(config.air_quality_index.split('.')[0])) throw new Error('Please define a sensor entity.'); 60 | 61 | 62 | const cardConfig = Object.assign({}, config); 63 | const card = document.createElement('ha-card'); 64 | const content = document.createElement('div'); 65 | const style = document.createElement('style'); 66 | 67 | style.textContent = ` 68 | ha-card { 69 | /* sample css */ 70 | background-color: rgba(0,0,0,0); 71 | box-shadow: none; 72 | overflow: hidden; 73 | } 74 | 75 | body { 76 | margin: 0; 77 | font-family: Arial, Helvetica, sans-serif; 78 | } 79 | 80 | .grid-container { 81 | display: grid; 82 | grid-template-areas: "city city city" "face aqiSensor aplSensor" "face country mainPollutantSensor" "temp humidity wind"; 83 | grid-template-columns: 85px 30% auto; 84 | grid-template-rows: auto auto auto auto; 85 | grid-gap: 0; 86 | text-align: center; 87 | } 88 | 89 | .city { 90 | grid-area: city; 91 | font-size: 1.6em; 92 | font-weight: bold; 93 | color: var(--primary-text-color); 94 | filter: opacity(80%); 95 | padding-bottom: 5px; 96 | } 97 | 98 | .face { 99 | border-radius: var(--ha-card-border-radius) 0px 0px ${cardConfig.hide_weather ? 'var(--ha-card-border-radius)' : '0px'}; 100 | grid-area: face; 101 | justify-items: center; 102 | align-items: center; 103 | display: grid; 104 | } 105 | 106 | .face img { 107 | display: block; 108 | height: 60px; 109 | } 110 | 111 | .aqiSensor { 112 | grid-area: aqiSensor; 113 | font-size: 3em; 114 | height: 60px; 115 | padding-top: 4px; 116 | display: flex; 117 | align-items: center; 118 | justify-content: center; 119 | border-radius: ${cardConfig.hide_face ? 'var(--ha-card-border-radius)' : '0px'} 0px 0px 0px; 120 | } 121 | 122 | .aplSensor { 123 | grid-area: aplSensor; 124 | font-size: 1.4em; 125 | display: flex; 126 | align-items: center; 127 | justify-content: center; 128 | border-radius: 0px var(--ha-card-border-radius) 0px 0px; 129 | } 130 | 131 | .mainPollutantSensor { 132 | grid-area: mainPollutantSensor; 133 | border-radius: 0px 0px ${cardConfig.hide_weather ? 'var(--ha-card-border-radius)' : '0px'} 0px ; 134 | display: flex; 135 | align-items: center; 136 | justify-content: center; 137 | padding: 0px 0px 5px 0px; 138 | } 139 | 140 | .mainPollutantSensorText { 141 | background-color: white; 142 | border-radius: 4px; 143 | font-size: 0.9em; 144 | font-weight: bold; 145 | width: 70%; 146 | 147 | } 148 | 149 | .country { 150 | grid-area: country; 151 | border-radius: 0px 0px 0px ${cardConfig.hide_face ? 'var(--ha-card-border-radius)' : '0px'}; 152 | } 153 | 154 | .temp { 155 | grid-area: temp; 156 | text-align: left; 157 | font-size: 1.2em; 158 | background-color: rgba(255,255,255,0.2); 159 | color: var(--text-color); 160 | border-radius: 0px 0px 0px var(--ha-card-border-radius); 161 | border-bottom: 1px solid rgba(230, 230, 230, 1); 162 | border-left: 1px solid rgba(230, 230, 230, 1); 163 | border-right: 1px solid rgba(230, 230, 230, 1); 164 | display: flex; 165 | align-items: center; 166 | justify-content: center; 167 | } 168 | .temp img { 169 | width: 34px; 170 | padding-right: 2px; 171 | 172 | } 173 | 174 | .humidity { 175 | grid-area: humidity; 176 | color: var(--text-color); 177 | border-bottom: 1px solid rgba(230, 230, 230, 1); 178 | background-color: rgba(255,255,255,0.2); 179 | display: flex; 180 | align-items: center; 181 | justify-content: center; 182 | padding: 5px 0px 5px 0px; 183 | } 184 | .humidity img { 185 | height: 25px; 186 | padding-right: 2px; 187 | } 188 | 189 | .wind { 190 | grid-area: wind; 191 | background-color: rgba(255,255,255,0.2); 192 | color: var(--text-color); 193 | border-radius: 0px 0px var(--ha-card-border-radius) 0px; 194 | border-bottom: 1px solid rgba(230, 230, 230, 1); 195 | border-right: 1px solid rgba(230, 230, 230, 1); 196 | display: flex; 197 | align-items: center; 198 | justify-content: center; 199 | } 200 | .wind img { 201 | height: 14px; 202 | padding-right: 2px; 203 | } 204 | ` 205 | content.innerHTML = ` 206 |
207 |
208 | `; 209 | 210 | card.appendChild(content); 211 | card.appendChild(style); 212 | root.appendChild(card); 213 | oldStates = {} 214 | this._config = cardConfig; 215 | } 216 | 217 | shouldNotUpdate(config, hass) { 218 | let clone = JSON.parse(JSON.stringify(config)) 219 | delete clone["city"] 220 | delete clone["type"] 221 | delete clone["icons"] 222 | delete clone["hide_title"] 223 | delete clone["hide_face"] 224 | delete clone["hide_weather"] 225 | delete clone["weather"] 226 | delete clone["speed_unit"] 227 | let states = {} 228 | for (let entity of Object.values(clone)) { 229 | states[entity] = hass.states[entity] 230 | } 231 | if (JSON.stringify(oldStates) === JSON.stringify(states)) { 232 | return true 233 | } 234 | oldStates = states 235 | return false 236 | } 237 | 238 | set hass(hass) { 239 | const config = this._config; 240 | const root = this.shadowRoot; 241 | const card = root.lastChild; 242 | if (this.shouldNotUpdate(config, hass)) { 243 | return 244 | } 245 | 246 | const hideTitle = config.hide_title ? 1 : 0; 247 | const hideFace = config.hide_face ? 1 : 0; 248 | const hideAQI = config.hide_aqi ? 1 : 0; 249 | const hideAPL = config.hide_apl ? 1 : 0; 250 | const hideWeather = config.hide_weather || !config.weather ? 1 : 0; 251 | const speedUnit = config.speed_unit || ''; 252 | // points to local directory created by HACS installation 253 | const iconDirectory = config.icons || "/hacsfiles/air-visual-card"; 254 | const country = config.country || ''; 255 | const city = config.city || ''; 256 | const weatherEntity = config.weather || ''; 257 | // value is used as a string instead of integer in order for 258 | const aqiSensor = { name: 'aqiSensor', config: config.air_quality_index || null, value: 0 }; 259 | const aplSensor = { name: 'aplSensor', config: config.air_pollution_level || null, value: 0 }; 260 | const mainPollutantSensor = { name: 'mainPollutantSensor', config: config.main_pollutant || null, value: '' }; 261 | const validPollutants = ['co', 'no2', 'o3', 'so2', 'pm10', 'pm25', 'neph']; 262 | const unitOfMeasurement = config.unit_of_measurement || 'AQI'; 263 | 264 | const AQIbgColor = { 265 | 266 | '1': `#B0E867`, 267 | '2': '#E3C143', 268 | '3': '#E48B4E', 269 | '4': '#E45F5E', 270 | '5': '#986EA9', 271 | '6': '#A5516B', 272 | }; 273 | const AQIfaceColor = { 274 | '1': `#A8E05F`, 275 | '2': '#FDD64B', 276 | '3': '#FF9B57', 277 | '4': '#FE6A69', 278 | '5': '#A97ABC', 279 | '6': '#A87383', 280 | }; 281 | const AQIfontColor = { 282 | '1': `#718B3A`, 283 | '2': '#A57F23', 284 | '3': '#B25826', 285 | '4': '#AF2C3B', 286 | '5': '#634675', 287 | '6': '#683E51', 288 | }; 289 | 290 | const weatherIcons = { 291 | 'clear-night': 'mdi:weather-night', 292 | 'cloudy': 'mdi:weather-cloudy', 293 | 'fog': 'mdi:weather-fog', 294 | 'hail': 'mdi:weather-hail', 295 | 'lightning': 'mdi:weather-lightning', 296 | 'lightning-rainy': 'mdi:weather-lightning-rainy', 297 | 'partlycloudy': 'mdi:weather-partly-cloudy', 298 | 'pouring': 'mdi:weather-pouring', 299 | 'rainy': 'mdi:weather-rainy', 300 | 'snowy': 'mdi:weather-snowy', 301 | 'snowy-rainy': 'mdi:weather-snowy-rainy', 302 | 'sunny': 'mdi:weather-sunny', 303 | 'windy': 'mdi:weather-windy', 304 | 'windy-variant': `mdi:weather-windy-variant`, 305 | 'exceptional': '!!', 306 | } 307 | const weatherSVG = { 308 | 'clear-night': 'night-clear-sky', 309 | 'cloudy': 'scattered-clouds', 310 | 'fog': 'scattered-clouds', 311 | 'hail': 'rain', 312 | 'lightning': 'rain', 313 | 'lightning-rainy': 'rain', 314 | 'partlycloudy': 'new-clouds', 315 | 'pouring': 'rain', 316 | 'rainy': 'rain', 317 | 'snowy': 'snow', 318 | 'snowy-rainy': 'snow', 319 | 'sunny': 'clear-sky', 320 | 'windy': 'scattered-clouds', 321 | 'windy-variant': `scattered-clouds`, 322 | 'exceptional': 'snow', 323 | } 324 | 325 | // WAQI sensor-specific stuff 326 | // AirVisual sensors have the APL description as part of the sensor state, but WAQI doesn't. These APL states will be used as backup if AirVisual sensors is not used. 327 | const APLdescription = { 328 | '1': 'Good', 329 | '2': 'Moderate', 330 | '3': 'Unhealthy for Sensitive Groups', 331 | '4': 'Unhealthy', 332 | '5': 'Very Unhealthy', 333 | '6': 'Hazardous', 334 | } 335 | const pollutantUnitValue = { 336 | 'pm25': 'µg/m³', 337 | 'pm10': 'µg/m³', 338 | 'o3': 'ppb', 339 | 'no2': 'ppb', 340 | 'so2': 'ppb', 341 | } 342 | const mainPollutantValue = { 343 | 'p2': 'PM2.5', 344 | 'pm25': 'PM2.5', 345 | 'pm10': 'PM10', 346 | 'o3': 'Ozone', 347 | 'no2': 'Nitrogen Dioxide', 348 | 'so2': 'Sulfur Dioxide', 349 | } 350 | const mainAirVisualPollutantValue = { 351 | 'p2': 'PM2.5', 352 | 'p1': 'PM10', 353 | 'co': 'Carbon Monoxide', 354 | 'o3': 'Ozone', 355 | 'n2': 'Nitrogen Dioxide', 356 | 's2': 'Sulfur Dioxide', 357 | } 358 | const mainPollutantUnit = { 359 | 'co': 'ppm', 360 | 'no2': 'ppb', 361 | 'o3': 'ppb', 362 | 'so2': 'ppb', 363 | 'pm10': 'µg/m³', 364 | 'pm25': 'µg/m³', 365 | 'neph': '1/Mm', 366 | } 367 | 368 | let currentCondition = ''; 369 | let humidity = ''; 370 | let windSpeed = ''; 371 | let tempValue = ''; 372 | let pollutantUnit = ''; 373 | let apl = ''; 374 | let mainPollutant = ''; 375 | let speed_unit = speedUnit; 376 | let getAQI = function () { 377 | switch (true) { 378 | case (aqiSensor.value <= 50): 379 | return '1'; 380 | case (aqiSensor.value <= 100): 381 | return '2'; 382 | case (aqiSensor.value <= 150): 383 | return '3'; 384 | case (aqiSensor.value <= 200): 385 | return '4'; 386 | case (aqiSensor.value <= 300): 387 | return '5'; 388 | case (aqiSensor.value > 300): 389 | return '6'; 390 | default: 391 | return '1'; 392 | } 393 | }; 394 | 395 | if (weatherEntity != '' && typeof hass.states[weatherEntity].attributes['wind_speed_unit'] != "undefined"){ 396 | speed_unit = hass.states[weatherEntity].attributes['wind_speed_unit']; 397 | } 398 | // if Main Pollutant is an Airvisual sensor, else if if it is an WAQI sensor 399 | if (typeof hass.states[mainPollutantSensor.config] != "undefined") { 400 | let mpParse = hass.states[mainPollutantSensor.config].state; 401 | if (typeof hass.states[mainPollutantSensor.config].attributes['pollutant_unit'] != "undefined") { 402 | pollutantUnit = hass.states[mainPollutantSensor.config].attributes['pollutant_unit']; 403 | mainPollutant = mainAirVisualPollutantValue[hass.states[mainPollutantSensor.config].attributes['pollutant_symbol']]; 404 | } else if (validPollutants.includes(mpParse)) { 405 | mainPollutant = mainPollutantValue[mpParse]; 406 | pollutantUnit = 'AQI'; 407 | } else { 408 | pollutantUnit = 'pollutant unit'; 409 | mainPollutant = 'main pollutant'; 410 | } 411 | } 412 | // Check if APL is an WAQI sensor (because the state is an integer). Returns 'NaN' if it is not a number 413 | if (typeof hass.states[aplSensor.config] != "undefined") { 414 | let aplParse = parseInt(hass.states[aqiSensor.config].state); 415 | aqiSensor.value = aplParse; 416 | if (!isNaN(aplParse)) { 417 | apl = APLdescription[getAQI()]; 418 | } else { 419 | let aplState = hass.states[aplSensor.config].state; 420 | apl = hass.localize("component.sensor.state.airvisual__pollutant_level." + aplState) 421 | } 422 | } else if (typeof hass.states[aqiSensor.config] != "undefined") { 423 | aqiSensor.value = hass.states[aqiSensor.config].state; 424 | apl = APLdescription[getAQI()]; 425 | } 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | let faceHTML = ``; 434 | 435 | let card_content = `
`; 436 | if (!hideTitle) { 437 | card_content += `
${city} Air Quality Index
`; 438 | } 439 | 440 | if (weatherEntity.split('.')[0] == 'weather' && hass.states[weatherEntity]) { 441 | tempValue = hass.states[weatherEntity].attributes['temperature'] + 'º'; 442 | currentCondition = hass.states[weatherEntity].state; 443 | humidity = hass.states[weatherEntity].attributes['humidity'] + '%'; 444 | windSpeed = hass.states[weatherEntity].attributes['wind_speed'] + ' ' + speed_unit; 445 | } 446 | if (!hideWeather) { 447 | card_content += ` 448 |
${tempValue}
449 |
${humidity}
450 |
${windSpeed}
451 | `; 452 | } 453 | 454 | 455 | if (!hideFace){ 456 | card_content += ` 457 |
458 | 459 |
460 | `; 461 | } 462 | 463 | if (!hideAQI){ 464 | card_content += ` 465 |
466 | ${aqiSensor.value}
467 |
${country} ${unitOfMeasurement}
468 | `; 469 | } 470 | if (!hideAPL){ 471 | card_content += ` 472 |
473 | ${apl} 474 |
475 |
476 |
${mainPollutant} | ${pollutantUnit}
477 |
478 | `; 479 | } 480 | card_content += ` 481 |
482 | `; 483 | 484 | 485 | root.lastChild.hass = hass; 486 | root.querySelector('#content').innerHTML = card_content; 487 | 488 | // hard-coded version of click event 489 | if (!hideFace){ 490 | card.shadowRoot.querySelector('#face').addEventListener('click', event => { // when selecting HTML id, do not use dash '-' 491 | fireEvent(this, "hass-more-info", { entityId: aqiSensor.config }); 492 | }); 493 | } 494 | if (!hideAQI){ 495 | card.shadowRoot.querySelector('#aqiSensor').addEventListener('click', event => { // when selecting HTML id, do not use dash '-' 496 | fireEvent(this, "hass-more-info", { entityId: aqiSensor.config }); 497 | }); 498 | } 499 | if (!hideAPL){ 500 | card.shadowRoot.querySelector('#aplSensor').addEventListener('click', event => { // when selecting HTML id, do not use dash '-' 501 | fireEvent(this, "hass-more-info", { entityId: aplSensor.config }); 502 | }); 503 | card.shadowRoot.querySelector('#mainPollutantSensor').addEventListener('click', event => { // when selecting HTML id, do not use dash '-' 504 | fireEvent(this, "hass-more-info", { entityId: mainPollutantSensor.config }); 505 | }); 506 | } 507 | } 508 | 509 | // The height of your card. Home Assistant uses this to automatically 510 | // distribute all cards over the available columns. 511 | getCardSize() { 512 | return 1; 513 | } 514 | } 515 | 516 | customElements.define('air-visual-card', AirVisualCard); 517 | 518 | // Configure the preview in the Lovelace card picker 519 | // https://developers.home-assistant.io/docs/frontend/custom-ui/lovelace-custom-card/ 520 | window.customCards = window.customCards || []; 521 | window.customCards.push({ 522 | type: 'air-visual-card', 523 | name: 'Air Visual Card', 524 | preview: false, 525 | description: 'This is a Home Assistant Lovelace card that uses the AirVisual Sensor to provide air quality index (AQI) data and creates a card like the ones found on AirVisual website. Requires the AirVisual Sensor to be setup. Tested with Yahoo and Darksky Weather component.' 526 | }); 527 | --------------------------------------------------------------------------------