Lovelace Custom Fan Card Example

What did you change in the theme to get the button colors to be different?

Iā€™m using the clear theme.

https://community.home-assistant.io/t/clear-theme/100464

Thanks.

It looks like itā€™s using the primary-color for the dark (not-disabled) button color. I canā€™t even figure how to split that out and make it individually customizable. there is a reference to ā€œmdc-theme-primaryā€ in the home-assistant-polymer github style page which I thought should have selected the color for the buttons but I tried that and it didnā€™t work either.

Maybe this can help

It might if I knew what to do with it! :grinning:

Iā€™m not sure from that if they fixed it everywhere in HA or just put that work around in for the button card. Iā€™m on V88.1 now and it looks like itā€™s still broken for HA in general.

I posted an issue on the HA-polymer github so maybe it will get addressed.

UPDATE:

Balloob answered on the github issue and said that there was nothing that could be done about the mwc-button issue since it was maintained by a third party but he made a suggestion that, thinking a little bit about it, is now obvious - just use a regular button. Duh!

So with that in mind here is the code:

class CustomFanCard extends Polymer.Element {

    static get template() {
        return Polymer.html`
            <style is="custom-style" include="iron-flex iron-flex-alignment"></style>
            <style>
                :host {
                    line-height: inherit;
                }
                .speed {
                    min-width: 30px;
                    max-width: 30px;
                    height: 30px;
                    margin-left: 2px;
                    margin-right: 2px;
                    background-color:#759aaa;
	                border: 1px solid lightgrey; 
                    border-radius: 4px;
	                font-size: 10px !important;
                    color: inherit;
                    text-align: center;
	                float: right !important;
                    padding: 1px;
		    }
				
            </style>
            <hui-generic-entity-row hass="[[hass]]" config="[[_config]]">
                <div class='horizontal justified layout' on-click="stopPropagation">
                    <button
                            class='speed'
                            style='[[_lowOnColor]]'
                            toggles name="low"
                            on-click='setSpeed'
                            disabled='[[_isOnLow]]'>LOW</button>
                    <button
                            class='speed'
                            style='[[_medOnColor]]'
                            toggles name="medium"
                            on-click='setSpeed'
                            disabled='[[_isOnMed]]'>MED</button>
                    <button
                            class='speed'
                            style='[[_highOnColor]]'
                            toggles name="high"
                            on-click='setSpeed'
                            disabled='[[_isOnHigh]]'>HIGH</button>
                    <button
                            class='speed'
                            style='[[_offColor]]'
                            toggles name="off"
                            on-click='setSpeed'
                            disabled='[[_isOffState]]'>OFF</button>
                    </div>
            </hui-generic-entity-row>
        `;
    }

    static get properties() {
        return {
            hass: {
                type: Object,
                observer: 'hassChanged'
            },
            _config: Object,
            _stateObj: Object,
            _lowOnColor: String,
            _medOnColor: String,
            _highOnColor: String,
            _offColor: String,
            _isOffState: Boolean,
            _isOnState: Boolean,
            _isOnLow: Boolean,
            _isOnMed: Boolean,
            _isOnHigh: Boolean
        }
    }

    setConfig(config) {
        this._config = config;
    }

