+
+Version 2.x.x introduced the following breaking changes:
+* eventColor is no longer supported
+* progressBar is no longer supported
+* explicit adding moment.js to YAML config is no longer needed
+* Card was convertered from a module to js (see new YAML example below)
+
Features
-* show the next 5 events on your Google Calendar (default set by home assistant)
+* Show the next 5 events on your Google Calendar (default set by home assistant)
* Set custom time format for each event
-* click on event to open in your Google calendar app
+* Click on event to open in your Google calendar app
* Integrate multiple calendars
* Update notifications via custom_updater
-* Show event color
+* Click on event location to open maps app
Track Updates
@@ -35,11 +44,9 @@ You should have setup Google calendar integration or Caldav integration in HomeA
| ---- | ---- | ------- | -----------
| type | string | **Required** | `custom:calendar-card`
| title | string | **Optional** | `Calendar` Header shown at top of card
-| showProgressBar | boolean | **Optional** | `true` Option to show the progress bar
| numberOfDays | number | **Optional** | `7` Number of days to display from calendars
| entities | object | **Required** | List of calendars to display
| timeFormat | string | **Optional** | `HH:mm` Format to show event time (see [here](https://momentjs.com/docs/#/displaying/format/) for options)
-| showColors | boolean | **Optional** | `false` Add event color marker to event summary
Configuration
Go to your config directory and create a www folder. Inside the www run
@@ -52,10 +59,8 @@ In your ui-lovelace.yaml
```
resources:
- - url: https://unpkg.com/moment@2.23.0/moment.js
+ - url: /local/calendar-card/calendar-card.js?v=2.0.0
type: js
- - url: /local/calendar-card/calendar-card.js?v=1.2.2
- type: module
```
Add the custom card to views:
diff --git a/calendar-card.js b/calendar-card.js
index 45ceb2e..77acdba 100644
--- a/calendar-card.js
+++ b/calendar-card.js
@@ -1,113 +1,157 @@
-/**
- *
- */
-class CalendarCard extends HTMLElement {
-
-
+var LitElement = LitElement || Object.getPrototypeOf(customElements.get("hui-error-entity-row"));
+var html = LitElement.prototype.html;
+
+class CalendarCard extends LitElement {
+
+ static get properties() {
+ return {
+ hass: Object,
+ config: Object,
+ content: Object
+ };
+ }
+
+ constructor() {
+ super();
+
+ this.content = html``;
+ this.isSomethingChanged = true;
+ this.events;
+ this.lastUpdate;
+ this.isUpdating = false;
+
+ this.momentSrc = 'https://unpkg.com/moment@2.23.0/moment.js';
+ }
+
/**
- * called by hass - creates card, sets up any conmfig settings, and generates card
- * @param {[type]} hass [description]
- * @return {[type]} [description]
+ * merge the user configuration with default configuration
+ * @param {[type]} config
*/
- set hass(hass) {
-
- // if we don't have the card yet then create it
- if (!this.content) {
- const card = document.createElement('ha-card');
- card.header = this.config.title;
- this.content = document.createElement('div');
- this.content.style.padding = '0 16px 10px';
- card.appendChild(this.content);
- this.appendChild(card);
- moment.locale(hass.language);
+ setConfig(config) {
+ if (!config.entities) {
+ throw new Error('You need to define at least one calendar entity via entities');
}
- // save an instance of hass for later
- this._hass = hass;
-
- // save css rules
- this.cssRules = `
-
`;
+ }
- // update card with calendars
- this
- .getAllEvents(this.config.entities)
- .then(events => this.updateHtmlIfNecessary(events))
- .catch(error => console.log('error', error));
+ updated(){
+ this.isSomethingChanged = false;
}
+ render(){
+ (async () => {
+ try {
+
+ // since this is async then we need to know
+ // when we are updating outisde the LitElement hooks
+ if (this.isUpdating) return;
+ this.isUpdating = true;
+
+ await this.addScript();
+
+ const events = await this.getAllEvents(this.config.entities);
+
+ if (!this.isSomethingChanged) {
+ this.isUpdating = false;
+ return;
+ }
+
+ await this.updateCard(events);
+ this.isUpdating = false;
+
+ } catch(e){
+ console.log(e);
+ }
+ })();
+
+ return this.content;
+ }
+
/**
- * [getAllEvents description]
- * @param {[type]} entities [description]
- * @return {[type]} [description]
+ * gets all events for all calendars added to this card's config
+ * @param {CalendarEntity[]} entities
+ * @return {Promise>}
*/
async getAllEvents(entities) {
// don't update if it's only been 15 min
if(this.lastUpdate && moment().diff(this.lastUpdate, 'minutes') <= 15) {
+ console.log('testtt');
return this.events;
}
@@ -118,78 +162,32 @@ class CalendarCard extends HTMLElement {
const end = today.add(this.config.numberOfDays, 'days').format(dateFormat);
// generate urls for calendars and get each calendar data
- const urls = this.createCalendarUrls(entities);
- const events = await this.getAllUrls(urls);
-
- // show progress bar if turned on
- if (this.config.showProgressBar && events.length > 0 && moment().format('DD') === moment(events[0].startDateTime).format('DD')) {
- let now = {startDateTime: moment().format(), type: 'now'}
- events.push(now);
- }
+ const urls = entities.map(entity => `calendars/${entity}?start=${start}Z&end=${end}Z`);
+ let allResults = await this.getAllUrls(urls);
+ // convert each calendar object to a UI event
+ let events = [].concat.apply([], allResults).map(event => new CalendarEvent(event));
+
// sort events by date starting with soonest
events.sort((a, b) => new Date(a.startDateTime) - new Date(b.startDateTime));
// see if anything changed since last time, save events, and update last time we updated
- const isSomethingChanged = this.isSomethingChanged(events);
+ console.log(events, this.events);
+ this.isSomethingChanged = JSON.stringify(events) !== JSON.stringify(this.events);
this.events = events;
this.lastUpdate = moment();
- return { events, isSomethingChanged };
- }
-
- /**
- * generate calendar urls to get calendars
- * @param {Array} entities
- * @return {Array}
- */
- createCalendarUrls(entities){
-
- // create url params
- let start = new Date();
- start = this.getFormattedDate(start);
-
- let end = new Date();
- end = this.addDays(end, this.config.numberOfDays);
- end = this.getFormattedDate(end);
- // generate urls for calendars and get each calendar data
- return entities.map(entity => `calendars/${entity}?start=${start}&end=${end}`);
- }
-
- /**
- * get date in YYYY-MM-DDTHH:MM:SST format
- * @param {Date} date the date object to format
- * @return {string}
- */
- getFormattedDate(date){
- const month = ( '0' + (date.getMonth()+1) ).slice(-2);
- const day = ( '0' + date.getDate() ).slice(-2);
- const year = date.getFullYear();
- return `${year}-${month}-${day}T00:00:00Z`;
- }
-
- /**
- * [addDays description]
- * @param {Date} date the date object
- * @param {number} days number of days to add
- * @return {Date} new date object with days added
- */
- addDays(date, days) {
- let newDate = new Date(date.valueOf());
- newDate.setDate(newDate.getDate() + days);
- return newDate;
+ return events;
}
/**
* given a list of urls get the data from them
* @param {Array} urls
- * @return {Array}
+ * @return {Promise>}
*/
async getAllUrls(urls) {
try {
- const allResults = await Promise.all(urls.map(url => this._hass.callApi('get', url)));
- return [].concat.apply([], allResults).map(event => new CalendarEvent(event));
-
+ return await Promise.all(urls.map(url => this.__hass.callApi('get', url)));
} catch (error) {
throw error;
}
@@ -197,205 +195,151 @@ class CalendarCard extends HTMLElement {
/**
* updates the entire card if we need to
- * @param {[type]} eventList [description]
- * @return {[type]} [description]
- */
- updateHtmlIfNecessary(eventList) {
- if(!eventList.isSomethingChanged) return;
-
- // save CSS rules then group events by day
- this.content.innerHTML = this.cssRules;
- const events = eventList.events;
- const groupedEventsPerDay = this.groupBy(events, event => moment(event.startDateTime).format('YYYY-MM-DD'));
-
- // for each group event create a UI 'day'
- groupedEventsPerDay.forEach((events, day) => {
- const eventStateCardContentElement = document.createElement('div');
- eventStateCardContentElement.classList.add('day-wrapper');
- eventStateCardContentElement.innerHTML = this.getDayHtml(day, events);
- this.content.append(eventStateCardContentElement);
- });
- }
-
- /**
- * generates the HTML for a single day
- * @param {[type]} day [description]
- * @param {[type]} events [description]
- * @return {[type]} [description]
+ * @param {Array} eventList
+ * @return {TemplateResult}
*/
- getDayHtml(day, events) {
- const className = moment().format('DD') === moment(day).format('DD') ? 'date now' : 'date';
- let momentDay = moment(day);
-
- return `
-
+
+ `;
}
/**
- * gets the ebent title with a colored marker if user wants
- * @return {[type]} [description]
+ * group events by the day it's on
+ * @param {Array} events
+ * @return {Array
`
- }
-
- return locationHtml;
- }
-
- /**
- * merge the user configuration with default configuration
- * @param {[type]} config [description]
- */
- setConfig(config) {
- if (!config.entities) {
- throw new Error('You need to define at least one calendar entity via entities');
- }
-
- this.config = {
- title: 'Calendar',
- showProgressBar: true,
- numberOfDays: 7,
- showColors: false,
- timeFormat: 'HH:mm',
- ...config
- };
- }
-
- /**
- * get the size of the card
- * @return {[type]} [description]
- */
- getCardSize() {
- return 3;
- }
-
- /**
- * did any event change since the last time we checked?
- * @param {[type]} events [description]
- * @return {Boolean} [description]
- */
- isSomethingChanged(events) {
- let isSomethingChanged = JSON.stringify(events) !== JSON.stringify(this.events);
- return isSomethingChanged;
- }
-
- /**
- * ddep clone a js object
- * @param {[type]} obj [description]
- * @return {[type]} [description]
- */
- deepClone(obj) {
- return JSON.parse(JSON.stringify(obj));
+ if (!event.location || !event.locationAddress)
+ return html``;
+
+ return html`
+
+
+
+ `;
}
/**
- * group evbents by a givenkey
- * @param {[type]} list [description]
- * @param {[type]} keyGetter [description]
- * @return {[type]} [description]
+ * adds the moment.js script
+ * @return {Promise}
*/
- groupBy(list, keyGetter) {
- const map = new Map();
-
- list.forEach(item => {
- const key = keyGetter(item);
- const collection = map.get(key);
-
- if (!collection) {
- map.set(key, [item]);
- } else {
- collection.push(item);
- }
+ async addScript() {
+ return new Promise(resolve => {
+ if(window.moment) return resolve();
+
+ const s = document.createElement('script');
+ s.setAttribute('src', this.momentSrc);
+ s.onload = () => {
+ moment.locale(this.hass.language);
+ return resolve();
+ };
+
+ document.body.appendChild(s);
});
-
- return map;
}
}
+customElements.define('calendar-card', CalendarCard);
+
+
/**
- * Creaates an generalized Calendar Event to use when creating the calendar card
+ * Creates an generalized Calendar Event to use when creating the calendar card
* There can be Google Events and CalDav Events. This class normalizes those
*/
class CalendarEvent {
-
+
/**
- * [constructor description]
- * @param {[type]} calendarEvent [description]
- * @return {[type]} [description]
+ * @param {Object} calendarEvent
*/
constructor(calendarEvent) {
this.calendarEvent = calendarEvent;
@@ -403,11 +347,11 @@ class CalendarEvent {
/**
* get the start time for an event
- * @return {[type]} [description]
+ * @return {String}
*/
get startDateTime() {
if (this.calendarEvent.start.date) {
- let dateTime = moment(this.calendarEvent.start.date);
+ const dateTime = moment(this.calendarEvent.start.date);
return dateTime.toISOString();
}
@@ -416,7 +360,7 @@ class CalendarEvent {
/**
* get the end time for an event
- * @return {[type]} [description]
+ * @return {String}
*/
get endDateTime() {
return this.calendarEvent.end && this.calendarEvent.end.dateTime || this.calendarEvent.end || '';
@@ -424,23 +368,23 @@ class CalendarEvent {
/**
* get the URL for an event
- * @return {[type]} [description]
+ * @return {String}
*/
- get htmlLink(){
- return this.calendarEvent.htmlLink;
+ get htmlLink() {
+ return this.calendarEvent.htmlLink || '';
}
/**
* get the title for an event
- * @return {[type]} [description]
+ * @return {String}
*/
get title() {
- return this.calendarEvent.summary || this.calendarEvent.title;
+ return this.calendarEvent.summary || this.calendarEvent.title || '';
}
/**
* get the description for an event
- * @return {[type]} [description]
+ * @return {String}
*/
get description() {
return this.calendarEvent.description;
@@ -448,34 +392,30 @@ class CalendarEvent {
/**
* parse location for an event
- * @return {[type]} [description]
+ * @return {String}
*/
get location() {
- if(this.calendarEvent.location) {
- return this.calendarEvent.location.split(',')[0]
- }
-
- return undefined;
+ if (!this.calendarEvent.location) return '';
+ return this.calendarEvent.location.split(',')[0] || '';
}
/**
* get location address for an event
- * @return {[type]} [description]
+ * @return {String}
*/
get locationAddress() {
- if(this.calendarEvent.location) {
- let address = this.calendarEvent.location.substring(this.calendarEvent.location.indexOf(',') + 1);
- return address.split(' ').join('+');
- }
- return undefined;
+ if (!this.calendarEvent.location) return '';
+
+ let address = this.calendarEvent.location.substring(this.calendarEvent.location.indexOf(',') + 1);
+ return address.split(' ').join('+');
}
/**
* is the event a full day event?
- * @return {Boolean} [description]
+ * @return {Boolean}
*/
get isFullDayEvent() {
- if (this.calendarEvent.start && this.calendarEvent.start.date){
+ if (this.calendarEvent.start && this.calendarEvent.start.date) {
return this.calendarEvent.start.date;
}
@@ -484,9 +424,4 @@ class CalendarEvent {
let diffInHours = end.diff(start, 'hours');
return diffInHours >= 24;
}
-}
-
-/**
- * add card definition to hass
- */
-customElements.define('calendar-card', CalendarCard);
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/custom_updater.json b/custom_updater.json
index 6f76a61..af6356b 100644
--- a/custom_updater.json
+++ b/custom_updater.json
@@ -1,7 +1,7 @@
{
"calendar-card": {
- "updated_at": "2018-12-26",
- "version": "1.2.2",
+ "updated_at": "2019-02-06",
+ "version": "2.0.0",
"remote_location": "https://raw.githubusercontent.com/ljmerza/calendar-card/master/calendar-card.js",
"visit_repo": "https://github.com/ljmerza/calendar-card",
"changelog": "https://github.com/ljmerza/calendar-card/releases/latest"