Fetch a location's weather.

        *id* can be either a WOEID or LID. The weather data returned for each
        is identical except that the WOEID returns a 2-day forecast and the LID
        returns a 5-day forecast. The LID uses an undocumented API, so use it
        at your own risk.

        Args:
            id: (string) the location's WOEID or LID.
            metric: (bool) return metric data; defaults to False.

        Returns:
            a dict containing the location's weather data or None if
                the weather data couldn't be fetched.

        Raises:
            urllib.error.URLError: urllib.request could not open the URL
                (Python 3).
            urllib2.URLError: urllib2 could not open the URL (Python 2).
            xml.etree.ElementTree.ParseError: xml.etree.ElementTree failed to
                parse the XML document.

def fetch_weather(self, id, metric=False):
        """Fetch a location's weather.

        *id* can be either a WOEID or LID. The weather data returned for each
        is identical except that the WOEID returns a 2-day forecast and the LID
        returns a 5-day forecast. The LID uses an undocumented API, so use it
        at your own risk.

        Args:
            id: (string) the location's WOEID or LID.
            metric: (bool) return metric data; defaults to False.

        Returns:
            a dict containing the location's weather data or None if
                the weather data couldn't be fetched.

        Raises:
            urllib.error.URLError: urllib.request could not open the URL
                (Python 3).
            urllib2.URLError: urllib2 could not open the URL (Python 2).
            xml.etree.ElementTree.ParseError: xml.etree.ElementTree failed to
                parse the XML document.

        """

        units = "c" if metric else "f"

        # WOEID is a 32-bit integer, while LID is XXXXNNNN, where X is a letter
        # and N is a number. So, we pick the URL to use based on whether or not
        # the *id* begins with a letter.

        if re.match("^[A-Za-z]", id):
            url = LID_WEATHER_URL.format(id, units)
        else:
            url = WEATHER_URL.format(id, units)

        rss = self._fetch_xml(url)

        if rss.find("channel/item/title").text == "City not found":
            return None

        # xml_items details which tags should be read and what their
        # destination dict key should be. These tags don't appear
        # multiple times.
        # {XML tag: [ElementTree access method, dict key]}

        xml_items = {
            "channel/title": ["text", "title"],
            "channel/link": ["text", "link"],
            "channel/language": ["text", "language"],
            "channel/description": ["text", "description"],
            "channel/lastBuildDate": ["text", "lastBuildDate"],
            "channel/ttl": ["text", "ttl"],
            "channel/image/url": ["text", "logo"],
            "channel/item/guid": ["text", "guid"],
            "channel/{%s}location" % WEATHER_NS:
                ["attrib", "location"],
            # "channel/{%s}units" % WEATHER_NS:
            #     ["attrib", "units"],
            "channel/{%s}wind" % WEATHER_NS:
                ["attrib", "wind"],
            "channel/{%s}atmosphere" % WEATHER_NS:
                ["attrib", "atmosphere"],
            "channel/{%s}astronomy" % WEATHER_NS:
                ["attrib", "astronomy"],
            "channel/item/{%s}condition" % WEATHER_NS:
                ["attrib", "condition"],
        }
        weather = {}
        weather["units"] = UNITS[units]

        for (tag, meta) in xml_items.items():
            if meta[0] == "text":
                try:
                    weather[meta[1]] = rss.find(tag).text
                except AttributeError:
                    weather[meta[1]] = None
            elif meta[0] == "attrib":
                try:
                    weather[meta[1]] = rss.find(tag).attrib
                except AttributeError:
                    weather[meta[1]] = None
            else:
                weather[meta[1]] = None

        try:
            image_url = CONDITION_IMAGE_URL.format(weather["condition"]["code"])
            weather["condition"]["image"] = image_url
        except (AttributeError, TypeError):
            pass

        try:
            state = weather["atmosphere"]["rising"]
            if state == "0":
                weather["atmosphere"]["state"] = "steady"
            elif state == "1":
                weather["atmosphere"]["state"] = "rising"
            elif state == "2":
                weather["atmosphere"]["state"] = "falling"
            else:
                weather["atmosphere"]["state"] = None
        except (AttributeError, TypeError):
            pass

        weather["forecast"] = []
        try:
            for item in rss.findall(
                    "channel/item/{%s}forecast" % WEATHER_NS):
                weather["forecast"].append(item.attrib)
        except AttributeError:
            weather["forecast"] = None

        weather["geo"] = {}
        try:
            weather["geo"]["lat"] = rss.find(
                "channel/item/{%s}lat" % GEO_NS).text
            weather["geo"]["long"] = rss.find(
                "channel/item/{%s}long" % GEO_NS).text
        except AttributeError:
            weather["geo"] = None

        try:
            weather["wind"]["compass"] = self._degrees_to_direction(
                weather["wind"]["direction"])
        except TypeError:
            pass

        return weather