    hassChanged(hass) {

        const config = this._config;
        const stateObj = hass.states[config.entity];

        let speed;
        if (stateObj && stateObj.attributes) {
            speed = stateObj.attributes.speed || 'off';
        }
		
        let low;
	    let med;
	    let high;
	    let offstate;
		
	    if (stateObj && stateObj.attributes) {
	        if (stateObj.state == 'on' && stateObj.attributes.speed == 'low') {
		        low = 'on';
		} else if (stateObj.state == 'on' && stateObj.attributes.speed == 'medium') {
		        med = 'on';
		} else if (stateObj.state == 'on' && stateObj.attributes.speed == 'high') {
		        high = 'on';
		} else {
			offstate = 'on';
		}
	}
		
        let lowcolor;
        let medcolor;
	    let hicolor;
	    let offcolor;
		
	    if (low == 'on') {
            lowcolor = 'background-color: #43A047';
	    } else { 
            lowcolor = '';
	    }
	
        if (med == 'on') {
            medcolor = 'background-color: #43A047';
	    } else {
            medcolor = '';
	    }
	
	    if (high == 'on') {
            hicolor = 'background-color: #43A047';
	    } else {
            hicolor = '';
	    }
		
	    if (offstate == 'on') {
            //offcolor = 'background-color: #43A047';
            offcolor = 'background-color: #f44c09';
	    } else {
            offcolor = '';
	    }
		
	this.setProperties({
        _stateObj: stateObj,
	    _isOffState: stateObj.state == 'off',
        _isOnLow: low === 'on',
	    _isOnMed: med === 'on',
	    _isOnHigh: high === 'on',
	    _lowOnColor: lowcolor,
	    _medOnColor: medcolor,
	    _highOnColor: hicolor,
	    _offColor: offcolor
    });
}

    stopPropagation(e) {
        e.stopPropagation();
    }

    setSpeed(e) {
        const speed = e.currentTarget.getAttribute('name');
        this.hass.callService('fan', 'set_speed', {
            entity_id: this._config.entity, speed: speed
        });
    }

}

customElements.define('custom-fan-card', CustomFanCard);

and the result:

ex

the only difference I can see between this one and the previous paper-button is that the text on the disabled button doesnā€™t grey out. Not a big deal for me since I use the colors to see the state.

1 Like

Building from @slipx06 and @finity, I switched to a button, then copied out all the shadow dom css from mwc-button into this element. I havenā€™t tried, but Iā€™m hoping itā€™ll pick up most theme changes this way.

class CustomFanCard extends Polymer.Element {

