---
layout: page
title: "Components"
description: "List of the built-in components of Home Assistant."
date: 2014-12-21 13:35
sidebar: false
comments: false
sharing: true
footer: true
is_homepage: true
hide_github_edit: true
body_id: components-page
regenerate: false
---

{%- comment -%}Can't use where to count nil because of https://github.com/jekyll/jekyll/issues/6038{%- endcomment -%}
{%- assign tot = 0 -%}
{%- for comp in site.components -%}
  {%- if comp.ha_category -%}
    {%- if comp.ha_category.first -%}
      {%- assign tot = tot | plus: comp.ha_category.size -%}
    {%- else -%}
      {%- assign tot = tot | plus: 1 -%}
    {%- endif -%}
  {%- endif %}
{%- endfor -%}

{%- assign components = site.components | sort: 'title' -%}
{%- assign categories = components | map: 'ha_category' | join: ',' | join: ',' | split: ',' | uniq | sort -%}
{%- capture current_version -%}{{ site.current_major_version }}.{{ site.current_minor_version }}{% endcapture -%}
{%- assign added_one_ago_minor_version = site.current_minor_version|minus: 1 -%}
{%- capture added_one_ago_version -%}{{ site.current_major_version }}.{{ added_one_ago_minor_version }}{% endcapture -%}
{%- assign added_two_ago_minor_version = site.current_minor_version|minus: 2 -%}
{%- capture added_two_ago_version -%}{{ site.current_major_version }}.{{ added_two_ago_minor_version }}{% endcapture -%}
{%- assign current_version_components_count = site.components | where: 'ha_release', current_version | size -%}
{%- assign one_ago_version_components_count = site.components | where: 'ha_release', added_one_ago_version | size -%}
{%- assign two_ago_version_components_count = site.components | where: 'ha_release', added_two_ago_version | size -%}

<p class='note'>
Support for these components is provided by the Home Assistant community.
</p>
<div class="grid">
  <div class="grid__item one-sixth lap-one-whole palm-one-whole">

    <div class="filter-button-group">
      <a href='#all' class="btn">All ({{tot}})</a>
      <a href='#featured' class="btn featured">Featured</a>
      <a href='#version/{{ current_version }}' class="btn added_in_current_version">Added in {{ current_version }} ({{ current_version_components_count }})</a>
      <a href='#version/{{ added_one_ago_version }}' class="btn added_one_version_ago">Added in {{ added_one_ago_version }} ({{ one_ago_version_components_count }})</a>
      <a href='#version/{{ added_two_ago_version }}' class="btn added_two_versions_ago">Added in {{ added_two_ago_version }} ({{ two_ago_version_components_count }})</a>

{%- for category in categories -%}
  {%- assign components_count = components | where: 'ha_category', category | size -%}
  {%- if category and category != 'Other' and components_count != 0 -%}
    <a href='#{{ category | slugify }}' class="btn">{{ category }} ({{ components_count }})</a>
  {%- endif -%}
{%- endfor -%}

      <a href='#other' class="btn">Other ({{ components | where: 'ha_category', 'Other' | size }})</a>
    </div>
  </div>
  <div class="grid__item five-sixths lap-one-whole palm-one-whole">
    <div class="component-search">
      <form onsubmit="event.preventDefault(); return false">
        <input type="text" name="search" id="search" class="search" placeholder="Search components...">
      </form>
    </div>
    <div class="hass-option-cards" id="componentContainer"> </div>
  </div>
</div>

{% comment %}
## Pages without categories
{%- for component in components -%}
  {% unless component.ha_category %}
<p>{{ component.title }}</p>
  {% endunless %}
{%- endfor -%}
{% endcomment %}

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vanilla-lazyload/10.17.0/lazyload.min.js"></script>