    static get template() {
        return Polymer.html`
            <style is="custom-style" include="iron-flex iron-flex-alignment"></style>
            <style>
                .flex-container {
                    display: flex;
                    justify-content: center;
                    align-items: center;
                }
                @keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}.mdc-ripple-surface--test-edge-var-bug{--mdc-ripple-surface-test-edge-var: 1px solid #000;visibility:hidden}.mdc-ripple-surface--test-edge-var-bug::before{border:var(--mdc-ripple-surface-test-edge-var)}.mdc-button{font-family:Roboto,sans-serif;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-size:.875rem;line-height:2.25rem;font-weight:500;letter-spacing:.0892857143em;text-decoration:none;text-transform:uppercase;--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity;padding:0 8px 0 8px;display:inline-flex;position:relative;align-items:center;justify-content:center;box-sizing:border-box;min-width:64px;height:36px;border:none;outline:none;line-height:inherit;user-select:none;-webkit-appearance:none;overflow:hidden;vertical-align:middle;border-radius:4px}.mdc-button::before,.mdc-button::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-button::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-button.mdc-ripple-upgraded::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-button.mdc-ripple-upgraded::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-button.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-button.mdc-ripple-upgraded--foreground-activation::after{animation:225ms mdc-ripple-fg-radius-in forwards,75ms mdc-ripple-fg-opacity-in forwards}.mdc-button.mdc-ripple-upgraded--foreground-deactivation::after{animation:150ms mdc-ripple-fg-opacity-out;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-button::before,.mdc-button::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-button.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-button::-moz-focus-inner{padding:0;border:0}.mdc-button:active{outline:none}.mdc-button:hover{cursor:pointer}.mdc-button:disabled{background-color:transparent;color:rgba(0,0,0,.37);cursor:default;pointer-events:none}.mdc-button.mdc-button--dense{border-radius:4px}.mdc-button:not(:disabled){background-color:transparent}.mdc-button:not(:disabled){color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}.mdc-button::before,.mdc-button::after{background-color:#6200ee}@supports not (-ms-ime-align: auto){.mdc-button::before,.mdc-button::after{background-color:var(--mdc-theme-primary, #6200ee)}}.mdc-button:hover::before{opacity:.04}.mdc-button:not(.mdc-ripple-upgraded):focus::before,.mdc-button.mdc-ripple-upgraded--background-focused::before{transition-duration:75ms;opacity:.12}.mdc-button:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-button:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.16}.mdc-button.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.16}.mdc-button .mdc-button__icon{margin-left:0;margin-right:8px;display:inline-block;width:18px;height:18px;font-size:18px;vertical-align:top}[dir=rtl] .mdc-button .mdc-button__icon,.mdc-button .mdc-button__icon[dir=rtl]{margin-left:8px;margin-right:0}.mdc-button svg.mdc-button__icon{fill:currentColor}.mdc-button--raised .mdc-button__icon,.mdc-button--unelevated .mdc-button__icon,.mdc-button--outlined .mdc-button__icon{margin-left:-4px;margin-right:8px}[dir=rtl] .mdc-button--raised .mdc-button__icon,.mdc-button--raised .mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--unelevated .mdc-button__icon,.mdc-button--unelevated .mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--outlined .mdc-button__icon,.mdc-button--outlined .mdc-button__icon[dir=rtl]{margin-left:8px;margin-right:-4px}.mdc-button--raised,.mdc-button--unelevated{padding:0 16px 0 16px}.mdc-button--raised:disabled,.mdc-button--unelevated:disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.37)}.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){background-color:#6200ee}@supports not (-ms-ime-align: auto){.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){background-color:var(--mdc-theme-primary, #6200ee)}}.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){color:#fff;color:var(--mdc-theme-on-primary, #fff)}.mdc-button--raised::before,.mdc-button--raised::after,.mdc-button--unelevated::before,.mdc-button--unelevated::after{background-color:#fff}@supports not (-ms-ime-align: auto){.mdc-button--raised::before,.mdc-button--raised::after,.mdc-button--unelevated::before,.mdc-button--unelevated::after{background-color:var(--mdc-theme-on-primary, #fff)}}.mdc-button--raised:hover::before,.mdc-button--unelevated:hover::before{opacity:.08}.mdc-button--raised:not(.mdc-ripple-upgraded):focus::before,.mdc-button--raised.mdc-ripple-upgraded--background-focused::before,.mdc-button--unelevated:not(.mdc-ripple-upgraded):focus::before,.mdc-button--unelevated.mdc-ripple-upgraded--background-focused::before{transition-duration:75ms;opacity:.24}.mdc-button--raised:not(.mdc-ripple-upgraded)::after,.mdc-button--unelevated:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-button--raised:not(.mdc-ripple-upgraded):active::after,.mdc-button--unelevated:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.32}.mdc-button--raised.mdc-ripple-upgraded,.mdc-button--unelevated.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.32}.mdc-button--raised{box-shadow:0px 3px 1px -2px rgba(0, 0, 0, 0.2),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 1px 5px 0px rgba(0,0,0,.12);transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-button--raised:hover,.mdc-button--raised:focus{box-shadow:0px 2px 4px -1px rgba(0, 0, 0, 0.2),0px 4px 5px 0px rgba(0, 0, 0, 0.14),0px 1px 10px 0px rgba(0,0,0,.12)}.mdc-button--raised:active{box-shadow:0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0,0,0,.12)}.mdc-button--raised:disabled{box-shadow:0px 0px 0px 0px rgba(0, 0, 0, 0.2),0px 0px 0px 0px rgba(0, 0, 0, 0.14),0px 0px 0px 0px rgba(0,0,0,.12)}.mdc-button--outlined{border-style:solid;padding:0 14px 0 14px;border-width:2px}.mdc-button--outlined:disabled{border-color:rgba(0,0,0,.37)}.mdc-button--outlined:not(:disabled){border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-button--dense{height:32px;font-size:.8125rem}.material-icons{font-family:var(--mdc-icon-font, "Material Icons");font-weight:normal;font-style:normal;font-size:var(--mdc-icon-size, 24px);line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-feature-settings:"liga";-webkit-font-smoothing:antialiased}:host{display:inline-flex;outline:none}.mdc-button{flex:1}
                :host {
                    display: inherit;
                }
                .speeds button:host {
                    display: inline-flex;
                    outline: none;
                }
                .speeds button {
                    min-width: 34px !important;
                    width: 34px;
                }
                /*ha-entity-toggle {
                    margin-left: 16px;
                }*/
            </style>
            <hui-generic-entity-row hass="[[hass]]" config="[[_config]]">
                <div class='flex-container' on-click="_stopPropagation">
                    <div class="speeds">
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="off"
                            on-click='_setSpeed'
                            disabled='[[_isOff]]'>
                            <span class="mdc-button__label">OFF</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="low"
                            on-click='_setSpeed'
                            disabled='[[_isLowSpeed]]'>
                            <span class="mdc-button__label">LO</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="medium"
                            on-click='_setSpeed'
                            disabled='[[_isMedSpeed]]'>
                            <span class="mdc-button__label">MED</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="high"
                            on-click='_setSpeed'
                            disabled='[[_isHghSpeed]]'>
                            <span class="mdc-button__label">HI</span>
                        </button>
                    </div>
                    <!--<ha-entity-toggle hass="[[hass]]" state-obj="[[_stateObj]]"></ha-entity-toggle>-->
                </div>
            </hui-generic-entity-row>
        `;
    }

    static get properties() {
        return {
            hass: {
                type: Object,
                observer: '_hassChanged'
            },
            _config: Object,
            _stateObj: Object,
            _isOff: Boolean,
            _isLowSpeed: Boolean,
            _isMedSpeed: Boolean,
            _isHghSpeed: Boolean
        }
    }

    setConfig(config) {
        this._config = config;
    }

    _hassChanged(hass) {

        const config = this._config;
        const stateObj = hass.states[config.entity];

        let speed;
        if (stateObj && stateObj.attributes) {
            speed = stateObj.attributes.speed || 'off';
        }

        this.setProperties({
            _stateObj: stateObj,
            _isOff: speed === 'off',
            _isLowSpeed: speed == 'low',
            _isMedSpeed: speed === 'medium',
            _isHghSpeed: speed === 'high'
        });
    }

    _stopPropagation(e) {
        e.stopPropagation();
    }

    _setSpeed(e) {
        const speed = e.currentTarget.getAttribute('name');
        this.hass.callService('fan', 'set_speed', {
            entity_id: this._config.entity, speed: speed
        });
    }

}

customElements.define('custom-fan-card', CustomFanCard);

and the result:

image

image

2 Likes

It looks good. I love a good group effort that gets us all where we want to end up! :slightly_smiling_face:

@rhodges Thank you. This is my updated code based on some of your changes.

class CustomFanCard extends Polymer.Element {