{% raw %}
<script id="component-template" type="text/x-custom-template">
{{#components}}
  <a href="{{url}}" class="option-card">
    <div class="img-container">{{{image}}}</div>
    <div class='title'>{{title}}</div>
    <div class='category'>{{cat}}</div>
  </a>
{{/components}}
{{^components}}
  <p class='note'>Nothing found!</p>
{{/components}}
</script>
{% endraw %}

<script type="text/javascript">
// This object contains all components we have
var allComponents = [
  {%- for component in components -%}
    {%- if component.ha_category -%}
{%- assign sliced_version = component.ha_release | split: '.' -%}
{%- assign minor_version = sliced_version[1]|plus: 0 -%}
{%- assign major_version = sliced_version[0]|plus: 0 -%}
{%- assign featured_first = true -%}
      {%- for ha_category in component.ha_category -%}
{url:"{{ component.url }}", title:"{{component.title}}", cat:"{{ha_category | slugify}}", featured: {% if component.featured and featured_first %}true{% else %}false{% endif %}, v: "{{major_version}}.{{minor_version}}", logo: "{{component.logo}}"},
      {%- assign featured_first = false -%}
      {%- endfor -%}
    {% endif -%}
  {%- endfor -%}
  false
];
allComponents.pop(); // remove placeholder element at the end
</script>
<script type="text/javascript">
(function(){
  var template = $('#component-template').html();
  Mustache.parse(template); // make future calls to render faster

  function init() {
    // do the lowerCase transformation once
    for (i=0; i < (allComponents.length); i++) {
      allComponents[i].titleLC = allComponents[i].title.toLowerCase();
      allComponents[i].catLC = allComponents[i].cat.toLowerCase();
    }

    // sort the components alphabetically
    allComponents.sort(function(a, b){
      return a.titleLC.localeCompare(b.titleLC);
    });

    if (location.hash !== '' && location.hash.indexOf('#search/') === 0) {
      // set default value in search from URL
      jQuery('.component-search input').val(decodeURIComponent(location.hash).substring(8));
    }

    // add focus to the search field - even on IE
    setTimeout(function () {
      jQuery('.component-search input').focus();
    }, 1);
  }
  init();

  /**
   * filter all components, based on the location's hash and render them into the component box
   */
  function applyFilter() {
    var logoLazyLoad = new LazyLoad({
      elements_selector: ".option-card img"
    });
    var rendered, i, filter, search;
    var hash = location.hash || '';
    var data = {
      components: [],
      image: function () {
        if(this.logo === '') {
          return '';
        } else {
          return '<img data-src="/images/supported_brands/' + this.logo + '">';
        }
      }
    };

    // fade-out css effect on the old elements. This is actually not visible on fast browsers
    $('#componentContainer').addClass('remove-items');

    if (hash.indexOf('#search/') === -1) {
      // reset search box when not searching
      jQuery('.component-search input').val(null);
    }

    if (hash === '#all') {
      // shortcut: no need to filter
      data.components = allComponents;
    } else {
      if (hash.indexOf('#search/') === 0) {
        // search through title and category
        search = decodeURIComponent(hash).substring(8).toLowerCase();
        filter = function(comp) {
          return (comp.titleLC.indexOf(search) !== -1) ||
                 (comp.catLC.indexOf(search) !== -1);
        };

      } else if(hash === '#featured' || hash === '') {
        // only show those with featured = true
        filter = function(comp) {
          return comp.featured;
        };

      } else if(hash.indexOf('#version/') === 0) {
        // compare against a version
        search = decodeURIComponent(hash).substring(9).toLowerCase();
        filter = function(comp) {
          // compare version string against version js
          return comp.v === search;
        };

      } else {
        // regular filter categories
        search = hash.substring(1);
        filter = function(comp) {
          return comp.catLC === search;
        };
      }

      // filter all components using the filter function
      for (i=0; i < (allComponents.length); i++) {
        if (filter(allComponents[i])) {
          data.components.push(allComponents[i]);
        }
      }
    }

    rendered = Mustache.render(template, data);

    // remove previous elements and css classes, add the new stuff and then trigger the fade-in css animation
    $('#componentContainer').html('').removeClass('show-items remove-items').html(rendered).addClass('show-items');
    logoLazyLoad.update();
  }

  /**
   * update the browser location hash. This enables users to use the browser-history
   */
  function updateHash(newHash) {
    if ('replaceState' in history) {
      history.replaceState('', '', newHash);
    } else {
      location.hash = newHash;
    }
  }

  // update view by filter selection
  jQuery('.filter-button-group a').click(function() {
    updateHash(this.getAttribute('href'));
    applyFilter();

    return false;
  });

  /**
   * Simple debounce implementation, based on http://davidwalsh.name/javascript-debounce-function
   */
  function debounce(func, wait, immediate) {
    var timeout;
    return function() {
      var context = this, args = arguments;
      var later = function() {
        timeout = null;
        if (!immediate) {
          func.apply(context, args);
        }
      };
      var callNow = immediate && !timeout;
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
      if (callNow) {
        func.apply(context, args);
      }
    };
  };

  // update view by search text
  $('.component-search input').keyup(debounce(function() {
    var text = $(this).val();
    // sanitize input
    text = text.replace(/[(\?|\&\{\}\(\))]/gi, '');
    updateHash('#search/' + text);
    applyFilter();
  }, 500));

  window.addEventListener('hashchange', applyFilter);
  applyFilter();
})();
</script>

<noscript>
  <ul>
  {%- for component in components -%}
    {%- if component.ha_category -%}
      <li><a href='{{ component.url }}'>{{ component.title }}</a></li>
    {%- endif -%}
  {%- endfor -%}
  </ul>
</noscript>