    static get template() {
        return Polymer.html`
            <style>
                .flex-container {
                    display: flex;
                    justify-content: center;
                    align-items: center;
                }
                @keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}.mdc-ripple-surface--test-edge-var-bug{--mdc-ripple-surface-test-edge-var: 1px solid #000;visibility:hidden}.mdc-ripple-surface--test-edge-var-bug::before{border:var(--mdc-ripple-surface-test-edge-var)}.mdc-button{font-family:Roboto,sans-serif;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-size:.875rem;line-height:2.25rem;font-weight:500;letter-spacing:.0892857143em;text-decoration:none;text-transform:uppercase;--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity;padding:0 8px 0 8px;display:inline-flex;position:relative;align-items:center;justify-content:center;box-sizing:border-box;min-width:64px;height:36px;border:none;outline:none;line-height:inherit;user-select:none;-webkit-appearance:none;overflow:hidden;vertical-align:middle;border-radius:4px}.mdc-button::before,.mdc-button::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-button::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-button.mdc-ripple-upgraded::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-button.mdc-ripple-upgraded::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-button.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-button.mdc-ripple-upgraded--foreground-activation::after{animation:225ms mdc-ripple-fg-radius-in forwards,75ms mdc-ripple-fg-opacity-in forwards}.mdc-button.mdc-ripple-upgraded--foreground-deactivation::after{animation:150ms mdc-ripple-fg-opacity-out;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-button::before,.mdc-button::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-button.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-button::-moz-focus-inner{padding:0;border:0}.mdc-button:active{outline:none}.mdc-button:hover{cursor:pointer}.mdc-button:disabled{background-color:transparent;color:rgba(0,0,0,.37);cursor:default;pointer-events:none}.mdc-button.mdc-button--dense{border-radius:4px}.mdc-button:not(:disabled){background-color:transparent}.mdc-button:not(:disabled){color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}.mdc-button::before,.mdc-button::after{background-color:#6200ee}@supports not (-ms-ime-align: auto){.mdc-button::before,.mdc-button::after{background-color:var(--mdc-theme-primary, #6200ee)}}.mdc-button:hover::before{opacity:.04}.mdc-button:not(.mdc-ripple-upgraded):focus::before,.mdc-button.mdc-ripple-upgraded--background-focused::before{transition-duration:75ms;opacity:.12}.mdc-button:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-button:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.16}.mdc-button.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.16}.mdc-button .mdc-button__icon{margin-left:0;margin-right:8px;display:inline-block;width:18px;height:18px;font-size:18px;vertical-align:top}[dir=rtl] .mdc-button .mdc-button__icon,.mdc-button .mdc-button__icon[dir=rtl]{margin-left:8px;margin-right:0}.mdc-button svg.mdc-button__icon{fill:currentColor}.mdc-button--raised .mdc-button__icon,.mdc-button--unelevated .mdc-button__icon,.mdc-button--outlined .mdc-button__icon{margin-left:-4px;margin-right:8px}[dir=rtl] .mdc-button--raised .mdc-button__icon,.mdc-button--raised .mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--unelevated .mdc-button__icon,.mdc-button--unelevated .mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--outlined .mdc-button__icon,.mdc-button--outlined .mdc-button__icon[dir=rtl]{margin-left:8px;margin-right:-4px}.mdc-button--raised,.mdc-button--unelevated{padding:0 16px 0 16px}.mdc-button--raised:disabled,.mdc-button--unelevated:disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.37)}.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){background-color:#6200ee}@supports not (-ms-ime-align: auto){.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){background-color:var(--mdc-theme-primary, #6200ee)}}.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){color:#fff;color:var(--mdc-theme-on-primary, #fff)}.mdc-button--raised::before,.mdc-button--raised::after,.mdc-button--unelevated::before,.mdc-button--unelevated::after{background-color:#fff}@supports not (-ms-ime-align: auto){.mdc-button--raised::before,.mdc-button--raised::after,.mdc-button--unelevated::before,.mdc-button--unelevated::after{background-color:var(--mdc-theme-on-primary, #fff)}}.mdc-button--raised:hover::before,.mdc-button--unelevated:hover::before{opacity:.08}.mdc-button--raised:not(.mdc-ripple-upgraded):focus::before,.mdc-button--raised.mdc-ripple-upgraded--background-focused::before,.mdc-button--unelevated:not(.mdc-ripple-upgraded):focus::before,.mdc-button--unelevated.mdc-ripple-upgraded--background-focused::before{transition-duration:75ms;opacity:.24}.mdc-button--raised:not(.mdc-ripple-upgraded)::after,.mdc-button--unelevated:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-button--raised:not(.mdc-ripple-upgraded):active::after,.mdc-button--unelevated:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.32}.mdc-button--raised.mdc-ripple-upgraded,.mdc-button--unelevated.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.32}.mdc-button--raised{box-shadow:0px 3px 1px -2px rgba(0, 0, 0, 0.2),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 1px 5px 0px rgba(0,0,0,.12);transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-button--raised:hover,.mdc-button--raised:focus{box-shadow:0px 2px 4px -1px rgba(0, 0, 0, 0.2),0px 4px 5px 0px rgba(0, 0, 0, 0.14),0px 1px 10px 0px rgba(0,0,0,.12)}.mdc-button--raised:active{box-shadow:0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0,0,0,.12)}.mdc-button--raised:disabled{box-shadow:0px 0px 0px 0px rgba(0, 0, 0, 0.2),0px 0px 0px 0px rgba(0, 0, 0, 0.14),0px 0px 0px 0px rgba(0,0,0,.12)}.mdc-button--outlined{border-style:solid;padding:0 14px 0 14px;border-width:2px}.mdc-button--outlined:disabled{border-color:rgba(0,0,0,.37)}.mdc-button--outlined:not(:disabled){border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-button--dense{height:32px;font-size:.8125rem}.material-icons{font-family:var(--mdc-icon-font, "Material Icons");font-weight:normal;font-style:normal;font-size:var(--mdc-icon-size, 24px);line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-feature-settings:"liga";-webkit-font-smoothing:antialiased}:host{display:inline-flex;outline:none}.mdc-button{flex:1}
                :host {
                    display: inherit;
                }
                .speeds button:host {
                    display: inline-flex;
                    outline: none;
                }
                .speeds button {
                    min-width: 34px !important;
                    width: 34px;
                    font-size: 11px !important;
                }
            </style>
  
            <hui-generic-entity-row hass="[[hass]]" config="[[_config]]">
                <div class='flex-container' on-click="stopPropagation">
                   <div class="speeds">
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="off"
                            on-click='setSpeed'
                            disabled='[[_isOff]]'>
                            <span class="mdc-button__label">OFF</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="low"
                            on-click='setSpeed'
                            disabled='[[_isLowSpeed]]'>
                            <span class="mdc-button__label">LO</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="medium"
                            on-click='setSpeed'
                            disabled='[[_isMedSpeed]]'>
                            <span class="mdc-button__label">MED</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="high"
                            on-click='_setSpeed'
                            disabled='[[_isHghSpeed]]'>
                            <span class="mdc-button__label">HI</span>
                        </button>
                    </div>
            </hui-generic-entity-row>
        `;
    }

    static get properties() {
        return {
            hass: {
                type: Object,
                observer: 'hassChanged'
            },
            _config: Object,
            _stateObj: Object,
            _isOff: Boolean,
            _isLowSpeed: Boolean,
            _isMedSpeed: Boolean,
            _isHghSpeed: Boolean
        }
    }

    setConfig(config) {
        this._config = config;
    }

    hassChanged(hass) {

        const config = this._config;
        const stateObj = hass.states[config.entity];

        let speed;
        if (stateObj && stateObj.attributes) {
            speed = stateObj.attributes.speed || 'off';
        }

        this.setProperties({
            _stateObj: stateObj,
            _isOff: stateObj.state  === 'off',
            _isLowSpeed: speed === 'low' && stateObj.state === 'on',
            _isMedSpeed: speed === 'medium' && stateObj.state === 'on',
            _isHghSpeed: speed == 'high' && stateObj.state === 'on'
        });
    }

    stopPropagation(e) {
        e.stopPropagation();
    }

    setSpeed(e) {
        const speed = e.currentTarget.getAttribute('name');
        this.hass.callService('fan', 'set_speed', {
            entity_id: this._config.entity, speed: speed
        });
    }
}

customElements.define('custom-fan-card', CustomFanCard);

image

image

2 Likes

I vote this card gets moved in the core repoā€¦ :grin: Very nice work guys

Which one? Thereā€™s at least three versions right now in this thread for v88 alone! :laughing:

I vote for mine. It has more pretty colors. :heart_eyes:

@slipx06 @finity

Great card guys. Using 88.1 here and working great with slipx06ā€™s latest posted code. The only change Iā€™d like to make is to make the selected option highlighted with the rest greyed out. Any chance you point me to where in the code I could change this?

This is definity worthy of a github repo! Would be great to get added to the custom updater/tracker card too. :slightly_smiling_face:

the code that Iā€™m using is quite a bit different than the other one. Maybe I need to split it out of here and call it something else to minimize any confusion.

But if you use the code for my card you can make the on and off colors anything you want.

to set the on color your change the number after the # in these lines:

           if (low == 'on') {
			lowcolor = 'background-color: #43A047';
		} else {
			lowcolor = '';
		}
		
		if (med == 'on') {
			medcolor = 'background-color: #43A047';
		} else {
			medcolor = '';
		}
		
		if (high == 'on') {
			hicolor = 'background-color: #43A047';
		} else {
			hicolor = '';
		}
		
		if (offstate == 'on') {
			offcolor = 'background-color: #f44c09';
		} else {
			offcolor = '';
		}

then to change the off color just change the "background: " value under the ā€œ.speedā€ style section.

Using the other code I pretty sure you donā€™t have a choice in the colors. It just follows your theme.

Sorry @finity, I should have mentioned I did see those css settings in your code. I can basically follow your code but I really canā€™t make heads or tails of what is happening in slip06ā€™s version.
Your example there is the result I was trying to achieve though!

I actually chose to use @slipx06ā€™s code because it does follow the theme but with all the themes I tried it kinda looks like the ā€˜onā€™ option is actually the one that is off. On some themes the ā€˜onā€™ option basically disappears. For example, try the ā€œHalloweenā€ theme from the community themes package (not that I would ever actually use this one myself.).

I think this issue could be fixed by using a standard ā€˜greyed outā€™ disabled colour for the off options and using the theme colour for the selected or ā€˜onā€™ option. If I had any clue where to accomplish this I would just do it myself but like I said, itā€™s gobbledegook to me.

Also, perhaps there is a way to put a flag/option in the code somewhere so the user has a choice to use the theme or set custom colours as an option in the lovelace ui? Is that even possible? Just a thought.

I will note that one issue using your version is that one would have to go back and change the colour back every time the js file gets updated.

Either way, Iā€™m grateful for the card. It solves one of my long standing irritants with my system, so thank you!

I donā€™t think that is something youā€™ll have to worry about. Unless there is a change to the HA base code that requires this card to be forced to change (like what happened this time) then I donā€™t plan on modifying it. And since it doesnā€™t use the custom updater there is no way for it to automatically update your card anyway. So you have total control over what you do with the card. I kind of like that. :wink:

EDIT: unless, of course, the common name we are all using ā€œcustom-fan-cardā€ gets added to the custom updater by someone. Then I guess thatā€™s another reason why I should change the name of my card to something else. :thinking:

IMHO that is a great reason to utilize something like the custom updater. You never know whatā€™s going to happen, especially with a developing platform like Home Assistant.

For people for whom HA isnā€™t their primary hobby it does make keeping track of things much simpler! Heck even my utterly technically illiterate wife can handle updating everything when itā€™s a one click solution. :joy:

I see what youā€™re saying. Maybe Iā€™ll have to look into that too.

If you want to use @finityā€™s version with your theme colours try the code below. Credit to @undertoe. Iā€™ve made the font and buttons slightly bigger as I prefer how it looks. Iā€™ve also moved the ā€œoffā€ button to the start of the row.

class CustomFanCard extends Polymer.Element {

    static get template() {
        return Polymer.html`
            <style is="custom-style" include="iron-flex iron-flex-alignment"></style>
            <style>
                :host {
                    line-height: inherit;
                }
                .speed {
                    min-width: 34px;
                    max-width: 34px;
                    height: 34px;
                    margin-left: 2px;
                    margin-right: 2px;
                    background-color:'var(--dark-accent-color)';
	                border: 1px var(--dark-theme-disabled-color);  
                    border-radius: 4px;
	                font-size: 11px !important;
                    text-align: center;
	                float: right !important;
                    padding: 1px;
                    font-family : inherit;
		    }
				
            </style>
            <hui-generic-entity-row hass="[[hass]]" config="[[_config]]">
                <div class='horizontal justified layout' on-click="stopPropagation">
                    <button
                            class='speed'
                            style='[[_offColor]]'
                            toggles name="off"
                            on-click='setSpeed'
                            disabled='[[_isOffState]]'>OFF</button>
				    <button
                            class='speed'
                            style='[[_lowOnColor]]'
                            toggles name="low"
                            on-click='setSpeed'
                            disabled='[[_isOnLow]]'>LOW</button>
                    <button
                            class='speed'
                            style='[[_medOnColor]]'
                            toggles name="medium"
                            on-click='setSpeed'
                            disabled='[[_isOnMed]]'>MED</button>
                    <button
                            class='speed'
                            style='[[_highOnColor]]'
                            toggles name="high"
                            on-click='setSpeed'
                            disabled='[[_isOnHigh]]'>HIGH</button>

                    </div>
            </hui-generic-entity-row>
        `;
    }

    static get properties() {
        return {
            hass: {
                type: Object,
                observer: 'hassChanged'
            },
            _config: Object,
            _stateObj: Object,
            _lowOnColor: String,
            _medOnColor: String,
            _highOnColor: String,
            _offColor: String,
            _isOffState: Boolean,
            _isOnState: Boolean,
            _isOnLow: Boolean,
            _isOnMed: Boolean,
            _isOnHigh: Boolean
        }
    }

    setConfig(config) {
        this._config = config;
    }

    hassChanged(hass) {

        const config = this._config;
        const stateObj = hass.states[config.entity];

        let speed;
        if (stateObj && stateObj.attributes) {
            speed = stateObj.attributes.speed || 'off';
        }
		
        let low;
	    let med;
	    let high;
	    let offstate;
		
	    if (stateObj && stateObj.attributes) {
	        if (stateObj.state == 'on' && stateObj.attributes.speed == 'low') {
		        low = 'on';
		} else if (stateObj.state == 'on' && stateObj.attributes.speed == 'medium') {
		        med = 'on';
		} else if (stateObj.state == 'on' && stateObj.attributes.speed == 'high') {
		        high = 'on';
		} else {
			offstate = 'on';
		}
	}
		
        let lowcolor;
        let medcolor;
	    let hicolor;
	    let offcolor;
		
	   if (low == 'on') {
        lowcolor = 'background-color: var(--dark-primary-color); color: white;';
    } else { 
        lowcolor = '';
    }

    if (med == 'on') {
        medcolor = 'background-color: var(--dark-primary-color); color: white;';
    } else {
        medcolor = '';
    }

    if (high == 'on') {
        hicolor = 'background-color: var(--dark-primary-color); color: white;';
    } else {
        hicolor = '';
    }
	
    if (offstate == 'on') {
        offcolor = 'background-color: var(--dark-primary-color); color: white;';
    } else {
        offcolor = '';
    }
		
	this.setProperties({
        _stateObj: stateObj,
	    _isOffState: stateObj.state == 'off',
        _isOnLow: low === 'on',
	    _isOnMed: med === 'on',
	    _isOnHigh: high === 'on',
	    _lowOnColor: lowcolor,
	    _medOnColor: medcolor,
	    _highOnColor: hicolor,
	    _offColor: offcolor
    });
}

    stopPropagation(e) {
        e.stopPropagation();
    }

    setSpeed(e) {
        const speed = e.currentTarget.getAttribute('name');
        this.hass.callService('fan', 'set_speed', {
            entity_id: this._config.entity, speed: speed
        });
    }

}

customElements.define('custom-fan-card', CustomFanCard);

image
image

4 Likes

@slipx06 @undertoe

Finally had a chance to try this out. Exactly the behaviour I was hoping for. Thanks so much!

I just tried to convert an old non-smart fan that Iā€™ve been controlling with a broadlink RM Pro from HA to a real fan component and I realized that the way my component works it would give an error on trying to turn that type of fan off.

I made a modification to my fan control and since itā€™s becoming kind of its own entity at this point I decided to create itā€™s own thread so I donā€™t keep mucking up this one.

Here is the new thread: